v0.10.5.1

This commit is contained in:
Juan Pablo Arce
2020-09-22 11:31:56 -03:00
parent 44032d0ae0
commit 0002ad2c50
343 changed files with 12276 additions and 5023 deletions
@@ -90,6 +90,19 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
//switched control to some other character during the transition -> remove control again
if (Character.Controlled != null)
{
prevControlled = Character.Controlled;
if (RemoveControlFromCharacter)
{
#if CLIENT
GameMain.LightManager.LosEnabled = false;
#endif
Character.Controlled = null;
}
}
if (prevControlled != null && prevControlled.Removed)
{
prevControlled = null;
@@ -13,12 +13,6 @@ namespace Barotrauma
private AIState state;
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
// Update only when the value changes, not when it keeps the same.
protected AITarget _lastAiTarget;
// Updated each time the value is updated (also when the value is the same).
@@ -142,6 +136,17 @@ namespace Barotrauma
}
}
public virtual void Reset()
{
ResetAITarget();
}
protected void ResetAITarget()
{
_lastAiTarget = null;
_selectedAiTarget = null;
}
protected virtual void OnStateChanged(AIState from, AIState to) { }
protected virtual void OnTargetChanged(AITarget previousTarget, AITarget newTarget) { }
@@ -153,6 +153,11 @@ namespace Barotrauma
{
throw new Exception($"Tried to create an enemy ai controller for human!");
}
if (Character.Params.Group.Equals("human", StringComparison.OrdinalIgnoreCase))
{
// Pet
Character.TeamID = Character.TeamType.FriendlyNPC;
}
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(c.SpeciesName);
var mainElement = prefab.XDocument.Root.IsOverride() ? prefab.XDocument.Root.FirstElement() : prefab.XDocument.Root;
targetMemories = new Dictionary<AITarget, AITargetMemory>();
@@ -733,7 +738,7 @@ namespace Barotrauma
{
if (door.LinkedGap.Size > ConvertUnits.ToDisplayUnits(colliderWidth))
{
LatchOntoAI?.DeattachFromBody();
LatchOntoAI?.DeattachFromBody(cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
var velocity = Vector2.Normalize(door.LinkedGap.FlowTargetHull.WorldPosition - Character.WorldPosition);
steeringManager.SteeringManual(deltaTime, velocity);
@@ -1121,7 +1126,7 @@ namespace Barotrauma
{
IsSteeringThroughGap = true;
wallTarget = null;
LatchOntoAI?.DeattachFromBody();
LatchOntoAI?.DeattachFromBody(cooldown: 2);
Character.AnimController.ReleaseStuckLimbs();
Hull targetHull = section.gap?.FlowTargetHull;
float maxDistance = Math.Min(wall.Rect.Width, wall.Rect.Height);
@@ -1303,7 +1308,7 @@ namespace Barotrauma
bool wasLatched = IsLatchedOnSub;
Character.AnimController.ReleaseStuckLimbs();
LatchOntoAI?.DeattachFromBody();
LatchOntoAI?.DeattachFromBody(cooldown: 1);
if (attacker == null || attacker.AiTarget == null) { return; }
bool isFriendly = IsFriendly(Character, attacker);
if (wasLatched)
@@ -1544,39 +1549,6 @@ namespace Barotrauma
//sight/hearing range
public AITarget UpdateTargets(Character character, out CharacterParams.TargetParams targetingParams)
{
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
{
var wall = SelectedAiTarget.Entity as Structure;
if (wall == null)
{
wall = wallTarget?.Structure;
}
// The target is not a wall or it's not the same as we are attached to -> release
bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB);
if (!releaseTarget)
{
for (int i = 0; i < wall.Sections.Length; i++)
{
if (CanPassThroughHole(wall, i))
{
releaseTarget = true;
}
}
}
if (releaseTarget)
{
SelectedAiTarget = null;
wallTarget = null;
LatchOntoAI.DeattachFromBody(cooldown: 1);
}
else if (SelectedAiTarget?.Entity == wallTarget?.Structure)
{
// If attached to a valid target, just keep the target.
// Priority not used in this case.
targetingParams = null;
return SelectedAiTarget;
}
}
AITarget newTarget = null;
targetValue = 0;
selectedTargetMemory = null;
@@ -1611,42 +1583,44 @@ namespace Barotrauma
{
targetingTag = tP.Tag;
}
else if (targetCharacter.AIController is EnemyAIController enemy)
else
{
if (targetCharacter.Params.CompareGroup(Character.Params.Group))
if (IsFriendly(Character, targetCharacter))
{
// Ignore targets that are in the same group (treat them like they were of the same species)
continue;
}
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (targetCharacter.AIController is EnemyAIController enemy)
{
targetingTag = "husk";
}
else
{
if (enemy.CombatStrength > CombatStrength)
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "stronger";
targetingTag = "husk";
}
else if (enemy.CombatStrength < CombatStrength)
else
{
targetingTag = "weaker";
}
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
if (SelectedAiTarget == aiTarget)
if (enemy.CombatStrength > CombatStrength)
{
// Freightened -> hold on to the target
valueModifier *= 2;
targetingTag = "stronger";
}
if (IsBeingChasedBy(targetCharacter))
else if (enemy.CombatStrength < CombatStrength)
{
valueModifier *= 2;
targetingTag = "weaker";
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
if (targetingTag == "stronger" && (State == AIState.Avoid || State == AIState.Escape || State == AIState.Flee))
{
// Inside but in a different room
valueModifier /= 2;
if (SelectedAiTarget == aiTarget)
{
// Freightened -> hold on to the target
valueModifier *= 2;
}
if (IsBeingChasedBy(targetCharacter))
{
valueModifier *= 2;
}
if (Character.CurrentHull != null && !VisibleHulls.Contains(targetCharacter.CurrentHull))
{
// Inside but in a different room
valueModifier /= 2;
}
}
}
}
@@ -1855,25 +1829,28 @@ namespace Barotrauma
if (valueModifier == 0.0f) { continue; }
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
if (targetingTag != "decoy")
{
// Halve the priority for each swarm mate targeting the same target -> reduces stacking
foreach (Character otherCharacter in SwarmBehavior.Members)
if (SwarmBehavior != null && SwarmBehavior.Members.Any())
{
if (otherCharacter == character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
valueModifier /= 2;
// 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)
else
{
if (otherCharacter == character) { continue; }
if (otherCharacter.AIController?.SelectedAiTarget != aiTarget) { continue; }
if (!IsFriendly(character, otherCharacter)) { continue; }
valueModifier /= 2;
// 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;
}
}
}
@@ -1969,7 +1946,34 @@ namespace Barotrauma
SelectedAiTarget = newTarget;
if (SelectedAiTarget != _previousAiTarget)
{
wallTarget = null;
if ((SelectedAiTarget != null || wallTarget != null) && IsLatchedOnSub)
{
if (!(SelectedAiTarget.Entity is Structure wall))
{
wall = wallTarget?.Structure;
}
// The target is not a wall or it's not the same as we are attached to -> release
bool releaseTarget = wall == null || !wall.Bodies.Contains(LatchOntoAI.AttachJoints[0].BodyB);
if (!releaseTarget)
{
for (int i = 0; i < wall.Sections.Length; i++)
{
if (CanPassThroughHole(wall, i))
{
releaseTarget = true;
}
}
}
if (releaseTarget)
{
wallTarget = null;
LatchOntoAI.DeattachFromBody(cooldown: 1);
}
}
else
{
wallTarget = null;
}
}
return SelectedAiTarget;
}
@@ -12,7 +12,7 @@ namespace Barotrauma
{
public static bool DisableCrewAI;
private AIObjectiveManager objectiveManager;
private readonly AIObjectiveManager objectiveManager;
private float sortTimer;
private float crouchRaycastTimer;
@@ -29,6 +29,7 @@ namespace Barotrauma
private const float FlipInterval = 0.5f;
public static float HULL_SAFETY_THRESHOLD = 50;
private static readonly float characterWaitOnSwitch = 5;
public readonly HashSet<Hull> UnreachableHulls = new HashSet<Hull>();
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
@@ -90,8 +91,7 @@ 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>();
private readonly HashSet<Character> attackers = new HashSet<Character>();
public HumanAIController(Character c) : base(c)
{
@@ -106,11 +106,38 @@ namespace Barotrauma
sortTimer = Rand.Range(0f, sortObjectiveInterval);
InitProjSpecific();
}
partial void InitProjSpecific();
private bool freezeAI;
public override void Update(float deltaTime)
{
if (DisableCrewAI || Character.IsIncapacitated || Character.Removed) { return; }
if (DisableCrewAI || Character.Removed) { return; }
//slowly forget about damage done by attackers
foreach (Character enemy in attackers)
{
float cumulativeDamage = damageDoneByAttacker[enemy];
if (cumulativeDamage > 0)
{
float reduction = deltaTime;
if (cumulativeDamage < 2)
{
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
reduction *= 0.5f;
}
damageDoneByAttacker[enemy] -= reduction;
}
}
bool isIncapacitated = Character.IsIncapacitated;
if (freezeAI && !isIncapacitated)
{
freezeAI = false;
}
if (isIncapacitated) { return; }
base.Update(deltaTime);
foreach (var values in knownHulls)
@@ -164,15 +191,6 @@ 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;
@@ -277,7 +295,10 @@ namespace Barotrauma
{
newDir = Direction.Left;
}
if (Character.SelectedConstruction != null) Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
if (Character.SelectedConstruction != null)
{
Character.SelectedConstruction.SecondaryUse(deltaTime, Character);
}
}
else if (Math.Abs(Character.AnimController.TargetMovement.X) > 0.1f && !Character.AnimController.InWater)
{
@@ -295,53 +316,49 @@ namespace Barotrauma
{
if (Character.LockHands) { return; }
if (ObjectiveManager.CurrentObjective == null) { return; }
if (ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>()) { return; }
if (findItemState == FindItemState.None || findItemState == FindItemState.Extinguisher)
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
bool isCarrying = ObjectiveManager.HasActiveObjective<AIObjectiveContainItem>() || ObjectiveManager.HasActiveObjective<AIObjectiveDecontainItem>();
bool NeedsDivingGearOnPath(AIObjectiveGoTo gotoObjective)
{
if (!ObjectiveManager.IsCurrentObjective<AIObjectiveExtinguishFires>() && !objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>())
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
Hull targetHull = gotoObjective.GetTargetHull();
return gotoObjective.Target != null && targetHull == null ||
NeedsDivingGear(targetHull, out _) ||
insideSteering && (PathSteering.CurrentPath.HasOutdoorsNodes || PathSteering.CurrentPath.Nodes.Any(n => NeedsDivingGear(n.CurrentHull, out _)));
}
if (isCarrying)
{
if (findItemState != FindItemState.OtherItem)
{
var extinguisher = Character.Inventory.FindItemByTag("extinguisher");
if (extinguisher != null && Character.HasEquippedItem(extinguisher))
if (ObjectiveManager.GetActiveObjective() is AIObjectiveGoTo gotoObjective && NeedsDivingGearOnPath(gotoObjective))
{
if (ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
extinguisher.Drop(Character);
}
else
{
findItemState = FindItemState.Extinguisher;
if (FindSuitableContainer(extinguisher, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, extinguisher, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
extinguisher.Drop(Character);
}
}
}
gotoObjective.Abandon = true;
}
}
if (!oxygenLow)
{
return;
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit || findItemState == FindItemState.DivingMask)
// Diving gear
if (oxygenLow || findItemState != FindItemState.OtherItem)
{
if (!NeedsDivingGear(Character, Character.CurrentHull, out _))
if (!NeedsDivingGear(Character.CurrentHull, out bool needsSuit) || !needsSuit || oxygenLow)
{
bool shouldKeepTheGearOn = Character.AnimController.HeadInWater
|| ObjectiveManager.IsCurrentObjective<AIObjectiveFindSafety>()
|| ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn);
bool oxygenLow = !Character.AnimController.HeadInWater && Character.OxygenAvailable < CharacterHealth.LowOxygenThreshold;
if (oxygenLow)
if (oxygenLow && Character.CurrentHull.Oxygen > 0)
{
shouldKeepTheGearOn = false;
}
else if (Character.CurrentHull.Oxygen < CharacterHealth.LowOxygenThreshold)
{
shouldKeepTheGearOn = true;
}
bool removeDivingSuit = !shouldKeepTheGearOn;
bool takeMaskOff = !shouldKeepTheGearOn;
if (!shouldKeepTheGearOn && !oxygenLow)
@@ -359,10 +376,7 @@ namespace Barotrauma
{
if (objective is AIObjectiveGoTo gotoObjective)
{
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 (NeedsDivingGearOnPath(gotoObjective))
{
removeDivingSuit = false;
takeMaskOff = false;
@@ -390,81 +404,75 @@ namespace Barotrauma
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR);
if (divingSuit != null)
{
if (removeDivingSuit)
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
var divingSuit = Character.Inventory.FindItemByTag("divingsuit");
if (divingSuit != null)
divingSuit.Drop(Character);
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingSuit)
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, out Item targetContainer))
{
if (oxygenLow || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
divingSuit.Drop(Character);
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
{
findItemState = FindItemState.DivingSuit;
if (FindSuitableContainer(divingSuit, out Item targetContainer))
divingSuit.Drop(Character);
}
}
}
}
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR))
{
var mask = Character.Inventory.FindItemByTag(AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR);
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 if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
var decontainObjective = new AIObjectiveDecontainItem(Character, divingSuit, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>())
{
DropIfFailsToContain = false
};
decontainObjective.Abandoned += () =>
{
IgnoredItems.Add(targetContainer);
};
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
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);
}
}
}
}
}
}
@@ -472,39 +480,37 @@ namespace Barotrauma
}
}
}
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
}
// Other items
if (isCarrying) { return; }
if (!ObjectiveManager.CurrentObjective.AllowAutomaticItemUnequipping || !ObjectiveManager.GetActiveObjective().AllowAutomaticItemUnequipping) { return; }
foreach (var item in Character.Inventory.Items)
{
if (item == null) { continue; }
if (Character.HasEquippedItem(item) &&
(Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.LeftHand) ||
Character.Inventory.IsInLimbSlot(item, InvSlotType.RightHand | InvSlotType.LeftHand)))
{
if (!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 (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }))
findItemState = FindItemState.OtherItem;
if (FindSuitableContainer(item, out Item targetContainer))
{
if (FindSuitableContainer(item, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
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);
}
var decontainObjective = new AIObjectiveDecontainItem(Character, item, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () => IgnoredItems.Add(targetContainer);
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
findItemState = FindItemState.OtherItem;
item.Drop(Character);
}
}
}
@@ -518,7 +524,6 @@ namespace Barotrauma
None,
DivingSuit,
DivingMask,
Extinguisher,
OtherItem
}
private FindItemState findItemState;
@@ -687,16 +692,25 @@ namespace Barotrauma
totalDamage -= affliction.Prefab.KarmaChangeOnApplied * affliction.Strength;
}
if (totalDamage <= 0) { return; }
if (attacker != null)
if (Character.IsBot)
{
if (!damageDoneByAttacker.ContainsKey(attacker))
if (attacker != null)
{
damageDoneByAttacker[attacker] = 0.0f;
if (!damageDoneByAttacker.ContainsKey(attacker))
{
damageDoneByAttacker[attacker] = 0.0f;
}
damageDoneByAttacker[attacker] += totalDamage;
attackers.Add(attacker);
}
if (!freezeAI && !Character.IsDead && Character.IsIncapacitated)
{
// Removes the combat objective and resets all objectives.
objectiveManager.CreateAutonomousObjectives();
objectiveManager.SortObjectives();
freezeAI = true;
}
damageDoneByAttacker[attacker] += totalDamage;
attackers.Add(attacker);
}
if (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (attacker == null || attacker.IsDead || attacker.Removed)
{
// Don't react on the damage if there's no attacker.
@@ -720,76 +734,99 @@ namespace Barotrauma
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
return;
}
if (attacker.IsBot)
float cumulativeDamage = GetDamageDoneByAttacker(attacker);
if (!Character.IsSecurity && attacker.IsBot && !attacker.IsInstigator)
{
// Don't retaliate on damage done by human ai, because we know it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
if (cumulativeDamage > 1)
{
// Don't retaliate on damage done by human ai, because we know it's accidental
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
if (Character.IsSecurity)
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
// Inform other NPCs
if (cumulativeDamage > 1)
{
// TODO
}
else
{
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)
{
if (otherCharacter == Character || otherCharacter.TeamID != Character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
if (otherCharacter == Character || otherCharacter.IsDead || otherCharacter.IsUnconscious || otherCharacter.Removed ||
otherCharacter.Info?.Job == null || otherCharacter.TeamID != Character.TeamType.FriendlyNPC ||
!(otherCharacter.AIController is HumanAIController otherHumanAI) ||
otherCharacter.TurnedHostileByEvent)
otherCharacter.IsInstigator)
{
continue;
continue;
}
if (!otherHumanAI.IsFriendly(Character)) { 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);
otherHumanAI.AddCombatObjective(DetermineCombatMode(otherCharacter, cumulativeDamage), attacker, delay);
}
else if (isWitnessing)
{
var mode = Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
// Other witnesses retreat to safety
otherHumanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime());
otherHumanAI.AddCombatObjective(mode, attacker, GetReactionTime());
}
}
(GameMain.GameSession?.GameMode as CampaignMode)?.OutpostNPCAttacked(Character, attacker, attackResult);
}
if (attacker.TeamID != Character.TeamID)
if (Character.IsBot)
{
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 (ObjectiveManager.CurrentObjective is AIObjectiveFightIntruders) { return; }
if (Character.IsSecurity)
{
if (!Character.IsSecurity)
if (attacker.TeamID != Character.TeamID && cumulativeDamage > 1 || cumulativeDamage > 10)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker, GetReactionTime() * 2);
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityarrest"), null, 0.50f, "attackedbyfriendlysecurityarrest", minDurationBetweenSimilar: 30.0f);
}
else
{
Character.Speak(TextManager.Get("dialogattackedbyfriendlysecurityresponse"), null, 0.50f, "attackedbyfriendlysecurityresponse", minDurationBetweenSimilar: 30.0f);
}
}
else if (!Character.IsInstigator && cumulativeDamage > 1)
{
Character.Speak(TextManager.Get("DialogAttackedByFriendly"), null, 0.50f, "attackedbyfriendly", minDurationBetweenSimilar: 30.0f);
}
if (cumulativeDamage > 1 && attacker.TeamID != Character.TeamID)
{
// If the attacker is using a low damage and high frequency weapon like a repair tool, we shouldn't use any delay.
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage), attacker, delay: realDamage > 1 ? GetReactionTime() : 0);
}
else
{
AddCombatObjective(DetermineCombatMode(Character, dmgThreshold: 20, allowOffensive: false), attacker, GetReactionTime() * 2);
bool allowOffensive = HasItem(attacker, "handlocker", out _, requireEquipped: true);
if (attackResult.Afflictions.Any(a => a is AfflictionHusk))
{
cumulativeDamage = 100;
}
// Don't react on minor (accidental) dmg done by characters that are in the same team
if (cumulativeDamage < 10)
{
if (!Character.IsSecurity && cumulativeDamage > 1)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
}
else
{
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage, dmgThreshold: 20, allowOffensive: allowOffensive), attacker, GetReactionTime() * 2);
}
}
}
}
}
else
else if (Character.IsBot)
{
AddCombatObjective(DetermineCombatMode(Character), attacker);
// Non-friendly
AddCombatObjective(DetermineCombatMode(Character, cumulativeDamage: realDamage), attacker);
}
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float dmgThreshold = 10, bool allowOffensive = true)
AIObjectiveCombat.CombatMode DetermineCombatMode(Character c, float cumulativeDamage, float dmgThreshold = 10, bool allowOffensive = true)
{
if (!IsFriendly(attacker))
{
@@ -797,13 +834,38 @@ namespace Barotrauma
}
else
{
if (GetDamageDoneByAttacker(attacker) > dmgThreshold)
if (attacker.TeamID == Character.TeamType.FriendlyNPC)
{
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
if (c.IsSecurity)
{
return Character.CombatAction != null ? Character.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.None;
}
else
{
return Character.CombatAction != null ? Character.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.None;
}
}
else
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
if (Character.IsInstigator)
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
else if (cumulativeDamage > dmgThreshold)
{
if (c.IsSecurity)
{
return c.IsSecurity && allowOffensive ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
}
else
{
return c == Character ? AIObjectiveCombat.CombatMode.Defensive : AIObjectiveCombat.CombatMode.Retreat;
}
}
else
{
return c.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat;
}
}
}
}
@@ -811,6 +873,8 @@ namespace Barotrauma
private void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character attacker, float delay = 0, Func<bool> abortCondition = null, Action onAbort = null, bool allowHoldFire = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated) { return; }
if (ObjectiveManager.CurrentObjective is AIObjectiveCombat combatObjective)
{
// Don't replace offensive mode with something else
@@ -896,6 +960,19 @@ namespace Barotrauma
SelectedAiTarget = target;
}
public override void Reset()
{
base.Reset();
objectiveManager.SortObjectives();
sortTimer = sortObjectiveInterval;
float waitDuration = characterWaitOnSwitch;
if (ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
{
waitDuration *= 2;
}
ObjectiveManager.WaitTimer = waitDuration;
}
private void CheckCrouching(float deltaTime)
{
crouchRaycastTimer -= deltaTime;
@@ -964,13 +1041,13 @@ namespace Barotrauma
return targetInventory.TryPutItem(item, targetSlot, false, false, Character);
}
public static bool NeedsDivingGear(Character character, Hull hull, out bool needsSuit)
public static bool NeedsDivingGear(Hull hull, out bool needsSuit)
{
needsSuit = false;
if (hull == null ||
hull.WaterPercentage > 80 ||
(hull.LethalPressure > 0 && character.PressureProtection <= 0) ||
(hull.ConnectedGaps.Any() && hull.ConnectedGaps.Max(g => AIObjectiveFixLeaks.GetLeakSeverity(g)) > 60))
hull.WaterPercentage > 90 ||
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.5f))
{
needsSuit = true;
return true;
@@ -987,25 +1064,28 @@ 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", out _, "oxygensource", conditionPercentage, requireEquipped: true);
public static bool HasDivingSuit(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
/// <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", out _, "oxygensource", conditionPercentage, requireEquipped: true);
public static bool HasDivingMask(Character character, float conditionPercentage = 0) => HasItem(character, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out _, AIObjectiveFindDivingGear.OXYGEN_SOURCE, conditionPercentage, requireEquipped: true);
public static bool HasItem(Character character, string tagOrIdentifier, out Item item, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
private static List<Item> matchingItems = new List<Item>();
public static bool HasItem(Character character, string tagOrIdentifier, out IEnumerable<Item> items, string containedTag = null, float conditionPercentage = 0, bool requireEquipped = false)
{
item = null;
matchingItems.Clear();
items = matchingItems;
if (character == null) { return false; }
if (character.Inventory == null) { return false; }
item = character.Inventory.FindItemByIdentifier(tagOrIdentifier) ?? character.Inventory.FindItemByTag(tagOrIdentifier);
return item != null &&
item.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(item)) &&
matchingItems = character.Inventory.FindAllItems(i => i.Prefab.Identifier == tagOrIdentifier || i.HasTag(tagOrIdentifier), recursive: true, matchingItems);
items = matchingItems;
return matchingItems.Any(i => i != null &&
i.ConditionPercentage >= conditionPercentage &&
(!requireEquipped || character.HasEquippedItem(i)) &&
(containedTag == null ||
(item.ContainedItems != null &&
item.ContainedItems.Any(i => i.HasTag(containedTag) && i.ConditionPercentage > conditionPercentage)));
(i.OwnInventory?.Items != null &&
i.OwnInventory.Items.Any(it => it != null && it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage))));
}
public static void ItemTaken(Item item, Character character)
@@ -1040,6 +1120,8 @@ namespace Barotrauma
otherCharacter.Speak(TextManager.Get("dialogstealwarning"), null, Rand.Range(0.5f, 1.0f), "thief", 10.0f);
someoneSpoke = true;
}
// Don't react if the player is taking an extinguisher and there's any fires on the sub -> allow them to use the emergency items
if (item.HasTag("fireextinguisher") && character.Submarine.GetHulls(alsoFromConnectedSubs: true).Any(h => h.FireSources.Any())) { continue; }
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
@@ -1240,6 +1322,7 @@ namespace Barotrauma
{
if (hull == null) { return 0; }
if (hull.LethalPressure > 0 && character.PressureProtection <= 0) { return 0; }
// TODO: take the visiblehulls into account?
float oxygenFactor = ignoreOxygen ? 1 : MathHelper.Lerp(0.25f, 1, hull.OxygenPercentage / 100);
float waterFactor = ignoreWater ? 1 : MathHelper.Lerp(1, 0.25f, hull.WaterPercentage / 100);
if (!character.NeedsAir)
@@ -1409,14 +1492,18 @@ namespace Barotrauma
if (target?.Item == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (character != null && c == character) { continue; }
if (character?.AIController is HumanAIController humanAi && !humanAi.IsFriendly(c)) { continue; }
if (character == null) { continue; }
if (c == character) { continue; }
if (c.IsDead || c.IsIncapacitated) { continue; }
if (c.SelectedConstruction != target.Item) { continue; }
if (!IsFriendly(character, c, onlySameTeam: true)) { continue; }
operatingCharacter = c;
// If the other character is player, don't try to operate
if (c.IsRemotePlayer || Character.Controlled == c) { return true; }
if (c.IsPlayer) { return true; }
if (c.AIController is HumanAIController controllingHumanAi)
{
Item otherTarget = controllingHumanAi.objectiveManager.GetActiveObjective<AIObjectiveOperateItem>()?.Component.Item ?? c.SelectedConstruction;
if (otherTarget != target.Item) { continue; }
// If the other character is ordered to operate the item, let him do it
if (controllingHumanAi.ObjectiveManager.IsCurrentOrder<AIObjectiveOperateItem>())
{
@@ -111,9 +111,9 @@ namespace Barotrauma
IsPathDirty = true;
}
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
public void SteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisiblity = true)
{
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter);
steering += CalculateSteeringSeek(target, weight, startNodeFilter, endNodeFilter, nodeFilter, checkVisiblity);
}
/// <summary>
@@ -158,7 +158,7 @@ namespace Barotrauma
}
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null)
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any())
@@ -172,40 +172,42 @@ namespace Barotrauma
targetDiff += subDiff;
}
}
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || currentPath.Finished || targetDiff.LengthSquared() > 1);
bool needsNewPath = character.Params.PathFinderPriority > 0.5f && (currentPath == null || currentPath.Unreachable || targetDiff.LengthSquared() > 1);
//find a new path if one hasn't been found yet or the target is different from the current target
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (findPathTimer > 0.0f) { return Vector2.Zero; }
currentTarget = target;
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
if (findPathTimer < 0)
{
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
currentTarget = target;
Vector2 currentPos = host.SimPosition;
if (character != null && character.Submarine == null)
{
currentPos -= targetHull.Submarine.SimPosition;
var targetHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(target), null, false);
if (targetHull != null && targetHull.Submarine != null)
{
currentPos -= targetHull.Submarine.SimPosition;
}
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
if (useNewPath)
{
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
}
pathFinder.InsideSubmarine = character.Submarine != null;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", startNodeFilter, endNodeFilter, nodeFilter);
bool useNewPath = currentPath == null || needsNewPath || currentPath.Finished;
if (!useNewPath && currentPath != null && currentPath.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{
// It's possible that the current path was calculated from a start point that is no longer valid.
// Therefore, let's accept also paths with a greater cost than the current, if the current node is much farther than the new start node.
useNewPath = newPath.Cost < currentPath.Cost ||
Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
if (useNewPath)
{
currentPath = newPath;
}
float priority = MathHelper.Lerp(3, 1, character.Params.PathFinderPriority);
findPathTimer = priority * Rand.Range(1.0f, 1.2f);
IsPathDirty = false;
return DiffToCurrentNode();
}
Vector2 diff = DiffToCurrentNode();
@@ -221,7 +223,7 @@ namespace Barotrauma
return Vector2.Normalize(diff) * weight;
}
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight, null, null, null);
protected override Vector2 DoSteeringSeek(Vector2 target, float weight) => CalculateSteeringSeek(target, weight);
private Vector2 DiffToCurrentNode()
{
@@ -260,7 +262,7 @@ namespace Barotrauma
}
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController;
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
var ladders = GetNextLadder();
if (canClimb && !isDiving && ladders != null && character.SelectedConstruction != ladders.Item)
{
@@ -588,7 +590,7 @@ namespace Barotrauma
//non-humanoids can't climb up ladders
if (!(character.AnimController is HumanoidAnimController))
{
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && nextNode.Waypoint.Ladders.Item.NonInteractable ||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null && (nextNode.Waypoint.Ladders.Item.NonInteractable || character.LockHands)||
(nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y)) //upper node not underwater
{
@@ -628,6 +630,7 @@ namespace Barotrauma
return penalty;
}
public static float smallRoomSize = 500;
public void Wander(float deltaTime, float wallAvoidDistance = 150, bool stayStillInTightSpace = true)
{
//steer away from edges of the hull
@@ -637,7 +640,7 @@ namespace Barotrauma
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
if (stayStillInTightSpace && roomWidth < wallAvoidDistance * 4)
if (stayStillInTightSpace && roomWidth < Math.Max(wallAvoidDistance * 3, smallRoomSize))
{
Reset();
}
@@ -125,8 +125,14 @@ namespace Barotrauma
}
}
attachCooldown -= deltaTime;
deattachTimer -= deltaTime;
if (attachCooldown > 0)
{
attachCooldown -= deltaTime;
}
if (deattachTimer > 0)
{
deattachTimer -= deltaTime;
}
Vector2 transformedAttachPos = wallAttachPos;
if (character.Submarine == null && attachTargetSubmarine != null)
@@ -255,6 +261,7 @@ namespace Barotrauma
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
{
if (attachCooldown > 0) { return; }
//already attached to something
if (attachJoints.Count > 0)
{
@@ -17,8 +17,7 @@ namespace Barotrauma
public virtual bool AllowSubObjectiveSorting => false;
/// <summary>
/// Can there be multiple objective instaces of the same type? Currently multiple instances allowed only for main objectives and the subobjectives of objetive loops.
/// In theory, there could be multiple subobjectives of same type for concurrent objectives, but that would make things more complex -> potential issues
/// Can there be multiple objective instaces of the same type?
/// </summary>
public virtual bool AllowMultipleInstances => false;
@@ -29,7 +28,10 @@ namespace Barotrauma
public virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool UnequipItems => false;
/// <summary>
/// There's a separate property for diving suit and mask: KeepDivingGearOn.
/// </summary>
public virtual bool AllowAutomaticItemUnequipping => false;
public virtual bool AllowOutsideSubmarine => false;
public virtual bool AllowInFriendlySubs => false;
@@ -173,6 +175,7 @@ namespace Barotrauma
{
if (!AllowSubObjectiveSorting) { return; }
if (subObjectives.None()) { return; }
var previousSubObjective = subObjectives.First();
subObjectives.ForEach(so => so.GetPriority());
subObjectives.Sort((x, y) => y.Priority.CompareTo(x.Priority));
if (ConcurrentObjectives)
@@ -181,7 +184,13 @@ namespace Barotrauma
}
else
{
subObjectives.First().SortSubObjectives();
var currentSubObjective = subObjectives.First();
if (previousSubObjective != currentSubObjective)
{
previousSubObjective.OnDeselected();
currentSubObjective.OnSelected();
}
currentSubObjective.SortSubObjectives();
}
}
@@ -222,7 +231,7 @@ namespace Barotrauma
private void UpdateDevotion(float deltaTime)
{
var currentObjective = objectiveManager.CurrentObjective;
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.Any(so => so == this)))
if (currentObjective != null && (currentObjective == this || currentObjective.subObjectives.FirstOrDefault() == this))
{
CumulatedDevotion += Devotion * deltaTime;
}
@@ -327,6 +336,7 @@ namespace Barotrauma
public virtual void Reset()
{
subObjectives.Clear();
isCompleted = false;
hasBeenChecked = false;
_abandon = false;
@@ -369,8 +379,7 @@ namespace Barotrauma
{
if (Check())
{
isCompleted = true;
OnCompleted();
IsCompleted = true;
}
}
return isCompleted;
@@ -1,16 +1,16 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
class AIObjectiveChargeBatteries : AIObjectiveLoop<PowerContainer>
{
public override string DebugTag => "charge batteries";
public override bool UnequipItems => true;
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<PowerContainer> batteryList;
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, string option, float priorityModifier)
@@ -26,8 +26,7 @@ namespace Barotrauma
if (item.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
if (item.ConditionPercentage <= 0) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
@@ -37,6 +36,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (Option == "charge")
{
return Targets.Max(t => MathHelper.Lerp(100, 0, Math.Abs(PowerContainer.aiRechargeTargetRatio - t.RechargeRatio)));
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
namespace Barotrauma
{
@@ -17,12 +18,11 @@ namespace Barotrauma
private readonly CombatMode initialMode;
private float seekWeaponsTimer;
private readonly float seekWeaponsInterval = 1;
private float checkWeaponsTimer;
private readonly float checkWeaponsInterval = 1;
private float ignoreWeaponTimer;
private readonly float ignoredWeaponsClearTime = 10;
// 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;
@@ -42,7 +42,7 @@ namespace Barotrauma
_weapon = value;
_weaponComponent = null;
hasAimed = false;
RemoveSubObjective(ref seekAmmunition);
RemoveSubObjective(ref seekAmmunitionObjective);
}
}
private ItemComponent _weaponComponent;
@@ -69,13 +69,14 @@ namespace Barotrauma
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private AIObjectiveContainItem seekAmmunition;
private AIObjectiveContainItem seekAmmunitionObjective;
private AIObjectiveGoTo retreatObjective;
private AIObjectiveGoTo followTargetObjective;
private AIObjectiveGetItem seekWeaponObjective;
private Hull retreatTarget;
private float coolDownTimer;
private IEnumerable<FarseerPhysics.Dynamics.Body> myBodies;
private IEnumerable<Body> myBodies;
private float aimTimer;
private bool canSeeTarget;
@@ -99,17 +100,27 @@ namespace Barotrauma
Defensive,
Offensive,
Arrest,
Retreat
Retreat,
None
}
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode == CombatMode.Offensive || initialMode == CombatMode.Arrest;
private bool TargetEliminated => Enemy == null || Enemy.Removed || Enemy.IsUnconscious;
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private bool EnemyIsClose() => Enemy != null && character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
: base(character, objectiveManager, priorityModifier)
{
if (mode == CombatMode.None)
{
#if DEBUG
DebugConsole.ThrowError("Combat mode == None");
#endif
return;
}
Enemy = enemy;
coolDownTimer = coolDown;
findSafety = objectiveManager.GetObjective<AIObjectiveFindSafety>();
@@ -145,12 +156,16 @@ namespace Barotrauma
{
base.Update(deltaTime);
ignoreWeaponTimer -= deltaTime;
seekWeaponsTimer -= deltaTime;
checkWeaponsTimer -= deltaTime;
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
}
if (findSafety != null)
{
findSafety.Priority = 0;
}
}
protected override bool Check()
@@ -164,8 +179,6 @@ namespace Barotrauma
return IsEnemyDisabled || (!IsOffensiveOrArrest && coolDownTimer <= 0);
}
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
protected override void Act(float deltaTime)
{
if (abortCondition != null && abortCondition())
@@ -178,13 +191,13 @@ namespace Barotrauma
{
coolDownTimer -= deltaTime;
}
if (seekAmmunition == null)
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
if (Mode != CombatMode.Retreat && TryArm() && !IsEnemyDisabled)
{
OperateWeapon(deltaTime);
}
if (!HoldPosition)
if (!HoldPosition && seekAmmunitionObjective == null && seekWeaponObjective == null)
{
Move(deltaTime);
}
@@ -193,8 +206,7 @@ namespace Barotrauma
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
character.Speak(TextManager.Get("DialogTargetDown"), null, 3.0f, "targetdown", 30.0f);
}
break;
case CombatMode.Arrest:
@@ -233,11 +245,11 @@ namespace Barotrauma
Weapon = null;
return false;
}
if (seekWeaponsTimer < 0)
if (checkWeaponsTimer < 0)
{
seekWeaponsTimer = seekWeaponsInterval;
checkWeaponsTimer = checkWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
var allWeapons = GetAllWeapons();
var allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
@@ -273,12 +285,12 @@ namespace Barotrauma
if (Weapon == null)
{
// No weapon found with the conditions above. Try again, now let's try to seek ammunition too
Weapon = GetWeapon(out _weaponComponent);
Weapon = FindWeapon(out _weaponComponent);
if (Weapon != null)
{
if (!CheckWeapon(seekAmmo: true))
{
if (seekAmmunition != null)
if (seekAmmunitionObjective != null)
{
// No loaded weapon, but we are trying to seek ammunition.
return false;
@@ -290,9 +302,58 @@ namespace Barotrauma
}
}
}
if (Weapon == null)
bool isAllowedToSeekWeapons = !EnemyIsClose() && character.TeamID != Character.TeamType.FriendlyNPC && IsOffensiveOrArrest;
if (!isAllowedToSeekWeapons)
{
Mode = CombatMode.Retreat;
if (WeaponComponent == null)
{
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || WeaponComponent.CombatPriority < goodWeaponPriority))
{
// Poor weapon equipped -> try to find better.
RemoveSubObjective(ref seekAmmunitionObjective);
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref followTargetObjective);
TryAddSubObjective(ref seekWeaponObjective,
constructor: () => new AIObjectiveGetItem(character, "weapon", objectiveManager, equip: true, checkInventory: false)
{
GetItemPriority = i =>
{
if (Weapon != null && (i == Weapon || i.Prefab.Identifier == Weapon.Prefab.Identifier)) { return 0; }
if (i.IsOwnedBy(character)) { return 0; }
var mw = i.GetComponent<MeleeWeapon>();
var rw = i.GetComponent<RangedWeapon>();
float priority = 0;
if (mw != null)
{
priority = mw.CombatPriority / 100;
}
else if (rw != null)
{
priority = rw.CombatPriority / 100;
}
if (i.HasTag("stunner"))
{
if (Mode == CombatMode.Arrest)
{
priority *= 2;
}
else
{
priority /= 2;
}
}
return priority;
}
},
onCompleted: () => RemoveSubObjective(ref seekWeaponObjective),
onAbandon: () =>
{
RemoveSubObjective(ref seekWeaponObjective);
Mode = CombatMode.Retreat;
});
}
}
else
@@ -342,32 +403,20 @@ namespace Barotrauma
}
}
private Item GetWeapon(out ItemComponent weaponComponent) => GetWeapon(GetAllWeapons(), out weaponComponent);
private Item FindWeapon(out ItemComponent weaponComponent) => GetWeapon(FindWeaponsFromInventory(), out weaponComponent);
private Item GetWeapon(IEnumerable<ItemComponent> weaponList, out ItemComponent weaponComponent)
{
weaponComponent = null;
float bestPriority = 0;
float lethalDmg = -1;
bool enemyIsClose = EnemyIsClose();
foreach (var weapon in weaponList)
{
// 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())
if (weapon is RangedWeapon && enemyIsClose)
{
// Close to the enemy. Ignore weapons that don't have any ammunition (-> Don't seek ammo).
continue;
@@ -420,6 +469,11 @@ namespace Barotrauma
}
}
}
else if (weapon is MeleeWeapon && weapon.Item.HasTag("stunner") && !CanMeleeStunnerStun(weapon))
{
Attack attack = GetAttackDefinition(weapon);
priority = attack?.GetTotalDamage() ?? priority / 2;
}
if (priority > bestPriority)
{
weaponComponent = weapon;
@@ -449,9 +503,7 @@ namespace Barotrauma
}
return weaponComponent.Item;
bool EnemyIsClose() => character.CurrentHull == Enemy.CurrentHull || Vector2.DistanceSquared(character.Position, Enemy.Position) < 500;
Attack GetAttackDefinition(ItemComponent weapon)
static Attack GetAttackDefinition(ItemComponent weapon)
{
Attack attack = null;
if (weapon is MeleeWeapon meleeWeapon)
@@ -465,7 +517,7 @@ namespace Barotrauma
return attack;
}
float GetLethalDamage(ItemComponent weapon)
static float GetLethalDamage(ItemComponent weapon)
{
float lethalDmg = 0;
Attack attack = GetAttackDefinition(weapon);
@@ -499,25 +551,38 @@ namespace Barotrauma
});
return attack.Stun + afflictionsStun + effectsStun;
}
bool CanMeleeStunnerStun(ItemComponent weapon)
{
// If there's an item container that takes a battery,
// assume that it's required for the stun effect
// as we can't check the status effect conditions here.
var mobileBatteryTag = "mobilebattery";
var containers = weapon.Item.Components.Where(ic => ic is ItemContainer container &&
container.ContainableItems.Any(containable => containable.Identifiers.Any(id => id.Equals(mobileBatteryTag))));
// If there's no such container, assume that the melee weapon can stun without a battery.
return containers.None() || containers.Any(container =>
(container as ItemContainer)?.Inventory.Items.Any(i => i != null && i.HasTag(mobileBatteryTag) && i.Condition > 0.0f) ?? false);
}
}
private HashSet<ItemComponent> GetAllWeapons()
private HashSet<ItemComponent> FindWeaponsFromInventory()
{
weapons.Clear();
foreach (var item in character.Inventory.Items)
{
if (item == null) { continue; }
if (ignoredWeapons.Contains(item)) { continue; }
SeekWeapons(item, weapons);
GetWeapons(item, weapons);
if (item.OwnInventory != null)
{
item.OwnInventory.Items.ForEach(i => SeekWeapons(i, weapons));
item.OwnInventory.Items.ForEach(i => GetWeapons(i, weapons));
}
}
return weapons;
}
private void SeekWeapons(Item item, ICollection<ItemComponent> weaponList)
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
@@ -571,7 +636,7 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunition);
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
{
RemoveSubObjective(ref retreatObjective);
@@ -611,6 +676,12 @@ namespace Barotrauma
private void Engage()
{
if (WeaponComponent == null)
{
RemoveFollowTarget();
SteeringManager.Reset();
return;
}
if (character.LockHands || Enemy == null)
{
Mode = CombatMode.Retreat;
@@ -619,7 +690,8 @@ namespace Barotrauma
}
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekAmmunition);
RemoveSubObjective(ref seekAmmunitionObjective);
RemoveSubObjective(ref seekWeaponObjective);
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
RemoveFollowTarget();
@@ -639,7 +711,7 @@ namespace Barotrauma
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.Stun > 2)
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs))
if (HumanAIController.HasItem(character, "handlocker", out _))
{
if (!arrestingRegistered)
{
@@ -650,16 +722,19 @@ namespace Barotrauma
}
else
{
if (character.TeamID == Character.TeamType.FriendlyNPC)
{
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs");
if (prefab != null)
{
Entity.Spawner.AddToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInOutpost = true);
}
}
RemoveFollowTarget();
SteeringManager.Reset();
}
}
else if (WeaponComponent == null)
{
RemoveFollowTarget();
SteeringManager.Reset();
}
else
if (followTargetObjective != null)
{
followTargetObjective.CloseEnough =
WeaponComponent is RangedWeapon ? 1000 :
@@ -682,8 +757,9 @@ namespace Barotrauma
private void OnArrestTargetReached()
{
if (HumanAIController.HasItem(character, "handlocker", out Item handCuffs) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
if (HumanAIController.HasItem(character, "handlocker", out IEnumerable<Item> matchingItems) && Enemy.Stun > 0 && character.CanInteractWith(Enemy))
{
var handCuffs = matchingItems.First();
if (HumanAIController.TryToMoveItem(handCuffs, Enemy.Inventory))
{
handCuffs.Equip(Enemy);
@@ -704,8 +780,7 @@ namespace Barotrauma
character.Inventory.TryPutItem(item, character, new List<InvSlotType>() { InvSlotType.Any });
}
}
// TODO: enable
//character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
character.Speak(TextManager.Get("DialogTargetArrested"), null, 3.0f, "targetarrested", 30.0f);
IsCompleted = true;
}
}
@@ -717,18 +792,19 @@ namespace Barotrauma
{
retreatTarget = null;
RemoveSubObjective(ref retreatObjective);
RemoveSubObjective(ref seekWeaponObjective);
RemoveFollowTarget();
TryAddSubObjective(ref seekAmmunition,
TryAddSubObjective(ref seekAmmunitionObjective,
constructor: () => new AIObjectiveContainItem(character, ammunitionIdentifiers, Weapon.GetComponent<ItemContainer>(), objectiveManager)
{
targetItemCount = Weapon.GetComponent<ItemContainer>().Capacity,
checkInventory = false
},
onCompleted: () => RemoveSubObjective(ref seekAmmunition),
onCompleted: () => RemoveSubObjective(ref seekAmmunitionObjective),
onAbandon: () =>
{
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunition);
RemoveSubObjective(ref seekAmmunitionObjective);
ignoredWeapons.Add(Weapon);
Weapon = null;
});
@@ -742,7 +818,8 @@ namespace Barotrauma
{
if (WeaponComponent == null) { return false; }
if (!WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) { return false; }
var containedItems = Weapon.ContainedItems;
var containedItems = Weapon.OwnInventory?.Items;
if (containedItems == null) { return true; }
// Drop empty ammo
foreach (Item containedItem in containedItems)
{
@@ -757,7 +834,7 @@ namespace Barotrauma
string[] ammunitionIdentifiers = null;
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{
ammunition = containedItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
ammunition = containedItems.FirstOrDefault(it => it != null && it.Condition > 0 && requiredItem.MatchesItem(it));
if (ammunition != null)
{
// Ammunition still remaining
@@ -831,36 +908,50 @@ namespace Barotrauma
aimTimer -= deltaTime;
return;
}
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 0) { return; }
if (Mode == CombatMode.Arrest && isLethalWeapon && Enemy.Stun > 1) { 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)
{
bool closeEnough = true;
float sqrRange = meleeWeapon.Range * meleeWeapon.Range;
if (character.AnimController.InWater)
{
if (sqrDist > sqrRange) { return; }
if (sqrDist > sqrRange)
{
closeEnough = false;
}
}
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; }
if (xDiff > meleeWeapon.Range)
{
closeEnough = false;
}
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)
if (yDiff > Math.Max(meleeWeapon.Range, 100))
{
closeEnough = false;
}
if (closeEnough && 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);
if (closeEnough)
{
SteeringManager.Reset();
character.SetInput(InputType.Shoot, false, true);
Weapon.Use(deltaTime, character);
}
else if (!character.IsFacing(Enemy.WorldPosition))
{
// Don't do the facing check if we are close to the target, because it easily causes the character to get stuck here when it flips around.
aimTimer = Rand.Range(1f, 1.5f);
}
}
else
{
@@ -916,6 +1007,19 @@ namespace Barotrauma
}
}
public override void Reset()
{
base.Reset();
hasAimed = false;
isLethalWeapon = false;
canSeeTarget = false;
seekWeaponObjective = null;
seekAmmunitionObjective = null;
retreatObjective = null;
followTargetObjective = null;
retreatTarget = null;
}
//private float CalculateEnemyStrength()
//{
// float enemyStrength = 0;
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -30,6 +29,7 @@ namespace Barotrauma
private readonly HashSet<Item> containedItems = new HashSet<Item>();
public bool AllowToFindDivingGear { get; set; } = true;
public bool AllowDangerousPressure { get; set; }
public float ConditionLevel { get; set; }
public bool Equip { get; set; }
public bool RemoveEmpty { get; set; } = true;
@@ -53,13 +53,17 @@ namespace Barotrauma
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
}
this.container = container;
}
protected override bool Check()
{
if (IsCompleted) { return true; }
if (container == null)
{
Abandon = true;
return false;
}
if (item != null)
{
return container.Inventory.Items.Contains(item);
@@ -143,8 +147,8 @@ namespace Barotrauma
DialogueIdentifier = "dialogcannotreachtarget",
TargetName = container.Item.Name
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective));
}
}
else
@@ -156,7 +160,9 @@ namespace Barotrauma
GetItemPriority = GetItemPriority,
ignoredContainerIdentifiers = ignoredContainerIdentifiers,
ignoredItems = containedItems,
AllowToFindDivingGear = this.AllowToFindDivingGear
AllowToFindDivingGear = AllowToFindDivingGear,
AllowDangerousPressure = AllowDangerousPressure,
TargetCondition = ConditionLevel
}, onAbandon: () =>
{
Abandon = true;
@@ -166,20 +172,17 @@ namespace Barotrauma
{
containedItems.Add(getItemObjective.TargetItem);
}
else
{
if (container.Inventory.FindItem(i => CheckItem(i), recursive: false) != null)
{
IsCompleted = true;
}
else
{
Abandon = true;
}
}
RemoveSubObjective(ref getItemObjective);
});
}
}
}
public override void Reset()
{
base.Reset();
getItemObjective = null;
goToObjective = null;
containedItems.Clear();
}
}
}
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
@@ -130,5 +129,12 @@ namespace Barotrauma
IsCompleted = true;
}
}
public override void Reset()
{
base.Reset();
goToObjective = null;
containObjective = null;
}
}
}
@@ -60,7 +60,7 @@ namespace Barotrauma
private float sinTime;
protected override void Act(float deltaTime)
{
var extinguisherItem = character.Inventory.FindItemByIdentifier("fireextinguisher") ?? character.Inventory.FindItemByTag("fireextinguisher");
var extinguisherItem = character.Inventory.FindItemByTag("fireextinguisher");
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
{
TryAddSubObjective(ref getExtinguisherObjective, () =>
@@ -147,5 +147,14 @@ namespace Barotrauma
}
}
}
public override void Reset()
{
base.Reset();
getExtinguisherObjective = null;
gotoObjective = null;
useExtinquisherTimer = 0;
sinTime = 0;
}
}
}
@@ -1,7 +1,6 @@
using System.Linq;
using System.Collections.Generic;
using Barotrauma.Extensions;
using System;
namespace Barotrauma
{
@@ -14,7 +13,7 @@ namespace Barotrauma
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() => objectiveManager.CurrentObjective == this ? 100 : Targets.Sum(t => GetFireSeverity(t));
protected override float TargetEvaluation() => Targets.Sum(t => GetFireSeverity(t));
public static float GetFireSeverity(Hull hull) => hull.FireSources.Sum(fs => fs.Size.X);
@@ -31,11 +30,22 @@ namespace Barotrauma
if (hull == null) { return false; }
if (hull.FireSources.None()) { return false; }
if (hull.Submarine == null) { return false; }
if (hull.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
if (character.Submarine == null) { return false; }
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
if (hull.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true)) { return false; }
if (hull.Submarine.TeamID != character.TeamID)
{
if (humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveExtinguishFires>())
{
// For orders, allow targets in the current sub (for example if the bot is inside an outpost or a wreck)
if (hull.Submarine != character.Submarine) { return false; }
}
else
{
return false;
}
}
}
return true;
}
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -9,7 +10,6 @@ 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) { }
@@ -20,7 +20,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
// TODO: sorting criteria
return 100;
return Targets.None() ? 0 : 100;
}
protected override AIObjective ObjectiveConstructor(Character target)
@@ -56,8 +56,7 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (character.Submarine != null)
{
if (target.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, true)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
}
return true;
}
@@ -1,5 +1,4 @@
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -9,21 +8,24 @@ namespace Barotrauma
public override string DebugTag => $"find diving gear ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
private readonly string gearTag;
private readonly string fallbackTag;
private AIObjectiveGetItem getDivingGear;
private AIObjectiveContainItem getOxygen;
private Item targetItem;
public static float lowOxygenThreshold = 10;
public static float MIN_OXYGEN = 10;
public static string HEAVY_DIVING_GEAR = "heavydiving";
public static string LIGHT_DIVING_GEAR = "lightdiving";
public static string OXYGEN_SOURCE = "oxygensource";
protected override bool Check() => HumanAIController.HasItem(character, gearTag, out _, "oxygensource", requireEquipped: true) || HumanAIController.HasItem(character, fallbackTag, out _, "oxygensource", requireEquipped: true);
protected override bool Check() => targetItem != null && character.HasEquippedItem(targetItem);
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
gearTag = needDivingSuit ? "divingsuit" : "divingmask";
fallbackTag = needDivingSuit ? "divingsuit" : "diving";
gearTag = needsDivingSuit ? HEAVY_DIVING_GEAR : LIGHT_DIVING_GEAR;
}
protected override void Act(float deltaTime)
@@ -33,98 +35,95 @@ namespace Barotrauma
Abandon = true;
return;
}
var item = character.Inventory.FindItemByIdentifier(gearTag, true) ?? character.Inventory.FindItemByTag(gearTag, true);
if (item == null && fallbackTag != gearTag)
{
item = character.Inventory.FindItemByTag(fallbackTag, true);
}
if (item == null || !character.HasEquippedItem(item))
targetItem = character.Inventory.FindItemByTag(gearTag, true);
if (targetItem == null || !character.HasEquippedItem(targetItem))
{
TryAddSubObjective(ref getDivingGear, () =>
{
if (item == null)
if (targetItem == null)
{
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
}
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true) { AllowToFindDivingGear = false };
return new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
AllowToFindDivingGear = false,
AllowDangerousPressure = true
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getDivingGear));
}
else
{
var containedItems = item.ContainedItems;
if (containedItems == null)
if (!DropEmptyTanks(character, targetItem, out Item[] containedItems))
{
#if DEBUG
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + item + "\" has no proper inventory");
DebugConsole.ThrowError($"{character.Name}: AIObjectiveFindDivingGear failed - the item \"" + targetItem + "\" has no proper inventory");
#endif
Abandon = true;
return;
}
// Drop empty tanks
foreach (Item containedItem in containedItems)
if (containedItems.None(it => it != null && it.HasTag(OXYGEN_SOURCE) && it.Condition > MIN_OXYGEN))
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
// No valid oxygen source loaded.
// Seek oxygen that has min 10% condition left.
TryAddSubObjective(ref getOxygen, () =>
{
containedItem.Drop(character);
}
}
if (containedItems.None(it => it.HasTag("oxygensource") && it.Condition > lowOxygenThreshold))
{
var oxygenTank = character.Inventory.FindItemByTag("oxygensource", true);
if (oxygenTank != null)
{
var container = item.GetComponent<ItemContainer>();
if (container.Item.ParentInventory == character.Inventory)
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
{
if (!container.Inventory.CanBePut(oxygenTank))
{
Abandon = true;
}
character.Inventory.RemoveItem(oxygenTank);
if (!container.Inventory.TryPutItem(oxygenTank, null))
{
oxygenTank.Drop(character);
Abandon = true;
}
}
else
{
container.Combine(oxygenTank, character);
}
}
else
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
ConditionLevel = MIN_OXYGEN
};
},
onAbandon: () =>
{
// Seek oxygen that has min 10% condition left
// Try to seek any oxygen sources.
TryAddSubObjective(ref getOxygen, () =>
{
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
return new AIObjectiveContainItem(character, OXYGEN_SOURCE, targetItem.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC)
{
AllowToFindDivingGear = false,
ConditionLevel = lowOxygenThreshold
AllowDangerousPressure = true,
ConditionLevel = 0
};
},
onAbandon: () =>
{
// Try to seek any oxygen sources
TryAddSubObjective(ref getOxygen, () =>
{
return new AIObjectiveContainItem(character, new string[] { "oxygensource" }, item.GetComponent<ItemContainer>(), objectiveManager)
{
AllowToFindDivingGear = false,
ConditionLevel = 0
};
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
},
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
},
onCompleted: () => RemoveSubObjective(ref getOxygen));
}
}
}
/// <summary>
/// Returns false only when no inventory can be found from the item.
/// </summary>
public static bool DropEmptyTanks(Character actor, Item target, out Item[] containedItems)
{
containedItems = target.OwnInventory?.Items;
if (containedItems == null)
{
return false;
}
foreach (Item containedItem in containedItems)
{
if (containedItem == null) { continue; }
if (containedItem.Condition <= 0.0f)
{
containedItem.Drop(actor);
}
}
return true;
}
public override void Reset()
{
base.Reset();
getDivingGear = null;
getOxygen = null;
targetItem = null;
}
}
}
@@ -14,7 +14,8 @@ namespace Barotrauma
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool IsLoop { get => true; set => throw new System.Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace); }
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace); }
// TODO: expose?
const float priorityIncrease = 100;
@@ -48,7 +49,7 @@ namespace Barotrauma
}
else
{
if (HumanAIController.NeedsDivingGear(character, character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
if (HumanAIController.NeedsDivingGear(character.CurrentHull, out _) && !HumanAIController.HasDivingGear(character))
{
Priority = 100;
}
@@ -64,6 +65,14 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
if (retryTimer > 0)
{
retryTimer -= deltaTime;
if (retryTimer <= 0)
{
retryCounter = 0;
}
}
if (resetPriority)
{
Priority = 0;
@@ -92,39 +101,56 @@ namespace Barotrauma
private Hull currentSafeHull;
private Hull previousSafeHull;
private bool cannotFindSafeHull;
private bool cannotFindDivingGear;
private readonly int findDivingGearAttempts = 2;
private int retryCounter;
private readonly float retryResetTime = 5;
private float retryTimer;
protected override void Act(float deltaTime)
{
var currentHull = character.CurrentHull;
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0;
if (!dangerousPressure)
bool dangerousPressure = currentHull == null || currentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (!character.LockHands && (!dangerousPressure || cannotFindSafeHull))
{
// Don't try to seek diving gear if the pressure is dangerous. Just get out.
bool needsDivingGear = HumanAIController.NeedsDivingGear(character, currentHull, out bool needsDivingSuit);
bool needsDivingGear = HumanAIController.NeedsDivingGear(currentHull, out bool needsDivingSuit);
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
}
if (needsEquipment && divingGearObjective == null && !character.LockHands)
if (needsEquipment)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
if (cannotFindDivingGear && retryCounter < findDivingGearAttempts)
{
retryTimer = retryResetTime;
retryCounter++;
needsDivingSuit = !needsDivingSuit;
RemoveSubObjective(ref divingGearObjective);
}
if (divingGearObjective == null)
{
cannotFindDivingGear = false;
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref divingGearObjective,
constructor: () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () =>
{
searchHullTimer = Math.Min(1, searchHullTimer);
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
cannotFindDivingGear = true;
// Don't reset the diving gear objective, because it's possible that there is no diving gear -> seek a safe hull and then reset so that we can check again.
},
onCompleted: () =>
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
RemoveSubObjective(ref divingGearObjective);
});
}
}
}
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
@@ -142,6 +168,7 @@ namespace Barotrauma
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != Character.TeamType.FriendlyNPC);
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
@@ -153,35 +180,38 @@ namespace Barotrauma
RemoveSubObjective(ref goToObjective);
}
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
AllowGoingOutside = HumanAIController.HasDivingSuit(character, conditionPercentage: 50)
},
onCompleted: () =>
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
if (cannotFindDivingGear)
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(character, currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
searchHullTimer = Math.Min(1, searchHullTimer);
}
RemoveSubObjective(ref goToObjective);
// If diving gear objective failed, let's reset it here.
RemoveSubObjective(ref divingGearObjective);
},
onAbandon: () =>
}
},
onAbandon: () =>
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
{
// Don't ignore any hulls if outside, because apparently it happens that we can't find a path, in which case we just want to try again.
// If we ignore the hull, it might be the only airlock in the target sub, which ignores the whole sub.
if (currentHull != null && goToObjective != null)
if (goToObjective.Target is Hull hull)
{
if (goToObjective.Target is Hull hull)
{
HumanAIController.UnreachableHulls.Add(hull);
}
HumanAIController.UnreachableHulls.Add(hull);
}
RemoveSubObjective(ref goToObjective);
});
}
RemoveSubObjective(ref goToObjective);
});
}
else
{
@@ -194,12 +224,14 @@ namespace Barotrauma
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
// TODO: optimize
foreach (FireSource fireSource in HumanAIController.VisibleHulls.SelectMany(h => h.FireSources))
foreach (Hull hull in HumanAIController.VisibleHulls)
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
foreach (FireSource fireSource in hull.FireSources)
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
foreach (Character enemy in Character.CharacterList)
{
@@ -335,5 +367,17 @@ namespace Barotrauma
}
return bestHull;
}
public override void Reset()
{
base.Reset();
goToObjective = null;
divingGearObjective = null;
currentSafeHull = null;
previousSafeHull = null;
retryCounter = 0;
cannotFindDivingGear = false;
cannotFindSafeHull = false;
}
}
}
@@ -20,12 +20,12 @@ namespace Barotrauma
private AIObjectiveGoTo gotoObjective;
private AIObjectiveOperateItem operateObjective;
public bool IgnoreSeverityAndDistance { get; private set; }
public readonly bool isPriority;
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool ignoreSeverityAndDistance = false) : base (character, objectiveManager, priorityModifier)
public AIObjectiveFixLeak(Gap leak, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false) : base (character, objectiveManager, priorityModifier)
{
Leak = leak;
IgnoreSeverityAndDistance = ignoreSeverityAndDistance;
this.isPriority = isPriority;
}
protected override bool Check() => Leak.Open <= 0 || Leak.Removed;
@@ -41,15 +41,21 @@ namespace Barotrauma
{
Priority = 0;
}
else if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeak>()?.Leak == Leak))
{
Priority = 0;
Abandon = true;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = IgnoreSeverityAndDistance || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, xDist + yDist * 3.0f));
float severity = IgnoreSeverityAndDistance ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float max = Math.Min((AIObjectiveManager.OrderPriority - 1), 90);
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
@@ -68,7 +74,7 @@ namespace Barotrauma
}
else
{
var containedItems = weldingTool.ContainedItems;
var containedItems = weldingTool.OwnInventory?.Items;
if (containedItems == null)
{
#if DEBUG
@@ -86,7 +92,7 @@ namespace Barotrauma
containedItem.Drop(character);
}
}
if (containedItems.None(i => i.HasTag("weldingfuel") && i.Condition > 0.0f))
if (containedItems.None(i => i != null && i.HasTag("weldingfuel") && i.Condition > 0.0f))
{
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, "weldingfuel", weldingTool.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onAbandon: () => Abandon = true,
@@ -130,11 +136,10 @@ namespace Barotrauma
{
TryAddSubObjective(ref gotoObjective, () => new AIObjectiveGoTo(Leak, character, objectiveManager)
{
// 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
TargetName = Leak.FlowTargetHull?.DisplayName,
CheckVisibility = false
},
onAbandon: () =>
{
@@ -153,5 +158,14 @@ namespace Barotrauma
onCompleted: () => RemoveSubObjective(ref gotoObjective));
}
}
public override void Reset()
{
base.Reset();
getWeldingTool = null;
refuelObjective = null;
gotoObjective = null;
operateObjective = null;
}
}
}
@@ -37,11 +37,9 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
int totalLeaks = Targets.Count();
if (totalLeaks == 0) { return 0; }
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated, onlyBots: true);
bool anyFixers = otherFixers > 0;
if (objectiveManager.CurrentOrder == this)
{
@@ -50,6 +48,8 @@ namespace Barotrauma
}
else
{
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
{
@@ -62,7 +62,7 @@ namespace Barotrauma
protected override IEnumerable<Gap> GetList() => Gap.GapList;
protected override AIObjective ObjectiveConstructor(Gap gap)
=> new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, ignoreSeverityAndDistance: gap.FlowTargetHull == PrioritizedHull);
=> new AIObjectiveFixLeak(gap, character, objectiveManager, priorityModifier: PriorityModifier, isPriority: gap.FlowTargetHull == PrioritizedHull);
protected override void OnObjectiveCompleted(AIObjective objective, Gap target)
=> HumanAIController.RemoveTargets<AIObjectiveFixLeaks, Gap>(character, target);
@@ -75,8 +75,7 @@ namespace Barotrauma
if (gap.Submarine.TeamID != character.TeamID) { return false; }
if (character.Submarine != null)
{
if (gap.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(gap, true)) { return false; }
if (!character.Submarine.IsConnectedTo(gap.Submarine)) { return false; }
}
return true;
}
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -17,10 +16,9 @@ namespace Barotrauma
public Func<Item, float> GetItemPriority;
public Func<Item, bool> ItemFilter;
public float TargetCondition { get; set; } = 1;
public bool AllowDangerousPressure { get; set; }
//can be either tags or identifiers
private string[] itemIdentifiers;
public IEnumerable<string> Identifiers => itemIdentifiers;
private string[] identifiersOrTags;
//if the item can't be found, spawn it in the character's inventory (used by outpost NPCs)
private bool spawnItemIfNotFound = false;
@@ -50,26 +48,26 @@ namespace Barotrauma
moveToTarget = targetItem?.GetRootInventoryOwner();
}
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 identifierOrTag, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: this(character, new string[] { identifierOrTag }, objectiveManager, equip, checkInventory, priorityModifier, spawnItemIfNotFound) { }
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
public AIObjectiveGetItem(Character character, string[] identifiersOrTags, 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.identifiersOrTags = identifiersOrTags;
this.spawnItemIfNotFound = spawnItemIfNotFound;
for (int i = 0; i < itemIdentifiers.Length; i++)
for (int i = 0; i < identifiersOrTags.Length; i++)
{
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
identifiersOrTags[i] = identifiersOrTags[i].ToLowerInvariant();
}
this.checkInventory = checkInventory;
}
private bool CheckInventory()
{
if (itemIdentifiers == null) { return false; }
if (identifiersOrTags == null) { return false; }
var item = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (item != null)
{
@@ -86,7 +84,7 @@ namespace Barotrauma
Abandon = true;
return;
}
if (itemIdentifiers != null && !isDoneSeeking)
if (identifiersOrTags != null && !isDoneSeeking)
{
if (checkInventory)
{
@@ -97,14 +95,18 @@ namespace Barotrauma
}
if (!isDoneSeeking)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0;
if (dangerousPressure)
if (!AllowDangerousPressure)
{
bool dangerousPressure = character.CurrentHull == null || character.CurrentHull.LethalPressure > 0 && character.PressureProtection <= 0;
if (dangerousPressure)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Seeking item aborted, because the pressure is dangerous.", Color.Yellow);
string itemName = targetItem != null ? targetItem.Name : identifiersOrTags.FirstOrDefault();
DebugConsole.NewMessage($"{character.Name}: Seeking item ({itemName}) aborted, because the pressure is dangerous.", Color.Yellow);
#endif
Abandon = true;
return;
Abandon = true;
return;
}
}
FindTargetItem();
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
@@ -174,34 +176,20 @@ namespace Barotrauma
return;
}
if (equip)
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
{
if (HumanAIController.TryToMoveItem(targetItem, character.Inventory))
if (equip)
{
targetItem.Equip(character);
IsCompleted = true;
}
else
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
Abandon = true;
}
IsCompleted = true;
}
else
{
if (character.Inventory.TryPutItem(targetItem, character, new List<InvSlotType>() { InvSlotType.Any }))
{
IsCompleted = true;
}
else
{
Abandon = true;
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
DebugConsole.NewMessage($"{character.Name}: Failed to equip/move the item '{targetItem.Name}' into the character inventory. Aborting.", Color.Red);
#endif
}
Abandon = true;
}
}
else if (moveToTarget != null)
@@ -228,7 +216,7 @@ namespace Barotrauma
private void FindTargetItem()
{
if (itemIdentifiers == null)
if (identifiersOrTags == null)
{
if (targetItem == null)
{
@@ -244,18 +232,16 @@ namespace Barotrauma
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
Submarine mySub = character.Submarine;
if (itemSub == null) { continue; }
if (itemSub.TeamID != character.TeamID) { continue; }
if (mySub == null) { continue; }
if (itemSub.TeamID != mySub.TeamID && itemSub.TeamID != character.TeamID) { continue; }
if (!CheckItem(item)) { continue; }
if (ignoredContainerIdentifiers != null && item.Container != null)
{
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
}
if (character.Submarine != null)
{
if (itemSub.Info.Type != character.Submarine.Info.Type) { continue; }
if (character.Submarine.GetConnectedSubs().None(s => s == itemSub && itemSub.TeamID == character.TeamID && itemSub.Info.Type == character.Submarine.Info.Type)) { continue; }
}
if (!mySub.IsConnectedTo(itemSub)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
float itemPriority = 1;
if (GetItemPriority != null)
@@ -283,10 +269,10 @@ namespace Barotrauma
{
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 (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && identifiersOrTags.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)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: Cannot find the item with the following identifier(s): {string.Join(", ", identifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
#endif
Abandon = true;
}
@@ -305,7 +291,7 @@ namespace Barotrauma
else
{
#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(", ", identifiersOrTags)}", Color.Yellow);
#endif
Abandon = true;
}
@@ -320,7 +306,7 @@ namespace Barotrauma
{
return character.HasItem(targetItem, equip);
}
else if (itemIdentifiers != null)
else if (identifiersOrTags != null)
{
var matchingItem = character.Inventory.FindItem(i => CheckItem(i), recursive: true);
if (matchingItem != null)
@@ -338,13 +324,13 @@ namespace Barotrauma
if (ignoredItems.Contains(item)) { return false; };
if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; }
return itemIdentifiers.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
return identifiersOrTags.Any(id => id == item.Prefab.Identifier || item.HasTag(id));
}
public override void Reset()
{
base.Reset();
RemoveSubObjective(ref goToObjective);
goToObjective = null;
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
@@ -1,7 +1,6 @@
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -31,7 +30,7 @@ namespace Barotrauma
public bool mimic;
private float _closeEnough = 50;
private readonly float minDistance = 25;
private readonly float minDistance = 50;
/// <summary>
/// Display units
/// </summary>
@@ -43,6 +42,8 @@ namespace Barotrauma
_closeEnough = Math.Max(minDistance, value);
}
}
public bool CheckVisibility { get; set; }
public bool IgnoreIfTargetDead { get; set; }
public bool AllowGoingOutside { get; set; }
@@ -103,7 +104,7 @@ namespace Barotrauma
else if (Target is Character)
{
//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);
CloseEnough = Math.Max(closeEnough, MathUtils.NearlyEqual(closeEnough, 0.0f) ? AIObjectiveGetItem.DefaultReach : minDistance);
}
else
{
@@ -138,7 +139,7 @@ namespace Barotrauma
}
Target = Character.Controlled;
}
if (Target == character)
if (Target == character || character.SelectedBy != null && HumanAIController.IsFriendly(character.SelectedBy))
{
// Wait
character.AIController.SteeringManager.Reset();
@@ -207,7 +208,7 @@ namespace Barotrauma
{
Character followTarget = Target as Character;
bool needsDivingSuit = targetIsOutside;
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(character, targetHull, out needsDivingSuit);
bool needsDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
if (!needsDivingGear && mimic)
{
if (HumanAIController.HasDivingSuit(followTarget))
@@ -223,17 +224,25 @@ namespace Barotrauma
bool needsEquipment = false;
if (needsDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
needsEquipment = !HumanAIController.HasDivingSuit(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
}
else if (needsDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.lowOxygenThreshold);
needsEquipment = !HumanAIController.HasDivingGear(character, AIObjectiveFindDivingGear.MIN_OXYGEN);
}
if (needsEquipment)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
if (findDivingGear != null && !findDivingGear.CanBeCompleted)
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
else
{
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit, objectiveManager),
onCompleted: () => RemoveSubObjective(ref findDivingGear));
}
return;
}
}
@@ -262,11 +271,14 @@ namespace Barotrauma
{
if (n.Waypoint.isObstructed) { return false; }
return (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null);
}, endNodeFilter, nodeFilter);
}, endNodeFilter, nodeFilter, CheckVisibility);
if (!isInside && PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable)
{
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: 5, weight: 15);
}
}
}
else
@@ -392,5 +404,11 @@ namespace Barotrauma
HumanAIController.FaceTarget(Target);
base.OnCompleted();
}
public override void Reset()
{
base.Reset();
findDivingGear = null;
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
class AIObjectiveIdle : AIObjective
{
public override string DebugTag => "idle";
public override bool UnequipItems => true;
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
private BehaviorType behavior;
@@ -69,6 +69,8 @@ namespace Barotrauma
const float chairCheckInterval = 5.0f;
private float chairCheckTimer;
private float autonomousObjectiveRetryTimer = 10;
private readonly List<Hull> targetHulls = new List<Hull>(20);
private readonly List<float> hullWeights = new List<float>(20);
@@ -88,11 +90,6 @@ namespace Barotrauma
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)
{
//Random = Rand.Range(0.5f, 1.5f);
@@ -149,6 +146,18 @@ namespace Barotrauma
{
if (PathSteering == null) { return; }
if (objectiveManager.FailedAutonomousObjectives)
{
if (autonomousObjectiveRetryTimer > 0)
{
autonomousObjectiveRetryTimer -= deltaTime;
}
else
{
objectiveManager.CreateAutonomousObjectives();
}
}
//don't keep dragging others when idling
if (character.SelectedCharacter != null)
{
@@ -160,13 +169,34 @@ namespace Barotrauma
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
bool IsSteeringFinished() => PathSteering.CurrentPath != null && PathSteering.CurrentPath.Finished;
bool IsSteeringFinished() => PathSteering.CurrentPath != null && (PathSteering.CurrentPath.Finished || PathSteering.CurrentPath.Unreachable);
if (currentTargetIsInvalid || currentTarget == null || IsSteeringFinished() && (IsForbidden(character.CurrentHull) || IsInWrongSub()))
if (currentTarget != null && !currentTargetIsInvalid)
{
//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();
if (character.TeamID == Character.TeamType.FriendlyNPC)
{
if (currentTarget.Submarine.TeamID != character.TeamID)
{
currentTargetIsInvalid = true;
}
}
else
{
if (currentTarget.Submarine != character.Submarine)
{
currentTargetIsInvalid = true;
}
}
}
if (currentTargetIsInvalid || currentTarget == null || IsForbidden(character.CurrentHull) && IsSteeringFinished())
{
if (newTargetTimer > timerMargin)
{
//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 (character.IsClimbing)
{
@@ -200,7 +230,8 @@ namespace Barotrauma
{
//choose a random available hull
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isCurrentHullAllowed = !IsInWrongSub() && !IsForbidden(character.CurrentHull);
bool isInWrongSub = character.TeamID == Character.TeamType.FriendlyNPC && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, errorMsgStr: $"AIObjectiveIdle {character.DisplayName}", nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
@@ -285,12 +316,12 @@ namespace Barotrauma
if (standStillTimer > 0.0f)
{
walkDuration = Rand.Range(walkDurationMin, walkDurationMax);
if (character.CurrentHull != null && character.CurrentHull.Rect.Width > 150 && tooCloseCharacter == null)
var currentHull = character.CurrentHull;
if (currentHull != null && currentHull.Rect.Width > IndoorsSteeringManager.smallRoomSize / 2 && tooCloseCharacter == null)
{
foreach (Character c in Character.CharacterList)
{
if (c == character || !c.IsBot || c.CurrentHull != character.CurrentHull || !(c.AIController is HumanAIController humanAI)) { continue; }
if (c == character || !c.IsBot || c.CurrentHull != 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))
@@ -303,7 +334,7 @@ namespace Barotrauma
tooCloseCharacter = null;
break;
}
tooCloseCharacter = c;
tooCloseCharacter = c;
}
HumanAIController.FaceTarget(c);
}
@@ -313,9 +344,24 @@ namespace Barotrauma
{
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));
if (Math.Abs(diff.X) > 0 &&
(character.WorldPosition.X > currentHull.WorldRect.Right - 50 || character.WorldPosition.X < currentHull.WorldRect.Left + 50))
{
// Between a wall and a character -> move away
tooCloseCharacter = null;
PathSteering.Reset();
standStillTimer = 0;
walkDuration = Math.Min(walkDuration, walkDurationMin);
if (Behavior != BehaviorType.StayInHull && (currentHull.Size.X < IndoorsSteeringManager.smallRoomSize || currentHull.Size.X < (IndoorsSteeringManager.smallRoomSize / 2 * Character.CharacterList.Count(c => c.CurrentHull == currentHull))))
{
// Small room -> find another
newTargetTimer = Math.Min(newTargetTimer, 1);
}
}
else
{
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
}
return;
}
else
@@ -329,14 +375,13 @@ namespace Barotrauma
{
foreach (Item item in Item.ItemList)
{
if (item.CurrentHull != character.CurrentHull || !item.HasTag("chair")) { continue; }
if (item.CurrentHull != 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)
@@ -376,7 +421,9 @@ namespace Barotrauma
}
if (IsForbidden(hull)) { continue; }
// Check that the hull is linked
if (!character.Submarine.GetConnectedSubs().Contains(hull.Submarine)) { continue; }
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
// Ignore very narrow hulls.
if (hull.RectWidth < 200) { continue; }
// Ignore hulls that are too low to stand inside.
if (character.AnimController is HumanoidAnimController animController)
{
@@ -388,13 +435,14 @@ namespace Barotrauma
if (!targetHulls.Contains(hull))
{
targetHulls.Add(hull);
float weight = hull.Volume;
float weight = hull.RectWidth;
// Prefer rooms that are closer. Avoid rooms that are not in the same level.
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 2500, dist));
weight *= distanceFactor;
float waterFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, 100, hull.WaterPercentage * 2));
weight *= distanceFactor * waterFactor;
hullWeights.Add(weight);
}
}
@@ -418,5 +466,16 @@ namespace Barotrauma
if (hullName == null) { return false; }
return hullName.Contains("ballast", StringComparison.OrdinalIgnoreCase) || hullName.Contains("airlock", StringComparison.OrdinalIgnoreCase);
}
public override void Reset()
{
base.Reset();
currentTarget = null;
searchingNewHull = false;
tooCloseCharacter = null;
targetHulls.Clear();
hullWeights.Clear();
autonomousObjectiveRetryTimer = 10;
}
}
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
@@ -89,10 +88,6 @@ namespace Barotrauma
{
syncTimer -= deltaTime;
}
if (Objectives.None() && Targets.Any(t => !ignoreList.Contains(t)))
{
CreateObjectives();
}
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
@@ -113,7 +108,7 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
if (character.LockHands || character.Submarine == null || Targets.None())
if (character.LockHands || character.Submarine == null)
{
Priority = 0;
}
@@ -92,6 +92,7 @@ namespace Barotrauma
}
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
public bool FailedAutonomousObjectives { get; private set; }
private void ClearIgnored()
{
@@ -118,10 +119,11 @@ namespace Barotrauma
}
DelayedObjectives.Clear();
Objectives.Clear();
FailedAutonomousObjectives = false;
AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this));
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjective)
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
var orderPrefab = Order.GetPrefab(autonomousObjective.identifier);
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.identifier}'"); }
@@ -129,6 +131,7 @@ namespace Barotrauma
var order = new Order(orderPrefab, item ?? character.CurrentHull as Entity,
item?.Components.FirstOrDefault(ic => ic.GetType() == orderPrefab.ItemComponentType), orderGiver: character);
if (order == null) { continue; }
if (autonomousObjective.ignoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != Character.TeamType.FriendlyNPC) { continue; }
var objective = CreateObjective(order, autonomousObjective.option, character, isAutonomous: true, autonomousObjective.priorityModifier);
if (objective != null && objective.CanBeCompleted)
{
@@ -220,7 +223,7 @@ namespace Barotrauma
if (objective.IsCompleted)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightGreen);
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it is completed.", Color.LightBlue);
#endif
Objectives.Remove(objective);
}
@@ -230,6 +233,7 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Removing objective {objective.DebugTag}, because it cannot be completed.", Color.Red);
#endif
Objectives.Remove(objective);
FailedAutonomousObjectives = true;
}
else
{
@@ -286,6 +290,7 @@ namespace Barotrauma
}
else
{
// This should be redundant, because all the objectives are reset when they are selected as active.
CurrentOrder.Reset();
}
}
@@ -8,8 +8,9 @@ namespace Barotrauma
{
class AIObjectiveOperateItem : AIObjective
{
public override string DebugTag => "operate item";
public override bool UnequipItems => true;
public override string DebugTag => $"operate item {component.Name}";
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
private ItemComponent component, controller;
private Entity operateTarget;
@@ -22,6 +23,8 @@ namespace Barotrauma
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
public override bool IsDuplicate<T>(T otherObjective) => base.IsDuplicate(otherObjective) && otherObjective is AIObjectiveOperateItem operateObjective && operateObjective.component == component;
public Entity OperateTarget => operateTarget;
public ItemComponent Component => component;
@@ -32,7 +35,7 @@ namespace Barotrauma
public override float GetPriority()
{
if (!IsAllowed)
if (!IsAllowed || character.LockHands)
{
Priority = 0;
return Priority;
@@ -43,7 +46,8 @@ namespace Barotrauma
}
else
{
if (objectiveManager.CurrentOrder == this)
bool isOrder = objectiveManager.CurrentOrder == this;
if (isOrder)
{
Priority = AIObjectiveManager.OrderPriority;
}
@@ -61,6 +65,16 @@ namespace Barotrauma
var reactor = component?.Item.GetComponent<Reactor>();
if (reactor != null)
{
if (!isOrder)
{
if (reactor.LastUserWasPlayer && character.TeamID != Character.TeamType.FriendlyNPC ||
HumanAIController.IsTrueForAnyCrewMember(c =>
c.ObjectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder.GetTarget() == target))
{
Priority = 0;
return Priority;
}
}
switch (Option)
{
case "shutdown":
@@ -73,7 +87,7 @@ namespace Barotrauma
case "powerup":
// Check that we don't already have another order that is targeting the same item.
// Without this the autonomous objective will tell the bot to turn the reactor on again.
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target)
if (objectiveManager.CurrentOrder is AIObjectiveOperateItem operateOrder && operateOrder != this && operateOrder.GetTarget() == target && operateOrder.Option != Option)
{
Priority = 0;
return Priority;
@@ -82,7 +96,7 @@ namespace Barotrauma
}
}
if (targetItem.CurrentHull == null ||
targetItem.Submarine != character.Submarine && objectiveManager.CurrentOrder != this ||
targetItem.Submarine != character.Submarine && !isOrder ||
targetItem.CurrentHull.FireSources.Any() ||
HumanAIController.IsItemOperatedByAnother(target, out _) ||
Character.CharacterList.Any(c => c.CurrentHull == targetItem.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c)))
@@ -92,7 +106,12 @@ namespace Barotrauma
else
{
float value = CumulatedDevotion + (AIObjectiveManager.OrderPriority * PriorityModifier);
float max = objectiveManager.CurrentOrder == this ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
float max = isOrder ? MathHelper.Min(AIObjectiveManager.OrderPriority, 90) : AIObjectiveManager.RunPriority - 1;
if (!isOrder && reactor != null && reactor.PowerOn && Option == "powerup")
{
// Decrease the priority when targeting a reactor that is already on.
value /= 2;
}
Priority = MathHelper.Clamp(value, 0, max);
}
}
@@ -142,6 +161,15 @@ namespace Barotrauma
// Don't abandon
return;
}
if (operateTarget != null)
{
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{
// Another crew member is already targeting this entity.
Abandon = true;
return;
}
}
if (target.CanBeSelected)
{
if (character.CanInteractWith(target.Item, out _, checkLinked: false))
@@ -227,5 +255,12 @@ namespace Barotrauma
}
protected override bool Check() => isDoneOperating && !IsLoop;
public override void Reset()
{
base.Reset();
goToObjective = null;
getItemObjective = null;
}
}
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -10,7 +11,7 @@ namespace Barotrauma
{
public override string DebugTag => "pump water";
public override bool KeepDivingGearOn => true;
public override bool UnequipItems => true;
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<Pump> pumpList;
@@ -35,8 +36,7 @@ namespace Barotrauma
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
if (character.Submarine != null)
{
if (pump.Item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(pump.Item, true)) { return false; }
if (!character.Submarine.IsConnectedTo(pump.Item.Submarine)) { return false; }
}
if (Character.CharacterList.Any(c => c.CurrentHull == pump.Item.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
if (IsReady(pump)) { return false; }
@@ -54,6 +54,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 0; }
if (Option == "stoppumping")
{
return Targets.Max(t => MathHelper.Lerp(0, 100, Math.Abs(t.FlowPercentage / 100)));
@@ -9,7 +9,6 @@ namespace Barotrauma
class AIObjectiveRepairItem : AIObjective
{
public override string DebugTag => "repair item";
public override bool KeepDivingGearOn => true;
public Item Item { get; private set; }
@@ -18,9 +17,11 @@ namespace Barotrauma
private float previousCondition = -1;
private RepairTool repairTool;
private bool IsRepairing => character.SelectedConstruction == Item && Item.GetComponent<Repairable>()?.CurrentFixer == character;
private bool IsRepairing() => IsRepairing(character, Item);
private readonly bool isPriority;
public static bool IsRepairing(Character character, Item item) => character.SelectedConstruction == item && item.Repairables.Any(r => r.CurrentFixer == character);
public AIObjectiveRepairItem(Character character, Item item, AIObjectiveManager objectiveManager, float priorityModifier = 1, bool isPriority = false)
: base(character, objectiveManager, priorityModifier)
{
@@ -49,12 +50,15 @@ namespace Barotrauma
float yDist = Math.Abs(character.WorldPosition.Y - Item.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 5 : 0;
float dist = Math.Abs(character.WorldPosition.X - Item.WorldPosition.X) + yDist;
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 5000, dist));
distanceFactor = MathHelper.Lerp(1, 0.25f, MathUtils.InverseLerp(0, 4000, dist));
}
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);
float requiredSuccessFactor = objectiveManager.IsCurrentOrder<AIObjectiveRepairItems>() ? 0 : AIObjectiveRepairItems.RequiredSuccessFactor;
float severity = isPriority ? 1 : AIObjectiveRepairItems.GetTargetPriority(Item, character, requiredSuccessFactor) / 100;
bool isSelected = IsRepairing();
float selectedBonus = isSelected ? 100 - MaxDevotion : 0;
float devotion = (CumulatedDevotion + selectedBonus) / 100;
float reduction = isPriority ? 1 : isSelected ? 2 : 3;
float max = MathHelper.Min(AIObjectiveManager.OrderPriority - reduction, 90);
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
}
return Priority;
@@ -63,7 +67,7 @@ namespace Barotrauma
protected override bool Check()
{
IsCompleted = Item.IsFullCondition;
if (IsCompleted && IsRepairing)
if (IsCompleted && IsRepairing())
{
character?.Speak(TextManager.GetWithVariable("DialogItemRepaired", "[itemname]", Item.Name, true), null, 0.0f, "itemrepaired", 10.0f);
}
@@ -95,7 +99,7 @@ namespace Barotrauma
}
if (repairTool != null)
{
var containedItems = repairTool.Item.ContainedItems;
var containedItems = repairTool.Item.OwnInventory?.Items;
if (containedItems == null)
{
#if DEBUG
@@ -118,13 +122,13 @@ namespace Barotrauma
foreach (RelatedItem requiredItem in repairTool.requiredItems[RelatedItem.RelationType.Contained])
{
item = requiredItem;
fuel = containedItems.FirstOrDefault(it => it.Condition > 0.0f && requiredItem.MatchesItem(it));
fuel = containedItems.FirstOrDefault(it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
if (fuel != null) { break; }
}
if (fuel == null)
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager),
TryAddSubObjective(ref refuelObjective, () => new AIObjectiveContainItem(character, item.Identifiers, repairTool.Item.GetComponent<ItemContainer>(), objectiveManager, spawnItemIfNotFound: character.TeamID == Character.TeamType.FriendlyNPC),
onCompleted: () => RemoveSubObjective(ref refuelObjective),
onAbandon: () => Abandon = true);
return;
@@ -142,7 +146,7 @@ namespace Barotrauma
if (repairable.CurrentFixer != null && repairable.CurrentFixer != character)
{
// Someone else is repairing the target. Abandon the objective if the other is better at this than us.
Abandon = repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
Abandon = repairable.CurrentFixer.IsPlayer || repairable.DegreeOfSuccess(character) < repairable.DegreeOfSuccess(repairable.CurrentFixer);
}
if (!Abandon)
{
@@ -166,7 +170,7 @@ namespace Barotrauma
}
if (Abandon)
{
if (IsRepairing)
if (IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -201,7 +205,7 @@ namespace Barotrauma
onAbandon: () =>
{
Abandon = true;
if (IsRepairing)
if (IsRepairing())
{
character.Speak(TextManager.GetWithVariable("DialogCannotRepair", "[itemname]", Item.Name, true), null, 0.0f, "cannotrepair", 10.0f);
}
@@ -251,5 +255,14 @@ namespace Barotrauma
repairTool.Use(deltaTime, character);
}
}
public override void Reset()
{
base.Reset();
goToObjective = null;
refuelObjective = null;
previousCondition = -1;
repairTool = null;
}
}
}
@@ -90,18 +90,23 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (character.SelectedConstruction != null && Targets.Any(t => character.SelectedConstruction == t && t.ConditionPercentage < 100))
var selectedItem = character.SelectedConstruction;
if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100)
{
// Don't stop fixing until done
// Don't stop fixing until completely done
return 100;
}
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
int items = Targets.Count;
if (items == 0)
{
return 0;
}
bool anyFixers = otherFixers > 0;
float ratio = anyFixers ? items / (float)otherFixers : 1;
if (objectiveManager.CurrentOrder == this)
{
return Targets.Sum(t => 100 - t.ConditionPercentage) * ratio;
return Targets.Sum(t => 100 - t.ConditionPercentage);
}
else
{
@@ -151,8 +156,7 @@ namespace Barotrauma
if (item.Repairables.None()) { return false; }
if (character.Submarine != null)
{
if (item.Submarine.Info.Type != character.Submarine.Info.Type) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, true)) { return false; }
if (!character.Submarine.IsConnectedTo(item.Submarine)) { return false; }
}
return true;
}
@@ -1,4 +1,5 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -21,10 +22,12 @@ namespace Barotrauma
private readonly Character targetCharacter;
private AIObjectiveGoTo goToObjective;
private AIObjectiveContainItem replaceOxygenObjective;
private AIObjectiveGetItem getItemObjective;
private float treatmentTimer;
private Hull safeHull;
private float findHullTimer;
private bool ignoreOxygen;
private readonly float findHullInterval = 1.0f;
public AIObjectiveRescue(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1)
@@ -69,65 +72,130 @@ namespace Barotrauma
}
if (targetCharacter != character)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (targetCharacter.IsIncapacitated && HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
if (targetCharacter.IsIncapacitated)
{
if (character.SelectedCharacter != targetCharacter)
// Check if the character needs more oxygen
if (!ignoreOxygen && character.SelectedCharacter == targetCharacter || character.CanInteractWith(targetCharacter))
{
if (targetCharacter.CurrentHull.DisplayName != null)
// Replace empty oxygen tank
// First remove empty tanks
if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out IEnumerable<Item> suits, requireEquipped: true))
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
Item suit = suits.FirstOrDefault();
if (suit != null)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
AIObjectiveFindDivingGear.DropEmptyTanks(character, suit, out _);
}
}
else if (HumanAIController.HasItem(targetCharacter, AIObjectiveFindDivingGear.LIGHT_DIVING_GEAR, out IEnumerable<Item> masks, requireEquipped: true))
{
Item mask = masks.FirstOrDefault();
if (mask != null)
{
AIObjectiveFindDivingGear.DropEmptyTanks(character, mask, out _);
}
}
bool ShouldRemoveDivingSuit() => targetCharacter.OxygenAvailable < CharacterHealth.InsufficientOxygenThreshold && targetCharacter.CurrentHull?.LethalPressure <= 0;
if (ShouldRemoveDivingSuit())
{
suits.ForEach(suit => suit.Drop(character));
}
else if (suits.Any() && suits.None(s => s.OwnInventory?.Items != null && s.OwnInventory.Items.Any(it => it != null && it.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) && it.ConditionPercentage > 0)))
{
// The target has a suit equipped with an empty oxygen tank.
// Can't remove the suit, because the target needs it.
// If we happen to have an extra oxygen tank in the inventory, let's swap it.
Item spareOxygenTank = FindOxygenTank(targetCharacter) ?? FindOxygenTank(character);
if (spareOxygenTank != null)
{
Item suit = suits.FirstOrDefault();
if (suit != null)
{
// Insert the new oxygen tank
TryAddSubObjective(ref replaceOxygenObjective, () => new AIObjectiveContainItem(character, spareOxygenTank, suit.GetComponent<ItemContainer>(), objectiveManager),
onCompleted: () => RemoveSubObjective(ref replaceOxygenObjective),
onAbandon: () =>
{
RemoveSubObjective(ref replaceOxygenObjective);
ignoreOxygen = true;
if (ShouldRemoveDivingSuit())
{
suits.ForEach(suit => suit.Drop(character));
}
});
return;
}
}
Item FindOxygenTank(Character c) =>
c.Inventory.FindItem(i =>
i.HasTag(AIObjectiveFindDivingGear.OXYGEN_SOURCE) &&
i.ConditionPercentage > 1 &&
i.FindParentInventory(inv => inv.Owner is Item otherItem && otherItem.HasTag("diving")) == null,
recursive: true);
}
}
if (HumanAIController.GetHullSafety(targetCharacter.CurrentHull, targetCharacter) < HumanAIController.HULL_SAFETY_THRESHOLD)
{
// Incapacitated target is not in a safe place -> Move to a safe place first
if (character.SelectedCharacter != targetCharacter)
{
if (targetCharacter.CurrentHull != null && HumanAIController.VisibleHulls.Contains(targetCharacter.CurrentHull) && targetCharacter.CurrentHull.DisplayName != null)
{
character.Speak(TextManager.GetWithVariables("DialogFoundUnconsciousTarget", new string[2] { "[targetname]", "[roomname]" },
new string[2] { targetCharacter.Name, targetCharacter.CurrentHull.DisplayName }, new bool[2] { false, true }),
null, 1.0f, "foundunconscioustarget" + targetCharacter.Name, 60.0f);
}
// Go to the target and select it
if (!character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
{
CloseEnough = CloseEnoughToTreat,
DialogueIdentifier = "dialogcannotreachpatient",
TargetName = targetCharacter.DisplayName
},
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
Abandon = true;
});
}
else
{
character.SelectCharacter(targetCharacter);
}
}
else
{
// Drag the character into safety
if (safeHull == null)
{
if (findHullTimer > 0)
{
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
character.SelectCharacter(targetCharacter);
}
}
if (safeHull != null && character.CurrentHull != safeHull)
else
{
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
// Drag the character into safety
if (safeHull == null)
{
if (findHullTimer > 0)
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
findHullTimer -= deltaTime;
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
if (safeHull != null && character.CurrentHull != safeHull)
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(safeHull, character, objectiveManager),
onCompleted: () => RemoveSubObjective(ref goToObjective),
onAbandon: () =>
{
RemoveSubObjective(ref goToObjective);
safeHull = character.CurrentHull;
});
}
}
}
}
@@ -137,6 +205,7 @@ namespace Barotrauma
if (targetCharacter != character && !character.CanInteractWith(targetCharacter))
{
RemoveSubObjective(ref replaceOxygenObjective);
RemoveSubObjective(ref goToObjective);
// Go to the target and select it
TryAddSubObjective(ref goToObjective, () => new AIObjectiveGoTo(targetCharacter, character, objectiveManager)
@@ -325,7 +394,7 @@ namespace Barotrauma
Priority = 0;
return Priority;
}
if (targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
if (character.LockHands || targetCharacter == null || targetCharacter.CurrentHull == null || targetCharacter.Removed || targetCharacter.IsDead)
{
Priority = 0;
}
@@ -346,5 +415,15 @@ namespace Barotrauma
}
public static IEnumerable<Affliction> GetSortedAfflictions(Character character) => CharacterHealth.SortAfflictionsBySeverity(character.CharacterHealth.GetAllAfflictions());
public override void Reset()
{
base.Reset();
goToObjective = null;
getItemObjective = null;
replaceOxygenObjective = null;
safeHull = null;
ignoreOxygen = false;
}
}
}
@@ -1,4 +1,5 @@
using System;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -34,6 +35,7 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
if (Targets.None()) { return 100; }
if (objectiveManager.CurrentOrder != this)
{
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
@@ -72,7 +74,7 @@ namespace Barotrauma
public static bool IsValidTarget(Character target, Character character)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.TurnedHostileByEvent) { return false; }
if (target.IsInstigator) { return false; }
if (!HumanAIController.IsFriendly(character, target, onlySameTeam: true)) { return false; }
if (character.AIController is HumanAIController humanAI)
{
@@ -83,7 +85,7 @@ namespace Barotrauma
{
// Don't allow to treat others autonomously
return false;
}
}
// Ignore unsafe hulls, unless ordered
if (humanAI.UnsafeHulls.Contains(target.CurrentHull))
{
@@ -100,10 +102,11 @@ namespace Barotrauma
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
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFixLeak>())
{
return false;
}
@@ -36,6 +36,9 @@ namespace Barotrauma
Order = orderInfo.Order;
OrderOption = orderInfo.OrderOption;
}
public bool MatchesOrder(Order order, string option) =>
order.Identifier == Order.Identifier && option == OrderOption && order.TargetEntity == Order.TargetEntity;
}
class Order
@@ -100,8 +103,7 @@ namespace Barotrauma
public Character OrderGiver;
private readonly OrderCategory? category;
public OrderCategory? Category => category;
public OrderCategory? Category { get; private set; }
//legacy support
public readonly string[] AppropriateJobs;
@@ -225,7 +227,7 @@ namespace Barotrauma
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
Options = orderElement.GetAttributeStringArray("options", new string[0]);
var category = orderElement.GetAttributeString("category", null);
if (!string.IsNullOrWhiteSpace(category)) { this.category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
if (!string.IsNullOrWhiteSpace(category)) { this.Category = (OrderCategory)Enum.Parse(typeof(OrderCategory), category, true); }
Weight = orderElement.GetAttributeFloat(0.0f, "weight");
MustSetTarget = orderElement.GetAttributeBool("mustsettarget", false);
AppropriateSkill = orderElement.GetAttributeString("appropriateskill", null);
@@ -299,7 +301,7 @@ namespace Barotrauma
Weight = prefab.Weight;
MustSetTarget = prefab.MustSetTarget;
AppropriateSkill = prefab.AppropriateSkill;
category = prefab.Category;
Category = prefab.Category;
MustManuallyAssign = prefab.MustManuallyAssign;
OrderGiver = orderGiver;
@@ -160,7 +160,7 @@ 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)
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, bool checkVisibility = true)
{
//sort nodes roughly according to distance
sortedNodes.Clear();
@@ -202,12 +202,12 @@ namespace Barotrauma
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
{
// Always check the visibility for the start node
var body = Submarine.PickBody(
start, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs);
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
@@ -257,14 +257,13 @@ namespace Barotrauma
if (endNodeFilter != null && !endNodeFilter(node)) { continue; }
//if searching for a path inside the sub, make sure the waypoint is visible
if (InsideSubmarine)
if (InsideSubmarine && checkVisibility)
{
// Only check the visibility for the end node when allowed (fix leaks)
var body = Submarine.PickBody(end, node.TempPosition, null,
Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionStairs );
if (body != null)
{
//if (body.UserData is Submarine) continue;
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) { continue; }
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) { continue; }
}
@@ -78,11 +78,10 @@ namespace Barotrauma
}
}
if (!aiController.Enabled) { return; }
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
if (!IsRemotelyControlled)
if (!IsRemotelyControlled && aiController != null && aiController.Enabled)
{
aiController.Update(deltaTime);
}
@@ -613,8 +613,13 @@ namespace Barotrauma
movementAngle -= MathHelper.TwoPi;
}
float offset = MathHelper.Pi * CurrentGroundedParams.StepLiftOffset;
if (CurrentGroundedParams.MultiplyByDir)
{
offset *= Dir;
}
float stepLift = TargetMovement.X == 0.0f ? 0 :
(float)Math.Sin(WalkPos * CurrentGroundedParams.StepLiftFrequency + MathHelper.Pi * CurrentGroundedParams.StepLiftOffset) * (CurrentGroundedParams.StepLiftAmount / 100);
(float)Math.Sin(WalkPos * Dir * CurrentGroundedParams.StepLiftFrequency + offset) * (CurrentGroundedParams.StepLiftAmount / 100);
float limpAmount = character.GetLegPenalty();
if (limpAmount > 0)
@@ -631,7 +636,7 @@ namespace Barotrauma
{
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, mainLimb, TorsoTorque);
}
if (TorsoPosition.HasValue)
if (TorsoPosition.HasValue && TorsoMoveForce > 0.0f)
{
Vector2 pos = colliderBottom + new Vector2(limpAmount, TorsoPosition.Value + stepLift);
@@ -649,11 +654,16 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head);
if (head != null)
{
bool headFacingBackwards = false;
if (HeadAngle.HasValue)
{
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, mainLimb, HeadTorque);
if (Math.Sign(head.SimPosition.X - mainLimb.SimPosition.X) != Math.Sign(Dir))
{
headFacingBackwards = true;
}
}
if (HeadPosition.HasValue)
if (HeadPosition.HasValue && HeadMoveForce > 0.0f && !headFacingBackwards)
{
Vector2 pos = colliderBottom + new Vector2(limpAmount, HeadPosition.Value + stepLift * CurrentGroundedParams.StepLiftHeadMultiplier);
@@ -1873,6 +1873,7 @@ namespace Barotrauma
forearm = GetLimb(LimbType.RightForearm);
RightHandIKPos = pos;
}
if (arm == null) { return; }
//distance from shoulder to holdpos
float c = Vector2.Distance(pos, shoulderPos);
@@ -2018,12 +2019,9 @@ namespace Barotrauma
{
break;
}
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed)
if (character.SelectedItems[i]?.body != null && !character.SelectedItems[i].Removed && character.SelectedItems[i].GetComponent<Holdable>() != null)
{
/*character.SelectedItems[i].body.SetTransform(
character.SelectedItems[i].body.SimPosition,
MathUtils.WrapAngleTwoPi(character.SelectedItems[i].body.Rotation + MathHelper.Pi));*/
character.SelectedItems[i].GetComponent<Holdable>()?.Flip();
character.SelectedItems[i].FlipX(relativeToSub: false);
}
}
@@ -764,6 +764,13 @@ namespace Barotrauma
}
}
if (!string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull?.AddDecal(character.BloodDecalName,
(limbJoint.LimbA.WorldPosition + limbJoint.LimbB.WorldPosition) / 2, MathHelper.Clamp(Math.Min(limbJoint.LimbA.Mass, limbJoint.LimbB.Mass), 0.5f, 2.0f), true);
}
SeverLimbJointProjSpecific(limbJoint, playSound: true);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
@@ -1430,6 +1437,11 @@ namespace Barotrauma
}
}
public void ForceRefreshFloorY()
{
lastFloorCheckPos = Vector2.Zero;
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
{
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
@@ -184,7 +184,7 @@ namespace Barotrauma
[Serialize("0.0, 0.0", true, description: "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards). The attacker's facing direction is taken into account."), Editable]
public Vector2 TargetForceWorld { get; private set; }
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed when the target is dead."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
[Serialize(0.0f, true, description: "How likely the attack causes target limbs to be severed."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f)]
public float SeverLimbsProbability { get; set; }
// TODO: disabled because not synced
@@ -235,7 +235,7 @@ namespace Barotrauma
List<Affliction> multipliedAfflictions = new List<Affliction>();
foreach (Affliction affliction in Afflictions.Keys)
{
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
multipliedAfflictions.Add(affliction.CreateMultiplied(multiplier));
}
return multipliedAfflictions;
}
@@ -495,14 +495,14 @@ namespace Barotrauma
if (SecondaryCoolDownTimer < 0) { SecondaryCoolDownTimer = 0; }
}
public void UpdateAttackTimer(float deltaTime)
public void UpdateAttackTimer(float deltaTime, Character character)
{
IsRunning = true;
AttackTimer += deltaTime;
if (AttackTimer >= Duration)
{
ResetAttackTimer();
SetCoolDown();
SetCoolDown(applyRandom: !character.IsPlayer);
}
}
@@ -512,13 +512,22 @@ namespace Barotrauma
IsRunning = false;
}
public void SetCoolDown()
public void SetCoolDown(bool applyRandom)
{
float randomFraction = CoolDown * CoolDownRandomFactor;
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
if (applyRandom)
{
float randomFraction = CoolDown * CoolDownRandomFactor;
CurrentRandomCoolDown = MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
CoolDownTimer = CoolDown + CurrentRandomCoolDown;
randomFraction = SecondaryCoolDown * CoolDownRandomFactor;
SecondaryCoolDownTimer = SecondaryCoolDown + MathHelper.Lerp(-randomFraction, randomFraction, Rand.Value());
}
else
{
CoolDownTimer = CoolDown;
SecondaryCoolDownTimer = SecondaryCoolDown;
CurrentRandomCoolDown = 0;
}
}
public void ResetCoolDown()
@@ -121,7 +121,8 @@ namespace Barotrauma
}
}
public bool TurnedHostileByEvent;
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
public CombatAction CombatAction;
public AnimController AnimController;
@@ -143,6 +144,8 @@ namespace Barotrauma
public bool IsHumanoid => Params.Humanoid;
public bool IsHusk => Params.Husk;
public string BloodDecalName => Params.BloodDecal;
public bool CanSpeak
{
get => Params.CanSpeak;
@@ -637,6 +640,8 @@ namespace Barotrauma
}
}
public bool GodMode = false;
public CampaignMode.InteractionType CampaignInteractionType;
private bool accessRemovedCharacterErrorShown;
@@ -1668,14 +1673,14 @@ namespace Barotrauma
return false;
}
public bool HasEquippedItem(string itemIdentifier, bool allowBroken = true)
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true)
{
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.Items[i].Prefab.Identifier == tagOrIdentifier || Inventory.Items[i].HasTag(tagOrIdentifier)) { return true; }
}
return false;
@@ -1738,7 +1743,7 @@ namespace Barotrauma
if (inventory.Owner is Item)
{
var owner = (Item)inventory.Owner;
if (!CanInteractWith(owner)) { return false; }
if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = owner.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
}
@@ -1833,7 +1838,7 @@ namespace Barotrauma
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
#endif
if (!CanInteract || hidden || item.NonInteractable) return false;
if (!CanInteract || hidden || item.NonInteractable) { return false; }
if (item.ParentInventory != null)
{
@@ -1845,6 +1850,7 @@ namespace Barotrauma
{
//locked wires are never interactable
if (wire.Locked) { return false; }
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { 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
@@ -2314,7 +2320,7 @@ namespace Barotrauma
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Implode();
return;
if (IsDead) { return; }
}
}
}
@@ -2329,7 +2335,7 @@ namespace Barotrauma
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
{
Implode();
return;
if (IsDead) { return; }
}
}
@@ -2509,6 +2515,12 @@ namespace Barotrauma
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
}
if (SelectedBy != null)
{
despawnTimer = 0.0f;
return;
}
float distToClosestPlayer = GetDistanceToClosestPlayer();
if (distToClosestPlayer > NetConfig.DisableCharacterDist)
{
@@ -2866,20 +2878,18 @@ namespace Barotrauma
wasSevered = severed;
}
if (severed)
{
{
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass);
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
otherLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
}
if (wasSevered)
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
{
if (targetLimb.character.AIController is EnemyAIController enemyAI)
{
enemyAI.ReevaluateAttacks();
}
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
}
enemyAI.ReevaluateAttacks();
}
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null)
@@ -3075,7 +3085,7 @@ namespace Barotrauma
private void Implode(bool isNetworkMessage = false)
{
if (CharacterHealth.Unkillable || IsDead) { return; }
if (CharacterHealth.Unkillable || GodMode || IsDead) { return; }
if (!isNetworkMessage)
{
@@ -3127,7 +3137,7 @@ namespace Barotrauma
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
{
if (IsDead || CharacterHealth.Unkillable) { return; }
if (IsDead || CharacterHealth.Unkillable || GodMode) { return; }
HealthUpdateInterval = 0.0f;
@@ -3447,7 +3457,7 @@ namespace Barotrauma
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsSecurity => HasJob("securityofficer");
public bool IsAsssitant => HasJob("assistant");
public bool IsAssistant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
@@ -100,7 +100,7 @@ namespace Barotrauma
head = value;
if (head.race == Race.None)
{
head.race = GetRandomRace();
head.race = GetRandomRace(Rand.RandSync.Unsynced);
}
CalculateHeadSpriteRange();
Head.HeadSpriteId = value.HeadSpriteId;
@@ -296,7 +296,7 @@ namespace Barotrauma
public Character.TeamType TeamID;
private NPCPersonalityTrait personalityTrait;
private readonly NPCPersonalityTrait personalityTrait;
public Order CurrentOrder { get; set; }
public string CurrentOrderOption { get; set; }
@@ -400,7 +400,7 @@ namespace Barotrauma
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
// Used for creating the data
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0)
public CharacterInfo(string speciesName, string name = "", JobPrefab jobPrefab = null, string ragdollFileName = null, int variant = 0, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
@@ -417,12 +417,12 @@ namespace Barotrauma
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders)
{
Head.gender = GetRandomGender();
Head.gender = GetRandomGender(randSync);
}
Head.race = GetRandomRace();
Head.race = GetRandomRace(randSync);
CalculateHeadSpriteRange();
Head.HeadSpriteId = GetRandomHeadID();
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab, variant);
Head.HeadSpriteId = GetRandomHeadID(randSync);
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Unsynced) : new Job(jobPrefab, variant);
if (!string.IsNullOrEmpty(name))
{
@@ -485,7 +485,7 @@ namespace Barotrauma
HasGenders = CharacterConfigElement.GetAttributeBool("genders", false);
if (HasGenders && gender == Gender.None)
{
gender = GetRandomGender();
gender = GetRandomGender(Rand.RandSync.Unsynced);
}
else if (!HasGenders)
{
@@ -539,11 +539,9 @@ namespace Barotrauma
LoadHeadAttachments();
}
public int SetRandomHead() => HeadSpriteId = GetRandomHeadID();
public Gender GetRandomGender() => (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Race GetRandomRace() => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
public int GetRandomHeadID() => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, Rand.RandSync.Server) : 0;
public Gender GetRandomGender(Rand.RandSync randSync) => (Rand.Range(0.0f, 1.0f, randSync) < CharacterConfigElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
public Race GetRandomRace(Rand.RandSync randSync) => new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(randSync);
public int GetRandomHeadID(Rand.RandSync randSync) => Head.headSpriteRange != Vector2.Zero ? Rand.Range((int)Head.headSpriteRange.X, (int)Head.headSpriteRange.Y + 1, randSync) : 0;
private List<XElement> hairs;
private List<XElement> beards;
@@ -670,12 +668,16 @@ namespace Barotrauma
{
if (HasGenders && gender == Gender.None)
{
gender = GetRandomGender();
gender = GetRandomGender(Rand.RandSync.Unsynced);
}
else if (!HasGenders)
{
gender = Gender.None;
}
if (heads == null)
{
LoadHeadPresets();
}
head = new HeadInfo(headID, gender, race, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
CalculateHeadSpriteRange();
ReloadHeadAttachments();
@@ -788,7 +790,7 @@ namespace Barotrauma
Head.FaceAttachmentIndex = faceAttachments.IndexOf(Head.FaceAttachment);
}
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
static List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type, float commonness = 1)
{
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
var emptyElement = new XElement("EmptyWearable", type.ToString(), new XAttribute("commonness", commonness));
@@ -799,10 +801,9 @@ namespace Barotrauma
XElement GetRandomElement(IEnumerable<XElement> elements)
{
var filtered = elements.Where(e => IsWearableAllowed(e)).ToList();
if (filtered.Count == 0) { return null; }
var weights = GetWeights(filtered).ToList();
var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server);
var filtered = elements.Where(e => IsWearableAllowed(e));
if (filtered.Count() == 0) { return null; }
var element = ToolBox.SelectWeightedRandom(filtered.ToList(), GetWeights(filtered).ToList(), Rand.RandSync.Unsynced);
return element == null || element.Name == "Empty" ? null : element;
}
@@ -825,8 +826,9 @@ namespace Barotrauma
return true;
}
bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
static bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
static IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
}
}
@@ -861,7 +863,7 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && (int)newLevel != (int)prevLevel)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer && !MathUtils.NearlyEqual(newLevel, prevLevel))
{
GameMain.NetworkMember.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
}
@@ -111,7 +111,7 @@ namespace Barotrauma
public static void LoadAll()
{
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.SelectedContentPackages, ContentType.Character))
foreach (ContentFile file in ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.Character))
{
LoadFromFile(file);
}
@@ -15,13 +15,24 @@ namespace Barotrauma
public Dictionary<string, SerializableProperty> SerializableProperties { get; set; }
protected float _strength;
[Serialize(0f, true), Editable]
public virtual float Strength
{
get { return _strength; }
set { _strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength); }
set
{
if (_nonClampedStrength < 0 && value > 0)
{
_nonClampedStrength = value;
}
_strength = MathHelper.Clamp(value, 0.0f, Prefab.MaxStrength);
}
}
private float _nonClampedStrength = -1;
public float NonClampedStrength => _nonClampedStrength > 0 ? _nonClampedStrength : _strength;
[Serialize("", true), Editable]
public string Identifier { get; private set; }
@@ -35,6 +46,8 @@ namespace Barotrauma
public float StrengthDiminishMultiplier = 1.0f;
public Affliction MultiplierSource;
public readonly Dictionary<AfflictionPrefab.PeriodicEffect, float> PeriodicEffectTimers = new Dictionary<AfflictionPrefab.PeriodicEffect, float>();
/// <summary>
/// Which character gave this affliction
/// </summary>
@@ -45,6 +58,11 @@ namespace Barotrauma
Prefab = prefab;
_strength = strength;
Identifier = prefab?.Identifier;
foreach (var periodicEffect in prefab.PeriodicEffects)
{
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
}
}
public void Serialize(XElement element)
@@ -59,24 +77,27 @@ namespace Barotrauma
public Affliction CreateMultiplied(float multiplier)
{
return Prefab.Instantiate(Strength * multiplier, Source);
return Prefab.Instantiate(NonClampedStrength * multiplier, Source);
}
public override string ToString() => Prefab == null ? "Affliction (Invalid)" : $"Affliction ({Prefab.Name})";
public float GetVitalityDecrease(CharacterHealth characterHealth)
{
if (Strength < Prefab.ActivationThreshold) return 0.0f;
if (Strength < Prefab.ActivationThreshold) { return 0.0f; }
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return 0.0f;
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f;
if (currentEffect == null) { return 0.0f; }
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) { return 0.0f; }
float currVitalityDecrease = MathHelper.Lerp(
currentEffect.MinVitalityDecrease,
currentEffect.MaxVitalityDecrease,
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
if (currentEffect.MultiplyByMaxVitality)
{
currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
}
return currVitalityDecrease;
}
@@ -173,8 +194,28 @@ namespace Barotrauma
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
{
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
{
PeriodicEffectTimers[periodicEffect] -= deltaTime;
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
PeriodicEffectTimers[periodicEffect] = 0.0f;
}
else
{
foreach (StatusEffect statusEffect in periodicEffect.StatusEffects)
{
ApplyStatusEffect(statusEffect, 1.0f, characterHealth, targetLimb);
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
}
}
}
}
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
if (currentEffect == null) return;
if (currentEffect == null) { return; }
if (currentEffect.StrengthChange < 0) // Reduce diminishing of buffs if boosted
{
@@ -184,32 +225,44 @@ namespace Barotrauma
{
_strength += currentEffect.StrengthChange * deltaTime * (1f - characterHealth.GetResistance(Prefab.Identifier));
}
// Don't use the property, because its virtual and some afflictions like husk overload it for external use.
// Don't use the property, because it's virtual and some afflictions like husk overload it for external use.
_strength = MathHelper.Clamp(_strength, 0.0f, Prefab.MaxStrength);
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
{
statusEffect.SetUser(Source);
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
}
ApplyStatusEffect(statusEffect, deltaTime, characterHealth, targetLimb);
}
}
public void ApplyStatusEffect(StatusEffect statusEffect, float deltaTime, CharacterHealth characterHealth, Limb targetLimb)
{
statusEffect.SetUser(Source);
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
}
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
var targets = new List<ISerializableEntity>();
statusEffect.GetNearbyTargets(characterHealth.Character.WorldPosition, targets);
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targets);
}
}
/// <summary>
/// Use this method to skip clamping and additional logic of the setters.
/// Intended only to be used when the value is already clamped! (networking code)
/// Ideally we would keep this private, but doing so would require too much refactoring.
/// </summary>
public void SetStrength(float strength) => _strength = strength;
}
}
@@ -29,8 +29,9 @@ namespace Barotrauma
set
{
// Don't allow to set the strength too high (from outside) to avoid rapid transformation into husk when taking lots of damage from husks.
// If the strength is more than the value, this will effectively reset the current strength to the max. That's why we use two steps.
float max = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
float previousValue = _strength;
float threshold = _strength > ActiveThreshold ? ActiveThreshold + 1 : DormantThreshold - 1;
float max = Math.Max(threshold, previousValue);
_strength = Math.Clamp(value, 0, max);
}
}
@@ -79,7 +80,7 @@ namespace Barotrauma
{
if (State != InfectionState.Active)
{
character.SetStun(Rand.Range(2, 4, Rand.RandSync.Server));
character.SetStun(Rand.Range(2, 4));
}
State = InfectionState.Active;
ActivateHusk();
@@ -101,7 +102,7 @@ namespace Barotrauma
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
float random = Rand.Value(Rand.RandSync.Server);
float random = Rand.Value();
huskInfection.Clear();
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
character.LastDamageSource = null;
@@ -68,13 +68,13 @@ namespace Barotrauma
HuskedSpeciesName = element.GetAttributeString("huskedspeciesname", null).ToLowerInvariant();
if (HuskedSpeciesName == null)
{
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
DebugConsole.NewMessage($"No 'huskedspeciesname' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
HuskedSpeciesName = "[speciesname]husk";
}
TargetSpecies = element.GetAttributeStringArray("targets", new string[0] { }, trim: true, convertToLowerInvariant: true);
if (TargetSpecies.Length == 0)
{
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element.ToString()}", Color.Orange);
DebugConsole.NewMessage($"No 'targets' defined for the husk affliction ({Identifier}) in {element}", Color.Orange);
TargetSpecies = new string[] { "human" };
}
var attachElement = element.GetChildElement("attachlimb");
@@ -188,6 +188,30 @@ namespace Barotrauma
}
}
public class PeriodicEffect
{
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly float MinInterval, MaxInterval;
public PeriodicEffect(XElement element, string parentDebugName)
{
foreach (XElement subElement in element.Elements())
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
if (element.Attribute("interval") != null)
{
MinInterval = MaxInterval = Math.Max(element.GetAttributeFloat("interval", 1.0f), 1.0f);
}
else
{
MinInterval = Math.Max(element.GetAttributeFloat("mininterval", 1.0f), 1.0f);
MaxInterval = Math.Max(element.GetAttributeFloat("maxinterval", 1.0f), MinInterval);
}
}
}
public static AfflictionPrefab InternalDamage;
public static AfflictionPrefab ImpactDamage;
public static AfflictionPrefab Bleeding;
@@ -267,8 +291,12 @@ namespace Barotrauma
public readonly Color[] IconColors;
private readonly List<Effect> effects = new List<Effect>();
private readonly List<PeriodicEffect> periodicEffects = new List<PeriodicEffect>();
public IEnumerable<Effect> Effects => effects;
public IList<PeriodicEffect> PeriodicEffects => periodicEffects;
private readonly string typeName;
private readonly ConstructorInfo constructor;
@@ -304,10 +332,10 @@ namespace Barotrauma
CharacterHealth.DamageOverlay = null;
CharacterHealth.DamageOverlayFile = string.Empty;
#endif
var prevPrefabs = Prefabs.ToList();
var prevPrefabs = Prefabs.AllPrefabs.SelectMany(kvp => kvp.Value).ToList();
foreach (var prefab in prevPrefabs)
{
prefab.Dispose();
prefab?.Dispose();
}
System.Diagnostics.Debug.Assert(Prefabs.Count() == 0, "All previous AfflictionPrefabs were not removed in AfflictionPrefab.LoadAll");
@@ -552,6 +580,9 @@ namespace Barotrauma
case "effect":
effects.Add(new Effect(subElement, Name));
break;
case "periodiceffect":
periodicEffects.Add(new PeriodicEffect(subElement, Name));
break;
}
}
@@ -170,12 +170,12 @@ namespace Barotrauma
{
get
{
if (!Character.NeedsOxygen || Unkillable) { return 100.0f; }
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return 100.0f; }
return -oxygenLowAffliction.Strength + 100;
}
set
{
if (!Character.NeedsOxygen || Unkillable) { return; }
if (!Character.NeedsOxygen || Unkillable || Character.GodMode) { return; }
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
}
}
@@ -399,7 +399,7 @@ namespace Barotrauma
public void ApplyAffliction(Limb targetLimb, Affliction affliction)
{
if (Unkillable) { return; }
if (Unkillable || Character.GodMode) { return; }
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
@@ -481,7 +481,7 @@ namespace Barotrauma
public void ApplyDamage(Limb hitLimb, AttackResult attackResult)
{
if (Unkillable) { return; }
if (Unkillable || Character.GodMode) { return; }
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
{
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
@@ -504,7 +504,7 @@ namespace Barotrauma
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
{
if (Unkillable) { return; }
if (Unkillable || Character.GodMode) { return; }
foreach (LimbHealth limbHealth in limbHealths)
{
limbHealth.Afflictions.RemoveAll(a =>
@@ -741,7 +741,7 @@ namespace Barotrauma
public void CalculateVitality()
{
Vitality = MaxVitality;
if (Unkillable) { return; }
if (Unkillable || Character.GodMode) { return; }
float damageResistanceMultiplier = 1f - GetResistance("damage");
@@ -777,7 +777,7 @@ namespace Barotrauma
private void Kill()
{
if (Unkillable) { return; }
if (Unkillable || Character.GodMode) { return; }
var causeOfDeath = GetCauseOfDeath();
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
@@ -913,6 +913,11 @@ namespace Barotrauma
msg.WriteRangedSingle(
MathHelper.Clamp(affliction.Strength, 0.0f, affliction.Prefab.MaxStrength),
0.0f, affliction.Prefab.MaxStrength, 8);
msg.Write((byte)affliction.Prefab.PeriodicEffects.Count());
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in affliction.Prefab.PeriodicEffects)
{
msg.WriteRangedSingle(affliction.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
}
}
limbAfflictions.Clear();
@@ -933,6 +938,11 @@ namespace Barotrauma
msg.WriteRangedSingle(
MathHelper.Clamp(limbAffliction.Second.Strength, 0.0f, limbAffliction.Second.Prefab.MaxStrength),
0.0f, limbAffliction.Second.Prefab.MaxStrength, 8);
msg.Write((byte)limbAffliction.Second.Prefab.PeriodicEffects.Count());
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in limbAffliction.Second.Prefab.PeriodicEffects)
{
msg.WriteRangedSingle(limbAffliction.Second.PeriodicEffectTimers[periodicEffect], periodicEffect.MinInterval, periodicEffect.MaxInterval, 8);
}
}
}
@@ -154,7 +154,10 @@ namespace Barotrauma
if (item.Prefab.Identifier == "idcard" || item.Prefab.Identifier == "idcardwreck")
{
item.AddTag("name:" + character.Name);
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
if (Level.Loaded != null)
{
item.ReplaceTag("wreck_id", Level.Loaded.GetWreckIDTag("wreck_id", submarine));
}
var job = character.Info?.Job;
if (job != null)
{
@@ -11,7 +11,8 @@ namespace Barotrauma
{
public string identifier;
public string option;
public float priorityModifier;
public readonly float priorityModifier;
public readonly bool ignoreAtOutpost;
public AutonomousObjective(XElement element)
{
@@ -26,6 +27,7 @@ namespace Barotrauma
option = element.GetAttributeString("option", null);
priorityModifier = element.GetAttributeFloat("prioritymodifier", 1);
priorityModifier = MathHelper.Max(priorityModifier, 0);
ignoreAtOutpost = element.GetAttributeBool("ignoreatoutpost", false);
}
}
@@ -64,7 +66,7 @@ namespace Barotrauma
public readonly Dictionary<int, List<string>> ItemIdentifiers = new Dictionary<int, List<string>>();
public readonly Dictionary<int, Dictionary<string, bool>> ShowItemPreview = new Dictionary<int, Dictionary<string, bool>>();
public readonly List<SkillPrefab> Skills = new List<SkillPrefab>();
public readonly List<AutonomousObjective> AutonomousObjective = new List<AutonomousObjective>();
public readonly List<AutonomousObjective> AutonomousObjectives = new List<AutonomousObjective>();
public readonly List<string> AppropriateOrders = new List<string>();
[Serialize("1,1,1,1", false)]
@@ -209,7 +211,7 @@ namespace Barotrauma
}
break;
case "autonomousobjectives":
subElement.Elements().ForEach(order => AutonomousObjective.Add(new AutonomousObjective(order)));
subElement.Elements().ForEach(order => AutonomousObjectives.Add(new AutonomousObjective(order)));
break;
case "appropriateobjectives":
case "appropriateorders":
@@ -639,6 +639,7 @@ namespace Barotrauma
}
private readonly List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
private readonly List<DamageModifier> tempModifiers = new List<DamageModifier>();
private readonly List<Affliction> afflictionsCopy = new List<Affliction>();
public AttackResult AddDamage(Vector2 simPosition, IEnumerable<Affliction> afflictions, bool playSound)
{
@@ -646,6 +647,7 @@ namespace Barotrauma
afflictionsCopy.Clear();
foreach (var affliction in afflictions)
{
tempModifiers.Clear();
var newAffliction = affliction;
float random = Rand.Value(Rand.RandSync.Unsynced);
if (random > affliction.Probability) { continue; }
@@ -660,8 +662,7 @@ namespace Barotrauma
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
tempModifiers.Add(damageModifier);
}
}
foreach (WearableSprite wearable in wearingItems)
@@ -676,18 +677,49 @@ namespace Barotrauma
}
if (SectorHit(damageModifier.ArmorSectorInRadians, simPosition))
{
newAffliction = affliction.CreateMultiplied(damageModifier.DamageMultiplier);
appliedDamageModifiers.Add(damageModifier);
tempModifiers.Add(damageModifier);
}
}
}
float finalDamageModifier = 1.0f;
foreach (DamageModifier damageModifier in tempModifiers)
{
finalDamageModifier *= damageModifier.DamageMultiplier;
}
if (!MathUtils.NearlyEqual(finalDamageModifier, 1.0f))
{
newAffliction = affliction.CreateMultiplied(finalDamageModifier);
}
if (applyAffliction)
{
afflictionsCopy.Add(newAffliction);
}
appliedDamageModifiers.AddRange(tempModifiers);
}
var result = new AttackResult(afflictionsCopy, this, appliedDamageModifiers);
AddDamageProjSpecific(playSound, result);
float bleedingDamage = 0;
if (character.CharacterHealth.DoesBleed)
{
foreach (var affliction in result.Afflictions)
{
if (affliction is AfflictionBleeding)
{
bleedingDamage += affliction.GetVitalityDecrease(character.CharacterHealth);
}
}
if (bleedingDamage > 0)
{
float bloodDecalSize = MathHelper.Clamp(bleedingDamage / 5, 0.1f, 1.0f);
if (character.CurrentHull != null && !string.IsNullOrEmpty(character.BloodDecalName))
{
character.CurrentHull.AddDecal(character.BloodDecalName, WorldPosition, MathHelper.Clamp(bloodDecalSize, 0.5f, 1.0f), true);
}
}
}
return result;
}
@@ -751,7 +783,7 @@ namespace Barotrauma
Vector2 simPos = ragdoll.SimplePhysicsEnabled ? character.SimPosition : SimPosition;
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(simPos, attackSimPos));
bool wasRunning = attack.IsRunning;
attack.UpdateAttackTimer(deltaTime);
attack.UpdateAttackTimer(deltaTime, character);
bool wasHit = false;
Body structureBody = null;
@@ -762,7 +794,11 @@ namespace Barotrauma
case HitDetection.Distance:
if (dist < attack.DamageRange)
{
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
structureBody = Submarine.PickBody(simPos, attackSimPos, collisionCategory: Physics.CollisionWall | Physics.CollisionLevel, allowInsideFixture: true);
if (structureBody?.UserData as string == "ruinroom")
{
structureBody = null;
}
if (damageTarget is Item i && i.GetComponent<Items.Components.Door>() != null)
{
// If the attack is aimed to an item and hits an item, it's successful.
@@ -917,7 +953,7 @@ namespace Barotrauma
StickTo(structureBody, from, to);
}*/
attack.ResetAttackTimer();
attack.SetCoolDown();
attack.SetCoolDown(applyRandom: !character.IsPlayer);
}
private WeldJoint attachJoint;
@@ -20,17 +20,17 @@ namespace Barotrauma
abstract class GroundedMovementParams : AnimationParams
{
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2)]
[Serialize("1.0, 1.0", true, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSize
{
get;
set;
}
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2)]
[Serialize(0f, true, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float HeadPosition { get; set; }
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2)]
[Serialize(0f, true, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float TorsoPosition { get; set; }
[Serialize(1f, true, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
@@ -39,7 +39,10 @@ namespace Barotrauma
[Serialize(0f, true, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
public float StepLiftAmount { get; set; }
[Serialize(-0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
[Serialize(true, true), Editable]
public bool MultiplyByDir { get; set; }
[Serialize(0.5f, true, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
public float StepLiftOffset { get; set; }
[Serialize(2f, true, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
@@ -51,7 +54,7 @@ namespace Barotrauma
abstract class SwimParams : AnimationParams
{
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 500, ValueStep = 1)]
public float SteerTorque { get; set; }
}
@@ -63,11 +66,11 @@ namespace Barotrauma
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED)]
[Serialize(1.0f, true), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed { get; set; }
[Serialize(1.0f, true, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
public float CycleSpeed { get; set; }
/// <summary>
@@ -87,19 +87,19 @@ namespace Barotrauma
[Serialize(8.0f, true, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveForce { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float FootTorque { get; set; }
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(0.0f, true, description: "Optional torque that's constantly applied to legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float LegTorque { get; set; }
/// <summary>
@@ -173,19 +173,19 @@ namespace Barotrauma
[Editable, Serialize(true, true, description: "Should the character face towards the direction it's heading.")]
public bool RotateTowardsMovement { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TorsoTorque { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float HeadTorque { get; set; }
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(50.0f, true, description: "How much torque is used to rotate the tail to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float TailTorque { get; set; }
[Serialize(1f, true, description: "Multiplier applied based on the angle difference between the tail and the main limb. Increasing the value prevents snake-like characters from getting tangled on themselves. Default = 1 (no boost)"), Editable(MinValueFloat = 1, MaxValueFloat = 100)]
public float TailTorqueMultiplier { get; set; }
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
[Serialize(25.0f, true, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
public float FootTorque { get; set; }
[Serialize(null, true), Editable]
@@ -5,6 +5,7 @@ using System.Linq;
using System.Security.Cryptography;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Steam;
namespace Barotrauma
{
@@ -23,7 +24,6 @@ namespace Barotrauma
NPCSets,
Factions,
Text,
Executable,
ServerExecutable,
LocationTypes,
MapGenerationParameters,
@@ -54,7 +54,22 @@ namespace Barotrauma
{
public static string Folder = "Data/ContentPackages/";
public static List<ContentPackage> List = new List<ContentPackage>();
private static List<ContentPackage> regularPackages = new List<ContentPackage>();
public static IReadOnlyList<ContentPackage> RegularPackages
{
get { return regularPackages; }
}
private static List<ContentPackage> corePackages = new List<ContentPackage>();
public static IReadOnlyList<ContentPackage> CorePackages
{
get { return corePackages; }
}
public static IEnumerable<ContentPackage> AllPackages
{
get { return corePackages.Concat(regularPackages); }
}
//these types of files are included in the MD5 hash calculation,
//meaning that the players must have the exact same files to play together
@@ -97,7 +112,6 @@ namespace Barotrauma
ContentType.Wreck,
ContentType.WreckAIConfig,
ContentType.Text,
ContentType.Executable,
ContentType.ServerExecutable,
ContentType.LocationTypes,
ContentType.MapGenerationParameters,
@@ -128,7 +142,7 @@ namespace Barotrauma
set;
}
public string SteamWorkshopUrl;
public ulong SteamWorkshopId;
public DateTime? InstallTime;
public bool HideInWorkshopMenu
@@ -160,10 +174,24 @@ namespace Barotrauma
//core packages are content packages that are required for the game to work
//e.g. they include the executable, some location types, level generation params and other files the game won't work without
//one (and only one) core package must always be selected
public bool CorePackage
private bool isCorePackage;
public bool IsCorePackage
{
get;
set;
get { return isCorePackage; }
set
{
isCorePackage = value;
if (isCorePackage && regularPackages.Contains(this))
{
corePackages.Add(this);
regularPackages.Remove(this);
}
else if (!isCorePackage && corePackages.Contains(this))
{
regularPackages.Add(this);
corePackages.Remove(this);
}
}
}
public Version GameVersion
@@ -171,7 +199,31 @@ namespace Barotrauma
get; set;
}
public List<ContentFile> Files;
private List<ContentFile> files;
private List<ContentFile> filesToAdd;
private List<ContentFile> filesToRemove;
public IReadOnlyList<ContentFile> Files
{
get { return files; }
}
public IEnumerable<ContentFile> FilesUnsaved
{
get { return files.Where(f => !filesToRemove.Contains(f)).Concat(filesToAdd); }
}
public IReadOnlyList<ContentFile> FilesToAdd
{
get { return filesToAdd; }
}
public IReadOnlyList<ContentFile> FilesToRemove
{
get { return filesToRemove; }
}
public bool HasMultiplayerIncompatibleContent
{
@@ -180,7 +232,9 @@ namespace Barotrauma
private ContentPackage()
{
Files = new List<ContentFile>();
files = new List<ContentFile>();
filesToAdd = new List<ContentFile>();
filesToRemove = new List<ContentFile>();
}
public ContentPackage(string filePath, string setPath = "")
@@ -200,8 +254,13 @@ namespace Barotrauma
Name = doc.Root.GetAttributeString("name", "");
HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false);
CorePackage = doc.Root.GetAttributeBool("corepackage", false);
SteamWorkshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
isCorePackage = doc.Root.GetAttributeBool("corepackage", false);
SteamWorkshopId = doc.Root.GetAttributeUInt64("steamworkshopid", 0);
string workshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
if (!string.IsNullOrEmpty(workshopUrl))
{
SteamWorkshopId = SteamManager.GetWorkshopItemIDFromUrl(workshopUrl);
}
GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0"));
if (doc.Root.Attribute("installtime") != null)
{
@@ -211,12 +270,13 @@ namespace Barotrauma
List<string> errorMsgs = new List<string>();
foreach (XElement subElement in doc.Root.Elements())
{
if (subElement.Name.ToString().Equals("executable", StringComparison.OrdinalIgnoreCase)) { continue; }
if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type))
{
errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type.");
type = ContentType.None;
}
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type, this));
}
if (Files.Count == 0)
@@ -227,7 +287,7 @@ namespace Barotrauma
string folder = System.IO.Path.GetDirectoryName(filePath);
if (File.Exists(System.IO.Path.Combine(folder, Name+".sub")))
{
Files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this));
files.Add(new ContentFile(System.IO.Path.Combine(folder, Name + ".sub"), ContentType.Submarine, this));
}
else
{
@@ -318,7 +378,6 @@ namespace Barotrauma
{
switch (file.Type)
{
case ContentType.Executable:
case ContentType.ServerExecutable:
case ContentType.None:
case ContentType.Outpost:
@@ -351,7 +410,7 @@ namespace Barotrauma
}
}
if (CorePackage && !ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes))
if (IsCorePackage && !ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes))
{
errorMessages.Add(TextManager.GetWithVariables("ContentPackageCantMakeCorePackage",
new string[2] { "[packagename]", "[missingfiletypes]" },
@@ -375,7 +434,6 @@ namespace Barotrauma
foreach (ContentFile file in Files)
{
//TODO: determine executable extension on platform and check for the presence of the executables
if (file.Type == ContentType.Executable) { continue; }
if (file.Type == ContentType.ServerExecutable) { continue; }
if (!File.Exists(file.Path))
@@ -394,7 +452,7 @@ namespace Barotrauma
{
Name = name,
Path = path,
CorePackage = corePackage,
isCorePackage = corePackage,
GameVersion = GameMain.Version
};
@@ -403,35 +461,91 @@ namespace Barotrauma
public ContentFile AddFile(string path, ContentType type)
{
if (Files.Find(file => file.Path == path && file.Type == type) != null) return null;
if (Files.Concat(FilesToAdd).Any(file => file.Path == path && file.Type == type)) return null;
ContentFile cf = new ContentFile(path, type)
{
ContentPackage = this
};
Files.Add(cf);
filesToAdd.Add(cf);
return cf;
}
public void RemoveFile(ContentFile file)
public void AddFile(ContentFile file)
{
Files.Remove(file);
if (filesToRemove.Contains(file)) { filesToRemove.Remove(file); }
if (Files.Concat(FilesToAdd).Any(f => f.Path == file.Path && f.Type == file.Type)) return;
filesToAdd.Add(file);
}
public void Save(string filePath)
public void RemoveFile(ContentFile file)
{
if (filesToAdd.Contains(file)) { filesToAdd.Remove(file); }
if (files.Contains(file) && !filesToRemove.Contains(file)) { filesToRemove.Add(file); }
}
public void Save(string filePath, bool reload = true)
{
var packagesToDeselect = corePackages.Concat(regularPackages).Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList();
bool refreshFiles = false;
if (packagesToDeselect.Any())
{
foreach (var p in packagesToDeselect)
{
if (p.IsCorePackage)
{
if (GameMain.Config.CurrentCorePackage == p)
{
refreshFiles = true;
}
corePackages.Remove(p);
}
else
{
if (GameMain.Config.EnabledRegularPackages.Contains(p))
{
refreshFiles = true;
}
regularPackages.Remove(p);
}
}
if (IsCorePackage)
{
corePackages.Add(this);
}
else
{
regularPackages.Add(this);
}
if (refreshFiles)
{
GameMain.Config.DisableContentPackageItems(filesToRemove);
GameMain.Config.EnableContentPackageItems(filesToAdd);
GameMain.Config.RefreshContentPackageItems(filesToRemove.Concat(filesToAdd).Distinct());
}
}
files.RemoveAll(f => filesToRemove.Contains(f));
files.AddRange(filesToAdd);
filesToRemove.Clear(); filesToAdd.Clear();
XDocument doc = new XDocument();
doc.Add(new XElement("contentpackage",
new XAttribute("name", Name),
new XAttribute("path", Path.CleanUpPathCrossPlatform(correctFilenameCase: false)),
new XAttribute("corepackage", CorePackage)));
new XAttribute("corepackage", IsCorePackage)));
doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString()));
if (!string.IsNullOrEmpty(SteamWorkshopUrl))
if (SteamWorkshopId != 0)
{
doc.Root.Add(new XAttribute("steamworkshopurl", SteamWorkshopUrl));
doc.Root.Add(new XAttribute("steamworkshopid", SteamWorkshopId.ToString()));
#if UNSTABLE
doc.Root.Add(new XAttribute("steamworkshopurl", $"http://steamcommunity.com/sharedfiles/filedetails/?source=Facepunch.Steamworks&id={SteamWorkshopId}"));
#endif
}
if (InstallTime != null)
@@ -445,41 +559,6 @@ namespace Barotrauma
}
doc.SaveSafe(filePath);
var packagesToDeselect = List.Where(p => p.Path.CleanUpPath() == Path.CleanUpPath()).ToList();
bool reselectPackage = false;
if (packagesToDeselect.Any())
{
foreach (var p in packagesToDeselect)
{
if (GameMain.Config.SelectedContentPackages.Contains(p))
{
reselectPackage = true;
if (p.CorePackage)
{
GameMain.Config.AutoSelectCorePackage(packagesToDeselect);
}
else
{
GameMain.Config.DeselectContentPackage(p);
}
}
List.Remove(p);
}
List.Add(this);
if (reselectPackage)
{
if (CorePackage)
{
GameMain.Config.SelectCorePackage(this);
}
else
{
GameMain.Config.SelectContentPackage(this);
}
}
}
}
public void CalculateHash(bool logging = false)
@@ -633,9 +712,31 @@ namespace Barotrauma
return Files.Where(f => f.Type == type).Select(f => f.Path);
}
public static void AddPackage(ContentPackage newPackage)
{
if (corePackages.Concat(regularPackages).Any(p => p.Name.Equals(newPackage.Name, StringComparison.OrdinalIgnoreCase)))
{
DebugConsole.ThrowError($"Attempted to add \"{newPackage.Name}\" more than once!\n{Environment.StackTrace}");
}
if (newPackage.IsCorePackage)
{
corePackages.Add(newPackage);
}
else
{
regularPackages.Add(newPackage);
}
}
public static void RemovePackage(ContentPackage package)
{
if (package.IsCorePackage) { corePackages.Remove(package); }
else { regularPackages.Remove(package); }
}
public static void LoadAll()
{
string folder = ContentPackage.Folder;
string folder = Folder;
if (!Directory.Exists(folder))
{
try
@@ -651,11 +752,13 @@ namespace Barotrauma
IEnumerable<string> files = Directory.GetFiles(folder, "*.xml");
List.Clear();
corePackages.Clear();
var prevRegularPackages = regularPackages.Select(p => p.Name.ToLowerInvariant()).ToList();
regularPackages.Clear();
foreach (string filePath in files)
{
List.Add(new ContentPackage(filePath));
AddPackage(new ContentPackage(filePath));
}
IEnumerable<string> modDirectories = Directory.GetDirectories("Mods");
@@ -671,54 +774,39 @@ namespace Barotrauma
}
else if (File.Exists(modFilePath))
{
List.Add(new ContentPackage(modFilePath));
AddPackage(new ContentPackage(modFilePath));
}
}
List = List
.OrderByDescending(p => p.CorePackage)
.ThenByDescending(p => GameMain.Config?.SelectedContentPackages.Contains(p))
.ThenBy(p => GameMain.Config?.SelectedContentPackages.IndexOf(p))
.ToList();
SortContentPackages(p => prevRegularPackages.IndexOf(p.Name.ToLowerInvariant()));
GameMain.Config?.SortContentPackages();
}
public static void SortContentPackages()
public static void SortContentPackages<T>(Func<ContentPackage, T> order, bool refreshAll = false)
{
if (GameMain.Config != null)
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
.ThenBy(p => List.IndexOf(p))
.ToList();
var sortedSelected = GameMain.Config.SelectedContentPackages
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => GameMain.Config.SelectedContentPackages.IndexOf(p))
.ToList();
GameMain.Config.SelectedContentPackages.Clear(); GameMain.Config.SelectedContentPackages.AddRange(sortedSelected);
var reportList = GameMain.Config.SelectedContentPackages;
DebugConsole.NewMessage($"Content package load order: { string.Join(" | ", reportList.Select(cp => cp.Name)) }");
}
else
{
List = List
.OrderByDescending(p => p.CorePackage)
.ThenBy(p => List.IndexOf(p))
.ToList();
}
var ordered = regularPackages
.OrderBy(p => order(p))
.ThenBy(p => regularPackages.IndexOf(p))
.ToList();
regularPackages.Clear(); regularPackages.AddRange(ordered);
GameMain.Config?.SortContentPackages(refreshAll);
}
public void Delete()
{
try
{
GameMain.Config.DeselectContentPackage(this);
if (IsCorePackage)
{
corePackages.Remove(this);
if (GameMain.Config.CurrentCorePackage == this) { GameMain.Config.AutoSelectCorePackage(null); }
}
else
{
regularPackages.Remove(this);
if (GameMain.Config.EnabledRegularPackages.Contains(this)) { GameMain.Config.DisableRegularPackage(this); }
}
GameMain.Config.SaveNewPlayerConfig();
List.Remove(this);
File.Delete(Path);
SortContentPackages();
}
catch (Exception e)
{
@@ -258,7 +258,7 @@ namespace Barotrauma
{
HumanAIController.DisableCrewAI = true;
NewMessage("Crew AI disabled", Color.Red);
}));
}, isCheat: true));
commands.Add(new Command("enablecrewai", "enablecrewai: Enable the AI of the NPCs in the crew.", (string[] args) =>
{
@@ -509,7 +509,21 @@ namespace Barotrauma
return new string[][] { ListCharacterNames() };
}, isCheat: true));
commands.Add(new Command("godmode", "godmode: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
(string[] args) =>
{
Character targetCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
if (targetCharacter == null) { return; }
targetCharacter.GodMode = !targetCharacter.GodMode;
},
() =>
{
return new string[][] { ListCharacterNames() };
}, isCheat: true));
commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
{
if (Submarine.MainSub == null) return;
@@ -517,6 +531,17 @@ namespace Barotrauma
NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White);
}, isCheat: true));
commands.Add(new Command("growthdelay", "growthdelay: Sets how long it takes for planters to attempt to advance a plant's growth.", (string[] args) =>
{
if (args.Length > 0 && float.TryParse(args[0], out float value))
{
Planter.GrowthTickDelay = value;
NewMessage($"Growth delay set to {value}.", Color.Green);
return;
}
NewMessage("Invalid value.", Color.Red);
}, isCheat: true));
commands.Add(new Command("lock", "lock: Lock movement of the main submarine.", (string[] args) =>
{
Submarine.LockX = !Submarine.LockX;
@@ -1208,6 +1233,17 @@ namespace Barotrauma
}
}));
commands.Add(new Command("togglecampaignteleport", "Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", args =>
{
if (GameMain.GameSession?.Campaign == null)
{
ThrowError("No campaign active.");
return;
}
GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport;
NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White);
}, isCheat: true));
commands.Add(new Command("money", "", args =>
{
if (args.Length == 0) { return; }
@@ -1238,14 +1274,14 @@ namespace Barotrauma
NewMessage((GameSettings.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
}, isCheat: false));
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", TaskPool.ListTasks));
commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(); }));
commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
{
if (args.Length > 0)
{
string packageName = string.Join(" ", args).ToLower();
var package = GameMain.Config.SelectedContentPackages.FirstOrDefault(p => p.Name.ToLower() == packageName);
var package = GameMain.Config.AllEnabledPackages.FirstOrDefault(p => p.Name.ToLower() == packageName);
if (package == null)
{
ThrowError("Content package \"" + packageName + "\" not found.");
@@ -1257,14 +1293,14 @@ namespace Barotrauma
}
else
{
GameMain.Config.SelectedContentPackages.First().CalculateHash(logging: true);
GameMain.Config.AllEnabledPackages.First().CalculateHash(logging: true);
}
},
() =>
{
return new string[][]
{
GameMain.Config.SelectedContentPackages.Select(cp => cp.Name).ToArray()
GameMain.Config.AllEnabledPackages.Select(cp => cp.Name).ToArray()
};
}));
@@ -1859,7 +1895,7 @@ namespace Barotrauma
if (GameSettings.VerboseLogging) NewMessage(message, Color.Gray);
}
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false)
public static void ThrowError(string error, Exception e = null, bool createMessageBox = false, bool appendStackTrace = false)
{
if (e != null)
{
@@ -1869,6 +1905,10 @@ namespace Barotrauma
error += "\n\nInner exception: " + e.InnerException.Message + "\n" + e.InnerException.StackTrace;
}
}
else if (appendStackTrace)
{
error += "\n" + Environment.StackTrace;
}
System.Diagnostics.Debug.WriteLine(error);
#if CLIENT
@@ -1888,27 +1928,7 @@ namespace Barotrauma
public static void AddWarning(string warning)
{
System.Diagnostics.Debug.WriteLine(warning);
#if CLIENT
if (listBox == null) { NewMessage($"WARNING: {warning}", Color.Yellow); return; }
var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White)
{
CanBeFocused = false
};
var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
warning, textAlignment: Alignment.TopLeft, font: GUI.SmallFont, wrap: true)
{
CanBeFocused = false,
TextColor = Color.Yellow
};
textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
textBlock.SetTextPos();
listBox.UpdateScrollBarSize();
listBox.BarScroll = 1.0f;
#else
NewMessage($"WARNING: {warning}", Color.Yellow);
#endif
}
#if CLIENT
@@ -0,0 +1,167 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
namespace Barotrauma
{
partial class Decal
{
public readonly DecalPrefab Prefab;
private Vector2 position;
private float fadeTimer;
public readonly Sprite Sprite;
public float FadeTimer
{
get { return fadeTimer; }
set { fadeTimer = MathHelper.Clamp(value, 0.0f, LifeTime); }
}
public float FadeInTime
{
get { return Prefab.FadeInTime; }
}
public float FadeOutTime
{
get { return Prefab.FadeOutTime; }
}
public float LifeTime
{
get { return Prefab.LifeTime; }
}
private float baseAlpha = 1.0f;
public float BaseAlpha
{
get { return baseAlpha; }
}
public Color Color
{
get;
set;
}
public Vector2 WorldPosition
{
get
{
Vector2 worldPos = position
+ clippedSourceRect.Size.ToVector2() / 2 * Scale
+ hull.Rect.Location.ToVector2();
if (hull.Submarine != null) { worldPos += hull.Submarine.DrawPosition; }
return worldPos;
}
}
public Vector2 Position
{
get { return position; }
}
public Vector2 NonClampedPosition
{
get;
private set;
}
private readonly HashSet<BackgroundSection> affectedSections;
private readonly Hull hull;
public readonly float Scale;
private Rectangle clippedSourceRect;
private bool cleaned = false;
public Decal(DecalPrefab prefab, float scale, Vector2 worldPosition, Hull hull)
{
Prefab = prefab;
this.hull = hull;
//transform to hull-relative coordinates so we don't have to worry about the hull moving
NonClampedPosition = position = worldPosition - hull.WorldRect.Location.ToVector2();
Vector2 drawPos = position + hull.Rect.Location.ToVector2();
Sprite = prefab.Sprites[Rand.Range(0, prefab.Sprites.Count, Rand.RandSync.Unsynced)];
Color = prefab.Color;
Rectangle drawRect = new Rectangle(
(int)(drawPos.X - Sprite.size.X / 2 * scale),
(int)(drawPos.Y + Sprite.size.Y / 2 * scale),
(int)(Sprite.size.X * scale),
(int)(Sprite.size.Y * scale));
Rectangle overFlowAmount = new Rectangle(
(int)Math.Max(hull.Rect.X - drawRect.X, 0.0f),
(int)Math.Max(drawRect.Y - hull.Rect.Y, 0.0f),
(int)Math.Max(drawRect.Right - hull.Rect.Right, 0.0f),
(int)Math.Max((hull.Rect.Y - hull.Rect.Height) - (drawRect.Y - drawRect.Height), 0.0f));
clippedSourceRect = new Rectangle(
Sprite.SourceRect.X + (int)(overFlowAmount.X / scale),
Sprite.SourceRect.Y + (int)(overFlowAmount.Y / scale),
Sprite.SourceRect.Width - (int)((overFlowAmount.X + overFlowAmount.Width) / scale),
Sprite.SourceRect.Height - (int)((overFlowAmount.Y + overFlowAmount.Height) / scale));
position -= new Vector2(Sprite.size.X / 2 * scale - overFlowAmount.X, -Sprite.size.Y / 2 * scale + overFlowAmount.Y);
this.Scale = scale;
foreach (BackgroundSection section in hull.GetBackgroundSectionsViaContaining(new Rectangle((int)position.X, (int)position.Y - drawRect.Height, drawRect.Width, drawRect.Height)))
{
affectedSections ??= new HashSet<BackgroundSection>();
affectedSections.Add(section);
}
}
public void Update(float deltaTime)
{
fadeTimer += deltaTime;
}
public void ForceRefreshFadeTimer(float val)
{
cleaned = false;
fadeTimer = val;
}
public void StopFadeIn()
{
Color *= GetAlpha();
fadeTimer = Prefab.FadeInTime;
}
public bool AffectsSection(BackgroundSection section)
{
return affectedSections != null && affectedSections.Contains(section);
}
public void Clean(float val)
{
cleaned = true;
float sizeModifier = MathHelper.Clamp(Sprite.size.X * Sprite.size.Y * Scale / 10000, 1.0f, 25.0f);
baseAlpha -= val * -1 / sizeModifier;
}
private float GetAlpha()
{
if (fadeTimer < Prefab.FadeInTime && !cleaned)
{
return baseAlpha * fadeTimer / Prefab.FadeInTime;
}
else if (cleaned || fadeTimer > Prefab.LifeTime - Prefab.FadeOutTime)
{
return baseAlpha * Math.Min((Prefab.LifeTime - fadeTimer) / Prefab.FadeOutTime, 1.0f);
}
return baseAlpha;
}
}
}
@@ -0,0 +1,124 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Xml.Linq;
namespace Barotrauma
{
class DecalManager
{
public PrefabCollection<DecalPrefab> Prefabs { get; private set; }
public readonly List<Sprite> GrimeSprites = new List<Sprite>();
private Dictionary<string, List<Sprite>> grimeSpritesByFile = new Dictionary<string, List<Sprite>>();
public DecalManager()
{
Prefabs = new PrefabCollection<DecalPrefab>();
foreach (ContentFile configFile in GameMain.Instance.GetFilesOfType(ContentType.Decals))
{
LoadFromFile(configFile);
}
}
public void LoadFromFile(ContentFile configFile)
{
XDocument doc = XMLExtensions.TryLoadXml(configFile.Path);
if (doc == null) { return; }
if (grimeSpritesByFile.ContainsKey(configFile.Path))
{
foreach (Sprite sprite in grimeSpritesByFile[configFile.Path])
{
sprite.Remove();
GrimeSprites.Remove(sprite);
}
grimeSpritesByFile.Remove(configFile.Path);
}
bool allowOverriding = false;
var mainElement = doc.Root;
if (doc.Root.IsOverride())
{
mainElement = doc.Root.FirstElement();
allowOverriding = true;
}
foreach (XElement sourceElement in mainElement.Elements())
{
var element = sourceElement.IsOverride() ? sourceElement.FirstElement() : sourceElement;
string name = element.Name.ToString().ToLowerInvariant();
switch (name)
{
case "grime":
if (!grimeSpritesByFile.ContainsKey(configFile.Path))
{
grimeSpritesByFile.Add(configFile.Path, new List<Sprite>());
}
var grimeSprite = new Sprite(element);
GrimeSprites.Add(grimeSprite);
grimeSpritesByFile[configFile.Path].Add(grimeSprite);
break;
default:
if (Prefabs.ContainsKey(name))
{
if (allowOverriding || sourceElement.IsOverride())
{
DebugConsole.NewMessage($"Overriding the existing decal prefab '{name}' using the file '{configFile.Path}'", Color.Yellow);
}
else
{
DebugConsole.ThrowError($"Error in '{configFile.Path}': Duplicate decal prefab '{name}' found in '{configFile.Path}'! Each decal prefab must have a unique name. " +
"Use <override></override> tags to override prefabs.");
continue;
}
}
Prefabs.Add(new DecalPrefab(element, configFile), allowOverriding || sourceElement.IsOverride());
break;
}
}
using MD5 md5 = MD5.Create();
foreach (DecalPrefab prefab in Prefabs)
{
prefab.UIntIdentifier = ToolBox.StringToUInt32Hash(prefab.Identifier, md5);
//it's theoretically possible for two different values to generate the same hash, but the probability is astronomically small
var collision = Prefabs.Find(p => p != prefab && p.UIntIdentifier == prefab.UIntIdentifier);
if (collision != null)
{
DebugConsole.ThrowError("Hashing collision when generating uint identifiers for Decals: " + prefab.Identifier + " has the same identifier as " + collision.Identifier + " (" + prefab.UIntIdentifier + ")");
collision.UIntIdentifier++;
}
}
}
public void RemoveByFile(string filePath)
{
Prefabs.RemoveByFile(filePath);
if (grimeSpritesByFile.ContainsKey(filePath))
{
foreach (Sprite sprite in grimeSpritesByFile[filePath])
{
sprite.Remove();
GrimeSprites.Remove(sprite);
}
grimeSpritesByFile.Remove(filePath);
}
}
public Decal CreateDecal(string decalName, float scale, Vector2 worldPosition, Hull hull)
{
if (!Prefabs.ContainsKey(decalName.ToLowerInvariant()))
{
DebugConsole.ThrowError("Decal prefab " + decalName + " not found!");
return null;
}
DecalPrefab prefab = Prefabs[decalName];
return new Decal(prefab, scale, worldPosition, hull);
}
}
}
@@ -0,0 +1,74 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma
{
class DecalPrefab : IPrefab, IDisposable
{
public readonly string Name;
public string OriginalName { get { return Name; } }
public string Identifier
{
get;
private set;
}
/// <summary>
/// Unique identifier that's generated by hashing the prefab's string identifier.
/// Used to reduce the amount of bytes needed to write decal data into network messages in multiplayer.
/// </summary>
public uint UIntIdentifier;
public string FilePath { get; private set; }
public ContentPackage ContentPackage { get; private set; }
public void Dispose()
{
foreach (Sprite spr in Sprites)
{
spr.Remove();
}
Sprites.Clear();
}
public readonly List<Sprite> Sprites;
public readonly Color Color;
public readonly float LifeTime;
public readonly float FadeOutTime;
public readonly float FadeInTime;
public DecalPrefab(XElement element, ContentFile file)
{
Name = element.Name.ToString();
Identifier = Name.ToLowerInvariant();
FilePath = file.Path;
ContentPackage = file.ContentPackage;
Sprites = new List<Sprite>();
foreach (XElement subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("sprite", StringComparison.OrdinalIgnoreCase))
{
Sprites.Add(new Sprite(subElement));
}
}
Color = new Color(element.GetAttributeVector4("color", Vector4.One));
LifeTime = element.GetAttributeFloat("lifetime", 10.0f);
FadeOutTime = Math.Min(LifeTime, element.GetAttributeFloat("fadeouttime", 1.0f));
FadeInTime = Math.Min(LifeTime - FadeOutTime, element.GetAttributeFloat("fadeintime", 0.0f));
}
}
}
@@ -20,6 +20,7 @@
OnEating,
OnDeath = OnBroken,
OnDamaged,
OnSevered
OnSevered,
OnProduceSpawned
}
}
@@ -67,6 +67,11 @@ namespace Barotrauma
return false;
}
protected bool HasBeenDetermined()
{
return succeeded.HasValue;
}
public override bool SetGoToTarget(string goTo)
{
if (Success != null && Success.SetGoToTarget(goTo))
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -34,11 +33,11 @@ namespace Barotrauma
if (!(target is Character chr)) { continue; }
if (chr.Inventory == null) { continue; }
if (itemTags.Any(tag => chr.Inventory.Items.Any(item => item != null && item.HasTag(tag)))) { return true; }
if (itemTags.Any(tag => chr.Inventory.FindItemByTag(tag, recursive: true) != null)) { return true; }
foreach (var identifier in itemIdentifierSplit)
{
if (chr.Inventory.Items.Any(it => it != null && it.Prefab.Identifier.Equals(identifier, StringComparison.InvariantCultureIgnoreCase)))
if (chr.Inventory.FindItemByIdentifier(identifier, recursive: true) != null)
{
return true;
}
@@ -50,15 +49,9 @@ namespace Barotrauma
public override string ToDebugString()
{
string subActionStr = "";
if (succeeded.HasValue)
{
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(CheckItemAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"ItemIdentifiers: {ItemIdentifiers.ColorizeObject()}" +
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -0,0 +1,35 @@
using System.Xml.Linq;
namespace Barotrauma
{
class CheckMoneyAction : BinaryOptionAction
{
[Serialize(0, true)]
public int Amount { get; set; }
public CheckMoneyAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
}
protected override bool? DetermineSuccess()
{
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
return campaign.Money >= Amount;
}
return false;
}
public override string ToDebugString()
{
string subActionStr = "";
if (succeeded.HasValue)
{
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckMoneyAction)} -> (Amount: {Amount.ColorizeObject()}" +
$" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
}
}
}
@@ -10,6 +10,15 @@ namespace Barotrauma
[Serialize(AIObjectiveCombat.CombatMode.Offensive, true)]
public AIObjectiveCombat.CombatMode CombatMode { get; set; }
[Serialize(false, true, description: "Did this NPC start the fight (as an aggressor)?")]
public bool IsInstigator { get; set; }
[Serialize(AIObjectiveCombat.CombatMode.None, true)]
public AIObjectiveCombat.CombatMode GuardReaction { get; set; }
[Serialize(AIObjectiveCombat.CombatMode.None, true)]
public AIObjectiveCombat.CombatMode WitnessReaction { get; set; }
[Serialize("", true)]
public string NPCTag { get; set; }
@@ -50,7 +59,8 @@ namespace Barotrauma
}
if (enemy == null) { continue; }
npc.TurnedHostileByEvent = true;
npc.CombatAction = this;
var objectiveManager = humanAiController.ObjectiveManager;
foreach (var goToObjective in objectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
{
@@ -55,6 +55,7 @@ namespace Barotrauma
private Character speaker;
private OrderInfo? prevSpeakerOrder;
private AIObjective prevIdleObjective, prevGotoObjective;
public List<SubactionGroup> Options { get; private set; }
@@ -169,13 +170,19 @@ namespace Barotrauma
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(speaker, new object[] { NetEntityEvent.Type.AssignCampaignInteraction });
#endif
if (prevSpeakerOrder != null)
var humanAI = speaker.AIController as HumanAIController;
if (humanAI != null)
{
(speaker.AIController as HumanAIController)?.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
(speaker.AIController as HumanAIController)?.SetOrder(null, string.Empty, orderGiver: null, speak: false);
if (prevSpeakerOrder != null)
{
humanAI.SetOrder(prevSpeakerOrder.Value.Order, prevSpeakerOrder.Value.OrderOption, orderGiver: null, speak: false);
}
else
{
humanAI.SetOrder(null, string.Empty, orderGiver: null, speak: false);
}
if (prevIdleObjective != null) { humanAI.ObjectiveManager.AddObjective(prevIdleObjective); }
if (prevGotoObjective != null) { humanAI.ObjectiveManager.AddObjective(prevGotoObjective); }
}
}
@@ -246,9 +253,12 @@ namespace Barotrauma
TryStartConversation(null);
}
}
else if (Options.Any())
else
{
Options[selectedOption].Update(deltaTime);
if (Options.Any())
{
Options[selectedOption].Update(deltaTime);
}
}
}
@@ -300,6 +310,8 @@ namespace Barotrauma
{
prevSpeakerOrder = new OrderInfo(humanAI.CurrentOrder, humanAI.CurrentOrderOption);
}
prevIdleObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveIdle>();
prevGotoObjective = humanAI.ObjectiveManager.GetObjective<AIObjectiveGoTo>();
humanAI.SetOrder(
Order.PrefabList.Find(o => o.Identifier.Equals("wait", StringComparison.OrdinalIgnoreCase)),
option: string.Empty, orderGiver: null, speak: false);
@@ -334,25 +346,11 @@ namespace Barotrauma
{
if (!interrupt)
{
SubactionGroup selOtion = null;
if (selectedOption >= 0 && Options.Count > selectedOption)
{
selOtion = Options[selectedOption];
}
EventAction subAction = null;
if (selOtion != null)
{
subAction = selOtion.CurrentSubAction;
}
return $"{ToolBox.GetDebugSymbol(selectedOption > -1)} {nameof(ConversationAction)} -> (Selected option: {selOtion?.Text.ColorizeObject()})\n" +
$" Sub action: {subAction.ColorizeObject()}";
return $"{ToolBox.GetDebugSymbol(selectedOption > -1, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Selected option: {selectedOption.ColorizeObject()})";
}
else
{
return $"{ToolBox.GetDebugSymbol(true)} {nameof(ConversationAction)} -> (Interrupted)\n" +
$" Sub action: {Interrupted?.CurrentSubAction.ColorizeObject()}";
return $"{ToolBox.GetDebugSymbol(true, selectedOption < 0 && dialogOpened)} {nameof(ConversationAction)} -> (Interrupted)";
}
}
}
@@ -42,7 +42,7 @@ namespace Barotrauma
var targets = ParentEvent.GetTargets(TargetTag).Where(e => e is Character).Select(e => e as Character);
foreach (var target in targets)
{
target.Info?.IncreaseSkillLevel(Skill, Amount, target.WorldPosition + Vector2.UnitY * 150.0f);
target.Info?.IncreaseSkillLevel(Skill?.ToLowerInvariant(), Amount, target.WorldPosition + Vector2.UnitY * 150.0f);
}
isFinished = true;
}
@@ -19,6 +19,8 @@ namespace Barotrauma
private List<Character> affectedNpcs = null;
private AIObjectiveGoTo gotoObjective;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
@@ -31,21 +33,18 @@ namespace Barotrauma
if (Wait)
{
var newObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
gotoObjective = new AIObjectiveGoTo(npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
}
else
{
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
if (gotoObjective != null)
{
if (goToObjective.Target == npc)
{
goToObjective.Abandon = true;
}
gotoObjective.Abandon = true;
}
}
}
@@ -64,13 +63,10 @@ namespace Barotrauma
foreach (var npc in affectedNpcs)
{
if (npc.Removed || !(npc.AIController is HumanAIController humanAiController)) { continue; }
foreach (var goToObjective in humanAiController.ObjectiveManager.GetActiveObjectives<AIObjectiveGoTo>())
if (gotoObjective != null)
{
if (goToObjective.Target == npc)
{
goToObjective.Abandon = true;
}
}
gotoObjective.Abandon = true;
}
}
affectedNpcs = null;
}
@@ -11,21 +11,18 @@ namespace Barotrauma
public RNGAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
private bool isFinished;
protected override bool? DetermineSuccess()
{
isFinished = true;
return Rand.Range(0.0, 1.0) <= Chance;
}
public override string ToDebugString()
{
string subActionStr = "";
if (succeeded.HasValue)
{
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(RNGAction)} -> (Chance: {Chance.ColorizeObject()}, "+
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -15,7 +16,17 @@ namespace Barotrauma
[Serialize(1, true)]
public int Amount { get; set; }
public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element) { }
public RemoveItemAction(ScriptedEvent parentEvent, XElement element) : base(parentEvent, element)
{
if (string.IsNullOrWhiteSpace(ItemIdentifier))
{
ItemIdentifier = element.GetAttributeString("itemidentifiers", "");
}
if (string.IsNullOrWhiteSpace(ItemIdentifier))
{
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\" - RemoveItemAction without an item identifier.");
}
}
private bool isFinished = false;
@@ -32,25 +43,33 @@ namespace Barotrauma
{
if (isFinished) { return; }
var targets = ParentEvent.GetTargets(TargetTag)
.Where(t => t is Character chr && chr.Inventory != null)
.Select(t => t as Character).ToList();
if (targets.Count <= 0) { return; }
int count = Amount;
while (count > 0 && targets.Count > 0)
var targets = ParentEvent.GetTargets(TargetTag);
bool hasValidTargets = false;
foreach (Entity target in targets)
{
var items = targets[0].Inventory.Items;
for (int i = 0; i < items.Length; i++)
if (target is Character character && character.Inventory != null)
{
if (items[i] != null && items[i].Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase))
{
Entity.Spawner.AddToRemoveQueue(items[i]);
count--;
if (count <= 0) { break; }
}
hasValidTargets = true;
break;
}
}
if (!hasValidTargets) { return; }
List<Item> usedItems = new List<Item>();
foreach (Entity target in targets)
{
Inventory inventory = (target as Character)?.Inventory;
if (inventory == null) { continue; }
while (usedItems.Count < Amount)
{
var item = inventory.FindItem(it =>
it != null &&
!usedItems.Contains(it) &&
it.Prefab.Identifier.Equals(ItemIdentifier, StringComparison.InvariantCultureIgnoreCase), recursive: true);
if (item == null) { break; }
Entity.Spawner.AddToRemoveQueue(item);
usedItems.Add(item);
}
targets.RemoveAt(0);
}
isFinished = true;
}
@@ -40,38 +40,42 @@ namespace Barotrauma
if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
object currentValue = campaign.CampaignMetadata.GetValue(Identifier);
object xmlValue = ConvertXMLValue();
float? originalValue = ConvertValueToFloat(currentValue ?? 0);
float? newValue = ConvertValueToFloat(xmlValue);
if ((originalValue == null || newValue == null) && Operation != OperationType.Set)
{
DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {xmlValue.GetType()})");
return;
}
if (Identifier != null)
{
switch (Operation)
{
case OperationType.Set:
campaign.CampaignMetadata.SetValue(Identifier, xmlValue);
break;
case OperationType.Add:
campaign.CampaignMetadata.SetValue(Identifier, originalValue + newValue ?? 0);
break;
case OperationType.Multiply:
campaign.CampaignMetadata.SetValue(Identifier, originalValue * newValue ?? 0);
break;
}
}
object xmlValue = ConvertXMLValue(Value);
PerformOperation(campaign.CampaignMetadata, Identifier, xmlValue, Operation);
}
isFinished = true;
}
public static void PerformOperation(CampaignMetadata metadata, string identifier, object value, OperationType operation)
{
if (metadata == null) { return; }
object currentValue = metadata.GetValue(identifier);
float? originalValue = ConvertValueToFloat(currentValue ?? 0);
float? newValue = ConvertValueToFloat(value);
if ((originalValue == null || newValue == null) && operation != OperationType.Set)
{
DebugConsole.ThrowError($"Tried to perform numeric operations to a non number via SetDataAction (Existing: {currentValue?.GetType()}, New: {value.GetType()})");
return;
}
switch (operation)
{
case OperationType.Set:
metadata.SetValue(identifier, value);
break;
case OperationType.Add:
metadata.SetValue(identifier, originalValue + newValue ?? 0);
break;
case OperationType.Multiply:
metadata.SetValue(identifier, originalValue * newValue ?? 0);
break;
}
}
private static float? ConvertValueToFloat(object value)
{
if (value is float || value is int)
@@ -82,24 +86,24 @@ namespace Barotrauma
return null;
}
private object ConvertXMLValue()
public static object ConvertXMLValue(string value)
{
if (bool.TryParse(Value, out bool b))
if (bool.TryParse(value, out bool b))
{
return b;
}
if (float.TryParse(Value, out float f))
if (float.TryParse(value, out float f))
{
return f;
}
return Value;
return value;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue().ColorizeObject()}, Operation: {Operation.ColorizeObject()})";
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(SetDataAction)} -> (Identifier: {Identifier.ColorizeObject()}, Value: {ConvertXMLValue(Value).ColorizeObject()}, Operation: {Operation.ColorizeObject()})";
}
}
}
@@ -32,15 +32,9 @@ namespace Barotrauma
public override string ToDebugString()
{
string subActionStr = "";
if (succeeded.HasValue)
{
subActionStr = $"\n Sub action: {(succeeded.Value ? Success : Failure)?.CurrentSubAction.ColorizeObject()}";
}
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(SkillCheckAction)} -> (TargetTag: {TargetTag.ColorizeObject()}, " +
$"Required skill: {RequiredSkill.ColorizeObject()}, Required level: {RequiredLevel.ColorizeObject()}, " +
$"Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})" +
subActionStr;
return $"{ToolBox.GetDebugSymbol(HasBeenDetermined())} {nameof(SkillCheckAction)} -> (Target: {TargetTag.ColorizeObject()}, " +
$"Skill: {RequiredSkill.ColorizeObject()}, Level: {RequiredLevel.ColorizeObject()}, " +
$"Succeeded: {succeeded.ColorizeObject()})";
}
}
}
@@ -41,13 +41,18 @@ namespace Barotrauma
}
public override void Reset()
{
isRunning = false;
isFinished = false;
}
public bool isRunning = false;
public override void Update(float deltaTime)
{
if (isFinished) { return; }
isRunning = true;
var targets1 = ParentEvent.GetTargets(Target1Tag);
if (!targets1.Any()) { return; }
@@ -155,6 +160,8 @@ namespace Barotrauma
{
ParentEvent.AddTarget(ApplyToTarget2, entity2);
}
isRunning = false;
isFinished = true;
}
@@ -162,11 +169,11 @@ namespace Barotrauma
{
if (string.IsNullOrEmpty(TargetModuleType))
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (Distance: {((int)distance).ColorizeObject()}, Radius: {Radius.ColorizeObject()}, TargetTags: {Target1Tag.ColorizeObject()}, {Target2Tag.ColorizeObject()})";
}
else
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})";
return $"{ToolBox.GetDebugSymbol(isFinished, isRunning)} {nameof(TriggerAction)} -> (TargetTags: {Target1Tag.ColorizeObject()}, {TargetModuleType.ColorizeObject()})";
}
}
}
@@ -19,6 +19,8 @@ namespace Barotrauma
const float CalculateDistanceTraveledInterval = 5.0f;
const int MaxEventHistory = 20;
private Level level;
private readonly List<Sprite> preloadedSprites = new List<Sprite>();
@@ -110,10 +112,25 @@ namespace Barotrauma
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
{
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab));
if (level.LevelData.EventHistory.Count > 10)
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - 10);
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
AddChildEvents(initialEventSet);
void AddChildEvents(EventSet eventSet)
{
foreach (EventPrefab ep in eventSet.EventPrefabs.Select(e => e.First))
{
if (!level.LevelData.NonRepeatableEvents.Contains(ep))
{
level.LevelData.NonRepeatableEvents.Add(ep);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
{
AddChildEvents(childSet);
}
}
}
@@ -301,6 +318,7 @@ namespace Barotrauma
private float CalculateCommonness(Pair<EventPrefab, float> eventPrefab)
{
if (level.LevelData.NonRepeatableEvents.Contains(eventPrefab.First)) { return 0.0f; }
float retVal = eventPrefab.Second;
if (level.LevelData.EventHistory.Contains(eventPrefab.First)) { retVal *= 0.1f; }
return retVal;
@@ -324,7 +342,13 @@ namespace Barotrauma
{
if (eventSet.EventPrefabs.Count > 0)
{
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
int seed = ToolBox.StringToInt(level.Seed);
foreach (var previousEvent in level.LevelData.EventHistory)
{
seed |= ToolBox.StringToInt(previousEvent.Identifier);
}
MTRandom rand = new MTRandom(seed);
List<Pair<EventPrefab, float>> unusedEvents = new List<Pair<EventPrefab, float>>(eventSet.EventPrefabs);
for (int j = 0; j < eventSet.EventCount; j++)
{
@@ -476,37 +500,42 @@ namespace Barotrauma
eventThreshold += settings.EventThresholdIncrease * deltaTime;
eventCoolDown -= deltaTime;
if (currentIntensity < eventThreshold)
{
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
bool recheck = false;
do
{
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
recheck = false;
//activate pending event sets that can be activated
for (int i = pendingEventSets.Count - 1; i >= 0; i--)
{
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
var eventSet = pendingEventSets[i];
if (eventCoolDown > 0.0f && !eventSet.IgnoreCoolDown) { continue; }
if (!CanStartEventSet(eventSet)) { continue; }
pendingEventSets.RemoveAt(i);
if (selectedEvents.ContainsKey(eventSet))
{
activeEvents.Add(ev);
//start events in this set
foreach (Event ev in selectedEvents[eventSet])
{
activeEvents.Add(ev);
eventThreshold = settings.DefaultEventThreshold;
eventCoolDown = settings.EventCooldown;
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
recheck = true;
}
}
//add child event sets to pending
foreach (EventSet childEventSet in eventSet.ChildSets)
{
pendingEventSets.Add(childEventSet);
}
}
} while (recheck);
}
foreach (Event ev in activeEvents)
@@ -568,7 +597,7 @@ namespace Barotrauma
{
//enemy outside and targeting the sub or something in it
//moloch adds 0.24 to enemy danger, a crawler 0.02
enemyDanger += enemyAI.CombatStrength / 5000.0f;
enemyDanger += enemyAI.CombatStrength / 2000.0f;
}
}
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
@@ -86,6 +86,8 @@ namespace Barotrauma
public readonly bool PerRuin;
public readonly bool PerWreck;
public readonly bool OncePerOutpost;
public readonly Dictionary<string, float> Commonness;
//Pair.First: event prefab, Pair.Second: commonness
@@ -134,6 +136,7 @@ namespace Barotrauma
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? false);
PerRuin = element.GetAttributeBool("perruin", false);
PerWreck = element.GetAttributeBool("perwreck", false);
OncePerOutpost = element.GetAttributeBool("perwreck", false);
Commonness[""] = 1.0f;
foreach (XElement subElement in element.Elements())
@@ -131,11 +131,9 @@ namespace Barotrauma
if (Submarine.MainSub != null && Submarine.MainSub.AtEndPosition)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
if (deliveredItemCount >= requiredDeliveryAmount)
{
GiveReward();
completed = true;
}
}
@@ -145,6 +143,7 @@ namespace Barotrauma
if (!item.Removed) { item.Remove(); }
}
items.Clear();
failed = !completed;
}
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
abstract partial class Mission
{
public readonly MissionPrefab Prefab;
protected bool completed;
protected bool completed, failed;
protected int state;
public int State
{
@@ -74,7 +74,12 @@ namespace Barotrauma
get { return completed; }
set { completed = value; }
}
public bool Failed
{
get { return failed; }
}
public virtual bool AllowRespawn
{
get { return true; }
@@ -219,6 +224,14 @@ namespace Barotrauma
if (faction != null) { faction.Reputation.Value += reputationReward.Value; }
}
}
if (Prefab.DataRewards != null)
{
foreach (var (identifier, value, operation) in Prefab.DataRewards)
{
SetDataAction.PerformOperation(campaign.CampaignMetadata, identifier, value, operation);
}
}
}
}
}
@@ -54,7 +54,8 @@ namespace Barotrauma
public readonly string AchievementIdentifier;
public readonly Dictionary<string, float> ReputationRewards = new Dictionary<string, float>();
public readonly Dictionary<string, float> ReputationRewards = new Dictionary<string, float>();
public readonly List<Tuple<string, object, SetDataAction.OperationType>> DataRewards = new List<Tuple<string, object, SetDataAction.OperationType>>();
public readonly int Commonness;
@@ -178,6 +179,23 @@ namespace Barotrauma
}
}
break;
case "metadata":
string identifier = subElement.GetAttributeString("identifier", string.Empty);
string stringValue = subElement.GetAttributeString("value", string.Empty);
if (!string.IsNullOrWhiteSpace(stringValue) && !string.IsNullOrWhiteSpace(identifier))
{
object value = SetDataAction.ConvertXMLValue(stringValue);
SetDataAction.OperationType operation = SetDataAction.OperationType.Set;
string operatingString = subElement.GetAttributeString("operation", string.Empty);
if (!string.IsNullOrWhiteSpace(operatingString))
{
operation = (SetDataAction.OperationType) Enum.Parse(typeof(SetDataAction.OperationType), operatingString);
}
DataRewards.Add(Tuple.Create(identifier, value, operation));
}
break;
}
}
@@ -37,7 +37,7 @@ namespace Barotrauma
}
else
{
yield return item.WorldPosition;
yield return item.GetRootInventoryOwner()?.WorldPosition ?? item.WorldPosition;
}
}
}
@@ -241,7 +241,8 @@ namespace Barotrauma
public override void End()
{
if (item.CurrentHull?.Submarine == null || (!item.CurrentHull.Submarine.AtEndPosition && !item.CurrentHull.Submarine.AtStartPosition) || item.Removed)
var root = item.GetRootContainer() ?? item;
if (root.CurrentHull?.Submarine == null || (!root.CurrentHull.Submarine.AtEndPosition && !root.CurrentHull.Submarine.AtStartPosition) || item.Removed)
{
return;
}
@@ -250,6 +251,7 @@ namespace Barotrauma
item = null;
GiveReward();
completed = true;
failed = !completed && state > 0;
}
}
}
@@ -202,16 +202,40 @@ namespace Barotrauma
foreach (var position in availablePositions)
{
Vector2 pos = position.Position.ToVector2();
float dist = Vector2.DistanceSquared(pos, GetReferenceSub().WorldPosition);
Submarine refSub = GetReferenceSub();
float dist = Vector2.DistanceSquared(pos, refSub.WorldPosition);
foreach (Submarine sub in Submarine.Loaded)
{
if (sub.Info.Type != SubmarineType.Player) { continue; }
float minDistToSub = GetMinDistanceToSub(sub);
if (dist > minDistToSub * minDistToSub && dist < closestDist)
if (dist < minDistToSub * minDistToSub) { continue; }
if (closestDist == float.PositiveInfinity)
{
closestDist = dist;
chosenPosition = position;
continue;
}
//chosen position behind the sub -> override with anything that's closer or to the right
if (chosenPosition.Position.X < refSub.WorldPosition.X)
{
if (dist < closestDist || pos.X > refSub.WorldPosition.X)
{
closestDist = dist;
chosenPosition = position;
}
}
//chosen position ahead of the sub -> only override with a position that's also ahead
else if (chosenPosition.Position.X > refSub.WorldPosition.X)
{
if (dist < closestDist && pos.X > refSub.WorldPosition.X)
{
closestDist = dist;
chosenPosition = position;
}
}
}
}
//only found a spawnpos that's very far from the sub, pick one that's closer
@@ -50,6 +50,27 @@ namespace Barotrauma.Extensions
}
}
public static T RandomElementByWeight<T>(this IEnumerable<T> source, Func<T, float> weightSelector, Rand.RandSync randSync = Rand.RandSync.Unsynced)
{
float totalWeight = source.Sum(weightSelector);
float itemWeightIndex = Rand.Range(0f, 1f, randSync) * totalWeight;
float currentWeightIndex = 0;
foreach (T weightedItem in source)
{
float weight = weightSelector(weightedItem);
currentWeightIndex += weight;
if (currentWeightIndex >= itemWeightIndex)
{
return weightedItem;
}
}
return default;
}
/// <summary>
/// Executes an action that modifies the collection on each element (such as removing items from the list).
/// Creates a temporary list.
@@ -63,16 +63,17 @@ namespace Barotrauma
GameSettings.SendUserStatistics = false;
return;
}
if (GameMain.Config?.SelectedContentPackages.Count > 0)
var allPackages = GameMain.Config?.AllEnabledPackages.ToList();
if (allPackages?.Count > 0)
{
StringBuilder sb = new StringBuilder("ContentPackage: ");
int i = 0;
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
foreach (ContentPackage cp in allPackages)
{
string trimmedName = cp.Name.Replace(":", "").Replace(" ", "");
sb.Append(trimmedName.Substring(0, Math.Min(32, trimmedName.Length)));
if (i < GameMain.Config.SelectedContentPackages.Count - 1) { sb.Append(" "); }
if (i < allPackages.Count - 1) { sb.Append(" "); }
}
GameAnalytics.AddDesignEvent(sb.ToString());
}
@@ -86,9 +87,9 @@ namespace Barotrauma
if (!GameSettings.SendUserStatistics) { return; }
if (sentEventIdentifiers.Contains(identifier)) { return; }
if (GameMain.SelectedPackages != null)
if (GameMain.Config.AllEnabledPackages != null)
{
if (GameMain.VanillaContent == null || GameMain.SelectedPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
if (GameMain.VanillaContent == null || GameMain.Config.AllEnabledPackages.Any(p => p.HasMultiplayerIncompatibleContent && p != GameMain.VanillaContent))
{
message = "[MODDED] " + message;
}
@@ -51,7 +51,7 @@ namespace Barotrauma
private readonly CampaignMode campaign;
private Location location => campaign.Map.CurrentLocation;
private Location Location => campaign?.Map?.CurrentLocation;
public Action OnItemsInBuyCrateChanged;
public Action OnItemsInSellCrateChanged;
@@ -120,7 +120,7 @@ namespace Barotrauma
// Exchange money
var itemValue = GetBuyValueAtCurrentLocation(item);
campaign.Money -= itemValue;
campaign.Map.CurrentLocation.StoreCurrentBalance += itemValue;
Location.StoreCurrentBalance += itemValue;
if (removeFromCrate)
{
@@ -136,11 +136,11 @@ namespace Barotrauma
OnPurchasedItemsChanged?.Invoke();
}
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && campaign?.Map?.CurrentLocation != null ?
item.Quantity* campaign.Map.CurrentLocation.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
public int GetBuyValueAtCurrentLocation(PurchasedItem item) => item?.ItemPrefab != null && Location != null ?
item.Quantity * Location.GetAdjustedItemBuyPrice(item.ItemPrefab) : 0;
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && campaign?.Map?.CurrentLocation != null ?
quantity * campaign.Map.CurrentLocation.GetAdjustedItemSellPrice(itemPrefab) : 0;
public int GetSellValueAtCurrentLocation(ItemPrefab itemPrefab, int quantity = 1) => itemPrefab != null && Location != null ?
quantity * Location.GetAdjustedItemSellPrice(itemPrefab) : 0;
public void CreatePurchasedItems()
{
@@ -185,7 +185,7 @@ namespace Barotrauma
float floorPos = cargoRoom.Rect.Y - cargoRoom.Rect.Height;
Vector2 position = new Vector2(
Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20),
cargoRoom.Rect.Width > 40 ? Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20) : cargoRoom.Rect.Center.X,
floorPos);
//check where the actual floor structure is in case the bottom of the hull extends below it
@@ -114,7 +114,7 @@ namespace Barotrauma
}
#if CLIENT
AddCharacterToCrewList(character);
DisplayCharacterOrder(character, character.CurrentOrder, character.CurrentOrderOption);
AddCurrentOrderIcon(character, character.CurrentOrder, character.CurrentOrderOption);
#endif
}
@@ -193,7 +193,8 @@ namespace Barotrauma
#endif
}
conversationTimer = Rand.Range(5.0f, 10.0f);
//longer delay in multiplayer to prevent the server from triggering NPC conversations while the players are still loading the round
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
public void FireCharacter(CharacterInfo characterInfo)
@@ -29,9 +29,17 @@ namespace Barotrauma
public float Value
{
get => Math.Min(MaxReputation, Metadata.GetFloat(metaDataIdentifier, InitialReputation));
set => Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
set
{
Metadata.SetValue(metaDataIdentifier, Math.Clamp(value, MinReputation, MaxReputation));
OnReputationValueChanged?.Invoke();
OnAnyReputationValueChanged?.Invoke();
}
}
public Action OnReputationValueChanged;
public static Action OnAnyReputationValueChanged;
public Reputation(CampaignMetadata metadata, string identifier, int minReputation, int maxReputation, int initialReputation)
{
System.Diagnostics.Debug.Assert(metadata != null);
@@ -109,7 +109,7 @@ namespace Barotrauma
{
get
{
if (Level.Loaded != null && !Level.Loaded.Generating &&
if (Level.Loaded?.EndLocation != null && !Level.Loaded.Generating &&
Level.Loaded.Type == LevelData.LevelType.LocationConnection &&
GetAvailableTransition(out _, out _) == TransitionType.ProgressToNextEmptyLocation)
{
@@ -270,7 +270,9 @@ namespace Barotrauma
{
if (leavingSub.AtEndPosition)
{
if (Map.EndLocation != null && map.SelectedLocation == Map.EndLocation)
if (Map.EndLocation != null &&
map.SelectedLocation == Map.EndLocation &&
Map.EndLocation.Connections.Any(c => c.LevelData == Level.Loaded.LevelData))
{
nextLevel = map.StartLocation.LevelData;
return TransitionType.End;
@@ -463,8 +465,12 @@ namespace Barotrauma
}
}
foreach (CharacterInfo ci in CrewManager.CharacterInfos)
foreach (CharacterInfo ci in CrewManager.CharacterInfos.ToList())
{
if (ci.CauseOfDeath != null)
{
CrewManager.RemoveCharacterInfo(ci);
}
ci?.ResetCurrentOrder();
}
@@ -677,7 +683,7 @@ namespace Barotrauma
public void OutpostNPCAttacked(Character npc, Character attacker, AttackResult attackResult)
{
if (npc == null || attacker == null || npc.IsDead || npc.TurnedHostileByEvent) { return; }
if (npc == null || attacker == null || npc.IsDead || npc.IsInstigator) { return; }
if (npc.TeamID != Character.TeamType.FriendlyNPC) { return; }
if (!attacker.IsRemotePlayer && attacker != Character.Controlled) { return; }
Location location = Map?.CurrentLocation;
@@ -36,6 +36,8 @@ namespace Barotrauma
get { return false; }
}
public virtual void UpdateWhilePaused(float deltaTime) { }
public GameModePreset Preset
{
get { return preset; }
@@ -178,11 +178,18 @@ namespace Barotrauma
characterData.Clear();
string characterDataPath = GetCharacterDataSavePath();
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
if (characterDataDoc?.Root == null) return;
foreach (XElement subElement in characterDataDoc.Root.Elements())
if (!File.Exists(characterDataPath))
{
characterData.Add(new CharacterCampaignData(subElement));
DebugConsole.ThrowError($"Failed to load the character data for the campaign. Could not find the file \"{characterDataPath}\".");
}
else
{
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
if (characterDataDoc?.Root == null) { return; }
foreach (XElement subElement in characterDataDoc.Root.Elements())
{
characterData.Add(new CharacterCampaignData(subElement));
}
}
#endif
}
@@ -149,9 +149,9 @@ namespace Barotrauma
GameMode = mpCampaign;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
mpCampaign.LoadNewLevel();
//save to ensure the campaign ID in the save file matches the one that got assigned to this campaign instance
SaveUtil.SaveGame(saveFile);
mpCampaign.LoadNewLevel();
}
break;
}
@@ -350,6 +350,7 @@ namespace Barotrauma
}
#endif
}
private void InitializeLevel(Level level)
{
//make sure no status effects have been carried on from the next round
@@ -436,6 +437,9 @@ namespace Barotrauma
Submarine.MainSub.SetPosition(Vector2.Zero);
return;
}
var originalSubPos = Submarine.WorldPosition;
if (level.StartOutpost != null)
{
//start by placing the sub below the outpost
@@ -504,6 +508,18 @@ namespace Barotrauma
Submarine.NeutralizeBallast();
Submarine.EnableMaintainPosition();
}
// Make sure that linked subs which are NOT docked to the main sub
// (but still close enough to NOT be considered as 'left behind')
// are also moved to keep their relative position to the main sub
var linkedSubs = MapEntity.mapEntityList.FindAll(me => me is LinkedSubmarine);
foreach (LinkedSubmarine ls in linkedSubs)
{
if (ls.Sub == null || ls.Submarine != Submarine) { continue; }
if (!ls.LoadSub || ls.Sub.DockedTo.Contains(Submarine)) { continue; }
if (Submarine.Info.LeftBehindDockingPortIDs.Contains(ls.OriginalLinkedToID)) { continue; }
ls.Sub.SetPosition(ls.Sub.WorldPosition + (Submarine.WorldPosition - originalSubPos));
}
}
public void Update(float deltaTime)
@@ -562,7 +578,7 @@ namespace Barotrauma
#endif
}
public static bool IsCompatibleWithSelectedContentPackages(IList<string> contentPackagePaths, out string errorMsg)
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackagePaths, out string errorMsg)
{
errorMsg = "";
//no known content packages, must be an older save file
@@ -571,13 +587,13 @@ namespace Barotrauma
List<string> missingPackages = new List<string>();
foreach (string packagePath in contentPackagePaths)
{
if (!GameMain.Config.SelectedContentPackages.Any(cp => cp.Path == packagePath))
if (!GameMain.Config.AllEnabledPackages.Any(cp => cp.Path == packagePath))
{
missingPackages.Add(packagePath);
}
}
List<string> excessPackages = new List<string>();
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
foreach (ContentPackage cp in GameMain.Config.AllEnabledPackages)
{
if (!cp.HasMultiplayerIncompatibleContent) { continue; }
if (!contentPackagePaths.Any(p => p == cp.Path))
@@ -589,10 +605,10 @@ namespace Barotrauma
bool orderMismatch = false;
if (missingPackages.Count == 0 && missingPackages.Count == 0)
{
var selectedPackages = GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
for (int i = 0; i < contentPackagePaths.Count && i < selectedPackages.Count; i++)
var enabledPackages = GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).ToList();
for (int i = 0; i < contentPackagePaths.Count && i < enabledPackages.Count; i++)
{
if (contentPackagePaths[i] != selectedPackages[i].Path)
if (contentPackagePaths[i] != enabledPackages[i].Path)
{
orderMismatch = true;
break;
@@ -653,7 +669,7 @@ namespace Barotrauma
}
doc.Root.Add(new XAttribute("mapseed", Map.Seed));
doc.Root.Add(new XAttribute("selectedcontentpackages",
string.Join("|", GameMain.Config.SelectedContentPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
string.Join("|", GameMain.Config.AllEnabledPackages.Where(cp => cp.HasMultiplayerIncompatibleContent).Select(cp => cp.Path))));
((CampaignMode)GameMode).Save(doc.Root);
@@ -627,6 +627,7 @@ namespace Barotrauma
pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
}
#if CLIENT
if (isSingleplayer)
{
SetPendingUpgrades(pendingUpgrades);
@@ -635,6 +636,9 @@ namespace Barotrauma
{
loadedUpgrades = pendingUpgrades;
}
#else
SetPendingUpgrades(pendingUpgrades);
#endif
}
public static void LogError(string text, Dictionary<string, object?> data, Exception e = null)
@@ -669,54 +673,6 @@ namespace Barotrauma
return values;
}
/// <summary>
/// Verifies that the client and the server are agreeing on the upgrade levels, if not something has gone wrong.
/// </summary>
/// <param name="clientUpgrades"></param>
/// <param name="serverUpgrades"></param>
public static void CompareUpgrades(Dictionary<string, int> clientUpgrades, Dictionary<string, int> serverUpgrades)
{
int mismatches = 0;
DebugLog("Comparing client upgrades to server upgrades...", Color.Orange);
foreach (var (key, value) in clientUpgrades)
{
if (!serverUpgrades.ContainsKey(key))
{
DebugLog($"Client has an upgrade the server doesn't! {key} lvl. {value}.", Color.Red);
mismatches++;
continue;
}
if (value != serverUpgrades[key])
{
DebugLog($"Client's upgrade level doesn't match the server's! Client: {key} {value}, Server: {key} {serverUpgrades[key]}.", Color.Red);
mismatches++;
}
}
DebugLog("...comparing server upgrades to client upgrades...", Color.Orange);
foreach (var (key, value) in serverUpgrades)
{
if (!clientUpgrades.ContainsKey(key))
{
DebugLog($"Server has an upgrade the client doesn't! {key} lvl. {value}.", Color.Red);
mismatches++;
}
}
if (mismatches == 0)
{
DebugLog("Everything ok!");
}
else
{
DebugLog($"{mismatches} mismatches found! This means that the client and the server are disagreeing on upgrade levels and might cause desync.\n", Color.Red);
#if CLIENT
DebugConsole.IsOpen = true;
#endif
}
}
/// <summary>
/// Used to sync the pending upgrades list in multiplayer.
/// </summary>
@@ -251,7 +251,24 @@ namespace Barotrauma
set { TextManager.Language = value; }
}
public readonly List<ContentPackage> SelectedContentPackages = new List<ContentPackage>();
public ContentPackage CurrentCorePackage { get; private set; }
private readonly List<ContentPackage> enabledRegularPackages = new List<ContentPackage>();
public IReadOnlyList<ContentPackage> EnabledRegularPackages
{
get { return enabledRegularPackages; }
}
public IEnumerable<ContentPackage> AllEnabledPackages
{
get
{
yield return CurrentCorePackage;
foreach (var package in EnabledRegularPackages)
{
yield return package;
}
}
}
public bool ContentPackageSelectionDirtyNotification
{
@@ -267,6 +284,8 @@ namespace Barotrauma
public volatile bool SuppressModFolderWatcher;
public volatile bool WaitingForAutoUpdate;
#if DEBUG
public bool AutomaticQuickStartEnabled { get; set; }
public bool AutomaticCampaignLoadEnabled { get; set; }
@@ -275,7 +294,7 @@ namespace Barotrauma
private System.IO.FileSystemWatcher modsFolderWatcher;
private int ContentFileLoadOrder(ContentFile a)
private static int ContentFileLoadOrder(ContentFile a)
{
switch (a.Type)
{
@@ -292,62 +311,185 @@ namespace Barotrauma
public void SelectCorePackage(ContentPackage contentPackage, bool forceReloadAll = false)
{
if (!contentPackage.IsCorePackage) { return; }
if (!contentPackage.ContainsRequiredCorePackageFiles(out _)) { return; }
ContentPackage otherCorePackage = SelectedContentPackages.Where(cp => cp.CorePackage).First();
ContentPackage prevCorePackage = CurrentCorePackage;
SelectedContentPackages.Remove(otherCorePackage);
SelectedContentPackages.Add(contentPackage);
CurrentCorePackage = contentPackage;
ContentPackage.SortContentPackages();
if (prevCorePackage != null)
{
List<ContentFile> filesToRemove = prevCorePackage.Files.Where(f1 => forceReloadAll ||
!contentPackage.Files.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
List<ContentFile> filesToRemove = otherCorePackage.Files.Where(f1 => forceReloadAll ||
!contentPackage.Files.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
List<ContentFile> filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll ||
!prevCorePackage.Files.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
List<ContentFile> filesToAdd = contentPackage.Files.Where(f1 => forceReloadAll ||
!otherCorePackage.Files.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
DisableContentPackageItems(filesToRemove);
EnableContentPackageItems(filesToAdd);
DisableContentPackageItems(filesToRemove.OrderBy(ContentFileLoadOrder));
EnableContentPackageItems(filesToAdd.OrderBy(ContentFileLoadOrder));
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
RefreshContentPackageItems(filesToAdd.Concat(filesToRemove));
}
else
{
EnableContentPackageItems(contentPackage.Files);
RefreshContentPackageItems(contentPackage.Files);
}
}
public void AutoSelectCorePackage(IEnumerable<ContentPackage> toRemove)
{
SelectCorePackage(ContentPackage.List.Find(cpp =>
cpp.CorePackage &&
!toRemove.Contains(cpp) &&
SelectCorePackage(ContentPackage.CorePackages.Find(cpp =>
(toRemove == null || !toRemove.Contains(cpp)) &&
cpp.ContainsRequiredCorePackageFiles(out _)));
}
public void SelectContentPackage(ContentPackage contentPackage)
{
if (!SelectedContentPackages.Contains(contentPackage))
{
SelectedContentPackages.Add(contentPackage);
ContentPackage.SortContentPackages();
private List<Tuple<ContentPackage, bool>> backupModOrder;
EnableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
public void SwapPackages(ContentPackage corePackage, List<ContentPackage> regularPackages)
{
backupModOrder = new List<Tuple<ContentPackage, bool>>();
backupModOrder.Add(new Tuple<ContentPackage, bool>(CurrentCorePackage, true));
for (int i=0;i<ContentPackage.RegularPackages.Count;i++)
{
var p = ContentPackage.RegularPackages[i];
backupModOrder.Add(new Tuple<ContentPackage, bool>(p, EnabledRegularPackages.Contains(p)));
}
List<ContentPackage> packagesToDisable = new List<ContentPackage>();
packagesToDisable.Add(CurrentCorePackage);
packagesToDisable.AddRange(EnabledRegularPackages.Where(p => p.HasMultiplayerIncompatibleContent));
List<ContentPackage> packagesToEnable = new List<ContentPackage>();
packagesToEnable.Add(corePackage);
packagesToEnable.AddRange(regularPackages);
IEnumerable<ContentFile> filesOfDisabledPkgs = packagesToDisable.SelectMany(p => p.Files);
IEnumerable<ContentFile> filesOfEnabledPkgs = packagesToEnable.SelectMany(p => p.Files);
List<ContentFile> filesToDisable = filesOfDisabledPkgs.Where(f1 =>
!filesOfEnabledPkgs.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
List<ContentFile> filesToEnable = filesOfEnabledPkgs.Where(f1 =>
!filesOfDisabledPkgs.Any(f2 =>
Path.GetFullPath(f1.Path).CleanUpPath() == Path.GetFullPath(f2.Path).CleanUpPath())).ToList();
CurrentCorePackage = corePackage;
enabledRegularPackages.RemoveAll(p => p.HasMultiplayerIncompatibleContent); enabledRegularPackages.AddRange(regularPackages);
DisableContentPackageItems(filesToDisable);
EnableContentPackageItems(filesToEnable);
RefreshContentPackageItems(filesOfEnabledPkgs.Concat(filesToDisable));
ContentPackage.SortContentPackages(p => -regularPackages.IndexOf(p));
}
public void RestoreBackupPackages()
{
if (backupModOrder == null) { return; }
SwapPackages(
backupModOrder[0].Item1,
backupModOrder.Skip(1).Where(p => p.Item2).Select(p => p.Item1).ToList());
ContentPackage.SortContentPackages(p => backupModOrder.FindIndex(n => n.Item1 == p));
backupModOrder = null;
}
public void EnableRegularPackage(ContentPackage contentPackage)
{
if (contentPackage.IsCorePackage) { return; }
if (!enabledRegularPackages.Contains(contentPackage))
{
enabledRegularPackages.Add(contentPackage);
SortContentPackages();
EnableContentPackageItems(contentPackage.Files);
RefreshContentPackageItems(contentPackage.Files);
}
}
public void DeselectContentPackage(ContentPackage contentPackage)
public void DisableRegularPackage(ContentPackage contentPackage)
{
if (SelectedContentPackages.Contains(contentPackage))
if (contentPackage.IsCorePackage) { return; }
if (enabledRegularPackages.Contains(contentPackage))
{
SelectedContentPackages.Remove(contentPackage);
ContentPackage.SortContentPackages();
DisableContentPackageItems(contentPackage.Files.OrderBy(ContentFileLoadOrder));
enabledRegularPackages.Remove(contentPackage);
SortContentPackages();
DisableContentPackageItems(contentPackage.Files);
RefreshContentPackageItems(contentPackage.Files);
}
}
private void EnableContentPackageItems(IOrderedEnumerable<ContentFile> files)
public void SortContentPackages(bool refreshAll = false)
{
for (int i = enabledRegularPackages.Count - 1; i >= 0; i--)
{
var package = enabledRegularPackages[i];
if (!ContentPackage.RegularPackages.Contains(package))
{
ContentPackage replacement = ContentPackage.RegularPackages.Find(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
if (replacement != null)
{
enabledRegularPackages[i] = replacement;
}
else
{
DisableRegularPackage(package);
}
}
}
if (CurrentCorePackage == null)
{
AutoSelectCorePackage(null);
}
else if (!ContentPackage.CorePackages.Contains(CurrentCorePackage))
{
ContentPackage replacement = ContentPackage.CorePackages.Find(p => p.Name.Equals(CurrentCorePackage.Name, StringComparison.OrdinalIgnoreCase));
if (replacement != null)
{
SelectCorePackage(replacement);
}
else
{
AutoSelectCorePackage(null);
}
}
var sortedSelected = enabledRegularPackages
.OrderBy(p => -ContentPackage.RegularPackages.IndexOf(p))
.ToList();
enabledRegularPackages.Clear(); enabledRegularPackages.AddRange(sortedSelected);
CharacterPrefab.Prefabs.SortAll();
AfflictionPrefab.Prefabs.SortAll();
JobPrefab.Prefabs.SortAll();
ItemPrefab.Prefabs.SortAll();
CoreEntityPrefab.Prefabs.SortAll();
ItemAssemblyPrefab.Prefabs.SortAll();
StructurePrefab.Prefabs.SortAll();
#if CLIENT
GameMain.DecalManager?.Prefabs.SortAll();
GameMain.ParticleManager?.Prefabs.SortAll();
#endif
if (refreshAll)
{
RefreshContentPackageItems(AllEnabledPackages.SelectMany(p => p.Files));
}
}
public void EnableContentPackageItems(IEnumerable<ContentFile> unorderedFiles)
{
if (WaitingForAutoUpdate) { return; }
IOrderedEnumerable<ContentFile> files = unorderedFiles.OrderBy(ContentFileLoadOrder);
foreach (ContentFile file in files)
{
switch (file.Type)
@@ -390,8 +532,10 @@ namespace Barotrauma
}
}
private void DisableContentPackageItems(IOrderedEnumerable<ContentFile> files)
public void DisableContentPackageItems(IEnumerable<ContentFile> unorderedFiles)
{
if (WaitingForAutoUpdate) { return; }
IOrderedEnumerable<ContentFile> files = unorderedFiles.OrderBy(ContentFileLoadOrder);
foreach (ContentFile file in files)
{
switch (file.Type)
@@ -434,9 +578,9 @@ namespace Barotrauma
}
}
private void RefreshContentPackageItems(IEnumerable<ContentFile> files)
public void RefreshContentPackageItems(IEnumerable<ContentFile> files)
{
if (files.Any(f => f.Type == ContentType.LocationTypes)) { LocationType.Init(); }
if (WaitingForAutoUpdate) { return; }
if (files.Any(f => f.Type == ContentType.Afflictions)) { AfflictionPrefab.LoadAll(GameMain.Instance.GetFilesOfType(ContentType.Afflictions)); }
if (files.Any(f => f.Type == ContentType.Submarine ||
f.Type == ContentType.Outpost ||
@@ -447,7 +591,13 @@ namespace Barotrauma
if (files.Any(f => f.Type == ContentType.Factions)) { FactionPrefab.LoadFactions(); }
if (files.Any(f => f.Type == ContentType.Item)) { ItemPrefab.InitFabricationRecipes(); }
if (files.Any(f => f.Type == ContentType.RuinConfig)) { RuinGeneration.RuinGenerationParams.ClearAll(); }
if (files.Any(f => f.Type == ContentType.RandomEvents)) { EventSet.LoadPrefabs(); }
if (files.Any(f => f.Type == ContentType.RandomEvents ||
f.Type == ContentType.LocationTypes))
{
LocationType.List.Clear();
EventSet.LoadPrefabs();
LocationType.Init();
}
if (files.Any(f => f.Type == ContentType.Missions)) { MissionPrefab.Init(); }
if (files.Any(f => f.Type == ContentType.LevelObjectPrefabs)) { LevelObjectPrefab.LoadAll(); }
if (files.Any(f => f.Type == ContentType.MapGenerationParameters)) { MapGenerationParams.Init(); }
@@ -515,49 +665,6 @@ namespace Barotrauma
}
}
public void ReorderSelectedContentPackages<T>(Func<ContentPackage, T> orderFunction)
{
ContentPackage.List = ContentPackage.List
.OrderByDescending(p => p.CorePackage)
.ThenBy(orderFunction)
.ToList();
ContentPackage.SortContentPackages();
CharacterPrefab.Prefabs.SortAll();
AfflictionPrefab.Prefabs.SortAll();
JobPrefab.Prefabs.SortAll();
ItemPrefab.Prefabs.SortAll();
CoreEntityPrefab.Prefabs.SortAll();
ItemAssemblyPrefab.Prefabs.SortAll();
StructurePrefab.Prefabs.SortAll();
SubmarineInfo.RefreshSavedSubs();
ItemPrefab.InitFabricationRecipes();
RuinGeneration.RuinGenerationParams.ClearAll();
EventSet.LoadPrefabs();
MissionPrefab.Init();
LevelObjectPrefab.LoadAll();
LocationType.Init();
MapGenerationParams.Init();
LevelGenerationParams.LoadPresets();
OutpostGenerationParams.LoadPresets();
TraitorMissionPrefab.Init();
Order.Init();
EventManagerSettings.Init();
WreckAIConfig.LoadAll();
SkillSettings.Load(GameMain.Instance.GetFilesOfType(ContentType.SkillSettings));
#if CLIENT
GameMain.DecalManager.Prefabs.SortAll();
GameMain.ParticleManager.Prefabs.SortAll();
SoundPlayer.Init().ForEach(_ => { return; });
#endif
}
private HashSet<string> selectedContentPackagePaths = new HashSet<string>();
public string MasterServerUrl { get; set; }
public string RemoteContentUrl { get; set; }
public bool AutoCheckUpdates { get; set; }
@@ -621,6 +728,7 @@ namespace Barotrauma
private bool showTutorialSkipWarning = true;
public static bool EnableSubmarineAutoSave { get; set; }
public static int MaximumAutoSaves { get; set; }
public static Color SubEditorBackgroundColor { get; set; }
public bool ShowTutorialSkipWarning
@@ -662,54 +770,64 @@ namespace Barotrauma
case System.IO.WatcherChangeTypes.Created:
{
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
if (File.Exists(cpPath) &&
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
{
var cp = new ContentPackage(cpPath);
ContentPackage.List.Add(cp);
ContentPackage.AddPackage(new ContentPackage(cpPath));
}
}
break;
case System.IO.WatcherChangeTypes.Deleted:
{
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
var packagesToDeselect = GameMain.Config.SelectedContentPackages.Where(p => toRemove.Contains(p)).ToList();
foreach (var cp in packagesToDeselect)
{
if (cp.CorePackage)
{
GameMain.Config.AutoSelectCorePackage(toRemove);
}
else
{
GameMain.Config.DeselectContentPackage(cp);
}
}
var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
foreach (var cp in toRemove)
{
ContentPackage.List.Remove(cp);
if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); }
}
toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath));
bool reselectCore = false;
foreach (var cp in toRemove)
{
ContentPackage.RemovePackage(cp);
if (cp.IsCorePackage)
{
reselectCore = true;
}
}
if (reselectCore) { AutoSelectCorePackage(null); }
}
break;
case System.IO.WatcherChangeTypes.Renamed:
{
System.IO.RenamedEventArgs renameArgs = e as System.IO.RenamedEventArgs;
string cpPath = Path.GetFullPath(Path.Combine(renameArgs.OldFullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
var toRemove = ContentPackage.List.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
string cpPath = Path.GetFullPath(Path.Combine(e.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
var toRemove = ContentPackage.RegularPackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath).ToList();
foreach (var cp in toRemove)
{
GameMain.Config.DeselectContentPackage(cp);
ContentPackage.List.Remove(cp);
if (enabledRegularPackages.Contains(cp)) { DisableRegularPackage(cp); }
}
toRemove.AddRange(ContentPackage.CorePackages.Where(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath));
bool reselectCore = false;
foreach (var cp in toRemove)
{
ContentPackage.RemovePackage(cp);
if (cp.IsCorePackage)
{
reselectCore = true;
}
}
cpPath = Path.GetFullPath(Path.Combine(renameArgs.FullPath, Steam.SteamManager.MetadataFileName)).CleanUpPath();
if (File.Exists(cpPath) && !ContentPackage.List.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
if (File.Exists(cpPath) &&
!ContentPackage.AllPackages.Any(cp => Path.GetFullPath(cp.Path).CleanUpPath() == cpPath))
{
var cp = new ContentPackage(cpPath);
ContentPackage.List.Add(cp);
ContentPackage.AddPackage(new ContentPackage(cpPath));
}
if (reselectCore) { AutoSelectCorePackage(null); }
}
break;
}
@@ -723,7 +841,7 @@ namespace Barotrauma
GraphicsWidth = 1024;
GraphicsHeight = 768;
MasterServerUrl = "";
SelectContentPackage(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage(""));
SelectCorePackage(ContentPackage.CorePackages.FirstOrDefault());
jobPreferences = new List<Pair<string, int>>();
return;
}
@@ -778,6 +896,7 @@ namespace Barotrauma
new XAttribute("verboselogging", VerboseLogging),
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
new XAttribute("maxautosaves", MaximumAutoSaves),
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
new XAttribute("enablesplashscreen", EnableSplashScreen),
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
@@ -831,11 +950,11 @@ namespace Barotrauma
new XAttribute("hudscale", HUDScale),
new XAttribute("inventoryscale", InventoryScale));
foreach (ContentPackage contentPackage in SelectedContentPackages)
foreach (ContentPackage contentPackage in ContentPackage.CorePackages)
{
if (contentPackage.Path.Contains(VanillaContentPackagePath))
{
doc.Root.Add(new XElement("contentpackage", new XAttribute("path", contentPackage.Path)));
doc.Root.Add(new XElement("contentpackages", new XElement("core", new XAttribute("name", contentPackage.Name))));
break;
}
}
@@ -978,112 +1097,6 @@ namespace Barotrauma
return true;
}
public void ReloadContentPackages()
{
LoadContentPackages(selectedContentPackagePaths);
}
private void LoadContentPackages(IEnumerable<string> contentPackagePaths)
{
var missingPackagePaths = new List<string>();
var incompatiblePackages = new List<ContentPackage>();
var packagesWithErrors = new List<ContentPackage>();
SelectedContentPackages.Clear();
foreach (string path in contentPackagePaths)
{
var matchingContentPackage = ContentPackage.List.Find(cp => Barotrauma.IO.Path.GetFullPath(cp.Path).CleanUpPath() == path.CleanUpPath());
if (matchingContentPackage == null)
{
missingPackagePaths.Add(path);
}
else if (!matchingContentPackage.IsCompatible())
{
DebugConsole.NewMessage(
$"Content package \"{matchingContentPackage.Name}\" is not compatible with this version of Barotrauma (game version: {GameMain.Version}, content package version: {matchingContentPackage.GameVersion})",
Color.Red);
incompatiblePackages.Add(matchingContentPackage);
}
else
{
if (!matchingContentPackage.CheckErrors(out List<string> errorMessages))
{
DebugConsole.NewMessage(
$"Errors found in content package \"{matchingContentPackage.Name}\": " + string.Join(", ", errorMessages),
Color.Red);
packagesWithErrors.Add(matchingContentPackage);
}
//add content packages with errors as they are generally able to load most of their assets
SelectedContentPackages.Add(matchingContentPackage);
}
}
EnsureCoreContentPackageSelected(gameLoaded: false);
ContentPackage.SortContentPackages();
TextManager.LoadTextPacks(SelectedContentPackages);
foreach (ContentPackage contentPackage in SelectedContentPackages)
{
foreach (ContentFile file in contentPackage.Files)
{
ToolBox.IsProperFilenameCase(file.Path);
}
}
//save to get rid of the invalid selected packages in the config file
if (missingPackagePaths.Count > 0 || incompatiblePackages.Count > 0 || packagesWithErrors.Count > 0) { SaveNewPlayerConfig(); }
//display error messages after all content packages have been loaded
//to make sure the package that contains text files has been loaded before we attempt to use TextManager
foreach (string missingPackagePath in missingPackagePaths)
{
DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageNotFound", "[packagepath]", missingPackagePath));
}
foreach (ContentPackage invalidPackage in packagesWithErrors)
{
DebugConsole.ThrowError(TextManager.GetWithVariable("ContentPackageHasErrors", "[packagename]", invalidPackage.Name), createMessageBox: true);
}
foreach (ContentPackage incompatiblePackage in incompatiblePackages)
{
DebugConsole.ThrowError(TextManager.GetWithVariables(incompatiblePackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage",
new string[3] { "[packagename]", "[packageversion]", "[gameversion]" }, new string[3] { incompatiblePackage.Name, incompatiblePackage.GameVersion.ToString(), GameMain.Version.ToString() }),
createMessageBox: true);
}
}
public void EnsureCoreContentPackageSelected(bool gameLoaded=true)
{
if (SelectedContentPackages.Any(cp => cp.CorePackage)) { return; }
if (GameMain.VanillaContent != null)
{
if (gameLoaded)
{
SelectContentPackage(GameMain.VanillaContent);
}
else
{
SelectedContentPackages.Add(GameMain.VanillaContent);
}
}
else
{
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
if (availablePackage != null)
{
if (gameLoaded)
{
SelectContentPackage(availablePackage);
}
else
{
SelectedContentPackages.Add(availablePackage);
}
}
}
}
#endregion
#region Save PlayerConfig
@@ -1106,6 +1119,7 @@ namespace Barotrauma
new XAttribute("verboselogging", VerboseLogging),
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
new XAttribute("submarineautosave", EnableSubmarineAutoSave),
new XAttribute("maxautosaves", MaximumAutoSaves),
new XAttribute("subeditorbackground", XMLExtensions.ColorToString(SubEditorBackgroundColor)),
new XAttribute("enablesplashscreen", EnableSplashScreen),
new XAttribute("usesteammatchmaking", UseSteamMatchmaking),
@@ -1202,11 +1216,32 @@ namespace Barotrauma
new XAttribute("hudscale", HUDScale),
new XAttribute("inventoryscale", InventoryScale));
foreach (ContentPackage contentPackage in SelectedContentPackages)
XElement contentPackagesElement = new XElement("contentpackages");
string corePackageName = (CurrentCorePackage ?? ContentPackage.CorePackages.FirstOrDefault()).Name;
contentPackagesElement.Add(new XElement("core", new XAttribute("name", corePackageName)));
XElement regularPackagesElement = new XElement("regular");
foreach (ContentPackage package in ContentPackage.RegularPackages)
{
doc.Root.Add(new XElement("contentpackage",
new XAttribute("path", contentPackage.Path)));
XElement packageElement = new XElement("package", new XAttribute("name", package.Name));
if (EnabledRegularPackages.Contains(package)) { packageElement.Add(new XAttribute("enabled", "true")); }
regularPackagesElement.Add(packageElement);
}
contentPackagesElement.Add(regularPackagesElement);
doc.Root.Add(contentPackagesElement);
#if UNSTABLE
//TODO: remove at some point
foreach (ContentPackage package in AllEnabledPackages)
{
XElement compatibilityElement = new XElement("contentpackage");
compatibilityElement.Add(new XAttribute("path", package.Path));
doc.Root.Add(compatibilityElement);
}
#endif
#if CLIENT
var keyMappingElement = new XElement("keymapping");
@@ -1318,6 +1353,7 @@ namespace Barotrauma
sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", sendUserStatistics);
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", QuickStartSubmarineName);
EnableSubmarineAutoSave = doc.Root.GetAttributeBool("submarineautosave", true);
MaximumAutoSaves = doc.Root.GetAttributeInt("maxautosaves", 8);
SubEditorBackgroundColor = doc.Root.GetAttributeColor("subeditorbackground", new Color(0.051f, 0.149f, 0.271f, 1.0f));
UseSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", UseSteamMatchmaking);
RequireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", RequireSteamAuthentication);
@@ -1441,18 +1477,79 @@ namespace Barotrauma
private void LoadContentPackages(XDocument doc)
{
selectedContentPackagePaths = new HashSet<string>();
foreach (XElement subElement in doc.Root.Elements())
CurrentCorePackage = null;
enabledRegularPackages.Clear();
var contentPackagesElement = doc.Root.Element("contentpackages");
if (contentPackagesElement != null)
{
switch (subElement.Name.ToString().ToLowerInvariant())
string coreName = contentPackagesElement.Element("core")?.GetAttributeString("name", "");
ContentPackage corePackage = ContentPackage.CorePackages.Find(p => p.Name.Equals(coreName, StringComparison.OrdinalIgnoreCase));
if (corePackage != null)
{
case "contentpackage":
string path = Path.GetFullPath(subElement.GetAttributeString("path", ""));
selectedContentPackagePaths.Add(path);
break;
CurrentCorePackage = corePackage;
}
XElement regularElement = contentPackagesElement.Element("regular");
List<XElement> subElements = regularElement?.Elements()?.ToList();
if (subElements != null)
{
ContentPackage.SortContentPackages(p =>
{
int index = subElements.FindIndex(e =>
{
string name = e.GetAttributeString("name", null);
return p.Name.Equals(name, StringComparison.OrdinalIgnoreCase);
});
return index;
});
foreach (var subElement in subElements)
{
if (!bool.TryParse(subElement.GetAttributeString("enabled", "false"), out bool enabled) || !enabled) { continue; }
string name = subElement.GetAttributeString("name", null);
if (string.IsNullOrEmpty(name)) { continue; }
var package = ContentPackage.RegularPackages.Find(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (package == null) { continue; }
enabledRegularPackages.Add(package);
}
}
}
LoadContentPackages(selectedContentPackagePaths);
else
{
var enabledContentPackagePaths = new List<string>();
foreach (XElement subElement in doc.Root.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "contentpackage":
string path = subElement.GetAttributeString("path", "");
enabledContentPackagePaths.Add(path.CleanUpPath().ToLowerInvariant());
break;
}
}
ContentPackage.SortContentPackages(p => enabledContentPackagePaths.IndexOf(p.Path.CleanUpPath().ToLowerInvariant()));
foreach (string path in enabledContentPackagePaths)
{
ContentPackage package = ContentPackage.AllPackages
.FirstOrDefault(p => p.Path.CleanUpPath().Equals(path, StringComparison.OrdinalIgnoreCase));
if (package == null) { continue; }
if (package.IsCorePackage) { CurrentCorePackage = package; }
else { enabledRegularPackages.Add(package); }
}
}
if (CurrentCorePackage == null)
{
CurrentCorePackage = ContentPackage.CorePackages.First();
}
TextManager.LoadTextPacks(AllEnabledPackages);
}
#endregion
@@ -18,5 +18,10 @@ namespace Barotrauma
Shoot,
Command,
ToggleInventory
#if DEBUG
,
NextFireMode,
PreviousFireMode
#endif
}
}
@@ -209,15 +209,17 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#endif
}
public void Lock(bool isNetworkMessage, bool forcePosition = false)
{
#if CLIENT
if (GameMain.Client != null && !isNetworkMessage) return;
if (GameMain.Client != null && !isNetworkMessage) { return; }
#endif
if (DockingTarget == null)
@@ -251,6 +253,7 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = DockingTarget.item.ID;
item.CreateServerEvent(this);
}
#else
@@ -332,20 +335,45 @@ namespace Barotrauma.Items.Components
{
if (DockingDir != 0) { return DockingDir; }
if (Door != null)
if (Door != null && Door.LinkedGap.linkedTo.Count > 0)
{
if (Door.LinkedGap.linkedTo.Count == 1)
Hull refHull = null;
float largestHullSize = 0.0f;
foreach (MapEntity linked in Door.LinkedGap.linkedTo)
{
if (!(linked is Hull hull)) { continue; }
if (hull.Volume > largestHullSize)
{
refHull = hull;
largestHullSize = hull.Volume;
}
}
if (refHull != null)
{
return IsHorizontal ?
Math.Sign(Door.Item.WorldPosition.X - Door.LinkedGap.linkedTo[0].WorldPosition.X) :
Math.Sign(Door.Item.WorldPosition.Y - Door.LinkedGap.linkedTo[0].WorldPosition.Y);
Math.Sign(Door.Item.WorldPosition.X - refHull.WorldPosition.X) :
Math.Sign(Door.Item.WorldPosition.Y - refHull.WorldPosition.Y);
}
else if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count == 1)
}
if (dockingTarget?.Door?.LinkedGap != null && dockingTarget.Door.LinkedGap.linkedTo.Count > 0)
{
Hull refHull = null;
float largestHullSize = 0.0f;
foreach (MapEntity linked in dockingTarget.Door.LinkedGap.linkedTo)
{
if (!(linked is Hull hull)) { continue; }
if (hull.Volume > largestHullSize)
{
refHull = hull;
largestHullSize = hull.Volume;
}
}
if (refHull != null)
{
return IsHorizontal ?
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
Math.Sign(dockingTarget.Door.LinkedGap.linkedTo[0].WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
}
Math.Sign(refHull.WorldPosition.X - dockingTarget.Door.Item.WorldPosition.X) :
Math.Sign(refHull.WorldPosition.Y - dockingTarget.Door.Item.WorldPosition.Y);
}
}
if (dockingTarget != null)
{
@@ -838,6 +866,7 @@ namespace Barotrauma.Items.Components
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true))
{
originalDockingTargetID = Entity.NullEntityID;
item.CreateServerEvent(this);
}
#endif
@@ -1010,9 +1039,7 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
#if CLIENT
if (GameMain.Client != null) return;
#endif
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool wasDocked = docked;
DockingPort prevDockingTarget = DockingTarget;
@@ -1020,7 +1047,10 @@ namespace Barotrauma.Items.Components
switch (connection.Name)
{
case "toggle":
Docked = !docked;
if (signal != "0")
{
Docked = !docked;
}
break;
case "set_active":
case "set_state":
@@ -1044,16 +1074,5 @@ namespace Barotrauma.Items.Components
}
#endif
}
public void ServerWrite(IWriteMessage msg, Client c, object[] extraData = null)
{
msg.Write(docked);
if (docked)
{
msg.Write(DockingTarget.item.ID);
msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null);
}
}
}
}
@@ -90,8 +90,10 @@ namespace Barotrauma.Items.Components
get { return stuck; }
set
{
if (isOpen || isBroken || !CanBeWelded) return;
if (isOpen || isBroken || !CanBeWelded) { return; }
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
//don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
if (stuck <= 0.0f) { IsStuck = false; }
if (stuck >= 99.0f) { IsStuck = true; }
}
@@ -366,12 +368,24 @@ namespace Barotrauma.Items.Components
}
else
{
Body.Enabled = Impassable || openState < 1.0f;
bool wasEnabled = Body.Enabled;
Body.Enabled = Impassable || openState < 1.0f;
if (wasEnabled && !Body.Enabled && IsHorizontal)
{
//when opening a hatch, force characters above it to refresh the floor position
//(otherwise the character won't fall through the hatch until it moves)
foreach (Character c in Character.CharacterList)
{
if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
c.AnimController?.ForceRefreshFloorY();
}
}
}
//don't use the predicted state here, because it might set
//other items to an incorrect state if the prediction is wrong
item.SendSignal(0, (isOpen) ? "1" : "0", "state_out", null);
item.SendSignal(0, isOpen ? "1" : "0", "state_out", null);
}
partial void UpdateProjSpecific(float deltaTime);
@@ -616,12 +630,13 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
{
if (IsStuck) return;
if (IsStuck) { return; }
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
if (connection.Name == "toggle")
{
if (signal == "0") { return; }
if (toggleCooldownTimer > 0.0f && sender != lastUser) { OnFailedToOpen(); return; }
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
toggleCooldownTimer = ToggleCoolDown;
@@ -0,0 +1,832 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Vector2 = Microsoft.Xna.Framework.Vector2;
namespace Barotrauma.Items.Components
{
internal class ProducedItem
{
[Serialize(0f, true)]
public float Probability { get; set; }
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
public readonly ItemPrefab? Prefab;
public ProducedItem(ItemPrefab prefab, float probability)
{
Prefab = prefab;
Probability = probability;
}
public ProducedItem(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
string itemIdentifier = element.GetAttributeString("identifier", string.Empty);
if (!string.IsNullOrWhiteSpace(itemIdentifier))
{
Prefab = ItemPrefab.Find(null, itemIdentifier);
}
LoadSubElements(element);
}
private void LoadSubElements(XElement element)
{
if (!element.HasElements) { return; }
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "statuseffect":
{
StatusEffect effect = StatusEffect.Load(subElement, Prefab?.Name);
if (effect.type != ActionType.OnProduceSpawned)
{
DebugConsole.ThrowError("Only OnProduceSpawned type can be used in <ProducedItem>.");
continue;
}
StatusEffects.Add(effect);
break;
}
}
}
}
}
// ReSharper disable UnusedMember.Global
internal enum VineTileType
{
Stem = 0b0000,
CrossJunction = 0b1111,
VerticalLane = 0b1010,
HorizontalLane = 0b0101,
TurnTopRight = 0b1001,
TurnTopLeft = 0b0011,
TurnBottomLeft = 0b0110,
TurnBottomRight = 0b1100,
TSectionTop = 0b1011,
TSectionLeft = 0b0111,
TSectionBottom = 0b1110,
TSectionRight = 0b1101,
StumpTop = 0b0001,
StumpLeft = 0b0010,
StumpBottom = 0b0100,
StumpRight = 0b1000
}
[Flags]
internal enum TileSide
{
None = 0,
Top = 1 << 0,
Left = 1 << 1,
Bottom = 1 << 2,
Right = 1 << 3
}
internal struct FoliageConfig
{
public static FoliageConfig EmptyConfig = new FoliageConfig { Variant = -1, Rotation = 0f, Scale = 1.0f };
public static readonly int EmptyConfigValue = EmptyConfig.Serialize();
public int Variant;
public float Rotation;
public float Scale;
public readonly int Serialize()
{
int variant = Math.Min(Variant + 1, 15);
int scale = (int) (Scale * 10f);
int rotation = (int) (Rotation / MathHelper.TwoPi * 10f);
return variant | (scale << 4) | (rotation << 8);
}
public static FoliageConfig Deserialize(int value)
{
int variant = value & 0x00F;
int scale = (value & 0x0F0) >> 4;
int rotation = (value & 0xF00) >> 8;
return new FoliageConfig { Variant = variant - 1, Scale = scale / 10f, Rotation = rotation / 10f * MathHelper.TwoPi };
}
public static FoliageConfig CreateRandomConfig(int maxVariants, float minScale, float maxScale, Random? random = null)
{
int flowerVariant = Growable.RandomInt(0, maxVariants, random);
float flowerScale = (float) Growable.RandomDouble(minScale, maxScale, random);
float flowerRotation = (float) Growable.RandomDouble(0, MathHelper.TwoPi, random);
return new FoliageConfig { Variant = flowerVariant, Scale = flowerScale, Rotation = flowerRotation };
}
}
internal partial class VineTile
{
public TileSide Sides = TileSide.None;
public TileSide BlockedSides = TileSide.None;
public readonly FoliageConfig FlowerConfig;
public readonly FoliageConfig LeafConfig;
public int FailedGrowthAttempts;
public Rectangle Rect;
public Vector2 Position;
public Color HealthColor = Color.Transparent;
public float DecayDelay;
private float VineStep;
private float FlowerStep;
private float growthStep;
public float GrowthStep
{
get => growthStep;
set
{
const float limit = 1.0f;
growthStep = value;
VineStep = Math.Min((float) Math.Pow(value, 2), limit);
if (value > limit)
{
FlowerStep = Math.Min((float) Math.Pow(value - limit, 2), limit);
}
}
}
private readonly float diameter;
private Vector2 offset;
private readonly Growable Parent;
public VineTileType Type;
public readonly Dictionary<TileSide, Vector2> AdjacentPositions;
public static int Size = 32;
public VineTile(Growable parent, Vector2 position, VineTileType type, FoliageConfig? flowerConfig = null, FoliageConfig? leafConfig = null, Rectangle? rect = null)
{
FlowerConfig = flowerConfig ?? FoliageConfig.EmptyConfig;
LeafConfig = leafConfig ?? FoliageConfig.EmptyConfig;
Position = position;
Rect = rect ?? CreatePlantRect(position);
Parent = parent;
Type = type;
diameter = Rect.Width / 2.0f;
AdjacentPositions = new Dictionary<TileSide, Vector2>
{
{ TileSide.Top, new Vector2(Position.X, Position.Y + Rect.Height) },
{ TileSide.Bottom, new Vector2(Position.X, Position.Y - Rect.Height) },
{ TileSide.Left, new Vector2(Position.X - Rect.Width, Position.Y) },
{ TileSide.Right, new Vector2(Position.X + Rect.Width, Position.Y) }
};
}
public void UpdateScale(float deltaTime)
{
if (Parent.Decayed && GrowthStep > 1.0f)
{
if (DecayDelay > 0)
{
DecayDelay -= deltaTime;
}
else
{
GrowthStep -= 0.25f * deltaTime;
}
}
if (GrowthStep >= 2.0f || Parent.Decayed) { return; }
GrowthStep += deltaTime;
if (GrowthStep < 1.0f)
{
// I don't know how or why this works
float offsetAmount = diameter * VineStep - diameter;
switch (Type)
{
case VineTileType.StumpLeft:
offset.X = offsetAmount;
break;
case VineTileType.StumpRight:
offset.X = -offsetAmount;
break;
case VineTileType.StumpTop:
offset.Y = offsetAmount;
break;
case VineTileType.Stem:
case VineTileType.StumpBottom:
offset.Y = -offsetAmount;
break;
default:
offset = Vector2.Zero;
break;
}
}
else
{
offset = Vector2.Zero;
}
}
public Vector2 GetWorldPosition(Planter planter, Vector2 slotOffset)
{
return planter.Item.WorldPosition + slotOffset + Position;
}
public void UpdateType()
{
if (Type == VineTileType.Stem) { return; }
Type = (VineTileType) Sides;
}
/// <summary>
/// Returns a random side that is not occupied.
/// </summary>
/// <remarks>
/// There is probably a much better way of doing this than allocating memory with an array
/// but this felt like the most reliable approach I could come up with.
/// </remarks>
/// <returns></returns>
public TileSide GetRandomFreeSide(Random? random = null)
{
const int maxSides = 4;
TileSide occupiedSides = Sides | BlockedSides;
int setBits = occupiedSides.Count();
if (setBits >= maxSides) { return TileSide.None; }
int possible = maxSides - setBits;
int[] pool = new int[possible];
for (int i = 0, j = 0; i < maxSides; i++)
{
if (!occupiedSides.IsBitSet((TileSide) (1 << i)))
{
pool[j] = i;
j++;
}
}
int value = pool[Growable.RandomInt(0, possible, random)];
return (TileSide) (1 << value);
}
public bool CanGrowMore() => (Sides | BlockedSides).Count() < 4;
public static Rectangle CreatePlantRect(Vector2 pos) => new Rectangle((int) pos.X - Size / 2, (int) pos.Y + Size / 2, Size, Size);
}
internal static class GrowthSideExtension
{
// Enum.HasFlag() sucks
public static bool IsBitSet(this TileSide side, TileSide bit)
{
return ((int) side & (int) bit) != 0;
}
// K&R algorithm for counting how many bits are set in a bit field
public static int Count(this TileSide side)
{
int n = (int) side;
int count = 0;
while (n != 0)
{
count += n & 1;
n >>= 1;
}
return count;
}
}
internal partial class Growable : ItemComponent, IServerSerializable
{
// used for debugging where a vine failed to grow
public readonly HashSet<Rectangle> FailedRectangles = new HashSet<Rectangle>();
[Serialize(1f, true, "How fast the plant grows.")]
public float GrowthSpeed { get; set; }
[Serialize(100f, true, "How long the plant can go without watering.")]
public float MaxHealth { get; set; }
[Serialize(1f, true, "How much damage the plant takes while in water.")]
public float FloodTolerance { get; set; }
[Serialize(1f, true, "How much damage the plant takes while growing.")]
public float Hardiness { get; set; }
[Serialize(0.01f, true, "How often a seed is produced.")]
public float SeedRate { get; set; }
[Serialize(0.01f, true, "How often a product item is produced.")]
public float ProductRate { get; set; }
[Serialize(0.5f, true, "Probability of an attribute being randomly modified in a newly produced seed.")]
public float MutationProbability { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the flowers.")]
public Color FlowerTint { get; set; }
[Serialize(3, true, "Number of flowers drawn when fully grown")]
public int FlowerQuantity { get; set; }
[Serialize(0.25f, true, "Size of the flower sprites.")]
public float BaseFlowerScale { get; set; }
[Serialize(0.5f, true, "Size of the leaf sprites.")]
public float BaseLeafScale { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the leaves.")]
public Color LeafTint { get; set; }
[Serialize(0.33f, true, "Chance of a leaf appearing behind a branch.")]
public float LeafProbability { get; set; }
[Serialize("1.0,1.0,1.0,1.0", true, "Color of the vines.")]
public Color VineTint { get; set; }
[Serialize(32, true, "Maximum number of vine tiles the plant can grow.")]
public int MaximumVines { get; set; }
[Serialize(0.25f, true, "Size of the vine sprites.")]
public float VineScale { get; set; }
[Serialize("0.26,0.27,0.29,1.0", true, "Tint of a dead plant.")]
public Color DeadTint { get; set; }
private const float increasedDeathSpeed = 10f;
private bool accelerateDeath;
private float health;
private int flowerVariants;
private int leafVariants;
private int[] flowerTiles;
public float Health
{
get => health;
set => health = Math.Clamp(value, 0, MaxHealth);
}
public bool Decayed;
public bool FullyGrown;
private const int maxProductDelay = 10,
maxVineGrowthDelay = 10;
private int productDelay;
private int vineDelay;
public readonly List<ProducedItem> ProducedItems = new List<ProducedItem>();
public readonly List<VineTile> Vines = new List<VineTile>();
private readonly ProducedItem ProducedSeed;
private static float MinFlowerScale = 0.5f, MaxFlowerScale = 1.0f, MinLeafScale = 0.5f, MaxLeafScale = 1.0f;
private const int VineChunkSize = 32;
public Growable(Item item, XElement element) : base(item, element)
{
SerializableProperty.DeserializeProperties(this, element);
Health = MaxHealth;
if (element.HasElements)
{
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "produceditem":
ProducedItems.Add(new ProducedItem(subElement));
break;
case "vinesprites":
LoadVines(subElement);
break;
}
}
}
ProducedSeed = new ProducedItem(this.item.Prefab, 1.0f);
flowerTiles = new int[FlowerQuantity];
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (flowerTiles.All(i => i == 0))
{
GenerateFlowerTiles();
}
}
private void GenerateFlowerTiles(Random? random = null)
{
flowerTiles = new int[FlowerQuantity];
List<int> pool = new List<int>();
for (int i = 0; i < MaximumVines - 1; i++) { pool.Add(i); }
for (int i = 0; i < flowerTiles.Length; i++)
{
int index = RandomInt(0, pool.Count, random);
flowerTiles[i] = pool[index];
pool.RemoveAt(index);
}
}
partial void LoadVines(XElement element);
public void OnGrowthTick(Planter planter, PlantSlot slot)
{
if (Decayed) { return; }
if (FullyGrown)
{
TryGenerateProduct(planter, slot);
}
if (Health > 0)
{
GrowVines(planter, slot);
Health -= accelerateDeath ? Hardiness * increasedDeathSpeed : Hardiness;
if (planter.Item.InWater)
{
Health -= FloodTolerance;
}
}
CheckPlantState();
#if CLIENT
UpdateBranchHealth();
#endif
}
private void UpdateBranchHealth()
{
Color healthColor = Color.White * (1.0f - Health / MaxHealth);
foreach (VineTile vine in Vines)
{
vine.HealthColor = healthColor;
}
}
private void TryGenerateProduct(Planter planter, PlantSlot slot)
{
productDelay++;
if (productDelay <= maxProductDelay) { return; }
productDelay = 0;
bool spawnProduct = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < ProductRate,
spawnSeed = Rand.Range(0f, 1f, Rand.RandSync.Unsynced) < SeedRate;
Vector2 spawnPos;
if (spawnProduct || spawnSeed)
{
VineTile vine = Vines.GetRandom();
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
}
else
{
return;
}
if (spawnProduct && ProducedItems.Any())
{
SpawnItem(ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos);
return;
}
if (spawnSeed)
{
SpawnItem(ProducedSeed, spawnPos);
}
static void SpawnItem(ProducedItem producedItem, Vector2 pos)
{
if (producedItem.Prefab == null) { return; }
Entity.Spawner?.AddToSpawnQueue(producedItem.Prefab, pos, onSpawned: it =>
{
foreach (StatusEffect effect in producedItem.StatusEffects)
{
it.ApplyStatusEffect(effect, ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
}
it.ApplyStatusEffects(ActionType.OnProduceSpawned, 1.0f, isNetworkEvent: true);
});
}
}
/// <summary>
/// Updates plant's state to fully grown or dead depending on its conditions.
/// </summary>
/// <returns>True if the plant has finished growing.</returns>
private bool CheckPlantState()
{
if (Decayed) { return true; }
if (0 >= Health)
{
Decayed = true;
#if CLIENT
foreach (VineTile vine in Vines)
{
vine.DecayDelay = (float) RandomDouble(0f, 30f);
}
#endif
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
if (Vines.Count >= MaximumVines && !FullyGrown)
{
FullyGrown = true;
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
if (!FullyGrown && !accelerateDeath && Vines.Any() && Vines.All(tile => !tile.CanGrowMore()))
{
accelerateDeath = true;
}
// if the player somehow finds a way to extract the seed out of a planter kill the plant
if (item.ParentInventory is CharacterInventory)
{
Decayed = true;
#if SERVER
item.CreateServerEvent(this);
#endif
return true;
}
return false;
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
#if CLIENT
foreach (VineTile vine in Vines)
{
vine.UpdateScale(deltaTime);
}
#endif
CheckPlantState();
}
private void GrowVines(Planter planter, PlantSlot slot)
{
if (FullyGrown) { return; }
vineDelay++;
if (vineDelay <= maxVineGrowthDelay / GrowthSpeed) { return; }
vineDelay = 0;
if (!Vines.Any())
{
// generate first stem
GenerateStem();
return;
}
int count = Vines.Count;
TryGenerateBranches(planter, slot);
if (Vines.Count > count)
{
#if SERVER
for (int i = 0; i < Vines.Count; i += VineChunkSize)
{
GameMain.Server.CreateEntityEvent(item, new object[] { NetEntityEvent.Type.ComponentState, item.GetComponentIndex(this), i });
}
#elif CLIENT
ResetPlanterSize();
#endif
}
}
private void GenerateStem()
{
VineTile stem = new VineTile(this, Vector2.Zero, VineTileType.Stem) { BlockedSides = TileSide.Bottom | TileSide.Left | TileSide.Right };
Vines.Add(stem);
}
private void TryGenerateBranches(Planter planter, PlantSlot slot, Random? random = null, Random? flowerRandom = null)
{
List<VineTile> newList = new List<VineTile>(Vines);
foreach (VineTile oldVines in newList)
{
if (oldVines.FailedGrowthAttempts > 8 || !oldVines.CanGrowMore()) { continue; }
if (RandomInt(0, Vines.Count(tile => tile.CanGrowMore()), random) != 0) { continue; }
TileSide side = oldVines.GetRandomFreeSide(random);
if (side == TileSide.None) { continue; }
Vector2 pos = oldVines.AdjacentPositions[side];
Rectangle rect = VineTile.CreatePlantRect(pos);
if (CollidesWithWorld(rect, planter, slot))
{
oldVines.BlockedSides |= side;
oldVines.FailedGrowthAttempts++;
continue;
}
FoliageConfig flowerConfig = FoliageConfig.EmptyConfig;
FoliageConfig leafConfig = FoliageConfig.EmptyConfig;
if (flowerTiles.Any(i => Vines.Count == i))
{
flowerConfig = FoliageConfig.CreateRandomConfig(flowerVariants, MinFlowerScale, MaxFlowerScale, flowerRandom);
}
if (LeafProbability >= RandomDouble(0d, 1.0d, flowerRandom) && leafVariants > 0)
{
leafConfig = FoliageConfig.CreateRandomConfig(leafVariants, MinLeafScale, MaxLeafScale, flowerRandom);
}
VineTile newVine = new VineTile(this, pos, VineTileType.CrossJunction, flowerConfig, leafConfig, rect);
foreach (VineTile otherVine in Vines)
{
var (distX, distY) = pos - otherVine.Position;
int absDistX = (int) Math.Abs(distX), absDistY = (int) Math.Abs(distY);
// check if the tile is within the with or height distance from us but ignore diagonals
if (absDistX > newVine.Rect.Width || absDistY > newVine.Rect.Height || absDistX > 0 && absDistY > 0) { continue; }
// determines what side the tile is relative to the new tile by comparing the X/Y distance values
// if the X value is bigger than Y it's to the left or right of us and then check if X is negative or positive to determine if it's right or left
TileSide connectingSide = absDistX > absDistY ? distX > 0 ? TileSide.Right : TileSide.Left : distY > 0 ? TileSide.Top : TileSide.Bottom;
// We use log2 to find the index and offset that index by 2 since the opposite side is always 2 offsets away
TileSide oppositeSide = (TileSide) (1 << ((int) Math.Log2((int) connectingSide) + 2) % 4);
if (otherVine.BlockedSides.IsBitSet(connectingSide))
{
newVine.BlockedSides |= oppositeSide;
continue;
}
if (otherVine != oldVines)
{
otherVine.BlockedSides |= connectingSide;
newVine.BlockedSides |= oppositeSide;
}
else
{
otherVine.Sides |= connectingSide;
newVine.Sides |= oppositeSide;
}
}
Vines.Add(newVine);
foreach (VineTile vine in Vines)
{
vine.UpdateType();
}
}
}
private bool CollidesWithWorld(Rectangle rect, Planter planter, PlantSlot slot)
{
if (Vines.Any(g => g.Rect.Contains(rect))) { return true; }
Rectangle worldRect = rect;
worldRect.Location = planter.Item.WorldPosition.ToPoint() + slot.Offset.ToPoint() + worldRect.Location;
worldRect.Y -= worldRect.Height;
Rectangle planterRect = planter.Item.WorldRect;
planterRect.Y -= planterRect.Height;
if (planterRect.Intersects(worldRect))
{
#if DEBUG
if (!FailedRectangles.Contains(worldRect))
{
FailedRectangles.Add(worldRect);
}
#endif
return true;
}
Vector2 topLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Top)),
topRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Top)),
bottomLeft = ConvertUnits.ToSimUnits(new Vector2(worldRect.Left, worldRect.Bottom)),
bottomRight = ConvertUnits.ToSimUnits(new Vector2(worldRect.Right, worldRect.Bottom));
// ray casting a cross on the corners didn't seem to work so we are ray casting along the perimeter
bool hasCollision = planterRect.Intersects(worldRect) || LineCollides(topLeft, topRight) || LineCollides(topRight, bottomRight) || LineCollides(bottomRight, bottomLeft) || LineCollides(bottomLeft, topLeft);
#if DEBUG
if (hasCollision)
{
if (!FailedRectangles.Contains(worldRect))
{
FailedRectangles.Add(worldRect);
}
}
#endif
return hasCollision;
static bool LineCollides(Vector2 point1, Vector2 point2)
{
const Category category = Physics.CollisionWall | Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionLevel;
return Submarine.PickBody(point1, point2, collisionCategory: category, customPredicate: f => !(f.UserData is Hull) && f.CollidesWith.HasFlag(Physics.CollisionItem)) != null;
}
}
public override XElement Save(XElement parentElement)
{
XElement element = base.Save(parentElement);
element.Add(new XAttribute("flowertiles", string.Join(",", flowerTiles)));
element.Add(new XAttribute("decayed", Decayed));
foreach (VineTile vine in Vines)
{
XElement vineElement = new XElement("Vine");
vineElement.Add(new XAttribute("sides", (int) vine.Sides));
vineElement.Add(new XAttribute("blockedsides", (int) vine.BlockedSides));
vineElement.Add(new XAttribute("pos", XMLExtensions.Vector2ToString(vine.Position)));
vineElement.Add(new XAttribute("tile", (int) vine.Type));
vineElement.Add(new XAttribute("failedattempts", vine.FailedGrowthAttempts));
#if SERVER
vineElement.Add(new XAttribute("growthscale", Decayed ? 1.0f : 2.0f));
#else
vineElement.Add(new XAttribute("growthscale", vine.GrowthStep));
#endif
vineElement.Add(new XAttribute("flowerconfig", vine.FlowerConfig.Serialize()));
vineElement.Add(new XAttribute("leafconfig", vine.LeafConfig.Serialize()));
element.Add(vineElement);
}
return element;
}
public override void Load(XElement componentElement, bool usePrefabValues)
{
base.Load(componentElement, usePrefabValues);
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", new int[0]);
Decayed = componentElement.GetAttributeBool("decayed", false);
Vines.Clear();
foreach (XElement element in componentElement.Elements())
{
if (element.Name.ToString().Equals("vine", StringComparison.OrdinalIgnoreCase))
{
VineTileType type = (VineTileType) element.GetAttributeInt("tile", 0);
Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero);
TileSide sides = (TileSide) element.GetAttributeInt("sides", 0);
TileSide blockedSides = (TileSide) element.GetAttributeInt("blockedsides", 0);
int failedAttempts = element.GetAttributeInt("failedattempts", 0);
float growthscale = element.GetAttributeFloat("growthscale", 0f);
int flowerConfig = element.GetAttributeInt("flowerconfig", FoliageConfig.EmptyConfigValue);
int leafConfig = element.GetAttributeInt("leafconfig", FoliageConfig.EmptyConfigValue);
VineTile tile = new VineTile(this, pos, type, FoliageConfig.Deserialize(flowerConfig), FoliageConfig.Deserialize(leafConfig))
{
Sides = sides, BlockedSides = blockedSides, FailedGrowthAttempts = failedAttempts, GrowthStep = growthscale
};
Vines.Add(tile);
}
}
}
private bool CanGrowMore() => Vines.Any(tile => tile.CanGrowMore());
public static int RandomInt(int min, int max, Random? random = null) => random?.Next(min, max) ?? Rand.Range(min, max);
public static double RandomDouble(double min, double max, Random? random = null) => random?.NextDouble() * (max - min) + min ?? Rand.Range(min, max);
}
}
@@ -5,6 +5,7 @@ using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -392,6 +393,8 @@ namespace Barotrauma.Items.Components
if (item.GetComponent<LevelResource>() != null) { return true; }
if (item.GetComponent<Planter>() is { } planter && planter.GrowableSeeds.Any(seed => seed != null)) { return false; }
//if the item has a connection panel and rewiring is disabled, don't allow deattaching
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && (connectionPanel.Locked || !(GameMain.NetworkMember?.ServerSettings?.AllowRewiring ?? true)))
@@ -476,12 +479,13 @@ namespace Barotrauma.Items.Components
}
}
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems != null)
{
foreach (Item contained in containedItems)
{
if (contained.body == null) continue;
if (contained == null) { continue; }
if (contained.body == null) { continue; }
contained.SetTransform(item.SimPosition, contained.body.Rotation);
}
}
@@ -573,7 +577,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (item.body == null || !item.body.Enabled) return;
if (item.body == null || !item.body.Enabled) { return; }
if (picker == null || !picker.HasEquippedItem(item))
{
if (Pusher != null) { Pusher.Enabled = false; }
@@ -598,7 +602,10 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) Flip();
if (item.body.Dir != picker.AnimController.Dir)
{
item.FlipX(relativeToSub: false);
}
item.Submarine = picker.Submarine;
@@ -635,11 +642,14 @@ namespace Barotrauma.Items.Components
}
}
public void Flip()
public override void FlipX(bool relativeToSub)
{
handlePos[0].X = -handlePos[0].X;
handlePos[1].X = -handlePos[1].X;
item.body.Dir = -item.body.Dir;
if (item.body != null)
{
item.body.Dir = -item.body.Dir;
}
}
public override void OnItemLoaded()
@@ -63,6 +63,13 @@ namespace Barotrauma.Items.Components
item.RequireAimToUse = true;
}
public override void Equip(Character character)
{
base.Equip(character);
reloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override bool Use(float deltaTime, Character character = null)
{
if (character == null || reloadTimer > 0.0f) { return false; }
@@ -151,7 +158,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
AnimController ac = picker.AnimController;
@@ -366,13 +373,15 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
bool success = Rand.Range(0.0f, 0.5f) < DegreeOfSuccess(User);
#if SERVER
if (GameMain.Server != null && targetCharacter != null) //TODO: Log structure hits
{
GameMain.Server.CreateEntityEvent(item, new object[]
{
Networking.NetEntityEvent.Type.ApplyStatusEffect,
ActionType.OnUse,
success ? ActionType.OnUse : ActionType.OnFailure,
null, //itemcomponent
targetCharacter.ID, targetLimb
});
@@ -389,7 +398,7 @@ namespace Barotrauma.Items.Components
if (targetCharacter != null) //TODO: Allow OnUse to happen on structures too maybe??
{
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: User);
ApplyStatusEffects(success ? ActionType.OnUse : ActionType.OnFailure, 1.0f, targetCharacter, targetLimb, user: User);
}
if (DeleteOnUse)
@@ -61,7 +61,7 @@ namespace Barotrauma.Items.Components
allowedSlots.Add(allowedSlot);
}
canBePicked = true;
canBePicked = true;
}
public override bool Pick(Character picker)
@@ -142,7 +142,8 @@ namespace Barotrauma.Items.Components
this,
item.WorldPosition,
pickTimer / requiredTime,
GUI.Style.Red, GUI.Style.Green);
GUI.Style.Red, GUI.Style.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
#endif
picker.AnimController.UpdateUseItem(true, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
@@ -72,6 +72,12 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
public override void Equip(Character character)
{
reloadTimer = Math.Min(reload, 1.0f);
IsActive = true;
}
public override void Update(float deltaTime, Camera cam)
{
reloadTimer -= deltaTime;
@@ -180,22 +186,25 @@ namespace Barotrauma.Items.Components
public Projectile FindProjectile(bool triggerOnUseOnContainers = false)
{
var containedItems = item.ContainedItems;
var containedItems = item.OwnInventory?.Items;
if (containedItems == null) { return null; }
foreach (Item item in containedItems)
{
if (item == null) { continue; }
Projectile projectile = item.GetComponent<Projectile>();
if (projectile != null) { return projectile; }
}
//projectile not found, see if one of the contained items contains projectiles
foreach (Item item in containedItems)
foreach (Item it in containedItems)
{
var containedSubItems = item.ContainedItems;
if (it == null) { continue; }
var containedSubItems = it.OwnInventory?.Items;
if (containedSubItems == null) { continue; }
foreach (Item subItem in containedSubItems)
{
if (subItem == null) { continue; }
Projectile projectile = subItem.GetComponent<Projectile>();
//apply OnUse statuseffects to the container in case it has to react to it somehow
//(play a sound, spawn more projectiles, reduce condition...)
@@ -52,12 +52,16 @@ namespace Barotrauma.Items.Components
{
get; set;
}
[Serialize(0.0f, false, description: "How much the item decreases the size of fires per second.")]
public float ExtinguishAmount
{
get; set;
}
[Serialize(0.0f, false, description: "How much water the item provides to planters per second.")]
public float WaterAmount { get; set; }
[Serialize("0.0,0.0", false, description: "The position of the barrel as an offset from the item's center (in pixels).")]
public Vector2 BarrelPos { get; set; }
@@ -82,13 +86,19 @@ namespace Barotrauma.Items.Components
[Serialize(0.0f, false, description: "Force applied to the entity the ray hits.")]
public float TargetForce { get; set; }
[Serialize(0.0f, false, description: "Rotation of the barrel in degrees."), Editable(MinValueFloat = 0, MaxValueFloat = 360, VectorComponentLabels = new string[] { "editable.minvalue", "editable.maxvalue" })]
public float BarrelRotation
{
get; set;
}
public Vector2 TransformedBarrelPos
{
get
{
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation);
Matrix bodyTransform = Matrix.CreateRotationZ(item.body.Rotation + MathHelper.ToRadians(BarrelRotation));
Vector2 flippedPos = BarrelPos;
if (item.body.Dir < 0.0f) flippedPos.X = -flippedPos.X;
if (item.body.Dir < 0.0f) { flippedPos.X = -flippedPos.X; }
return (Vector2.Transform(flippedPos, bodyTransform));
}
}
@@ -188,7 +198,7 @@ namespace Barotrauma.Items.Components
}
float spread = MathHelper.ToRadians(MathHelper.Lerp(UnskilledSpread, Spread, degreeOfSuccess));
float angle = item.body.Rotation + spread * Rand.Range(-0.5f, 0.5f);
float angle = item.body.Rotation + MathHelper.ToRadians(BarrelRotation) + spread * Rand.Range(-0.5f, 0.5f);
Vector2 rayEnd = rayStart +
ConvertUnits.ToSimUnits(new Vector2(
(float)Math.Cos(angle),
@@ -276,7 +286,7 @@ namespace Barotrauma.Items.Components
ignoreSensors: false,
customPredicate: (Fixture f) =>
{
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure) { return false; }
if (RepairThroughHoles && f.IsSensor && f.Body?.UserData is Structure || (f.Body?.UserData is Item it && it.GetComponent<Planter>() != null)) { return false; }
if (f.Body?.UserData as string == "ruinroom") { return false; }
return true;
},
@@ -373,6 +383,42 @@ namespace Barotrauma.Items.Components
}
}
if (WaterAmount > 0.0f && item.CurrentHull?.Submarine != null)
{
Vector2 pos = ConvertUnits.ToDisplayUnits(rayStart + item.Submarine.SimPosition);
// Could probably be done much efficiently here
foreach (Item it in Item.ItemList)
{
if (it.Submarine == item.Submarine && it.GetComponent<Planter>() is { } planter)
{
if (it.GetComponent<Holdable>() is { } holdable && holdable.Attachable && !holdable.Attached) { continue; }
Rectangle collisionRect = it.WorldRect;
collisionRect.Y -= collisionRect.Height;
if (collisionRect.Left < pos.X && collisionRect.Right > pos.X && collisionRect.Bottom < pos.Y)
{
Body collision = Submarine.PickBody(rayStart, it.SimPosition, ignoredBodies, collisionCategories);
if (collision == null)
{
for (var i = 0; i < planter.GrowableSeeds.Length; i++)
{
Growable seed = planter.GrowableSeeds[i];
if (seed == null || seed.Decayed) { continue; }
seed.Health += WaterAmount * deltaTime;
#if CLIENT
float barOffset = 10f * GUI.Scale;
Vector2 offset = planter.PlantSlots.ContainsKey(i) ? planter.PlantSlots[i].Offset : Vector2.Zero;
user.UpdateHUDProgressBar(planter, planter.Item.DrawPosition + new Vector2(barOffset, 0) + offset, seed.Health / seed.MaxHealth, GUI.Style.Blue, GUI.Style.Blue, "progressbar.watering");
#endif
}
}
}
}
}
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
@@ -464,7 +510,7 @@ namespace Barotrauma.Items.Components
}
else if (targetBody.UserData is Item targetItem)
{
if (!HitItems) { return false; }
if (!HitItems || targetItem.NonInteractable) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
if (levelResource != null && levelResource.Attached &&
@@ -477,8 +523,9 @@ namespace Barotrauma.Items.Components
this,
targetItem.WorldPosition,
levelResource.DeattachTimer / levelResource.DeattachDuration,
GUI.Style.Red, GUI.Style.Green);
GUI.Style.Red, GUI.Style.Green, "progressbar.deattaching");
#endif
FixItemProjSpecific(user, deltaTime, targetItem);
return true;
}
@@ -571,34 +618,31 @@ namespace Barotrauma.Items.Components
character.AIController.SteeringManager.SteeringSeek(standPos);
}
}
else
if (dist < reach / 2)
{
if (dist < reach / 2)
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist < reach * 2)
{
// In or almost in range
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
// Too close -> steer away
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.SimPosition - leak.SimPosition));
}
else if (dist <= reach)
{
// In range
character.CursorPosition = leak.Position;
character.CursorPosition += VectorExtensions.Forward(Item.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, dist / 2);
if (character.AnimController.InWater)
{
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
var torso = character.AnimController.GetLimb(LimbType.Torso);
// Turn facing the target when not moving (handled in the animcontroller if not moving)
Vector2 mousePos = ConvertUnits.ToSimUnits(character.CursorPosition);
Vector2 diff = (mousePos - torso.SimPosition) * character.AnimController.Dir;
float newRotation = MathUtils.VectorToAngle(diff);
character.AnimController.Collider.SmoothRotate(newRotation, 5.0f);
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
if (VectorExtensions.Angle(VectorExtensions.Forward(torso.body.TransformedRotation), fromCharacterToLeak) < MathHelper.PiOver4)
{
// Swim past
Vector2 moveDir = leak.IsHorizontal ? Vector2.UnitY : Vector2.UnitX;
moveDir *= character.AnimController.Dir;
character.AIController.SteeringManager.SteeringManual(deltaTime, moveDir);
}
}
}
@@ -674,9 +718,8 @@ namespace Barotrauma.Items.Components
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in targets)
{
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded) { continue; }
if (!(target is Door door)) { continue; }
if (!door.CanBeWelded || door.Item.NonInteractable) { continue; }
for (int i = 0; i < effect.propertyNames.Length; i++)
{
string propertyName = effect.propertyNames[i];
@@ -685,7 +728,7 @@ namespace Barotrauma.Items.Components
object value = property.GetValue(target);
if (door.Stuck > 0)
{
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White);
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White, "progressbar.welding");
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
}
}
@@ -0,0 +1,62 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class Sprayer : RangedWeapon
{
[Serialize(0.0f, false, description: "The distance at which the item can spray walls.")]
public float Range { get; set; }
[Serialize(1.0f, false, description: "How fast the item changes the color of the walls.")]
public float SprayStrength { get; set; }
private readonly Dictionary<string, Color> liquidColors;
private ItemContainer liquidContainer;
public Sprayer(Item item, XElement element) : base(item, element)
{
item.IsShootable = true;
item.RequireAimToUse = true;
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "paintcolors":
{
liquidColors = new Dictionary<string, Color>();
foreach (XElement paintElement in subElement.Elements())
{
string paintName = paintElement.GetAttributeString("paintitem", string.Empty);
Color paintColor = paintElement.GetAttributeColor("color", Color.Transparent);
if (paintName != string.Empty)
{
liquidColors.Add(paintName, paintColor);
}
}
}
break;
}
}
InitProjSpecific(element);
}
public override void OnItemLoaded()
{
liquidContainer = item.GetComponent<ItemContainer>();
}
partial void InitProjSpecific(XElement element);
#if SERVER
public override bool Use(float deltaTime, Character character = null)
{
return character != null || character.Removed;
}
#endif
}
}
@@ -84,7 +84,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
if (item.body.Dir != picker.AnimController.Dir) { Flip(); }
if (item.body.Dir != picker.AnimController.Dir) { item.FlipX(relativeToSub: false); }
AnimController ac = picker.AnimController;
@@ -76,6 +76,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize("", false, description: "What to display on the progress bar when this item is being picked.")]
public string PickingMsg
{
get;
set;
}
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
public Action<bool> OnActiveStateChanged;
@@ -44,6 +44,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, false, "Allow dragging and dropping items to deposit items into this inventory.")]
public bool AllowDragAndDrop
{
get;
set;
}
[Serialize(false, false, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
public bool AutoInteractWithContained
@@ -166,17 +173,21 @@ namespace Barotrauma.Items.Components
public bool CanBeContained(Item item)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(item)) != null);
return ContainableItems.Find(c => c.MatchesItem(item)) != null;
}
public bool CanBeContained(ItemPrefab itemPrefab)
{
if (ContainableItems.Count == 0) { return true; }
return (ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null);
return ContainableItems.Find(c => c.MatchesItem(itemPrefab)) != null;
}
public override void Update(float deltaTime, Camera cam)
{
if (item.body != null &&
if (item.ParentInventory is CharacterInventory)
{
item.SetContainedItemPositions();
}
else if (item.body != null &&
item.body.Enabled &&
item.body.FarseerBody.Awake)
{
@@ -209,22 +220,6 @@ namespace Barotrauma.Items.Components
}
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
}
}
}
public override bool HasRequiredItems(Character character, bool addMessage, string msg = null)
{
return (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
@@ -284,6 +279,8 @@ namespace Barotrauma.Items.Components
public override bool Combine(Item item, Character user)
{
if (!AllowDragAndDrop && user != null) { return false; }
if (!ContainableItems.Any(x => x.MatchesItem(item))) { return false; }
if (user != null && !user.CanAccessInventory(Inventory)) { return false; }
@@ -354,16 +351,28 @@ namespace Barotrauma.Items.Components
public override void OnMapLoaded()
{
if (itemIds == null) { return; }
for (ushort i = 0; i < itemIds.Length; i++)
{
if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; }
if (i >= Inventory.Capacity) { continue; }
Inventory.TryPutItem(item, i, false, false, null, false);
if (itemIds != null)
{
for (ushort i = 0; i < itemIds.Length; i++)
{
if (!(Entity.FindEntityByID(itemIds[i]) is Item item)) { continue; }
if (i >= Inventory.Capacity) { continue; }
Inventory.TryPutItem(item, i, false, false, null, false);
}
itemIds = null;
}
itemIds = null;
if (SpawnWithId.Length > 0)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == SpawnWithId);
if (prefab != null)
{
if (Inventory != null && Inventory.Items.Any(it => it == null))
{
Entity.Spawner?.AddToSpawnQueue(prefab, Inventory);
}
}
}
}
protected override void ShallowRemoveComponentSpecific()
@@ -1,8 +1,9 @@
using Microsoft.Xna.Framework;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
partial class ItemLabel : ItemComponent, IDrawableComponent
partial class ItemLabel : ItemComponent, IDrawableComponent, IServerSerializable
{
public Vector2 DrawSize
{
@@ -10,12 +11,16 @@ namespace Barotrauma.Items.Components
get { return Vector2.Zero; }
}
partial void OnStateChanged();
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0, float signalStrength = 1)
{
switch (connection.Name)
{
case "set_text":
if (Text == signal) { return; }
Text = signal;
OnStateChanged();
break;
}
}
@@ -151,6 +151,7 @@ namespace Barotrauma.Items.Components
if (user == null
|| user.Removed
|| user.SelectedConstruction != item
|| item.ParentInventory != null
|| !user.CanInteractWith(item)
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
@@ -221,7 +222,7 @@ namespace Barotrauma.Items.Components
user.AnimController.ResetPullJoints();
if (dir != 0) user.AnimController.TargetDir = dir;
if (dir != 0) { user.AnimController.TargetDir = dir; }
foreach (LimbPos lb in limbPositions)
{
@@ -14,7 +14,7 @@ namespace Barotrauma.Items.Components
private float maxForce;
private Attack propellerDamage;
private readonly Attack propellerDamage;
private float damageTimer;
@@ -24,6 +24,8 @@ namespace Barotrauma.Items.Components
private float controlLockTimer;
public Character User;
[Editable(0.0f, 10000000.0f),
Serialize(2000.0f, true, description: "The amount of force exerted on the submarine when the engine is operating at full power.")]
public float MaxForce
@@ -106,6 +108,11 @@ namespace Barotrauma.Items.Components
{
//arbitrary multiplier that was added to changes in submarine mass without having to readjust all engines
float forceMultiplier = 0.1f;
if (User != null)
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
}
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage / MinVoltage, 1.0f);
Vector2 currForce = new Vector2(force * maxForce * forceMultiplier * voltageFactor, 0.0f);
//less effective when in a bad condition
@@ -193,6 +200,7 @@ namespace Barotrauma.Items.Components
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
User = sender;
}
}
}

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