(965c31410a) Unstable v0.10.4.0

This commit is contained in:
Juan Pablo Arce
2020-07-21 08:57:50 -03:00
parent 4f8bd39789
commit 33d3a41104
546 changed files with 45952 additions and 25762 deletions
@@ -15,7 +15,7 @@ namespace Barotrauma
public static bool DisableEnemyAI;
/// <summary>
/// Enable the character to attack the outposts and the characters inside them. Disabled by default.
/// Enable the character to attack the outposts and the characters inside them. Disabled by default in normal levels, enabled in outpost levels.
/// </summary>
public bool TargetOutposts;
@@ -96,9 +96,13 @@ namespace Barotrauma
private float avoidTimer;
public bool StayInsideLevel = true;
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public CharacterParams.TargetParams SelectedTargetingParams { get { return selectedTargetingParams; } }
public bool AttackHumans
{
get
@@ -153,6 +157,8 @@ namespace Barotrauma
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
targetMemories = new Dictionary<AITarget, AITargetMemory>();
steeringManager = outsideSteering;
//allow targeting outposts and outpost NPCs in outpost levels
TargetOutposts = Level.Loaded != null && Level.Loaded.Type == LevelData.LevelType.Outpost;
List<XElement> aiElements = new List<XElement>();
List<float> aiCommonness = new List<float>();
@@ -298,9 +304,9 @@ namespace Barotrauma
else
{
CharacterParams.TargetParams targetingParams = null;
UpdateTargets(Character, out targetingParams);
if (!IsLatchedOnSub)
{
UpdateTargets(Character, out targetingParams);
UpdateWallTarget();
}
updateTargetsTimer = updateTargetsInterval * Rand.Range(0.75f, 1.25f);
@@ -1367,7 +1373,7 @@ namespace Barotrauma
}
else if (canAttack && attacker.IsHuman && AIParams.TryGetTarget(attacker.SpeciesName, out CharacterParams.TargetParams targetingParams))
{
if (targetingParams.State == AIState.Aggressive)
if (targetingParams.State == AIState.Aggressive || targetingParams.State == AIState.PassiveAggressive)
{
ChangeTargetState(attacker, AIState.Attack, 100);
}
@@ -1561,7 +1567,7 @@ namespace Barotrauma
{
SelectedAiTarget = null;
wallTarget = null;
LatchOntoAI.DeattachFromBody();
LatchOntoAI.DeattachFromBody(cooldown: 1);
}
else if (SelectedAiTarget?.Entity == wallTarget?.Structure)
{
@@ -1849,6 +1855,28 @@ namespace Barotrauma
if (valueModifier == 0.0f) { continue; }
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
{
// Halve the priority for each swarm mate targeting the same target -> reduces stacking
foreach (Character otherCharacter in SwarmBehavior.Members)
{
if (otherCharacter == character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
valueModifier /= 2;
}
}
else
{
// The same as above, but using all the friendly characters in the level.
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
if (!IsFriendly(character, otherCharacter)) { continue; }
valueModifier /= 2;
}
}
Vector2 toTarget = aiTarget.WorldPosition - character.WorldPosition;
float dist = toTarget.Length();
@@ -2199,23 +2227,23 @@ namespace Barotrauma
private float returnTimer;
private void SteerInsideLevel(float deltaTime)
{
if (SteeringManager is IndoorsSteeringManager) { return; }
if (SteeringManager is IndoorsSteeringManager || !StayInsideLevel) { return; }
if (Level.Loaded == null) { return; }
Vector2 levelSimSize = ConvertUnits.ToSimUnits(Level.Loaded.Size.X, Level.Loaded.Size.Y);
float returnTime = 3;
if (SimPosition.Y < 0)
Point levelSize = Level.Loaded.Size;
float returnTime = 10;
if (WorldPosition.Y < 0)
{
// Too far down
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitY;
}
if (SimPosition.X < 0)
if (WorldPosition.X < 0)
{
// Too far left
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
returnDir = Vector2.UnitX;
}
if (SimPosition.X > levelSimSize.X)
if (WorldPosition.X > levelSize.X)
{
// Too far right
returnTimer = returnTime * Rand.Range(0.75f, 1.25f);
@@ -2225,7 +2253,7 @@ namespace Barotrauma
{
returnTimer -= deltaTime;
SteeringManager.Reset();
SteeringManager.SteeringManual(deltaTime, returnDir);
SteeringManager.SteeringManual(deltaTime, returnDir * 2);
}
}
@@ -34,6 +34,37 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private class HullSafety
{
public float safety;
public float timer;
public bool IsStale => timer <= 0;
public HullSafety(float safety)
{
Reset(safety);
}
public void Reset(float safety)
{
this.safety = safety;
// How long before the hull safety is considered stale
timer = 0.5f;
}
/// <summary>
/// Returns true when the safety is stale
/// </summary>
public bool Update(float deltaTime)
{
timer = Math.Max(timer - deltaTime, 0);
return IsStale;
}
}
private readonly Dictionary<Hull, HullSafety> knownHulls = new Dictionary<Hull, HullSafety>();
private SteeringManager outsideSteering, insideSteering;
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
@@ -58,6 +89,10 @@ namespace Barotrauma
public float CurrentHullSafety { get; private set; } = 100;
private readonly Dictionary<Character, float> damageDoneByAttacker = new Dictionary<Character, float>();
private readonly List<Character> attackers = new List<Character>();
public HumanAIController(Character c) : base(c)
{
if (!c.IsHuman)
@@ -67,7 +102,7 @@ namespace Barotrauma
insideSteering = new IndoorsSteeringManager(this, true, false);
outsideSteering = new SteeringManager(this);
objectiveManager = new AIObjectiveManager(c);
reactTimer = Rand.Range(0f, reactionTime);
reactTimer = GetReactionTime();
sortTimer = Rand.Range(0f, sortObjectiveInterval);
InitProjSpecific();
}
@@ -78,6 +113,12 @@ namespace Barotrauma
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
base.Update(deltaTime);
foreach (var values in knownHulls)
{
HullSafety hullSafety = values.Value;
hullSafety.Update(deltaTime);
}
if (unreachableClearTimer > 0)
{
unreachableClearTimer -= deltaTime;
@@ -123,6 +164,15 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
//slowly forget about damage done by attackers
foreach (Character enemy in attackers)
{
if (damageDoneByAttacker[enemy] > 0)
{
damageDoneByAttacker[enemy] -= deltaTime * 0.01f;
}
}
if (reactTimer > 0.0f)
{
reactTimer -= deltaTime;
@@ -136,7 +186,15 @@ namespace Barotrauma
{
if (Character.CurrentHull != null)
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
if (Character.TeamID == Character.TeamType.FriendlyNPC)
{
// Outpost npcs don't inform each other about threads, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
}
else
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
}
}
if (Character.SpeechImpediment < 100.0f)
{
@@ -147,7 +205,7 @@ namespace Barotrauma
UpdateSpeaking();
}
UnequipUnnecessaryItems();
reactTimer = reactionTime * Rand.Range(0.75f, 1.25f);
reactTimer = GetReactionTime();
}
if (objectiveManager.CurrentObjective == null) { return; }
@@ -170,7 +228,7 @@ namespace Barotrauma
else
{
float xDiff = goTo.Target.WorldPosition.X - Character.WorldPosition.X;
run = Math.Abs(xDiff) > 300;
run = Math.Abs(xDiff) > 500;
}
}
}
@@ -276,127 +334,137 @@ namespace Barotrauma
{
if (!NeedsDivingGear(Character, Character.CurrentHull, out _))
{
bool oxygenLow = Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| Character.CurrentHull.WaterPercentage > 50
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool removeDivingSuit = !Character.AnimController.HeadInWater && oxygenLow;
bool takeMaskOff = !Character.AnimController.HeadInWater && oxygenLow;
if (!removeDivingSuit)
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
if (oxygenLow)
{
if (shouldKeepTheGearOn)
{
removeDivingSuit = false;
}
shouldKeepTheGearOn = false;
}
if (!takeMaskOff)
bool removeDivingSuit = !shouldKeepTheGearOn;
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
{
if (shouldKeepTheGearOn)
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
takeMaskOff = false;
removeDivingSuit = true;
takeMaskOff = true;
}
}
if (!shouldKeepTheGearOn && (!takeMaskOff || !removeDivingSuit))
{
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
else
{
if (objective is AIObjectiveGoTo gotoObjective)
bool removeSuit = false;
bool removeMask = false;
foreach (var objective in ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(includingSelf: true))
{
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _))
if (objective is AIObjectiveGoTo gotoObjective)
{
removeDivingSuit = false;
takeMaskOff = false;
break;
}
else if (gotoObjective.mimic)
{
if (!removeDivingSuit)
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
bool targetIsOutside = (gotoObjective.Target != null && targetHull == null) || (insideSteering && PathSteering.CurrentPath.HasOutdoorsNodes);
if (targetIsOutside || NeedsDivingGear(Character, targetHull, out _))
{
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
removeDivingSuit = false;
takeMaskOff = false;
break;
}
if (!takeMaskOff)
else if (gotoObjective.mimic)
{
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
divingSuit.Drop(Character);
}
else
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
if (!removeSuit)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
removeDivingSuit = !HasDivingSuit(gotoObjective.Target as Character);
if (removeDivingSuit)
{
DropIfFailsToContain = false
};
decontainObjective.Abandoned += () =>
{
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
removeSuit = true;
}
}
else
if (!removeMask)
{
divingSuit.Drop(Character);
takeMaskOff = !HasDivingMask(gotoObjective.Target as Character);
if (takeMaskOff)
{
removeMask = true;
}
}
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
if (takeMaskOff)
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
var mask = Character.Inventory.FindItemByTag("divingmask");
if (mask != null && Character.Inventory.IsInLimbSlot(mask, InvSlotType.Head))
if (removeDivingSuit)
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
divingSuit.Drop(Character);
}
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFailsToContain = false
};
decontainObjective.Abandoned += () =>
{
IgnoredItems.Add(targetContainer);
};
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
divingSuit.Drop(Character);
}
}
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
if (takeMaskOff)
{
if (Character.HasEquippedItem("divingmask"))
{
var mask = Character.Inventory.FindItemByTag("divingmask");
if (mask != null)
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
mask.Drop(Character);
}
else
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
}
}
}
}
}
}
@@ -404,41 +472,41 @@ namespace Barotrauma
}
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
foreach (var item in Character.Inventory.Items)
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
if (!ObjectiveManager.CurrentObjective.UnequipItems || !ObjectiveManager.GetActiveObjective().UnequipItems) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
foreach (var item in Character.Inventory.Items)
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
{
if (FindSuitableContainer(item, out Item targetContainer))
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
if (FindSuitableContainer(item, out Item targetContainer))
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
item.Drop(Character);
}
}
else
{
item.Drop(Character);
findItemState = FindItemState.OtherItem;
}
}
else
{
findItemState = FindItemState.OtherItem;
}
}
}
}
@@ -559,7 +627,7 @@ namespace Barotrauma
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveRepairItems.IsValidTarget(item, Character))
{
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairThreshold)) { continue; }
if (item.Repairables.All(r => item.ConditionPercentage > r.RepairIconThreshold)) { continue; }
if (AddTargets<AIObjectiveRepairItems, Item>(Character, item) && newOrder == null && !ObjectiveManager.HasActiveObjective<AIObjectiveRepairItem>())
{
var orderPrefab = Order.GetPrefab("reportbrokendevices");
@@ -574,7 +642,13 @@ namespace Barotrauma
}
if (newOrder != null)
{
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
if (Character.TeamID == Character.TeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Default,
identifier: newOrder.Prefab.Identifier + (targetHull?.DisplayName ?? "null"),
minDurationBetweenSimilar: 60.0f);
}
else if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName, givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
@@ -588,24 +662,40 @@ namespace Barotrauma
{
if (Character.Oxygen < 20.0f)
{
Character.Speak(TextManager.Get("DialogLowOxygen"), null, 0, "lowoxygen", 30.0f);
Character.Speak(TextManager.Get("DialogLowOxygen"), null, Rand.Range(0.5f, 5.0f), "lowoxygen", 30.0f);
}
if (Character.Bleeding > 2.0f)
{
Character.Speak(TextManager.Get("DialogBleeding"), null, 0, "bleeding", 30.0f);
Character.Speak(TextManager.Get("DialogBleeding"), null, Rand.Range(0.5f, 5.0f), "bleeding", 30.0f);
}
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
{
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, 0, "pressure", 30.0f);
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, true), null, Rand.Range(0.5f, 5.0f), "pressure", 30.0f);
}
}
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
float damage = attackResult.Damage;
if (damage <= 0) { return; }
// excluding poisons etc
float realDamage = attackResult.Damage;
// including poisons etc
float totalDamage = realDamage;
foreach (Affliction affliction in attackResult.Afflictions)
{
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
if (totalDamage <= 0) { return; }
if (attacker != null)
{
if (!damageDoneByAttacker.ContainsKey(attacker))
{
damageDoneByAttacker[attacker] = 0.0f;
}
damageDoneByAttacker[attacker] += totalDamage;
attackers.Add(attacker);
}
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed)
{
@@ -617,6 +707,11 @@ namespace Barotrauma
//if (Character.LastDamageSource == null) { return; }
//AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
}
else if (realDamage <= 0 && (attacker.IsBot || attacker.TeamID == Character.TeamID))
{
// Don't react on damage that is entirely based on karma penalties (medics, poisons etc), unless applier is player
return;
}
else if (IsFriendly(attacker))
{
if (attacker.AnimController.Anim == Barotrauma.AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
@@ -627,62 +722,133 @@ namespace Barotrauma
}
if (attacker.IsBot)
{
// Don't retaliate on damage done by friendly ai, because we know that it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
// Don't retaliate on damage done by human ai, because we know it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
}
else
{
// If not on the same team, always stay defensive
if (attacker.TeamID != Character.TeamID)
if (Character.IsSecurity)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
// TODO
}
else
{
float dmgPercentage = MathUtils.Percentage(damage, Character.CharacterHealth.Vitality);
if (dmgPercentage < 10)
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
}
if (Character.TeamID == Character.TeamType.FriendlyNPC && !Character.TurnedHostileByEvent)
{
// Inform other characters in the same team
foreach (Character otherCharacter in Character.CharacterList)
{
// Don't retaliate on minor (accidental) dmg done by characters that are in the same team
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
if (otherCharacter == Character || otherCharacter.TeamID != Character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.TurnedHostileByEvent)
{
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
if (otherCharacter.IsSecurity)
{
// Alert all the security officers magically
float delay = isWitnessing ? GetReactionTime() * 2 : Rand.Range(2.0f, 5.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter), attacker, delay);
}
else if (isWitnessing)
{
// Other witnesses retreat to safety
otherHumanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime());
}
}
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
if (attacker.TeamID != Character.TeamID)
{
AddCombatObjective(DetermineCombatMode(Character), attacker, GetReactionTime());
}
else
{
// Don't react on minor (accidental) dmg done by characters that are in the same team
if (GetDamageDoneByAttacker(attacker) < 10)
{
if (!Character.IsSecurity)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
}
}
else
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, Rand.Range(0.5f, 1f, Rand.RandSync.Unsynced));
AddCombatObjective(DetermineCombatMode(Character, dmgThreshold: 20, allowOffensive: false), attacker, GetReactionTime() * 2);
}
}
}
}
else
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive);
AddCombatObjective(DetermineCombatMode(Character), attacker);
}
void AddCombatObjective(AIObjectiveCombat.CombatMode mode, float delay = 0)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float dmgThreshold = 10, bool allowOffensive = true)
{
bool holdPosition = Character.Info?.Job?.Prefab.Identifier == "watchman";
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
if (!IsFriendly(attacker))
{
if (combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null))
{
// Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective);
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition});
}
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
if (delay > 0)
if (GetDamageDoneByAttacker(attacker) > dmgThreshold)
{
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition }, delay);
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker, mode, objectiveManager) { HoldPosition = holdPosition });
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
}
}
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
{
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
{
// Don't replace offensive mode with something else
if (combatObjective.Mode == AIObjectiveCombat.CombatMode.Offensive && mode != AIObjectiveCombat.CombatMode.Offensive) { return; }
if (combatObjective.Mode != mode || combatObjective.Enemy != attacker || (combatObjective.Enemy == null && attacker == null))
{
// Replace the old objective with the new.
ObjectiveManager.Objectives.Remove(combatObjective);
ObjectiveManager.AddObjective(CreateCombatObjective());
}
}
else
{
if (delay > 0)
{
ObjectiveManager.AddObjective(CreateCombatObjective(), delay);
}
else
{
ObjectiveManager.AddObjective(CreateCombatObjective());
}
}
AIObjectiveCombat CreateCombatObjective()
{
var objective = new AIObjectiveCombat(Character, attacker, mode, objectiveManager)
{
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
abortCondition = abortCondition,
allowHoldFire = allowHoldFire,
};
if (onAbort != null)
{
objective.Abandoned += onAbort;
}
return objective;
}
}
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
{
CurrentOrderOption = option;
@@ -733,7 +899,7 @@ namespace Barotrauma
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
if (crouchRaycastTimer > 0.0f) return;
if (crouchRaycastTimer > 0.0f) { return; }
crouchRaycastTimer = crouchRaycastInterval;
@@ -743,7 +909,59 @@ namespace Barotrauma
//do a raycast upwards to find any walls
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall, customPredicate: (fixture) => { return !(fixture.Body.UserData is Submarine); }) != null;
}
public bool AllowCampaignInteraction()
{
if (Character == null || Character.Removed || Character.IsIncapacitated) { return false; }
switch (ObjectiveManager.CurrentObjective)
{
case AIObjectiveCombat _:
case AIObjectiveFindSafety _:
case AIObjectiveExtinguishFires _:
case AIObjectiveFightIntruders _:
case AIObjectiveFixLeaks _:
return false;
}
return true;
}
public bool TryToMoveItem(Item item, Inventory targetInventory, bool dropIfCannotMove = true)
{
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < targetInventory.Items.Length; i++)
{
if (targetInventory is CharacterInventory characterInventory)
{
//slot not needed by the item, continue
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
if (dropIfCannotMove)
{
//if everything else fails, simply drop the existing item
otherItem.Drop(Character);
}
}
}
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
}
public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit)
@@ -769,28 +987,108 @@ 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, "divingsuit", "oxygensource", conditionPercentage);
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, "divingsuit", out _, "oxygensource", conditionPercentage, requireEquipped: true);
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
/// </summary>
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", "oxygensource", conditionPercentage);
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, "divingmask", out _, "oxygensource", conditionPercentage, requireEquipped: true);
public static bool HasItem(Character character, string tagOrIdentifier, string containedTag = null, float conditionPercentage = 0)
public static bool HasItem(Character character, string tagOrIdentifier, out Item item, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
{
item = null;
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
var item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
return item != null &&
item.ConditionPercentage > conditionPercentage &&
character.HasEquippedItem(item) &&
item.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(item)) &&
(containedTag == null ||
(item.ContainedItems != null &&
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
}
public static void ItemTaken(Item item, Character character)
{
if (item == null || character == null || item.GetComponent<LevelResource>() != null) { return; }
Character thief = character;
bool someoneSpoke = false;
if (item.SpawnedInOutpost && thief.TeamID != Character.TeamType.FriendlyNPC && !item.HasTag("handlocker"))
{
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeCharacter(thief)) { continue; }
if (!someoneSpoke)
{
if (!item.StolenDuringRound && GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.Value -= reputationLoss;
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
someoneSpoke = true;
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
{
if (TriggerSecurity(security.AIController as HumanAIController))
{
// Only alert one guard at a time
break;
}
}
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInOutpost, true) is { } foundItem)
{
ItemTaken(foundItem, character);
}
bool TriggerSecurity(HumanAIController humanAI)
{
if (humanAI == null) { return false; }
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,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
{
BasePriority = 10
});
}
},
allowHoldFire: true);
return true;
}
}
// 0.225 - 0.375
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
@@ -887,7 +1185,30 @@ namespace Barotrauma
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
public float GetDamageDoneByAttacker(Character attacker)
{
if (!damageDoneByAttacker.TryGetValue(attacker, out float dmg))
{
dmg = 0;
}
return dmg;
}
private void StoreHullSafety(Hull hull, HullSafety safety)
{
if (knownHulls.ContainsKey(hull))
{
// Update existing. Shouldn't currently happen, but things might change.
knownHulls[hull] = safety;
}
else
{
// Add new
knownHulls.Add(hull, safety);
}
}
private float CalculateHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
bool isCurrentHull = character == Character && character.CurrentHull == hull;
if (hull == null)
@@ -903,12 +1224,11 @@ namespace Barotrauma
// Use the cached visible hulls
visibleHulls = VisibleHulls;
}
// TODO: should we calculate the visible hulls for each hull? -> could be a bit heavy.
bool ignoreFire = objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreWater = HasDivingSuit(character);
bool ignoreOxygen = ignoreWater || HasDivingMask(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
float safety = GetHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
CurrentHullSafety = safety;
@@ -916,7 +1236,7 @@ namespace Barotrauma
return safety;
}
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
private static float CalculateHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
@@ -949,13 +1269,65 @@ namespace Barotrauma
return MathHelper.Clamp(safety * 100, 0, 100);
}
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
StoreHullSafety(hull, hullSafety);
}
else if (hullSafety.IsStale)
{
hullSafety.Reset(CalculateHullSafety(hull, character, visibleHulls));
}
return hullSafety.safety;
}
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
if (!controller.knownHulls.TryGetValue(hull, out hullSafety))
{
hullSafety = new HullSafety(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
controller.StoreHullSafety(hull, hullSafety);
}
else if (hullSafety.IsStale)
{
hullSafety.Reset(CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies));
}
}
else
{
#if DEBUG
DebugConsole.ThrowError("Cannot store the hull safety, because was unable to cast the AIController as HumanAIController. This should never happen!");
#endif
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
return hullSafety.safety;
}
public void FaceTarget(ISpatialEntity target) => Character.AnimController.TargetDir = target.WorldPosition.X > Character.WorldPosition.X ? Direction.Right : Direction.Left;
public static bool IsFriendly(Character me, Character other)
public static bool IsFriendly(Character me, Character other, bool onlySameTeam = false)
{
bool sameSpecies = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
bool differentTeam = me.TeamID == Character.TeamType.Team1 && other.TeamID == Character.TeamType.Team2 || me.TeamID == Character.TeamType.Team2 && other.TeamID == Character.TeamType.Team1;
return sameSpecies && !differentTeam;
bool sameTeam = me.TeamID == other.TeamID;
// Only enemies are in the Team "None"
bool friendlyTeam = me.TeamID != Character.TeamType.None && other.TeamID != Character.TeamType.None;
bool teamGood = sameTeam || friendlyTeam && !onlySameTeam;
if (!teamGood) { return false; }
bool speciesGood = other.SpeciesName == me.SpeciesName || other.Params.CompareGroup(me.Params.Group);
if (!speciesGood) { return false; }
if (me.TeamID == Character.TeamType.FriendlyNPC && other.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
}
return true;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
@@ -101,7 +101,7 @@ namespace Barotrauma
{
currentPath = path;
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
findPathTimer = 1.0f;
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -138,6 +138,21 @@ namespace Barotrauma
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && !node.Ladders.Item.NonInteractable)
{
return node.Ladders;
}
}
}
}
return null;
}
@@ -246,14 +261,33 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
if (canClimb && !isDiving && IsNextLadderSameAsCurrent)
var ladders = GetNextLadder();
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
{
var ladders = currentPath.CurrentNode.Ladders;
if (character.SelectedConstruction != ladders.Item && ladders.Item.IsInsideTrigger(character.WorldPosition))
if (IsNextNodeLadder || currentPath.CurrentIndex == currentPath.Nodes.Count - 1)
{
currentPath.CurrentNode.Ladders.Item.TryInteract(character, false, true);
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))
{
ladders.Item.TryInteract(character, false, true);
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !isDiving)
{
@@ -420,7 +454,7 @@ namespace Barotrauma
}
else
{
door = currentWaypoint.ConnectedGap.ConnectedDoor;
door = currentWaypoint.ConnectedDoor;
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
@@ -437,7 +471,7 @@ namespace Barotrauma
if (door == null) { return; }
//toggle the door if it's the previous node and open, or if it's current node and closed
if (door.IsOpen != shouldBeOpen)
if ((door.IsOpen || door.IsBroken) != shouldBeOpen)
{
Controller closestButton = null;
float closestDist = 0;
@@ -447,12 +481,12 @@ namespace Barotrauma
// Check that the button is on the right side of the door.
if (door.LinkedGap.IsHorizontal)
{
int dir = Math.Sign(nextWaypoint.WorldPosition.X - door.Item.WorldPosition.X);
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.X - door.Item.WorldPosition.X);
if (button.Item.WorldPosition.X * dir > door.Item.WorldPosition.X * dir) { return false; }
}
else
{
int dir = Math.Sign(nextWaypoint.WorldPosition.Y - door.Item.WorldPosition.Y);
int dir = Math.Sign((nextWaypoint ?? currentWaypoint).WorldPosition.Y - door.Item.WorldPosition.Y);
if (button.Item.WorldPosition.Y * dir > door.Item.WorldPosition.Y * dir) { return false; }
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
@@ -585,6 +619,12 @@ namespace Barotrauma
}
}
float yDist = Math.Abs(node.Position.Y - nextNode.Position.Y);
if (node.Waypoint.Ladders == null && nextNode.Waypoint.Ladders == null)
{
penalty += yDist * 10.0f;
}
return penalty;
}
@@ -305,13 +305,17 @@ namespace Barotrauma
attachJoints.Add(colliderJoint);
}
public void DeattachFromBody()
public void DeattachFromBody(float cooldown = 0)
{
foreach (Joint joint in attachJoints)
{
GameMain.World.Remove(joint);
}
attachJoints.Clear();
attachJoints.Clear();
if (cooldown > 0)
{
attachCooldown = cooldown;
}
}
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
@@ -166,11 +166,33 @@ namespace Barotrauma
private static List<string> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<string>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep");
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial");
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep"); }
if (GameMain.GameSession != null && Level.Loaded != null)
{
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) { currentFlags.Add("Initial"); }
}
else if (Level.Loaded.Type == LevelData.LevelType.Outpost)
{
if (Timing.TotalTime < GameMain.GameSession.RoundStartTime + 120.0f &&
speaker?.CurrentHull != null &&
speaker.TeamID == Character.TeamType.FriendlyNPC &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost");
}
}
if (GameMain.GameSession.EventManager.CurrentIntensity <= 0.2f)
{
currentFlags.Add("Casual");
}
}
if (speaker != null)
{
if (speaker.AnimController.InWater) currentFlags.Add("Underwater");
if (speaker.AnimController.InWater) { currentFlags.Add("Underwater"); }
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
if (Character.Controlled != null)
@@ -190,6 +212,15 @@ namespace Barotrauma
currentFlags.Add(currentEffect.DialogFlag);
}
}
if (speaker.TeamID == Character.TeamType.FriendlyNPC && speaker.Submarine != null && speaker.Submarine.Info.IsOutpost)
{
currentFlags.Add("OutpostNPC");
}
if (speaker.CampaignInteractionType != CampaignMode.InteractionType.None)
{
currentFlags.Add("CampaignNPC." + speaker.CampaignInteractionType);
}
}
return currentFlags;
@@ -207,7 +238,7 @@ namespace Barotrauma
return lines;
}
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, List<string> requiredFlags)
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, IEnumerable<string> requiredFlags)
{
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
@@ -215,7 +246,7 @@ namespace Barotrauma
kpv => kpv.Value.Where(conversation => kpv.Key == TextManager.Language && requiredFlags.All(f => conversation.Flags.Contains(f))))).ToList();
if (availableConversations.Count > 0)
{
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: false);
}
return lines;
}
@@ -229,11 +260,11 @@ namespace Barotrauma
bool ignoreFlags = false)
{
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
if (conversations.Count == 0) return;
if (conversations.Count == 0) { return; }
int conversationIndex = Rand.Int(conversations.Count);
NPCConversation selectedConversation = conversations[conversationIndex];
if (string.IsNullOrEmpty(selectedConversation.Line)) return;
if (string.IsNullOrEmpty(selectedConversation.Line)) { return; }
Character speaker = null;
//speaker already assigned for this line
@@ -265,8 +296,8 @@ namespace Barotrauma
//select a random line and attempt to find a speaker for it
// and if no valid speaker is found, choose another random line
selectedConversation = GetRandomConversation(potentialLines, baseConversation == null);
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return;
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) { return; }
//speaker already assigned for this line
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
{
@@ -280,21 +311,24 @@ namespace Barotrauma
if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) ||
selectedConversation.AllowedJobs.Count > 0)
{
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) { continue; }
}
//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;
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;
if (potentialSpeaker.Info?.PersonalityTrait == null) { continue; }
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) { continue; }
}
else
{
@@ -318,7 +352,7 @@ namespace Barotrauma
}
}
if (allowedSpeakers.Count == 0) return;
if (allowedSpeakers.Count == 0) { return; }
speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
availableSpeakers.Remove(speaker);
assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
@@ -31,6 +31,7 @@ namespace Barotrauma
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -46,6 +47,8 @@ namespace Barotrauma
/// Final priority value after all calculations.
/// </summary>
public float Priority { get; set; }
public float BasePriority { get; set; }
public float PriorityModifier { get; private set; } = 1;
public readonly Character character;
public readonly AIObjectiveManager objectiveManager;
@@ -182,7 +185,18 @@ namespace Barotrauma
}
}
protected bool IsAllowed => AllowOutsideSubmarine || character.Submarine != null && character.Submarine.TeamID == character.TeamID && character.Submarine.Info.IsPlayer;
protected bool IsAllowed
{
get
{
if (AllowOutsideSubmarine) { return true; }
if (character.Submarine == null) { return false; }
return
character.Submarine.TeamID == character.TeamID ||
(AllowInFriendlySubs && character.Submarine.TeamID == Character.TeamType.FriendlyNPC) ||
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.
@@ -200,7 +214,7 @@ namespace Barotrauma
}
else
{
Priority = CumulatedDevotion;
Priority = BasePriority + CumulatedDevotion;
}
return Priority;
}
@@ -336,7 +350,12 @@ namespace Barotrauma
}
protected set
{
if (isCompleted == value) { return; }
isCompleted = value;
if (isCompleted)
{
OnCompleted();
}
}
}
@@ -346,7 +365,7 @@ namespace Barotrauma
{
hasBeenChecked = true;
CheckSubObjectives();
if (subObjectives.None())
if (subObjectives.None() || ConcurrentObjectives && subObjectives.All(so => so is AIObjectiveGoTo))
{
if (Check())
{
@@ -1,9 +1,9 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -18,13 +18,17 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
const float seekWeaponsInterval = 1;
private readonly float seekWeaponsInterval = 1;
private float ignoreWeaponTimer;
const float ignoredWeaponsClearTime = 10;
private readonly float ignoredWeaponsClearTime = 10;
const float coolDown = 10.0f;
// Won't take the offensive with weapons that have lower priority than this
const float goodWeaponPriority = 30;
// Won't (by default) start the offensive with weapons that have lower priority than this
private readonly float goodWeaponPriority = 30;
private readonly float arrestHoldFireTime = 8;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -37,6 +41,7 @@ namespace Barotrauma
{
_weapon = value;
_weaponComponent = null;
hasAimed = false;
RemoveSubObjective(ref seekAmmunition);
}
}
@@ -73,16 +78,36 @@ namespace Barotrauma
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private float aimTimer;
private bool canSeeTarget;
private float visibilityCheckTimer;
private readonly float visibilityCheckInterval = 0.2f;
/// <summary>
/// Aborts the objective when this condition is true
/// </summary>
public Func<bool> abortCondition;
public bool allowHoldFire;
/// <summary>
/// Don't start using a weapon if this condition is true
/// </summary>
public Func<bool> holdFireCondition;
public enum CombatMode
{
Defensive,
Offensive,
Arrest,
Retreat
}
public CombatMode Mode { get; private set; }
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1)
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
{
Enemy = enemy;
@@ -103,7 +128,16 @@ namespace Barotrauma
public override float GetPriority()
{
Priority = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) ? 0 : Math.Min(100 * PriorityModifier, 100);
if (character.TeamID == Character.TeamType.FriendlyNPC && Enemy != null)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, HumanAIController.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
return Priority;
}
@@ -121,43 +155,55 @@ namespace Barotrauma
protected override bool Check()
{
if (initialMode == CombatMode.Offensive && Mode != CombatMode.Offensive)
if (IsOffensiveOrArrest && Mode != initialMode)
{
Abandon = true;
SteeringManager.Reset();
return false;
}
bool completed = (Enemy != null && (Enemy.Removed || Enemy.IsDead)) || (initialMode != CombatMode.Offensive && coolDownTimer <= 0);
if (completed)
{
if (objectiveManager.CurrentOrder == this && Enemy != null && Enemy.IsDead)
{
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
if (Weapon != null)
{
Unequip();
}
}
return completed;
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
}
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
protected override void Act(float deltaTime)
{
if (initialMode != CombatMode.Offensive)
if (abortCondition != null && abortCondition())
{
Abandon = true;
SteeringManager.Reset();
return;
}
if (!IsOffensiveOrArrest)
{
coolDownTimer -= deltaTime;
}
if (seekAmmunition == null)
{
if (Mode != CombatMode.Retreat && TryArm() && Enemy != null && !Enemy.Removed)
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition && seekAmmunition == null)
if (!HoldPosition)
{
Move(deltaTime);
}
switch (Mode)
{
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, "handlocker", out _, requireEquipped: true))
{
IsCompleted = true;
}
break;
}
}
}
@@ -166,6 +212,7 @@ namespace Barotrauma
switch (Mode)
{
case CombatMode.Offensive:
case CombatMode.Arrest:
Engage();
break;
case CombatMode.Defensive:
@@ -190,7 +237,7 @@ namespace Barotrauma
{
seekWeaponsTimer = seekWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons().ToList();
var allWeapons = GetAllWeapons();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -206,16 +253,6 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (initialMode == CombatMode.Offensive)
{
// In the offensive mode, let's ignore weapons that cannot be used in the offensive mode
if (WeaponComponent.CombatPriority < goodWeaponPriority)
{
allWeapons.Remove(WeaponComponent);
Weapon = null;
continue;
}
}
if (IsLoaded(WeaponComponent))
{
// All good, the weapon is loaded
@@ -253,6 +290,10 @@ namespace Barotrauma
}
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
}
else
{
@@ -261,14 +302,6 @@ namespace Barotrauma
Weapon = null;
}
}
if (Weapon == null)
{
Mode = CombatMode.Retreat;
}
else
{
Mode = WeaponComponent.CombatPriority >= goodWeaponPriority ? initialMode : CombatMode.Defensive;
}
return Weapon != null;
bool CheckWeapon(bool seekAmmo)
@@ -296,6 +329,7 @@ namespace Barotrauma
{
case CombatMode.Offensive:
case CombatMode.Defensive:
case CombatMode.Arrest:
if (Equip())
{
Attack(deltaTime);
@@ -308,29 +342,163 @@ namespace Barotrauma
}
}
private Item GetWeapon(out ItemComponent weaponComponent)
{
GetAllWeapons();
return GetWeapon(weapons, out weaponComponent);
}
private Item GetWeapon(out ItemComponent weaponComponent) => GetWeapon(GetAllWeapons(), out weaponComponent);
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = weaponList.OrderByDescending(w => CalculateWeaponPriority(w)).FirstOrDefault();
if (weaponComponent == null) { return null; }
if (weaponComponent.CombatPriority < 1) { return null; }
return weaponComponent.Item;
}
private float CalculateWeaponPriority(ItemComponent weapon)
{
float priority = weapon.CombatPriority;
// Halve the priority for weapons that don't have proper ammunition loaded.
if (!weapon.HasRequiredContainedItems(character, addMessage: false))
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
foreach (var weapon in weaponList)
{
priority /= 2;
// By default, the bots won't go offensive with bad weapons, unless they are close to the enemy or ordered to fight enemies.
// NPC characters ignore this check.
if ((initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest) && character.TeamID != Character.TeamType.FriendlyNPC)
{
if (!objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() && !EnemyIsClose())
{
if (weapon.CombatPriority < goodWeaponPriority)
{
continue;
}
}
}
float priority = weapon.CombatPriority;
if (!IsLoaded(weapon))
{
if (weapon is RangedWeapon && EnemyIsClose())
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
}
else
{
// Halve the priority for weapons that don't have proper ammunition loaded.
priority /= 2;
}
}
if (Enemy.Stun > 1)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float max = lethalDmg + 1;
if (weapon.Item.HasTag("stunner"))
{
priority = max;
}
else
{
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
priority = Math.Clamp(priority - Math.Max(diff * 2, 0), min: 1, max);
}
}
}
else if (Mode == CombatMode.Arrest)
{
// Enemy is not stunned, increase the priority of stunner weapons and decrease the priority of lethal weapons.
if (weapon.Item.HasTag("stunner"))
{
priority *= 2;
}
else
{
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
float stunDmg = ApproximateStunDamage(weapon, attack);
float diff = stunDmg - lethalDmg;
if (diff < 0)
{
priority /= 2;
}
}
}
}
if (priority > bestPriority)
{
weaponComponent = weapon;
bestPriority = priority;
}
}
if (weaponComponent == null) { return null; }
if (bestPriority < 1) { return null; }
if (Mode == CombatMode.Arrest)
{
if (weaponComponent.Item.HasTag("stunner"))
{
isLethalWeapon = false;
}
else
{
if (lethalDmg < 0)
{
lethalDmg = GetLethalDamage(weaponComponent);
}
isLethalWeapon = lethalDmg > 1;
}
if (allowHoldFire && !hasAimed && holdFireTimer <= 0)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
}
}
return weaponComponent.Item;
bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
{
attack = meleeWeapon.Attack;
}
else if (weapon is RangedWeapon rangedWeapon)
{
attack = rangedWeapon.FindProjectile(triggerOnUseOnContainers: false)?.Attack;
}
return attack;
}
float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
if (attack != null)
{
lethalDmg = attack.GetTotalDamage();
}
return lethalDmg;
}
float ApproximateStunDamage(ItemComponent weapon, Attack attack)
{
// Try to reduce the priority using the actual damage values and status effects.
// This is an approximation, because we can't check the status effect conditions here.
// The result might be incorrect if there is a high stun effect that's only applied in certain conditions.
var statusEffects = attack.StatusEffects.Where(se => !se.HasConditions && se.type == ActionType.OnUse && se.HasRequiredItems(character));
if (weapon.statusEffectLists != null && weapon.statusEffectLists.TryGetValue(ActionType.OnUse, out List<StatusEffect> hitEffects))
{
statusEffects = statusEffects.Concat(hitEffects);
}
float afflictionsStun = attack.Afflictions.Keys.Sum(a => a.Identifier == "stun" ? a.Strength : 0);
float effectsStun = statusEffects.None() ? 0 : statusEffects.Max(se =>
{
float stunAmount = 0;
var stunAffliction = se.Afflictions.Find(a => a.Identifier == "stun");
if (stunAffliction != null)
{
stunAmount = stunAffliction.Strength;
}
return stunAmount;
});
return attack.Stun + afflictionsStun + effectsStun;
}
return priority;
}
private HashSet<ItemComponent> GetAllWeapons()
@@ -354,30 +522,9 @@ namespace Barotrauma
if (item == null) { return; }
foreach (var component in item.Components)
{
if (component is RangedWeapon rw)
if (component.CombatPriority > 0)
{
weaponList.Add(rw);
}
else if (component is MeleeWeapon mw)
{
weaponList.Add(mw);
}
else
{
var effects = component.statusEffectLists;
if (effects != null)
{
foreach (var statusEffects in effects.Values)
{
foreach (var statusEffect in statusEffects)
{
if (statusEffect.Afflictions.Any())
{
weaponList.Add(component);
}
}
}
}
weaponList.Add(component);
}
}
}
@@ -423,11 +570,11 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
RemoveSubObjective(ref followTargetObjective);
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunition);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
retreatObjective = null;
RemoveSubObjective(ref retreatObjective);
}
if (retreatTarget == null || (retreatObjective != null && !retreatObjective.CanBeCompleted))
{
@@ -437,7 +584,7 @@ namespace Barotrauma
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls);
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -454,7 +601,6 @@ namespace Barotrauma
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
@@ -476,10 +622,10 @@ namespace Barotrauma
RemoveSubObjective(ref seekAmmunition);
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
followTargetObjective = null;
RemoveFollowTarget();
}
TryAddSubObjective(ref followTargetObjective,
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true)
constructor: () => new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: true, getDivingGearIfNeeded: true, closeEnough: 50)
{
IgnoreIfTargetDead = true,
DialogueIdentifier = "dialogcannotreachtarget",
@@ -490,7 +636,30 @@ namespace Barotrauma
Abandon = true;
SteeringManager.Reset();
});
if (followTargetObjective != null)
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs))
{
if (!arrestingRegistered)
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
}
followTargetObjective.CloseEnough = 100;
}
else
{
RemoveFollowTarget();
SteeringManager.Reset();
}
}
else if (WeaponComponent == null)
{
RemoveFollowTarget();
SteeringManager.Reset();
}
else
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
@@ -499,6 +668,48 @@ namespace Barotrauma
}
}
private bool arrestingRegistered;
private void RemoveFollowTarget()
{
if (arrestingRegistered)
{
followTargetObjective.Completed -= OnArrestTargetReached;
}
RemoveSubObjective(ref followTargetObjective);
arrestingRegistered = false;
}
private void OnArrestTargetReached()
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
{
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
{
handCuffs.Equip(Enemy);
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
#endif
}
// Confiscate stolen goods.
foreach (var item in Enemy.Inventory.Items)
{
if (item == null || item == handCuffs) { continue; }
if (item.StolenDuringRound)
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
}
}
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
IsCompleted = true;
}
}
/// <summary>
/// Seeks for more ammunition. Creates a new subobjective.
/// </summary>
@@ -506,7 +717,7 @@ namespace Barotrauma
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
RemoveFollowTarget();
TryAddSubObjective(ref seekAmmunition,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
@@ -588,7 +799,7 @@ namespace Barotrauma
{
return true;
}
else if (ammunition == null && !HoldPosition && initialMode == CombatMode.Offensive && seekAmmo && ammunitionIdentifiers != null)
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
@@ -598,46 +809,64 @@ namespace Barotrauma
private void Attack(float deltaTime)
{
character.CursorPosition = Enemy.Position;
if (!character.CanSeeCharacter(Enemy)) { return; }
visibilityCheckTimer -= deltaTime;
if (visibilityCheckTimer <= 0.0f)
{
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
}
if (!canSeeTarget) { return; }
if (Weapon.RequireAimToUse)
{
bool isOperatingButtons = false;
if (SteeringManager == PathSteering)
{
var door = PathSteering.CurrentPath?.CurrentNode?.ConnectedDoor;
if (door != null && !door.IsOpen && !door.IsBroken)
{
isOperatingButtons = door.HasIntegratedButtons || door.Item.GetConnectedComponents<Controller>(true).Any();
}
}
if (!isOperatingButtons)
{
character.SetInput(InputType.Aim, false, true);
}
character.SetInput(InputType.Aim, false, true);
}
bool isFacing = character.AnimController.Dir > 0 && Enemy.WorldPosition.X > character.WorldPosition.X || character.AnimController.Dir < 0 && Enemy.WorldPosition.X < character.WorldPosition.X;
if (!isFacing)
hasAimed = true;
if (holdFireTimer > 0)
{
aimTimer = Rand.Range(1f, 1.5f);
holdFireTimer -= deltaTime;
return;
}
if (aimTimer > 0)
{
aimTimer -= deltaTime;
return;
}
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
float sqrDist = Vector2.DistanceSquared(character.Position, Enemy.Position);
if (!character.IsFacing(Enemy.WorldPosition))
{
aimTimer = Rand.Range(1f, 1.5f);
return;
}
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
if (Vector2.DistanceSquared(character.Position, Enemy.Position) <= meleeWeapon.Range * meleeWeapon.Range)
float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
if (character.AnimController.InWater)
{
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
if (sqrDist > sqrRange) { return; }
}
else
{
// It's possible that the center point of the creature is out of reach, but we could still hit the character.
float xDiff = Math.Abs(Enemy.WorldPosition.X - character.WorldPosition.X);
if (xDiff > meleeWeapon.Range) { return; }
float yDiff = Math.Abs(Enemy.WorldPosition.Y - character.WorldPosition.Y);
if (yDiff > Math.Max(meleeWeapon.Range, 100)) { return; }
if (Enemy.WorldPosition.Y < character.WorldPosition.Y && yDiff > 25)
{
// The target is probably knocked down? -> try to reach it by crouching.
HumanAIController.AnimController.Crouching = true;
}
}
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
}
else
{
if (WeaponComponent is RepairTool repairTool)
{
if (Vector2.DistanceSquared(character.Position, Enemy.Position) > repairTool.Range * repairTool.Range) { return; }
if (sqrDist > repairTool.Range * repairTool.Range) { return; }
}
if (VectorExtensions.Angle(VectorExtensions.Forward(Weapon.body.TransformedRotation), Enemy.Position - Weapon.Position) < MathHelper.PiOver4)
{
@@ -645,7 +874,6 @@ namespace Barotrauma
{
myBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody);
}
var collisionCategories = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel;
var pickedBody = Submarine.PickBody(Weapon.SimPosition, Enemy.SimPosition, myBodies, collisionCategories);
if (pickedBody != null)
@@ -679,6 +907,15 @@ namespace Barotrauma
}
}
protected override void OnCompleted()
{
base.OnCompleted();
if (Weapon != null)
{
Unequip();
}
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
@@ -16,6 +16,9 @@ namespace Barotrauma
public string[] ignoredContainerIdentifiers;
public bool checkInventory = true;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
//can either be a tag or an identifier
public readonly string[] itemIdentifiers;
public readonly ItemContainer container;
@@ -38,13 +41,14 @@ namespace Barotrauma
this.item = item;
}
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier) { }
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { itemIdentifier }, container, objectiveManager, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
this.itemIdentifiers = itemIdentifiers;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
@@ -147,7 +151,7 @@ namespace Barotrauma
{
// No matching items in the inventory, try to get an item
TryAddSubObjective(ref getItemObjective, () =>
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory)
new AIObjectiveGetItem(character, itemIdentifiers, objectiveManager, equip: Equip, checkInventory: checkInventory, spawnItemIfNotFound: spawnItemIfNotFound)
{
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -8,6 +9,7 @@ namespace Barotrauma
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
@@ -21,8 +23,25 @@ namespace Barotrauma
return 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
=> new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
protected override AIObjective ObjectiveConstructor(Character target)
{
var combatObjective = new AIObjectiveCombat(character, target, AIObjectiveCombat.CombatMode.Offensive, objectiveManager, PriorityModifier);
if (character.TeamID == Character.TeamType.FriendlyNPC && target.TeamID == Character.TeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
combatObjective.holdFireCondition = () =>
{
//hold fire while the enemy is in the airlock (except if they've attacked us)
if (HumanAIController.GetDamageDoneByAttacker(target) > 0.0f) { return false; }
return target.CurrentHull == null || target.CurrentHull.OutpostModuleTags.Any(t => t.Equals("airlock", System.StringComparison.OrdinalIgnoreCase));
};
character.Speak(TextManager.Get("dialogenteroutpostwarning"), null, Rand.Range(0.5f, 1.0f), "leaveoutpostwarning", 30.0f);
}
}
return combatObjective;
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveFightIntruders, Character>(character, target);
@@ -18,7 +18,7 @@ namespace Barotrauma
public static float lowOxygenThreshold = 10;
protected override bool Check() => HumanAIController.HasItem(character, gearTag, "oxygensource") || HumanAIController.HasItem(character, fallbackTag, "oxygensource");
protected override bool Check() => HumanAIController.HasItem(character, gearTag, out _, "oxygensource", requireEquipped: true) || HumanAIController.HasItem(character, fallbackTag, out _, "oxygensource", requireEquipped: true);
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -140,7 +141,7 @@ namespace Barotrauma
{
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull();
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
@@ -233,12 +234,30 @@ namespace Barotrauma
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
{
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float EstimateHullSuitability(Hull hull)
{
float dist =
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
Math.Abs(hull.WorldPosition.Y - character.WorldPosition.Y) * 3;
float suitability = -dist;
if (hull.Submarine != character.Submarine)
{
suitability -= 10000.0f;
}
return suitability;
}
Hull bestHull = null;
float bestValue = 0;
foreach (Hull hull in Hull.hullList)
foreach (Hull hull in Hull.hullList.OrderByDescending(h => EstimateHullSuitability(h)))
{
if (hull.Submarine == null) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
@@ -255,6 +274,11 @@ namespace Barotrauma
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
//avoid airlock modules if not allowed to change the sub
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t.Equals("airlock", StringComparison.OrdinalIgnoreCase)))
{
continue;
}
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
@@ -61,7 +61,7 @@ namespace Barotrauma
var weldingTool = character.Inventory.FindItemByTag("weldingequipment", true);
if (weldingTool == null)
{
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, true),
TryAddSubObjective(ref getWeldingTool, () => new AIObjectiveGetItem(character, "weldingequipment", objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getWeldingTool));
return;
@@ -88,7 +88,7 @@ namespace Barotrauma
}
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager),
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref refuelObjective));
return;
@@ -130,7 +130,8 @@ namespace Barotrauma
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
// Disabled for now
//AllowGoingOutside = !Leak.IsRoomToRoom && objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() && HumanAIController.HasDivingSuit(character, conditionPercentage: 50),
CloseEnough = reach,
DialogueIdentifier = Leak.FlowTargetHull != null ? "dialogcannotreachleak" : null,
TargetName = Leak.FlowTargetHull?.DisplayName
@@ -22,7 +22,11 @@ namespace Barotrauma
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
private Item targetItem;
private Item originalTarget;
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
@@ -41,19 +45,21 @@ namespace Barotrauma
{
currSearchIndex = -1;
this.equip = equip;
originalTarget = targetItem;
this.targetItem = targetItem;
moveToTarget = targetItem?.GetRootInventoryOwner();
}
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier) { }
public AIObjectiveGetItem(Character character, string itemIdentifier, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { itemIdentifier }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1)
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
this.equip = equip;
this.itemIdentifiers = itemIdentifiers;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
@@ -109,6 +115,14 @@ namespace Barotrauma
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Target null or removed. Aborting.", Color.Red);
#endif
Abandon = true;
return;
}
else if (isDoneSeeking && moveToTarget == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Move target null. Aborting.", Color.Red);
#endif
Abandon = true;
return;
@@ -118,8 +132,15 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Found an item, but it's already equipped by someone else.", Color.Yellow);
#endif
// Try again
Reset();
if (originalTarget == null)
{
// Try again
Reset();
}
else
{
Abandon = true;
}
return;
}
bool canInteract = false;
@@ -155,30 +176,7 @@ namespace Barotrauma
if (equip)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
if (slots.HasFlag(InvSlotType.Any)) { continue; }
for (int i = 0; i < character.Inventory.Items.Length; i++)
{
//slot not needed by the item, continue
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) { continue; }
targetSlot = i;
//slot free, continue
var otherItem = character.Inventory.Items[i];
if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) &&
character.Inventory.TryPutItem(otherItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
continue;
}
//if everything else fails, simply drop the existing item
otherItem.Drop(character);
}
}
if (character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character))
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
{
targetItem.Equip(character);
IsCompleted = true;
@@ -193,7 +191,7 @@ namespace Barotrauma
}
else
{
if (character.Inventory.TryPutItem(targetItem, null, new List<InvSlotType>() { InvSlotType.Any }))
if (character.Inventory.TryPutItem(targetItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
}
@@ -283,10 +281,34 @@ namespace Barotrauma
isDoneSeeking = true;
if (targetItem == null)
{
if (spawnItemIfNotFound)
{
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && itemIdentifiers.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
Abandon = true;
}
else
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item spawnedItem) =>
{
targetItem = spawnedItem;
if (character.TeamID == Character.TeamType.FriendlyNPC && (character.Submarine?.Info.IsOutpost ?? false))
{
spawnedItem.SpawnedInOutpost = true;
}
});
}
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", itemIdentifiers)}", Color.Yellow);
#endif
Abandon = true;
}
}
}
}
@@ -323,8 +345,8 @@ namespace Barotrauma
{
base.Reset();
RemoveSubObjective(ref goToObjective);
targetItem = null;
moveToTarget = null;
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
}
@@ -25,10 +25,13 @@ namespace Barotrauma
public Func<bool> abortCondition;
public Func<PathNode, bool> endNodeFilter;
public Func<float> priorityGetter;
public bool followControlledCharacter;
public bool mimic;
private float _closeEnough = 50;
private readonly float minDistance = 25;
/// <summary>
/// Display units
/// </summary>
@@ -37,7 +40,7 @@ namespace Barotrauma
get { return _closeEnough; }
set
{
_closeEnough = Math.Max(_closeEnough, value);
_closeEnough = Math.Max(minDistance, value);
}
}
public bool IgnoreIfTargetDead { get; set; }
@@ -52,6 +55,8 @@ namespace Barotrauma
public ISpatialEntity Target { get; private set; }
public float? OverridePriority = null;
public override float GetPriority()
{
if (followControlledCharacter && Character.Controlled == null)
@@ -68,7 +73,18 @@ namespace Barotrauma
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
if (priorityGetter != null)
{
Priority = priorityGetter();
}
else if (OverridePriority.HasValue)
{
Priority = OverridePriority.Value;
}
else
{
Priority = objectiveManager.CurrentOrder == this ? AIObjectiveManager.OrderPriority : 10;
}
}
return Priority;
}
@@ -86,7 +102,8 @@ namespace Barotrauma
}
else if (Target is Character)
{
CloseEnough = Math.Max(closeEnough, AIObjectiveGetItem.DefaultReach);
//if closeEnough value is given, allow setting CloseEnough as low as 50, otherwise above AIObjectiveGetItem.DefaultReach
CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : 50);
}
else
{
@@ -289,7 +306,7 @@ namespace Barotrauma
return null;
}
private bool IsCloseEnough
public bool IsCloseEnough
{
get
{
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -12,12 +13,48 @@ namespace Barotrauma
public override bool UnequipItems => true;
public override bool AllowOutsideSubmarine => true;
private readonly float newTargetIntervalMin = 10;
private readonly float newTargetIntervalMax = 20;
private readonly float standStillMin = 2;
private readonly float standStillMax = 10;
private readonly float walkDurationMin = 5;
private readonly float walkDurationMax = 10;
private BehaviorType behavior;
public BehaviorType Behavior
{
get { return behavior; }
set
{
behavior = value;
switch (behavior)
{
case BehaviorType.Active:
newTargetIntervalMin = 10;
newTargetIntervalMax = 20;
standStillMin = 2;
standStillMax = 10;
walkDurationMin = 5;
walkDurationMax = 10;
break;
case BehaviorType.Passive:
newTargetIntervalMin = 60;
newTargetIntervalMax = 120;
standStillMin = 30;
standStillMax = 60;
walkDurationMin = 5;
walkDurationMax = 10;
break;
}
}
}
private float newTargetIntervalMin;
private float newTargetIntervalMax;
private float standStillMin;
private float standStillMax;
private float walkDurationMin;
private float walkDurationMax;
public enum BehaviorType
{
Active,
Passive,
StayInHull
}
private Hull currentTarget;
private float newTargetTimer;
@@ -27,13 +64,20 @@ namespace Barotrauma
private float standStillTimer;
private float walkDuration;
private Character tooCloseCharacter;
const float chairCheckInterval = 5.0f;
private float chairCheckTimer;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
public AIObjectiveIdle(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
Behavior = BehaviorType.Passive;
standStillTimer = Rand.Range(-10.0f, 10.0f);
walkDuration = Rand.Range(0.0f, 10.0f);
chairCheckTimer = Rand.Range(0.0f, chairCheckInterval);
CalculatePriority();
}
@@ -42,9 +86,12 @@ namespace Barotrauma
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
private float randomTimer;
private float randomUpdateInterval = 5;
public float Random { get; private set; }
public readonly HashSet<string> PreferredOutpostModuleTypes = new HashSet<string>();
private bool IsInWrongSub() =>
character.Submarine == null ||
currentTarget != null && currentTarget.Submarine != character.Submarine ||
character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
public void CalculatePriority(float max = 0)
{
@@ -73,6 +120,31 @@ namespace Barotrauma
//}
}
private float timerMargin;
private void SetTargetTimerLow()
{
// Increases the margin each time the method is called -> takes longer between the path finding calls.
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
{
// This method is used to the timer between the current value and the min so that it never reaches 0.
// Prevents pathfinder calls.
newTargetTimer = Math.Max(newTargetTimer, newTargetIntervalMin);
timerMargin = 0;
}
private void SetTargetTimerNormal()
{
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
timerMargin = 0;
}
protected override void Act(float deltaTime)
{
if (PathSteering == null) { return; }
@@ -82,97 +154,95 @@ namespace Barotrauma
{
character.DeselectCharacter();
}
if (!character.IsClimbing)
{
character.SelectedConstruction = null;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior != BehaviorType.StayInHull)
{
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (currentTargetIsInvalid || currentTarget == null && HumanAIController.VisibleHulls.Any(h => IsForbidden(h)))
{
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
//standStillTimer = 0.0f;
}
else if (character.IsClimbing)
{
if (currentTarget == null)
bool IsSteeringFinished() => PathSteering.CurrentPath != null && PathSteering.CurrentPath.Finished;
if (currentTargetIsInvalid || currentTarget == null || IsSteeringFinished() && (IsForbidden(character.CurrentHull) || IsInWrongSub()))
{
newTargetTimer = 0;
//don't reset to zero, otherwise the character will keep calling FindTargetHulls
//almost constantly when there's a small number of potential hulls to move to
SetTargetTimerLow();
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
else if (character.IsClimbing)
{
// Don't allow new targets when climbing straight up or down
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
newTargetTimer = Math.Min(newTargetTimer, 0.5f);
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
else if (targetHulls.Count > 0)
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
if (currentTarget == null)
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
SetTargetTimerLow();
}
else if (Math.Abs(character.AnimController.TargetMovement.Y) > 0.9f)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
// Don't allow new targets when climbing straight up or down
SetTargetTimerHigh();
}
}
else if (character.AnimController.InWater)
{
if (currentTarget == null)
{
SetTargetTimerLow();
}
}
if (newTargetTimer <= 0.0f)
{
if (!searchingNewHull)
{
//find all available hulls first
FindTargetHulls();
searchingNewHull = true;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
newTargetTimer = Math.Max(newTargetIntervalMin, newTargetTimer);
searchingNewHull = false;
}
else if (targetHulls.Count > 0)
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsInWrongSub() && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe or forbidden hulls on the way to the target
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
if (isCurrentHullAllowed && IsForbidden(node.Waypoint.CurrentHull)) { return false; }
return true;
});
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room next frame
int index = targetHulls.IndexOf(currentTarget);
targetHulls.RemoveAt(index);
hullWeights.RemoveAt(index);
PathSteering.Reset();
currentTarget = null;
return;
}
searchingNewHull = false;
}
else
{
// Couldn't find a target for some reason -> reset
SetTargetTimerHigh();
searchingNewHull = false;
}
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
newTargetTimer = currentTarget != null && character.AnimController.InWater ? newTargetIntervalMin : Rand.Range(newTargetIntervalMin, newTargetIntervalMax);
if (currentTarget != null)
{
character.AIController.SelectTarget(currentTarget.AiTarget);
string errorMsg = null;
#if DEBUG
bool isRoomNameFound = currentTarget.DisplayName != null;
errorMsg = "(Character " + character.Name + " idling, target " + (isRoomNameFound ? currentTarget.DisplayName : currentTarget.ToString()) + ")";
#endif
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: errorMsg, nodeFilter: node => node.Waypoint.CurrentHull != null);
PathSteering.SetPath(path);
}
SetTargetTimerNormal();
}
newTargetTimer -= deltaTime;
}
newTargetTimer -= deltaTime;
//wander randomly
// - if reached the end of the path
@@ -180,12 +250,13 @@ namespace Barotrauma
// - if the path requires going outside
if (!character.IsClimbing)
{
if (SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
if (behavior == BehaviorType.StayInHull || SteeringManager != PathSteering || (PathSteering.CurrentPath != null &&
(PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable || PathSteering.CurrentPath.HasOutdoorsNodes)))
{
Wander(deltaTime);
return;
}
character.SelectedConstruction = null;
}
if (currentTarget != null)
@@ -214,7 +285,58 @@ namespace Barotrauma
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
PathSteering.Reset();
if (character.CurrentHull != null && character.CurrentHull.Rect.Width > 150 && tooCloseCharacter == null)
{
foreach (Character c in Character.CharacterList)
{
if (c == character || !c.IsBot || c.CurrentHull != character.CurrentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
if (Vector2.DistanceSquared(c.WorldPosition, character.WorldPosition) > 60.0f * 60.0f) { continue; }
if ((humanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective && idleObjective.standStillTimer > 0.0f) ||
(humanAI.ObjectiveManager.CurrentObjective is AIObjectiveGoTo gotoObjective && gotoObjective.IsCloseEnough))
{
//if there are characters too close on both sides, don't try to steer away from them
//because it'll cause the character to spaz out trying to avoid both
if (tooCloseCharacter != null &&
Math.Sign(tooCloseCharacter.WorldPosition.X - character.WorldPosition.X) != Math.Sign(c.WorldPosition.X - character.WorldPosition.X))
{
tooCloseCharacter = null;
break;
}
tooCloseCharacter = c;
}
HumanAIController.FaceTarget(c);
}
}
if (tooCloseCharacter != null && !tooCloseCharacter.Removed && Vector2.DistanceSquared(tooCloseCharacter.WorldPosition, character.WorldPosition) < 50.0f * 50.0f)
{
Vector2 diff = character.WorldPosition - tooCloseCharacter.WorldPosition;
if (diff.LengthSquared() < 0.0001f) { diff = Rand.Vector(1.0f); }
if (diff.X > 0 && character.WorldPosition.X > character.CurrentHull.WorldRect.Right - 50) { diff.X = -diff.X; }
if (diff.X < 0 && character.WorldPosition.X < character.CurrentHull.WorldRect.X + 50) { diff.X = -diff.X; }
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
return;
}
else
{
PathSteering.Reset();
tooCloseCharacter = null;
}
chairCheckTimer -= deltaTime;
if (chairCheckTimer <= 0.0f && character.SelectedConstruction == null)
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != character.CurrentHull || !item.HasTag("chair")) { continue; }
var controller = item.GetComponent<Controller>();
if (controller == null || controller.User != null) { continue; }
item.TryInteract(character, forceSelectKey: true);
}
chairCheckTimer = chairCheckInterval;
}
return;
}
if (standStillTimer < -walkDuration)
@@ -222,6 +344,7 @@ namespace Barotrauma
standStillTimer = Rand.Range(standStillMin, standStillMax);
}
}
PathSteering.Wander(deltaTime);
}
@@ -234,12 +357,27 @@ namespace Barotrauma
if (HumanAIController.UnsafeHulls.Contains(hull)) { continue; }
if (hull.Submarine == null) { continue; }
if (character.Submarine == null) { break; }
if (hull.Submarine.TeamID != character.Submarine.TeamID) { continue; }
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { continue; }
// If the character is inside, only take connected subs into account.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { continue; }
if (character.TeamID == Character.TeamType.FriendlyNPC)
{
if (hull.Submarine.TeamID != character.TeamID)
{
// Don't allow npcs to idle in a sub that's not in their team (like the player sub)
continue;
}
}
else
{
if (hull.Submarine.TeamID != character.Submarine.TeamID)
{
// Don't allow to idle in the subs that are not in the same team as the current sub
// -> the crew ai bots can't change the sub from outpost to main sub or vice versa on their own
continue;
}
}
if (IsForbidden(hull)) { continue; }
// Ignore hulls that are too low to stand inside
// Check that the hull is linked
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
// Ignore hulls that are too low to stand inside.
if (character.AnimController is HumanoidAnimController animController)
{
if (hull.CeilingHeight < ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value))
@@ -260,6 +398,17 @@ namespace Barotrauma
hullWeights.Add(weight);
}
}
if (PreferredOutpostModuleTypes.Any() && character.CurrentHull != null)
{
for (int i = 0; i < targetHulls.Count; i++)
{
if (targetHulls[i].OutpostModuleTags.Any(t => PreferredOutpostModuleTypes.Contains(t)))
{
hullWeights[i] *= Rand.Range(10.0f, 100.0f);
}
}
}
}
public static bool IsForbidden(Hull hull)
@@ -23,6 +23,8 @@ namespace Barotrauma
private readonly Character character;
public HumanAIController HumanAIController => character.AIController as HumanAIController;
private float _waitTimer;
/// <summary>
@@ -123,7 +125,7 @@ namespace Barotrauma
{
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, false)?.GetRandom() : null;
var item = orderPrefab.MustSetTarget ? orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID)?.GetRandom() : null;
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
@@ -298,7 +300,7 @@ namespace Barotrauma
if (orderGiver == null) { return null; }
newObjective = new AIObjectiveGoTo(orderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
CloseEnough = 100,
CloseEnough = Rand.Range(90, 100) + Rand.Range(50, 70) * Math.Min(HumanAIController.CountCrew(c => c.ObjectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.Target == orderGiver, onlyBots: true), 4),
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
followControlledCharacter = orderGiver == character,
@@ -81,11 +81,11 @@ namespace Barotrauma
break;
}
}
if (targetItem.CurrentHull == null || targetItem.CurrentHull.FireSources.Any() || HumanAIController.IsItemOperatedByAnother(target, out _))
{
Priority = 0;
}
else if (Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
if (targetItem.CurrentHull == null ||
targetItem.Submarine != character.Submarine && objectiveManager.CurrentOrder != this ||
targetItem.CurrentHull.FireSources.Any() ||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
{
Priority = 0;
}
@@ -111,10 +111,10 @@ namespace Barotrauma
var target = GetTarget();
if (target == null)
{
Abandon = true;
#if DEBUG
throw new Exception("target null");
#endif
Abandon = true;
}
else if (target.Item.NonInteractable)
{
@@ -51,7 +51,7 @@ namespace Barotrauma
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
}
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character);
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor: objectiveManager.CurrentOrder != this ? AIObjectiveRepairItems.RequiredSuccessFactor : 0);
float isSelected = IsRepairing ? 50 : 0;
float devotion = (CumulatedDevotion + isSelected) / 100;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - 1, 90);
@@ -148,7 +148,8 @@ namespace Barotrauma
{
if (character.SelectedConstruction != Item)
{
if (!Item.TryInteract(character, true, true))
if (!Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) &&
!Item.TryInteract(character, ignoreRequiredItems: true, forceActionKey: true))
{
Abandon = true;
}
@@ -25,6 +25,8 @@ namespace Barotrauma
public override bool AllowMultipleInstances => true;
public readonly static float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) =>
(otherObjective as AIObjective) is AIObjectiveRepairItems repairObjective && repairObjective.RequireAdequateSkills == RequireAdequateSkills;
@@ -110,7 +112,7 @@ namespace Barotrauma
}
if (RequireAdequateSkills)
{
return Targets.Sum(t => GetTargetPriority(t, character)) * ratio;
return Targets.Sum(t => GetTargetPriority(t, character, RequiredSuccessFactor)) * ratio;
}
else
{
@@ -119,10 +121,14 @@ namespace Barotrauma
}
}
public static float GetTargetPriority(Item item, Character character)
public static float GetTargetPriority(Item item, Character character, float requiredSuccessFactor = 0)
{
float damagePriority = MathHelper.Lerp(1, 0, item.Condition / item.MaxCondition);
float successFactor = MathHelper.Lerp(0, 1, item.Repairables.Average(r => r.DegreeOfSuccess(character)));
if (successFactor < requiredSuccessFactor)
{
return 0;
}
return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1));
}
@@ -12,6 +12,8 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
const float TreatmentDelay = 0.5f;
const float CloseEnoughToTreat = 100.0f;
@@ -216,50 +218,53 @@ namespace Barotrauma
}
}
}
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
// Find treatments outside of own inventory only if inside the own sub.
if (character.Submarine != null && character.Submarine.TeamID == character.TeamID)
{
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
float cprSuitability = targetCharacter.Oxygen < 0.0f ? -targetCharacter.Oxygen * 100.0f : 0.0f;
//didn't have any suitable treatments available, try to find some medical items
if (currentTreatmentSuitabilities.Any(s => s.Value > cprSuitability))
{
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
itemNameList.Clear();
suitableItemIdentifiers.Clear();
foreach (KeyValuePair<string, float> treatmentSuitability in currentTreatmentSuitabilities)
{
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
if (treatmentSuitability.Value <= cprSuitability) { continue; }
if (MapEntityPrefab.Find(null, treatmentSuitability.Key, showErrorMessages: false) is ItemPrefab itemPrefab)
{
itemNameList.Add(itemPrefab.Name);
if (!Item.ItemList.Any(it => it.prefab.Identifier == treatmentSuitability.Key)) { continue; }
suitableItemIdentifiers.Add(treatmentSuitability.Key);
//only list the first 4 items
if (itemNameList.Count < 4)
{
itemNameList.Add(itemPrefab.Name);
}
}
}
}
if (itemNameList.Count > 0)
{
string itemListStr = "";
if (itemNameList.Count == 1)
if (itemNameList.Count > 0)
{
itemListStr = itemNameList[0];
string itemListStr = "";
if (itemNameList.Count == 1)
{
itemListStr = itemNameList[0];
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
else
{
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
}
if (targetCharacter != character)
{
character.Speak(TextManager.GetWithVariables("DialogListRequiredTreatments", new string[2] { "[targetname]", "[treatmentlist]" },
new string[2] { targetCharacter.Name, itemListStr }, new bool[2] { false, true }),
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
}
character.DeselectCharacter();
RemoveSubObjective(ref getItemObjective);
TryAddSubObjective(ref getItemObjective,
constructor: () => new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), objectiveManager, equip: true),
onCompleted: () => RemoveSubObjective(ref getItemObjective),
onAbandon: () => RemoveSubObjective(ref getItemObjective));
}
}
if (character != targetCharacter)
@@ -9,9 +9,10 @@ namespace Barotrauma
public override string DebugTag => "rescue all";
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
private const float vitalityThreshold = 80;
private const float vitalityThresholdForOrders = 100;
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 85;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
{
if (manager == null)
@@ -71,7 +72,8 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (!HumanAIController.IsFriendly(character, target)) { return false; }
if (target.TurnedHostileByEvent) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target)) { return false; }
@@ -94,13 +96,8 @@ namespace Barotrauma
if (GetVitalityFactor(target) >= vitalityThreshold) { return false; }
}
if (target.Submarine == null || character.Submarine == null) { return false; }
if (target.Submarine.TeamID != character.Submarine.TeamID) { return false; }
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (character.Submarine != null && !character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
}
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
if (target != character &&!target.IsPlayer && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
{
// Ignore all concious targets that are currently fighting, fleeing or treating characters
@@ -17,6 +17,27 @@ namespace Barotrauma
Operate
}
struct OrderInfo
{
public string ComponentIdentifier { get; set; }
public Order Order { get; private set; }
public string OrderOption { get; private set; }
public OrderInfo(Order order, string orderOption)
{
ComponentIdentifier = "currentorder";
Order = order;
OrderOption = orderOption;
}
public OrderInfo(OrderInfo orderInfo)
{
ComponentIdentifier = "previousorder";
Order = orderInfo.Order;
OrderOption = orderInfo.OrderOption;
}
}
class Order
{
public static Dictionary<string, Order> Prefabs { get; private set; }
@@ -335,7 +356,7 @@ namespace Barotrauma
return msg;
}
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub)
public List<Item> GetMatchingItems(Submarine submarine, bool mustBelongToPlayerSub, Character.TeamType? requiredTeam = null)
{
List<Item> matchingItems = new List<Item>();
if (submarine == null) { return matchingItems; }
@@ -346,12 +367,12 @@ namespace Barotrauma
Item.ItemList.FindAll(it => it.Components.Any(ic => ic.GetType() == ItemComponentType));
if (mustBelongToPlayerSub)
{
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineInfo.SubmarineType.Player);
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
matchingItems.RemoveAll(it => it.Submarine?.Info != null && it.Submarine.Info.Type != SubmarineType.Player);
}
else
matchingItems.RemoveAll(it => it.Submarine != submarine && !submarine.DockedTo.Contains(it.Submarine));
if (requiredTeam.HasValue)
{
matchingItems.RemoveAll(it => it.Submarine != submarine);
matchingItems.RemoveAll(it => it.Submarine == null || it.Submarine.TeamID != requiredTeam.Value);
}
matchingItems.RemoveAll(it => it.NonInteractable);
if (UseController)
@@ -7,34 +7,37 @@ namespace Barotrauma
{
class PathNode
{
private WayPoint wayPoint;
private int wayPointID;
private readonly int wayPointID;
public int state;
public PathNode Parent;
private Vector2 position;
public float F,G,H;
public float F, G, H;
public List<PathNode> connections;
public List<float> distances;
public WayPoint Waypoint
{
get { return wayPoint; }
}
public Vector2 TempPosition;
public float TempDistance;
public WayPoint Waypoint { get; private set; }
public Vector2 Position
{
get { return position; }
}
public override string ToString()
{
return $"PathNode {wayPointID}";
}
public PathNode(WayPoint wayPoint)
{
this.wayPoint = wayPoint;
this.Waypoint = wayPoint;
this.position = wayPoint.SimPosition;
wayPointID = wayPoint.ID;
@@ -57,15 +60,14 @@ namespace Barotrauma
nodes.Add(wayPoint.ID, new PathNode(wayPoint));
}
foreach (KeyValuePair<int,PathNode> node in nodes)
foreach (KeyValuePair<int, PathNode> node in nodes)
{
foreach (MapEntity linked in node.Value.wayPoint.linkedTo)
foreach (MapEntity linked in node.Value.Waypoint.linkedTo)
{
PathNode connectedNode = null;
nodes.TryGetValue(linked.ID, out connectedNode);
nodes.TryGetValue(linked.ID, out PathNode connectedNode);
if (connectedNode == null) { continue; }
node.Value.connections.Add(connectedNode);
if (!node.Value.connections.Contains(connectedNode)) { node.Value.connections.Add(connectedNode); }
if (!connectedNode.connections.Contains(node.Value)) { connectedNode.connections.Add(node.Value); }
}
}
@@ -74,10 +76,10 @@ namespace Barotrauma
foreach (PathNode node in nodeList)
{
node.distances = new List<float>();
for (int i = 0; i< node.connections.Count; i++)
for (int i = 0; i < node.connections.Count; i++)
{
node.distances.Add(Vector2.Distance(node.position, node.connections[i].position));
}
}
}
return nodeList;
@@ -89,7 +91,7 @@ namespace Barotrauma
public delegate float? GetNodePenaltyHandler(PathNode node, PathNode prevNode);
public GetNodePenaltyHandler GetNodePenalty;
private List<PathNode> nodes;
private readonly List<PathNode> nodes;
public bool InsideSubmarine { get; set; }
@@ -135,8 +137,7 @@ namespace Barotrauma
{
for (int i = 0; i < wp.linkedTo.Count; i++)
{
WayPoint connected = wp.linkedTo[i] as WayPoint;
if (connected == null) { continue; }
if (!(wp.linkedTo[i] is WayPoint connected)) { continue; }
//already connected, continue
if (node.connections.Any(n => n.Waypoint == connected)) { continue; }
@@ -157,47 +158,53 @@ namespace Barotrauma
}
}
private static readonly List<PathNode> sortedNodes = new List<PathNode>();
public SteeringPath FindPath(Vector2 start, Vector2 end, Submarine hostSub = null, string errorMsgStr = null, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
{
float closestDist = 0.0f;
PathNode startNode = null;
{
//sort nodes roughly according to distance
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
node.TempPosition = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
node.TempPosition -= diff;
}
float xDiff = Math.Abs(start.X - node.TempPosition.X);
float yDiff = Math.Abs(start.Y - node.TempPosition.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null) { yDiff += 10.0f; }
node.TempDistance = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
float xDiff = Math.Abs(start.X - nodePos.X);
float yDiff = Math.Abs(start.Y - nodePos.Y);
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
{
yDiff += 10.0f;
}
float dist = xDiff + (InsideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine) { node.TempDistance *= 10.0f; }
//prefer nodes that are closer to the end position
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null && InsideSubmarine)
node.TempDistance += (Math.Abs(end.X - node.TempPosition.X) + Math.Abs(end.Y - node.TempPosition.Y)) / 100.0f;
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
dist *= 10.0f;
i++;
}
if (dist < closestDist || startNode == null)
sortedNodes.Insert(i, node);
}
//find the most suitable start node, starting from the ones that are the closest
PathNode startNode = null;
foreach (PathNode node in sortedNodes)
{
if (startNode == null || node.TempDistance < startNode.TempDistance)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (startNodeFilter != null && !startNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(
start, nodePos, null,
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
//if (body.UserData is Submarine) continue;
@@ -205,8 +212,6 @@ namespace Barotrauma
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
closestDist = dist;
startNode = node;
}
}
@@ -216,36 +221,45 @@ namespace Barotrauma
#if DEBUG
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
#endif
return new SteeringPath(true);
}
closestDist = 0.0f;
PathNode endNode = null;
//sort nodes again, now based on distance from the end position
sortedNodes.Clear();
foreach (PathNode node in nodes)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
Vector2 nodePos = node.Position;
if (hostSub != null)
{
Vector2 diff = hostSub.SimPosition - node.Waypoint.Submarine.SimPosition;
nodePos -= diff;
}
float dist = Vector2.DistanceSquared(end, nodePos);
node.TempDistance = Vector2.DistanceSquared(end, node.TempPosition);
if (InsideSubmarine)
{
//much higher cost to waypoints that are outside
if (node.Waypoint.CurrentHull == null) { dist *= 10.0f; }
if (node.Waypoint.CurrentHull == null) { node.TempDistance *= 10.0f; }
//avoid stopping at a doorway
if (node.Waypoint.ConnectedDoor != null) { dist *= 10.0f; }
if (node.Waypoint.ConnectedDoor != null) { node.TempDistance *= 10.0f; }
//avoid stopping at a ladder
if (node.Waypoint.Ladders != null) { node.TempDistance *= 10.0f; }
}
if (dist < closestDist || endNode == null)
int i = 0;
while (i < sortedNodes.Count && sortedNodes[i].TempDistance < node.TempDistance)
{
i++;
}
sortedNodes.Insert(i, node);
}
//find the most suitable end node, starting from the ones closest to the end position
PathNode endNode = null;
foreach (PathNode node in sortedNodes)
{
if (endNode == null || node.TempDistance < endNode.TempDistance)
{
if (nodeFilter != null && !nodeFilter(node)) { continue; }
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
var body = Submarine.PickBody(end, nodePos, null,
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null)
@@ -255,8 +269,6 @@ namespace Barotrauma
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
}
closestDist = dist;
endNode = node;
}
}
@@ -269,25 +281,25 @@ namespace Barotrauma
return new SteeringPath(true);
}
var path = FindPath(startNode, endNode, nodeFilter);
var path = FindPath(startNode, endNode, nodeFilter, errorMsgStr);
return path;
}
public SteeringPath FindPath(WayPoint start, WayPoint end)
{
PathNode startNode=null, endNode=null;
PathNode startNode = null, endNode = null;
foreach (PathNode node in nodes)
{
if (node.Waypoint == start)
{
startNode = node;
if (endNode != null) break;
if (endNode != null) { break; }
}
if (node.Waypoint == end)
{
endNode = node;
if (startNode != null) break;
if (startNode != null) { break; }
}
}
@@ -302,13 +314,12 @@ namespace Barotrauma
return FindPath(startNode, endNode);
}
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null)
private SteeringPath FindPath(PathNode start, PathNode end, Func<PathNode, bool> filter = null, string errorMsgStr = "")
{
if (start == end)
{
var path1 = new SteeringPath();
path1.AddNode(start.Waypoint);
return path1;
}
@@ -328,8 +339,8 @@ namespace Barotrauma
float dist = float.MaxValue;
foreach (PathNode node in nodes)
{
if (filter != null && !filter(node)) { continue; }
if (node.state != 1) { continue; }
if (filter != null && !filter(node)) { continue; }
if (node.F < dist)
{
dist = node.F;
@@ -395,7 +406,7 @@ namespace Barotrauma
if (end.state == 0 || end.Parent == null)
{
#if DEBUG
DebugConsole.NewMessage("Path not found", Color.Yellow);
DebugConsole.NewMessage("Path not found. " + errorMsgStr, Color.Yellow);
#endif
return new SteeringPath(true);
}
@@ -425,15 +436,12 @@ namespace Barotrauma
}
finalPath.Add(start.Waypoint);
finalPath.Reverse();
foreach (WayPoint wayPoint in finalPath)
for (int i = finalPath.Count - 1; i >= 0; i--)
{
path.AddNode(wayPoint);
path.AddNode(finalPath[i]);
}
System.Diagnostics.Debug.Assert(finalPath.Count == path.Nodes.Count);
return path;
}
}
@@ -194,7 +194,7 @@ namespace Barotrauma
float minDist = Sonar.DefaultSonarRange * 2.0f;
foreach (Submarine submarine in Submarine.Loaded)
{
if (submarine.Info.Type != SubmarineInfo.SubmarineType.Player) { continue; }
if (submarine.Info.Type != SubmarineType.Player) { continue; }
if (Vector2.DistanceSquared(submarine.WorldPosition, Wreck.WorldPosition) < minDist * minDist)
{
someoneNearby = true;
@@ -82,7 +82,7 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
if (!IsRemotePlayer)
if (!IsRemotelyControlled)
{
aiController.Update(deltaTime);
}
@@ -221,7 +221,7 @@ namespace Barotrauma
//don't flip when simply physics is enabled
if (SimplePhysicsEnabled) { return; }
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
if (!character.IsRemotelyControlled && (character.AIController == null || character.AIController.CanFlip))
{
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
{
@@ -323,6 +323,7 @@ namespace Barotrauma
levitatingCollider = true;
ColliderIndex = Crouching ? 1 : 0;
if (character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false)
{
Crouching = false;
@@ -567,7 +568,7 @@ namespace Barotrauma
//TODO: take into account that the feet aren't necessarily in CurrentHull
//full slowdown (1.5f) when water is up to the torso
surfaceY = ConvertUnits.ToSimUnits(currentHull.Surface);
float bottomPos = Math.Max(colliderPos.Y, currentHull.Rect.Y - currentHull.Rect.Height);
float bottomPos = Math.Max(colliderPos.Y, ConvertUnits.ToSimUnits(currentHull.Rect.Y - currentHull.Rect.Height));
slowdownAmount = MathHelper.Clamp((surfaceY - bottomPos) / TorsoPosition.Value, 0.0f, 1.0f) * 1.5f;
}
@@ -609,7 +610,7 @@ namespace Barotrauma
if (torso == null) { return; }
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (onGround && isNotRemote)
{
@@ -659,30 +660,36 @@ namespace Barotrauma
float y = colliderPos.Y + stepLift;
if (TorsoPosition.HasValue)
if (!torso.Disabled)
{
y += TorsoPosition.Value;
if (TorsoPosition.HasValue)
{
y += TorsoPosition.Value;
}
torso.PullJointWorldAnchorB =
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
}
torso.PullJointWorldAnchorB =
MathUtils.SmoothStep(torso.SimPosition,
new Vector2(footMid + movement.X * TorsoLeanAmount, y), getUpForce);
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
if (!head.Disabled)
{
y += HeadPosition.Value;
y = colliderPos.Y + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier;
if (HeadPosition.HasValue)
{
y += HeadPosition.Value;
}
head.PullJointWorldAnchorB =
MathUtils.SmoothStep(head.SimPosition,
new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f);
}
head.PullJointWorldAnchorB =
MathUtils.SmoothStep(head.SimPosition,
new Vector2(footMid + movement.X * HeadLeanAmount, y), getUpForce * 1.2f);
if (waist != null)
if (waist != null && !waist.Disabled)
{
waist.PullJointWorldAnchorB = waist.SimPosition + movement * 0.06f;
}
}
if (TorsoAngle.HasValue)
if (TorsoAngle.HasValue && !torso.Disabled)
{
float torsoAngle = TorsoAngle.Value;
float herpesStrength = character.CharacterHealth.GetAfflictionStrength("spaceherpes");
@@ -787,8 +794,14 @@ namespace Barotrauma
if (Crouching)
{
footPos = new Vector2(
waistPos.X + Math.Sign(stepSize.X * i) * Dir * 0.1f,
colliderPos.Y - 0.1f);
Math.Sign(stepSize.X * i) * Dir * 0.4f,
colliderPos.Y);
if (Math.Sign(footPos.X) != Math.Sign(Dir))
{
//lift the foot at the back up a bit
footPos.Y += 0.15f;
}
footPos.X += torso.SimPosition.X;
}
else
{
@@ -852,7 +865,7 @@ namespace Barotrauma
{
Collider.LinearVelocity = movement;
}
else if (onGround && (!character.IsRemotePlayer || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)))
else if (onGround && (!character.IsRemotelyControlled || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)))
{
Collider.LinearVelocity = new Vector2(
movement.X,
@@ -921,7 +934,8 @@ namespace Barotrauma
rotation = MathHelper.ToDegrees(rotation);
if (rotation < 0.0f) rotation += 360;
if (!character.IsRemotePlayer && !aiming && Anim != Animation.UsingConstruction)
if (!character.IsRemotelyControlled && !aiming && Anim != Animation.UsingConstruction &&
!(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
if (rotation > 20 && rotation < 170)
TargetDir = Direction.Left;
@@ -981,7 +995,6 @@ namespace Barotrauma
{
//pull head above water
head.body.SmoothRotate(0.0f, 5.0f);
WalkPos += 0.05f;
}
else
@@ -999,7 +1012,7 @@ namespace Barotrauma
}
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (isNotRemote)
{
@@ -1010,9 +1023,18 @@ namespace Barotrauma
legCyclePos += Math.Min(movement.LengthSquared() + Collider.AngularVelocity, 1.0f);
handCyclePos += MathHelper.ToRadians(CurrentSwimParams.HandCycleSpeed) * Math.Sign(movement.X);
float legMoveMultiplier = 1.0f;
if (movement.LengthSquared() < 0.001f)
{
//TODO: expose these?
legMoveMultiplier = 0.3f;
legCyclePos += 0.4f;
handCyclePos += 0.1f;
}
var waist = GetLimb(LimbType.Waist);
footPos = waist == null ? Vector2.Zero : waist.SimPosition - new Vector2((float)Math.Sin(-Collider.Rotation), (float)Math.Cos(-Collider.Rotation)) * (upperLegLength + lowerLegLength);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount, 0.0f);
Vector2 transformedFootPos = new Vector2((float)Math.Sin(legCyclePos / CurrentSwimParams.LegCycleLength) * CurrentSwimParams.LegMoveAmount * legMoveMultiplier, 0.0f);
transformedFootPos = Vector2.Transform(transformedFootPos, Matrix.CreateRotationZ(Collider.Rotation));
float torque = CurrentSwimParams.FootRotateStrength * character.SpeedMultiplier * (1.2f - character.GetLegPenalty());
@@ -1085,7 +1107,7 @@ namespace Barotrauma
void UpdateClimbing()
{
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null)
if (character.SelectedConstruction == null || character.SelectedConstruction.GetComponent<Ladder>() == null || character.IsIncapacitated)
{
Anim = Animation.None;
return;
@@ -1109,6 +1131,8 @@ namespace Barotrauma
Limb leftHand = GetLimb(LimbType.LeftHand);
Limb rightHand = GetLimb(LimbType.RightHand);
if (leftHand == null || rightHand == null || head == null || torso == null) { return; }
Vector2 ladderSimPos = ConvertUnits.ToSimUnits(
character.SelectedConstruction.Rect.X + character.SelectedConstruction.Rect.Width / 2.0f,
character.SelectedConstruction.Rect.Y);
@@ -1121,10 +1145,14 @@ namespace Barotrauma
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition;
}
else if (currentHull.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine)
else if (currentHull?.Submarine != null && currentHull.Submarine != character.SelectedConstruction.Submarine && character.SelectedConstruction.Submarine != null)
{
ladderSimPos += character.SelectedConstruction.Submarine.SimPosition - currentHull.Submarine.SimPosition;
}
else if (currentHull?.Submarine != null && character.SelectedConstruction.Submarine == null)
{
ladderSimPos -= currentHull.Submarine.SimPosition;
}
float bottomPos = Collider.SimPosition.Y - ColliderHeightFromFloor - Collider.radius - Collider.height / 2.0f;
@@ -1162,7 +1190,7 @@ namespace Barotrauma
//only move the feet if they're above the bottom of the ladders
//(if not, they'll just dangle in air, and the character holds itself up with it's arms)
if (footPos.Y > -ladderSimSize.Y)
if (footPos.Y > -ladderSimSize.Y && leftFoot != null && rightFoot != null)
{
if (slide)
{
@@ -1227,7 +1255,7 @@ namespace Barotrauma
bool isClimbing = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
isRemote = character.IsRemotePlayer;
isRemote = character.IsRemotelyControlled;
}
if (isRemote)
{
@@ -1694,7 +1722,8 @@ namespace Barotrauma
// TODO: Remove this. Provide the position in params.
Vector2 itemPos = aim ? aimPos : holdPos;
bool usingController = character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Controller>() != null;
var controller = character.SelectedConstruction?.GetComponent<Controller>();
bool usingController = controller != null && !controller.AllowAiming;
bool isClimbing = character.IsClimbing && Math.Abs(character.AnimController.TargetMovement.Y) > 0.01f;
float itemAngle;
@@ -1813,17 +1842,14 @@ namespace Barotrauma
}
}
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
item.SetTransform(currItemPos, itemAngle + itemAngleRelativeToHoldAngle * Dir, setPrevTransform: false);
if (!isClimbing)
if (!isClimbing && !character.IsIncapacitated)
{
for (int i = 0; i < 2; i++)
{
if (character.SelectedItems[i] != item) continue;
if (itemPos == Vector2.Zero) continue;
if (character.SelectedItems[i] != item || itemPos == Vector2.Zero) { continue; }
Limb hand = (i == 0) ? rightHand : leftHand;
HandIK(hand, transformedHoldPos + transformedHandlePos[i]);
}
}
@@ -47,7 +47,9 @@ namespace Barotrauma
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
protected Hull currentHull;
private bool accessRemovedCharacterErrorShown;
private Limb[] limbs;
public Limb[] Limbs
{
@@ -55,16 +57,17 @@ namespace Barotrauma
{
if (limbs == null)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
if (!accessRemovedCharacterErrorShown)
{
string errorMsg = "Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this);
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce(
"Ragdoll.Limbs:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
"Attempted to access a potentially removed ragdoll. Character: " + character.Name + ", id: " + character.ID + ", removed: " + character.Removed + ", ragdoll removed: " + !list.Contains(this) + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
return new Limb[0];
}
return limbs;
@@ -170,7 +173,7 @@ namespace Barotrauma
pos1.Y -= collider[colliderIndex].height * ColliderHeightFromFloor;
Vector2 pos2 = pos1;
pos2.Y += collider[value].height * 1.1f;
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall))) { return; }
if (GameMain.World.RayCast(pos1, pos2).Any(f => f.CollisionCategories.HasFlag(Physics.CollisionWall) && !(f.Body.UserData is Submarine))) { return; }
}
Vector2 pos = collider[colliderIndex].SimPosition;
@@ -616,6 +619,7 @@ namespace Barotrauma
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
if (f2.UserData is Hull && character.Submarine != null) { return false; }
//using the velocity of the limb would make the impact damage more realistic,
//but would also make it harder to edit the animations because the forces/torques
@@ -690,14 +694,14 @@ namespace Barotrauma
private void ApplyImpact(Fixture f1, Fixture f2, Vector2 localNormal, Vector2 impactPos, Vector2 velocity)
{
if (character.DisableImpactDamageTimer > 0.0f) return;
if (character.DisableImpactDamageTimer > 0.0f) { return; }
Vector2 normal = localNormal;
float impact = Vector2.Dot(velocity, -normal);
if (f1.Body == Collider.FarseerBody || !Collider.Enabled)
{
bool isNotRemote = true;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) isNotRemote = !character.IsRemotePlayer;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { isNotRemote = !character.IsRemotelyControlled; }
if (isNotRemote)
{
@@ -930,7 +934,7 @@ namespace Barotrauma
if (setSubmarine)
{
//in -> out
if (newHull == null && currentHull.Submarine != null)
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(currentHull.ConnectedGaps, findPos, 150.0f) != null) { return; }
@@ -1259,6 +1263,7 @@ namespace Barotrauma
return false;
}
bool isColliderValid = CheckValidity(Collider);
if (!isColliderValid) { Collider.ResetDynamics(); }
bool limbsValid = true;
foreach (Limb limb in limbs)
{
@@ -1266,6 +1271,7 @@ namespace Barotrauma
if (!CheckValidity(limb.body))
{
limbsValid = false;
limb.body.ResetDynamics();
break;
}
}
@@ -1273,11 +1279,12 @@ namespace Barotrauma
if (!isValid)
{
validityResets++;
if (validityResets > 1)
if (validityResets > 3)
{
Invalid = true;
DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll freezed to prevent crashes.");
DebugConsole.ThrowError("Invalid ragdoll physics. Ragdoll frozen to prevent crashes.");
Collider.SetTransform(Vector2.Zero, 0.0f);
Collider.ResetDynamics();
foreach (Limb limb in Limbs)
{
limb.body?.SetTransform(Collider.SimPosition, 0.0f);
@@ -1310,7 +1317,7 @@ namespace Barotrauma
}
if (errorMsg != null)
{
if (character.IsRemotePlayer)
if (character.IsRemotelyControlled)
{
errorMsg += " Ragdoll controlled remotely.";
}
@@ -1489,6 +1496,7 @@ namespace Barotrauma
case Physics.CollisionWall:
case Physics.CollisionLevel:
if (!fixture.CollidesWith.HasFlag(Physics.CollisionCharacter)) { return -1; }
if (fixture.Body.UserData is Submarine && character.Submarine != null) { return -1; }
if (fraction < standOnFloorFraction)
{
standOnFloorFraction = fraction;
@@ -57,7 +57,33 @@ namespace Barotrauma
public Hull PreviousHull = null;
public Hull CurrentHull = null;
public bool IsRemotePlayer;
/// <summary>
/// Is the character controlled remotely (either by another player, or a server-side AIController)
/// </summary>
public bool IsRemotelyControlled
{
get
{
if (GameMain.NetworkMember == null)
{
return false;
}
else if (GameMain.NetworkMember.IsClient)
{
//all characters except the client's own character are controlled by the server
return this != Controlled;
}
else
{
return IsRemotePlayer;
}
}
}
/// <summary>
/// Is the character controlled by another human player (should always be false in single player)
/// </summary>
public bool IsRemotePlayer { get; set; }
public bool IsPlayer => Controlled == this || IsRemotePlayer;
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
@@ -91,10 +117,12 @@ namespace Barotrauma
set
{
teamID = value;
if (info != null) info.TeamID = value;
if (info != null) { info.TeamID = value; }
}
}
public bool TurnedHostileByEvent;
public AnimController AnimController;
private Vector2 cursorPosition;
@@ -240,7 +268,14 @@ namespace Barotrauma
var displayName = Params.DisplayName;
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
{
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
}
else
{
displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}", returnNull: true);
}
}
return string.IsNullOrWhiteSpace(displayName) ? Name : displayName;
}
@@ -287,6 +322,11 @@ namespace Barotrauma
public string customInteractHUDText;
private Action<Character, Character> onCustomInteract;
public bool AllowCustomInteract
{
get { return !IsIncapacitated && Stun <= 0.0f && !Removed; }
}
private float lockHandsTimer;
public bool LockHands
{
@@ -589,30 +629,42 @@ namespace Barotrauma
set { canInventoryBeAccessed = value; }
}
public bool CanAim
{
get
{
return SelectedConstruction == null || SelectedConstruction.GetComponent<Ladder>() != null || (SelectedConstruction.GetComponent<Controller>()?.AllowAiming ?? false);
}
}
public CampaignMode.InteractionType CampaignInteractionType;
private bool accessRemovedCharacterErrorShown;
public override Vector2 SimPosition
{
get
{
if (AnimController?.Collider == null)
{
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
if (!accessRemovedCharacterErrorShown)
{
errorMsg += " AnimController == null";
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
if (AnimController == null)
{
errorMsg += " AnimController == null";
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
errorMsg += '\n' + Environment.StackTrace;
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
accessRemovedCharacterErrorShown = true;
}
else if (AnimController.Collider == null)
{
errorMsg += " AnimController.Collider == null";
}
#if DEBUG || UNSTABLE
errorMsg += '\n' + Environment.StackTrace;
#endif
DebugConsole.NewMessage(errorMsg, Color.Red);
GameAnalyticsManager.AddErrorEventOnce(
"Character.SimPosition:AccessRemoved",
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
errorMsg + "\n" + Environment.StackTrace);
return Vector2.Zero;
}
@@ -756,6 +808,10 @@ namespace Barotrauma
Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
}
}
if (Info != null)
{
teamID = Info.TeamID;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
@@ -1044,10 +1100,24 @@ namespace Barotrauma
public void GiveJobItems(WayPoint spawnPoint = null)
{
if (info == null || info.Job == null) { return; }
if (info?.Job == null) { return; }
info.Job.GiveJobItems(this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint)
{
if (info?.Job == null || spawnPoint == null) { return; }
foreach (Item item in Inventory.Items)
{
if (item?.Prefab.Identifier != "idcard") { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
}
public float GetSkillLevel(string skillIdentifier)
{
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
@@ -1150,27 +1220,34 @@ namespace Barotrauma
float reduction = 0;
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
if (!(AnimController is HumanoidAnimController))
if (AnimController is HumanoidAnimController)
{
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
if (AnimController.InWater)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
// Currently only humans use hands for swimming.
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
}
}
if (destroyedTailLimbs > 0)
else
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
int totalTailLimbs = 0;
int destroyedTailLimbs = 0;
foreach (var limb in AnimController.Limbs)
{
if (limb.type == LimbType.Tail)
{
totalTailLimbs++;
if (limb.IsSevered)
{
destroyedTailLimbs++;
}
}
}
if (destroyedTailLimbs > 0)
{
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
}
}
return Math.Clamp(reduction, 0, 1f);
}
@@ -1236,8 +1313,8 @@ namespace Barotrauma
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
}
bool playerControlled = !(this is AICharacter) || Controlled == this || IsRemotePlayer;
if (playerControlled)
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
if (!aiControlled)
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
@@ -1249,7 +1326,7 @@ namespace Barotrauma
((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch);
}
if (playerControlled &&
if (!aiControlled &&
AnimController.onGround &&
!AnimController.InWater &&
AnimController.Anim != AnimController.Animation.UsingConstruction &&
@@ -1277,7 +1354,7 @@ namespace Barotrauma
{
if (GameMain.NetworkMember.IsServer)
{
if (playerControlled)
if (!aiControlled)
{
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
{
@@ -1447,7 +1524,7 @@ namespace Barotrauma
}
}
if (IsRemotePlayer && keys != null)
if (IsRemotelyControlled && keys != null)
{
foreach (Key key in keys)
{
@@ -1460,15 +1537,44 @@ namespace Barotrauma
{
if (target.Removed) { return false; }
Limb seeingLimb = GetSeeingLimb();
return target.AnimController.Limbs.Any(l => CanSeeTarget(l, seeingLimb));
if (CanSeeTarget(target, seeingLimb)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
Limb leftExtremity = null, rightExtremity = null;
float leftMostDot = 0.0f, rightMostDot = 0.0f;
Vector2 dir = target.WorldPosition - WorldPosition;
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
foreach (Limb limb in target.AnimController.Limbs)
{
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
Vector2 limbDir = limb.WorldPosition - WorldPosition;
float leftDot = Vector2.Dot(limbDir, leftDir);
if (leftDot > leftMostDot)
{
leftMostDot = leftDot;
leftExtremity = limb;
continue;
}
float rightDot = Vector2.Dot(limbDir, rightDir);
if (rightDot > rightMostDot)
{
rightMostDot = rightDot;
rightExtremity = limb;
continue;
}
}
if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; }
if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; }
}
return false;
}
private Limb GetSeeingLimb()
{
Limb selfLimb = AnimController.GetLimb(LimbType.Head);
if (selfLimb == null) { selfLimb = AnimController.GetLimb(LimbType.Torso); }
if (selfLimb == null) { selfLimb = AnimController.MainLimb; }
return selfLimb;
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
@@ -1531,12 +1637,12 @@ namespace Barotrauma
if (target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
if (closestBody == null) return true;
if (closestBody == null) { return true; }
}
else
{
closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff);
if (closestBody == null) return true;
if (closestBody == null) { return true; }
}
Structure wall = closestBody.UserData as Structure;
Item item = closestBody.UserData as Item;
@@ -1544,6 +1650,11 @@ namespace Barotrauma
return (wall == null || !wall.CastShadow) && (door == null || door.IsOpen || door.IsBroken);
}
/// <summary>
/// A simple check if the character Dir is towards the target or not. Uses the world coordinates.
/// </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 HasEquippedItem(Item item)
@@ -1551,7 +1662,7 @@ namespace Barotrauma
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) return true;
if (Inventory.Items[i] == item && Inventory.SlotTypes[i] != InvSlotType.Any) { return true; }
}
return false;
@@ -1559,12 +1670,12 @@ namespace Barotrauma
public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true)
{
if (Inventory == null) return false;
if (Inventory == null) { return false; }
for (int i = 0; i < Inventory.Capacity; i++)
{
if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) continue;
if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) continue;
if (Inventory.Items[i].Prefab.Identifier == itemIdentifier || Inventory.Items[i].HasTag(itemIdentifier)) return true;
if (Inventory.SlotTypes[i] == InvSlotType.Any || Inventory.Items[i] == null) { continue; }
if (!allowBroken && Inventory.Items[i].Condition <= 0.0f) { continue; }
if (Inventory.Items[i].Prefab.Identifier == itemIdentifier || Inventory.Items[i].HasTag(itemIdentifier)) { return true; }
}
return false;
@@ -1597,7 +1708,7 @@ namespace Barotrauma
public bool TrySelectItem(Item item, int index)
{
if (selectedItems[index] != null) return false;
if (selectedItems[index] != null) { return false; }
selectedItems[index] = item;
return true;
@@ -1698,7 +1809,7 @@ namespace Barotrauma
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && c.onCustomInteract == null) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!skipDistanceCheck)
{
@@ -1730,10 +1841,10 @@ namespace Barotrauma
}
Wire wire = item.GetComponent<Wire>();
if (wire != null)
if (wire != null && item.GetComponent<ConnectionPanel>() == null)
{
//locked wires are never interactable
if (wire.Locked) return false;
if (wire.Locked) { return false; }
//wires are interactable if the character has selected an item the wire is connected to,
//and it's disconnected from the other end
@@ -2003,10 +2114,21 @@ namespace Barotrauma
#endif
}
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Select) && FocusedCharacter.onCustomInteract != null)
else if (FocusedCharacter != null && IsKeyHit(InputType.Use) && FocusedCharacter.onCustomInteract != null && FocusedCharacter.AllowCustomInteract)
{
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
}
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Health) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
}
else if (focusedItem != null)
{
#if CLIENT
@@ -2023,14 +2145,7 @@ namespace Barotrauma
}
}
#endif
}
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
{
SelectedConstruction = null;
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
}
}
public static void UpdateAnimAll(float deltaTime)
@@ -2377,18 +2492,18 @@ namespace Barotrauma
}
private float despawnTimer;
private void UpdateDespawn(float deltaTime)
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false)
{
if (!EnableDespawn) { return; }
//clients don't despawn characters unless the server says so
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (!IsDead) { return; }
if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
int subCorpseCount = 0;
if (Submarine != null)
if (Submarine != null && !ignoreThresholds)
{
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
@@ -2427,12 +2542,14 @@ namespace Barotrauma
}
else
{
Spawner.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
Spawner?.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
}
void onItemContainerSpawned(Item item)
{
if (Inventory?.Items == null) { return; }
item.UpdateTransform();
item.AddTag("name:" + Name);
if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); }
@@ -2457,6 +2574,8 @@ namespace Barotrauma
public void DespawnNow()
{
despawnTimer = GameMain.Config.CorpseDespawnDelay;
UpdateDespawn(1.0f, ignoreThresholds: true);
Spawner.Update();
}
public static void RemoveByPrefab(CharacterPrefab prefab)
@@ -2706,13 +2825,13 @@ namespace Barotrauma
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
}
#endif
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage);
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: AIController == null || AIController is HumanAIController);
return attackResult;
}
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage)
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
#if DEBUG
@@ -2722,8 +2841,12 @@ namespace Barotrauma
return;
}
#endif
if (!IsDead && !targetLimb.CanBeSeveredAlive) { return; }
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
if (!targetLimb.CanBeSeveredAlive) { return; }
}
bool wasSevered = false;
float random = Rand.Value();
foreach (LimbJoint joint in AnimController.LimbJoints)
@@ -3323,7 +3446,7 @@ namespace Barotrauma
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsOfficer => HasJob("securityofficer");
public bool IsSecurity => HasJob("securityofficer");
public bool IsAsssitant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
@@ -147,6 +147,9 @@ namespace Barotrauma
}
}
public XElement InventoryData;
public XElement HealthData;
private static ushort idCounter;
public string Name;
@@ -287,21 +290,21 @@ namespace Barotrauma
public bool StartItemsGiven;
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
public Character.TeamType TeamID;
private NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set;}
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
//unique ID given to character infos in MP
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
public ushort ID;
public XElement InventoryData;
public List<string> SpriteTags
{
get;
@@ -564,6 +567,22 @@ namespace Barotrauma
}
}
public int GetIdentifier()
{
int id = ToolBox.StringToInt(Name);
id ^= HeadSpriteId;
id ^= (int)Race << 6;
id ^= HairIndex << 12;
id ^= BeardIndex << 18;
id ^= MoustacheIndex << 24;
id ^= FaceAttachmentIndex << 30;
if (Job != null)
{
id ^= ToolBox.StringToInt(Job.Prefab.Identifier);
}
return id;
}
public IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType)
{
if (elements == null) { return elements; }
@@ -813,20 +832,17 @@ namespace Barotrauma
partial void LoadAttachmentSprites(bool omitJob);
// TODO: change the formula so that it's not linear and so that it takes into account the usefulness of the skill
// -> give a weight to each skill, because some are much more valuable than others?
private int CalculateSalary()
{
if (Name == null || Job == null) return 0;
int salary = Math.Abs(Name.GetHashCode()) % 100;
if (Name == null || Job == null) { return 0; }
int salary = 0;
foreach (Skill skill in Job.Skills)
{
salary += (int)skill.Level * 50;
salary += (int)(skill.Level * skill.Prefab.PriceMultiplier);
}
return salary;
return (int)(salary * Job.Prefab.PriceMultiplier);
}
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
@@ -871,7 +887,7 @@ namespace Barotrauma
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
public virtual XElement Save(XElement parentElement)
public XElement Save(XElement parentElement)
{
XElement charElement = new XElement("Character");
@@ -971,7 +987,12 @@ namespace Barotrauma
}
}
}
public void ApplyHealthData(Character character, XElement healthData)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
}
public void ReloadHeadAttachments()
{
ResetLoadedAttachments();
@@ -0,0 +1,135 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class CorpsePrefab : HumanPrefab, IPrefab, IDisposable
{
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
private bool disposed = false;
public void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static CorpsePrefab Get(string identifier)
{
if (Prefabs == null)
{
DebugConsole.ThrowError("Issue in the code execution order: job prefabs not loaded.");
return null;
}
if (Prefabs.ContainsKey(identifier))
{
return Prefabs[identifier];
}
else
{
DebugConsole.ThrowError("Couldn't find a job prefab with the given identifier: " + identifier);
return null;
}
}
[Serialize(Level.PositionType.Wreck, false)]
public Level.PositionType SpawnPosition { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public CorpsePrefab(XElement element, string filePath, bool allowOverriding) : base(element, filePath)
{
Prefabs.Add(this, allowOverriding);
}
public static CorpsePrefab Random(Rand.RandSync sync = Rand.RandSync.Unsynced) => Prefabs.GetRandom(sync);
public static void LoadAll(IEnumerable<ContentFile> files)
{
foreach (ContentFile file in files)
{
LoadFromFile(file);
}
}
public static void LoadFromFile(ContentFile file)
{
DebugConsole.Log("*** " + file.Path + " ***");
RemoveByFile(file.Path);
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
if (doc == null) { return; }
var rootElement = doc.Root;
switch (rootElement.Name.ToString().ToLowerInvariant())
{
case "corpse":
new CorpsePrefab(rootElement, file.Path, false)
{
ContentPackage = file.ContentPackage
};
break;
case "corpses":
foreach (var element in rootElement.Elements())
{
if (element.IsOverride())
{
var itemElement = element.GetChildElement("item");
if (itemElement != null)
{
new CorpsePrefab(itemElement, file.Path, true)
{
ContentPackage = file.ContentPackage
};
}
else
{
DebugConsole.ThrowError($"Cannot find an item element from the children of the override element defined in {file.Path}");
}
}
else
{
new CorpsePrefab(element, file.Path, false)
{
ContentPackage = file.ContentPackage
};
}
}
break;
case "override":
var corpses = rootElement.GetChildElement("corpses");
if (corpses != null)
{
foreach (var element in corpses.Elements())
{
new CorpsePrefab(element, file.Path, true)
{
ContentPackage = file.ContentPackage,
};
}
}
foreach (var element in rootElement.GetChildElements("corpse"))
{
new CorpsePrefab(element, file.Path, true)
{
ContentPackage = file.ContentPackage
};
}
break;
default:
DebugConsole.ThrowError($"Invalid XML root element: '{rootElement.Name}' in {file.Path}");
break;
}
}
public static void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
}
}
}
@@ -5,6 +5,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Extensions;
using System.Globalization;
namespace Barotrauma
{
@@ -127,7 +128,7 @@ namespace Barotrauma
public bool IsUnconscious
{
get { return Vitality <= 0.0f; }
get { return Vitality <= 0.0f || Character.IsDead; }
}
public float PressureKillDelay { get; private set; } = 5.0f;
@@ -146,6 +147,11 @@ namespace Barotrauma
}
return maxVitality;
}
set
{
maxVitality = Math.Max(0, value);
}
}
public float MinVitality
@@ -937,7 +943,83 @@ namespace Barotrauma
partial void RemoveProjSpecific();
/// <summary>
/// Automatically filters out buffs.
/// </summary>
public static IEnumerable<Affliction> SortAfflictionsBySeverity(IEnumerable<Affliction> afflictions, bool excludeBuffs = true) =>
afflictions.Where(a => !excludeBuffs || !a.Prefab.IsBuff).OrderByDescending(a => a.DamagePerSecond).ThenByDescending(a => a.Strength);
public void Save(XElement healthElement)
{
foreach (Affliction affliction in afflictions)
{
if (affliction.Strength <= 0.0f) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
for (int i = 0; i < limbHealths.Count; i++)
{
var limbHealthElement = new XElement("LimbHealth", new XAttribute("i", i));
healthElement.Add(limbHealthElement);
foreach (Affliction affliction in limbHealths[i].Afflictions)
{
if (affliction.Strength <= 0.0f) { continue; }
limbHealthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));
}
}
}
public void Load(XElement element)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "affliction":
LoadAffliction(subElement);
break;
case "limbhealth":
int limbHealthIndex = subElement.GetAttributeInt("i", -1);
if (limbHealthIndex < 0 || limbHealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError($"Error while loading character health: limb index \"{limbHealthIndex}\" out of range.");
continue;
}
foreach (XElement afflictionElement in subElement.Elements())
{
LoadAffliction(afflictionElement, limbHealths[limbHealthIndex]);
}
break;
}
}
void LoadAffliction(XElement afflictionElement, LimbHealth limbHealth = null)
{
string id = afflictionElement.GetAttributeString("identifier", "");
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == id);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return;
}
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)
{
irremovableAffliction.Strength = strength;
}
else if (limbHealth != null)
{
limbHealth.Afflictions.Add(afflictionPrefab.Instantiate(strength));
}
else
{
afflictions.Add(afflictionPrefab.Instantiate(strength));
}
}
}
}
}
@@ -0,0 +1,178 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
class HumanPrefab
{
[Serialize("notfound", false)]
public string Identifier { get; protected set; }
[Serialize("any", false)]
public string Job { get; protected set; }
[Serialize(1f, false)]
public float Commonness { get; protected set; }
[Serialize(1f, false)]
public float HealthMultiplier { get; protected set; }
private readonly HashSet<string> moduleFlags = new HashSet<string>();
[Serialize("", true, "What outpost module tags does the NPC prefer to spawn in.")]
public string ModuleFlags
{
get => string.Join(",", moduleFlags);
set
{
moduleFlags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitFlags = value.Split(',');
foreach (var f in splitFlags)
{
moduleFlags.Add(f);
}
}
}
}
private readonly HashSet<string> spawnPointTags = new HashSet<string>();
[Serialize("", true, "Tag(s) of the spawnpoints the NPC prefers to spawn at.")]
public string SpawnPointTags
{
get => string.Join(",", spawnPointTags);
set
{
spawnPointTags.Clear();
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (var tag in splitTags)
{
spawnPointTags.Add(tag.ToLowerInvariant());
}
}
}
}
[Serialize("None", false)]
public CampaignMode.InteractionType CampaignInteractionType { get; protected set; }
[Serialize("Passive", false)]
public AIObjectiveIdle.BehaviorType BehaviorType { get; protected set; }
public List<string> PreferredOutpostModuleTypes { get; protected set; }
public string OriginalName { get { return Identifier; } }
public string FilePath { get; protected set; }
public XElement Element { get; protected set; }
public readonly Dictionary<XElement, float> ItemSets = new Dictionary<XElement, float>();
public HumanPrefab(XElement element, string filePath)
{
FilePath = filePath;
SerializableProperty.DeserializeProperties(this, element);
Identifier = Identifier.ToLowerInvariant();
Job = Job.ToLowerInvariant();
Element = element;
element.GetChildElements("itemset").ForEach(e => ItemSets.Add(e, e.GetAttributeFloat("commonness", 1)));
PreferredOutpostModuleTypes = element.GetAttributeStringArray("preferredoutpostmoduletypes", new string[0], convertToLowerInvariant: true).ToList();
}
public IEnumerable<string> GetModuleFlags()
{
return moduleFlags;
}
public IEnumerable<string> GetSpawnPointTags()
{
return spawnPointTags;
}
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
}
public void GiveItems(Character character, Submarine submarine, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets.Keys.ToList(), ItemSets.Values.ToList(), randSync);
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
{
InitializeItems(character, itemElement, submarine);
}
}
private void InitializeItems(Character character, XElement itemElement, Submarine submarine, Item parentItem = null)
{
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.");
return;
}
Item item = new Item(itemPrefab, character.Position, null);
#if SERVER
if (GameMain.Server != null && Entity.Spawner != null)
{
if (GameMain.Server.EntityEventManager.UniqueEvents.Any(ev => ev.Entity == item))
{
string errorMsg = $"Error while spawning job items. Item {item.Name} created network events before the spawn event had been created.";
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("Job.InitializeJobItem:EventsBeforeSpawning", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
GameMain.Server.EntityEventManager.UniqueEvents.RemoveAll(ev => ev.Entity == item);
GameMain.Server.EntityEventManager.Events.RemoveAll(ev => ev.Entity == item);
}
Entity.Spawner.CreateNetworkEvent(item, false);
}
#endif
if (itemElement.GetAttributeBool("equip", false))
{
List<InvSlotType> allowedSlots = new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
{
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
}
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
{
item.AddTag("name:" + character.Name);
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
var job = character.Info?.Job;
if (job != null)
{
item.AddTag("job:" + job.Name);
}
}
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = character.TeamID;
}
if (parentItem != null)
{
parentItem.Combine(item, user: null);
}
foreach (XElement childItemElement in itemElement.Elements())
{
InitializeItems(character, childItemElement, submarine, item);
}
}
}
}
@@ -33,6 +33,8 @@ namespace Barotrauma
public int Variant;
public Skill PrimarySkill { get; }
public Job(JobPrefab jobPrefab, int variant = 0)
{
prefab = jobPrefab;
@@ -41,14 +43,16 @@ namespace Barotrauma
skills = new Dictionary<string, Skill>();
foreach (SkillPrefab skillPrefab in prefab.Skills)
{
skills.Add(skillPrefab.Identifier, new Skill(skillPrefab));
var skill = new Skill(skillPrefab);
skills.Add(skillPrefab.Identifier, skill);
if (skillPrefab.IsPrimarySkill) { PrimarySkill = skill; }
}
}
public Job(XElement element)
{
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
JobPrefab p = null;
JobPrefab p;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
@@ -65,9 +69,9 @@ namespace Barotrauma
if (!subElement.Name.ToString().Equals("skill", System.StringComparison.OrdinalIgnoreCase)) { continue; }
string skillIdentifier = subElement.GetAttributeString("identifier", "");
if (string.IsNullOrEmpty(skillIdentifier)) { continue; }
skills.Add(
skillIdentifier,
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
var skill = new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0));
skills.Add(skillIdentifier, skill);
if (skillIdentifier == prefab.PrimarySkill?.Identifier) { PrimarySkill = skill; }
}
}
@@ -146,6 +146,13 @@ namespace Barotrauma
private set;
}
[Serialize(1.0f, false)]
public float PriceMultiplier
{
get;
private set;
}
// TODO: not used
[Serialize(10.0f, false)]
public float Commonness
@@ -164,6 +171,9 @@ namespace Barotrauma
public Sprite Icon;
public Sprite IconSmall;
public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill);
public string FilePath { get; private set; }
public XElement Element { get; private set; }
@@ -1,24 +1,12 @@
using Microsoft.Xna.Framework;
using System;
namespace Barotrauma
{
class Skill
{
private SkillPrefab prefab;
private float level;
static string[] levelNames = new string[] {
"Untrained", "Incompetent", "Novice",
"Adequate", "Competent", "Proficient",
"Professional", "Master", "Legendary" };
string identifier;
public string Identifier
{
get { return identifier; }
}
public string Identifier { get; }
public float Level
{
@@ -26,29 +14,58 @@ namespace Barotrauma
set { level = MathHelper.Clamp(value, 0.0f, 100.0f); }
}
private Sprite icon;
public Sprite Icon
{
get
{
if (icon == null)
{
icon = GetIcon();
}
return icon;
}
}
internal SkillPrefab Prefab { get; private set; }
public Skill(SkillPrefab prefab)
{
this.prefab = prefab;
this.identifier = prefab.Identifier;
this.level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
this.Prefab = prefab;
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
icon = GetIcon();
}
public Skill(string identifier, float level)
{
this.identifier = identifier;
Identifier = identifier;
this.level = level;
icon = GetIcon();
}
/// <summary>
/// returns the "name" of some skill level (0-10 -> untrained, etc)
/// </summary>
public static string GetLevelName(float level)
private Sprite GetIcon()
{
level = MathHelper.Clamp(level, 0.0f, 100.0f);
int scaledLevel = (int)Math.Floor((level / 100.0f) * levelNames.Length);
return levelNames[Math.Min(scaledLevel, levelNames.Length - 1)];
string jobId = null;
switch (Identifier.ToLowerInvariant())
{
case "electrical":
jobId = "engineer";
break;
case "helm":
jobId = "captain";
break;
case "mechanical":
jobId = "mechanic";
break;
case "medical":
jobId = "medicaldoctor";
break;
case "weapons":
jobId = "securityofficer";
break;
}
return jobId != null && JobPrefab.Prefabs.ContainsKey(jobId) ? JobPrefab.Prefabs[jobId].IconSmall : null;
}
}
}
@@ -9,10 +9,17 @@ namespace Barotrauma
public Vector2 LevelRange { get; private set; }
/// <summary>
/// How much this skill affects characters' hiring cost
/// </summary>
public readonly float PriceMultiplier;
public bool IsPrimarySkill { get; }
public SkillPrefab(XElement element)
{
Identifier = element.GetAttributeString("identifier", "");
PriceMultiplier = element.GetAttributeFloat("pricemultiplier", 25.0f);
var levelString = element.GetAttributeString("level", "");
if (levelString.Contains(","))
{
@@ -23,6 +30,8 @@ namespace Barotrauma
float skillLevel = float.Parse(levelString, System.Globalization.CultureInfo.InvariantCulture);
LevelRange = new Vector2(skillLevel, skillLevel);
}
IsPrimarySkill = element.GetAttributeBool("primary", false);
}
}
}
@@ -19,6 +19,9 @@ namespace Barotrauma
[Serialize("", true), Editable]
public string SpeciesName { get; private set; }
[Serialize("", true, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
public string SpeciesTranslationOverride { get; private set; }
[Serialize("", true, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }