(965c31410a) Unstable v0.10.4.0
This commit is contained in:
@@ -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())
|
||||
{
|
||||
|
||||
+354
-117
@@ -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;
|
||||
|
||||
+8
-4
@@ -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,
|
||||
|
||||
+21
-2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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)
|
||||
{
|
||||
|
||||
+27
-3
@@ -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)
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
+56
-34
@@ -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;
|
||||
}
|
||||
|
||||
+21
-4
@@ -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
|
||||
{
|
||||
|
||||
+246
-97
@@ -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)
|
||||
|
||||
+4
-2
@@ -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,
|
||||
|
||||
+6
-6
@@ -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)
|
||||
{
|
||||
|
||||
+3
-2
@@ -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;
|
||||
}
|
||||
|
||||
+8
-2
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+41
-36
@@ -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)
|
||||
|
||||
+7
-10
@@ -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))
|
||||
{
|
||||
|
||||
+59
-33
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user