Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions
@@ -184,6 +184,20 @@ namespace Barotrauma
}
}
/// <summary>
/// Is some condition met (e.g. entity null, indetectable, outside level) that prevents anyone from detecting the target?
/// </summary>
public bool ShouldBeIgnored()
{
if (InDetectable) { return true; }
if (Entity == null) { return true; }
if (Level.Loaded != null && WorldPosition.Y > Level.Loaded.Size.Y)
{
return true;
}
return false;
}
public AITarget(Entity e, XElement element) : this(e)
{
SightRange = element.GetAttributeFloat("sightrange", 0.0f);
@@ -216,7 +216,7 @@ namespace Barotrauma
private bool IsAttackingOwner(Character other) =>
PetBehavior != null && PetBehavior.Owner != null &&
!other.IsUnconscious && !other.IsArrested &&
!other.IsUnconscious && !other.IsHandcuffed &&
other.AIController is HumanAIController humanAI &&
humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat &&
combat.Enemy != null && combat.Enemy == PetBehavior.Owner;
@@ -2694,13 +2694,8 @@ namespace Barotrauma
float maxModifier = 5;
foreach (AITarget aiTarget in AITarget.List)
{
if (aiTarget.InDetectable) { continue; }
if (aiTarget.Entity == null) { continue; }
if (aiTarget.ShouldBeIgnored()) { continue; }
if (ignoredTargets.Contains(aiTarget)) { continue; }
if (Level.Loaded != null && aiTarget.WorldPosition.Y > Level.Loaded.Size.Y)
{
continue;
}
if (aiTarget.Type == AITarget.TargetType.HumanOnly) { continue; }
if (!TargetOutposts)
{
@@ -67,6 +67,12 @@ namespace Barotrauma
private readonly float reportProblemsInterval = 1.0f;
private float reportProblemsTimer;
/// <summary>
/// Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI.
/// Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it.
/// Only affects the "fight intruders" objective, which makes the character go and inspect noises.
/// </summary>
public float Hearing { get; set; } = 1.0f;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
@@ -339,39 +345,8 @@ namespace Barotrauma
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
CheckEnemies();
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
}
}
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
@@ -586,6 +561,42 @@ namespace Barotrauma
ShipCommandManager?.Update(deltaTime);
}
private void CheckEnemies()
{
//already in combat, no need to check
if (objectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return; }
float closestDistance = 0;
Character closestEnemy = null;
foreach (Character c in Character.CharacterList)
{
if (c.Submarine != Character.Submarine) { continue; }
if (c.Removed || c.IsDead || c.IsIncapacitated) { continue; }
if (IsFriendly(c)) { continue; }
Vector2 toTarget = c.WorldPosition - WorldPosition;
float dist = toTarget.LengthSquared();
float maxDistance = Character.Submarine == null ? enemySpotDistanceOutside : enemySpotDistanceInside;
if (dist > maxDistance * maxDistance) { continue; }
if (EnemyAIController.IsLatchedToSomeoneElse(c, Character)) { continue; }
var head = Character.AnimController.GetLimb(LimbType.Head);
if (head == null) { continue; }
float rotation = head.body.TransformedRotation;
Vector2 forward = VectorExtensions.Forward(rotation);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(toTarget, forward));
if (angle > 70) { continue; }
if (!Character.CanSeeTarget(c)) { continue; }
if (dist < closestDistance || closestEnemy == null)
{
closestEnemy = c;
closestDistance = dist;
}
}
if (closestEnemy != null)
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Defensive, closestEnemy);
}
}
private void UnequipUnnecessaryItems()
{
if (Character.LockHands) { return; }
@@ -632,7 +643,9 @@ namespace Barotrauma
isCurrentObjectiveFindSafety ||
Character.AnimController.InWater ||
Character.AnimController.HeadInWater ||
Character.IsClimbing ||
Character.Submarine == null ||
Character.Submarine.Info.HasTag(SubmarineTag.Shuttle) ||
(!Character.IsOnFriendlyTeam(Character.TeamID, Character.Submarine.TeamID) && !Character.IsEscorted) ||
ObjectiveManager.CurrentOrders.Any(o => o.Objective.KeepDivingGearOnAlsoWhenInactive) ||
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
@@ -845,8 +858,9 @@ namespace Barotrauma
{
// In the campaign mode, undocking happens after leaving the outpost, so we can't use that.
campaign.BeforeLevelLoading += Relocate;
campaign.OnSaveAndQuit += Relocate;
campaign.ItemsRelocatedToMainSub = true;
}
campaign.ItemsRelocatedToMainSub = true;
#if CLIENT
HintManager.OnItemMarkedForRelocation();
#endif
@@ -1011,6 +1025,7 @@ namespace Barotrauma
{
Order newOrder = null;
Hull targetHull = null;
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null)
@@ -1024,7 +1039,7 @@ namespace Barotrauma
if (target.CurrentHull != hull || !target.Enabled) { continue; }
if (AIObjectiveFightIntruders.IsValidTarget(target, Character, false))
{
if (!target.IsArrested && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
if (!target.IsHandcuffed && AddTargets<AIObjectiveFightIntruders, Character>(Character, target) && newOrder == null)
{
var orderPrefab = OrderPrefab.Prefabs["reportintruders"];
newOrder = new Order(orderPrefab, hull, null, orderGiver: Character);
@@ -1436,10 +1451,7 @@ namespace Barotrauma
{
return AIObjectiveCombat.CombatMode.Offensive;
}
return
humanAI.ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() ||
humanAI.ObjectiveManager.Objectives.Any(o => o is AIObjectiveFightIntruders) ?
AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
return humanAI.ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>() ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Defensive;
}
else
{
@@ -1479,28 +1491,36 @@ namespace Barotrauma
{
// The guards don't react to player's aggressions when there's an instigator around
isAttackerFightingEnemy = true;
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : (instigator.CombatAction != null ? instigator.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat);
return c.IsSecurity ? AIObjectiveCombat.CombatMode.None : instigator.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
if (attacker.TeamID == CharacterTeamType.FriendlyNPC && !eitherIsMentallyUnstable)
{
if (c.IsSecurity)
{
return attacker.CombatAction != null ? attacker.CombatAction.GuardReaction : AIObjectiveCombat.CombatMode.Offensive;
return attacker.CombatAction?.GuardReaction ?? AIObjectiveCombat.CombatMode.Offensive;
}
else
{
return attacker.CombatAction != null ? attacker.CombatAction.WitnessReaction : AIObjectiveCombat.CombatMode.Retreat;
return attacker.CombatAction?.WitnessReaction ?? AIObjectiveCombat.CombatMode.Retreat;
}
}
else
{
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>() is AIObjectiveCombat currentCombatObjective && currentCombatObjective.Enemy == attacker)
{
// Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2;
currentCombatObjective.AllowHoldFire = false;
c.IsCriminal = true;
}
if (c.IsCriminal)
{
// Always react if the attacker has been misbehaving earlier.
cumulativeDamage = Math.Max(cumulativeDamage, minorDamageThreshold);
}
if (cumulativeDamage > majorDamageThreshold)
{
c.IsCriminal = true;
if (c.IsSecurity)
{
return AIObjectiveCombat.CombatMode.Offensive;
@@ -1544,7 +1564,7 @@ namespace Barotrauma
}
}
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false)
public void AddCombatObjective(AIObjectiveCombat.CombatMode mode, Character target, float delay = 0, Func<AIObjective, bool> abortCondition = null, Action onAbort = null, Action onCompleted = null, bool allowHoldFire = false, bool speakWarnings = false)
{
if (mode == AIObjectiveCombat.CombatMode.None) { return; }
if (Character.IsDead || Character.IsIncapacitated || Character.Removed) { return; }
@@ -1579,6 +1599,7 @@ namespace Barotrauma
HoldPosition = Character.Info?.Job?.Prefab.Identifier == "watchman",
AbortCondition = abortCondition,
AllowHoldFire = allowHoldFire,
SpeakWarnings = speakWarnings
};
if (onAbort != null)
{
@@ -1777,7 +1798,7 @@ namespace Barotrauma
}
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
otherHumanAI.structureDamageAccumulator.TryAdd(character, 0.0f);
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
otherHumanAI.structureDamageAccumulator[character] += MathHelper.Clamp(damageAmount, -MaxDamagePerFrame, MaxDamagePerFrame);
float accumulatedDamage = Math.Max(otherHumanAI.structureDamageAccumulator[character], maxAccumulatedDamage);
@@ -1789,27 +1810,36 @@ namespace Barotrauma
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
}
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
if (!character.IsCriminal)
{
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
if (accumulatedDamage <= WarningThreshold) { return; }
if (accumulatedDamage > WarningThreshold && prevAccumulatedDamage <= WarningThreshold &&
!someoneSpoke && !character.IsIncapacitated && character.Stun <= 0.0f)
{
if (otherHumanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveIdle>())
//if the damage is still fairly low, wait and see if the character keeps damaging the walls to the point where we need to intervene
if (accumulatedDamage < ArrestThreshold)
{
(otherHumanAI.ObjectiveManager.CurrentObjective as AIObjectiveIdle)?.FaceTargetAndWait(character, 5.0f);
if (otherHumanAI.ObjectiveManager.CurrentObjective is AIObjectiveIdle idleObjective)
{
idleObjective.FaceTargetAndWait(character, 5.0f);
}
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
otherCharacter.Speak(TextManager.Get("dialogdamagewallswarning").Value, null, Rand.Range(0.5f, 1.0f), "damageoutpostwalls".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if ((accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
if (character.IsCriminal ||
(accumulatedDamage > ArrestThreshold && prevAccumulatedDamage <= ArrestThreshold) ||
(accumulatedDamage > KillThreshold && prevAccumulatedDamage <= KillThreshold))
{
var combatMode = accumulatedDamage > KillThreshold ? AIObjectiveCombat.CombatMode.Offensive : AIObjectiveCombat.CombatMode.Arrest;
if (combatMode == AIObjectiveCombat.CombatMode.Offensive)
{
character.IsCriminal = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
@@ -1830,17 +1860,18 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(), allowHoldFire: true, onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
anyAI.structureDamageAccumulator?.Remove(character);
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
}
}
});
});
return true;
}
}
@@ -1848,11 +1879,14 @@ namespace Barotrauma
public static void ItemTaken(Item item, Character thief)
{
if (item == null || thief == null || item.GetComponent<LevelResource>() != null) { return; }
bool someoneSpoke = false;
bool stolenItemsInside = item.OwnInventory?.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true).Any() ?? false;
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing || stolenItemsInside) && thief.TeamID != CharacterTeamType.FriendlyNPC && !item.HasTag(Tags.HandLockerItem))
if (item.Illegitimate && item.GetRootInventoryOwner() is Character itemOwner && itemOwner != thief && itemOwner.TeamID == thief.TeamID)
{
// The player attempts to use a bot as a mule or get them arrested -> just arrest the player instead.
thief.IsCriminal = true;
}
bool foundIllegitimateItems = item.Illegitimate || item.OwnInventory?.FindItem(it => it.Illegitimate, recursive: true) != null;
if (foundIllegitimateItems && thief.TeamID != CharacterTeamType.FriendlyNPC)
{
foreach (Character otherCharacter in Character.CharacterList)
{
@@ -1872,19 +1906,24 @@ namespace Barotrauma
if (item.HasTag(Tags.FireExtinguisher) && connectedHulls.Any(h => h.FireSources.Any())) { continue; }
if (item.HasTag(Tags.DivingGear) && connectedHulls.Any(h => h.ConnectedGaps.Any(g => AIObjectiveFixLeaks.IsValidTarget(g, thief)))) { continue; }
}
if (!someoneSpoke)
if (item.HasTag(Tags.Handcuffs) && thief.HasEquippedItem(item))
{
if (!item.StolenDuringRound)
{
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
// Handcuffed -> don't react.
continue;
}
if (!item.StolenDuringRound)
{
item.StolenDuringRound = true;
ApplyStealingReputationLoss(item);
#if CLIENT
HintManager.OnStoleItem(thief, item);
#endif
}
if (!someoneSpoke)
{
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
}
// React if we are security
if (!TriggerSecurity(otherHumanAI))
{
@@ -1900,7 +1939,7 @@ namespace Barotrauma
}
}
}
else if (item.OwnInventory?.FindItem(it => it.SpawnedInCurrentOutpost && !item.AllowStealing, true) is { } foundItem)
else if (item.OwnInventory?.FindItem(it => it.Illegitimate, true) is { } foundItem)
{
ItemTaken(foundItem, thief);
}
@@ -1914,11 +1953,12 @@ namespace Barotrauma
{
findThieves.InspectEveryone();
}
bool isCriminal = thief.IsCriminal;
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
abortCondition: obj => !isCriminal && thief.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null,
onAbort: () =>
{
if (item != null && !item.Removed && humanAI != null && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
if (!item.Removed && !humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveGetItem>())
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveGetItem(humanAI.Character, item, humanAI.ObjectiveManager, equip: false)
{
@@ -1926,7 +1966,8 @@ namespace Barotrauma
});
}
},
allowHoldFire: true);
allowHoldFire: !isCriminal,
speakWarnings: !isCriminal);
return true;
}
}
@@ -2084,7 +2125,7 @@ namespace Barotrauma
}
bool ignoreFire = objectiveManager.CurrentOrder is AIObjectiveExtinguishFires extinguishOrder && extinguishOrder.Priority > 0 || objectiveManager.HasActiveObjective<AIObjectiveExtinguishFire>();
bool ignoreOxygen = HasDivingGear(character);
bool ignoreEnemies = ObjectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>() || ObjectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
bool ignoreEnemies = ObjectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>();
float safety = CalculateHullSafety(hull, visibleHulls, character, ignoreWater: false, ignoreOxygen, ignoreFire, ignoreEnemies);
if (isCurrentHull)
{
@@ -2146,7 +2187,7 @@ namespace Barotrauma
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
if (IsActive(c) && !IsFriendly(character, c) && !c.IsHandcuffed)
{
enemyCount++;
}
@@ -492,14 +492,20 @@ namespace Barotrauma
var door = currentPath.CurrentNode.ConnectedDoor;
float margin = MathHelper.Lerp(1, 10, MathHelper.Clamp(Math.Abs(velocity.X) / 5, 0, 1));
float colliderHeight = collider.Height / 2 + collider.Radius;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
if (currentPath.CurrentNode.Stairs == null)
{
//the waypoint is between the top and bottom of the collider, no need to move vertically.
diff.Y = 0.0f;
float heightDiff = currentPath.CurrentNode.SimPosition.Y - collider.SimPosition.Y;
if (heightDiff < colliderHeight)
{
// Original comment:
//the waypoint is between the top and bottom of the collider, no need to move vertically.
// Note that the waypoint can be below collider too! This might be incorrect.
diff.Y = 0.0f;
}
}
if (currentPath.CurrentNode.Stairs != null)
else
{
// In stairs
bool isNextNodeInSameStairs = currentPath.NextNode?.Stairs == currentPath.CurrentNode.Stairs;
if (!isNextNodeInSameStairs)
{
@@ -883,7 +889,18 @@ namespace Barotrauma
//steer away from edges of the hull
bool wander = false;
bool inWater = character.AnimController.InWater;
var currentHull = character.CurrentHull;
Hull currentHull = character.CurrentHull;
// TODO: disabled for now, because seems to cause bots to walk towards walls/doors in some places. In some places it's because how the hulls are defined, but there is probably something else too, is it seems to happen also elsewhere.
// if (!inWater)
// {
// Vector2 colliderBottomPos = ConvertUnits.ToDisplayUnits(character.AnimController.GetColliderBottom());
// if (Hull.FindHull(colliderBottomPos, guess: currentHull, useWorldCoordinates: false) is Hull lowestHull)
// {
// // Use the hull found at the collider bottom, if found.
// // Makes difference in some rooms that have multiple hulls, of which the lowest hull where the feet are might not be the same as where the center position of the main collider is.
// currentHull = lowestHull;
// }
// }
if (currentHull != null && !inWater)
{
float roomWidth = currentHull.Rect.Width;
@@ -388,6 +388,7 @@ namespace Barotrauma
character.TeleportTo(ConvertUnits.ToDisplayUnits(forceColliderSimPosition.Value));
}
// TODO: Shouldn't multiply by LimbScale here, because it's already applied in attachLimb.Scale!
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.Scale * attachLimb.Params.Ragdoll.LimbScale;
if (jointDir < 0.0f)
{
@@ -15,7 +15,10 @@ namespace Barotrauma
public virtual string DebugTag => Identifier.Value;
public virtual bool ForceRun => false;
public virtual bool IgnoreUnsafeHulls => false;
public virtual bool AbandonWhenCannotCompleteSubjectives => true;
public virtual bool AbandonWhenCannotCompleteSubObjectives => true;
/// <summary>
/// Should subobjectives be sorted according to their priority?
/// </summary>
public virtual bool AllowSubObjectiveSorting => false;
public virtual bool PrioritizeIfSubObjectivesActive => false;
@@ -28,8 +31,7 @@ namespace Barotrauma
/// Run the main objective with all subobjectives concurrently?
/// If false, the main objective will continue only when all the subobjectives have been removed (done).
/// </summary>
public virtual bool ConcurrentObjectives => false;
protected virtual bool ConcurrentObjectives => false;
public virtual bool KeepDivingGearOn => false;
public virtual bool KeepDivingGearOnAlsoWhenInactive => false;
@@ -37,10 +39,36 @@ namespace Barotrauma
/// 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;
public virtual bool AllowInAnySub => false;
public virtual bool AllowWhileHandcuffed => true;
// These booleans are used for defining whether the objective is allowed in different contexts. E.g. AllowOutsideSubmarine needs to be true or the objective cannot be active when the bot is swimming outside.
protected virtual bool AllowOutsideSubmarine => false;
/// <summary>
/// When true, the objective is allowed in the player subs (when in the same team) and on friendly outposts (regardless of the alignment).
/// Note: ignored when <see cref="AllowInAnySub"/> is true.
/// </summary>
protected virtual bool AllowInFriendlySubs => false;
protected virtual bool AllowInAnySub => false;
protected virtual bool AllowWhileHandcuffed => true;
/// <summary>
/// Should the objective abandon when it's not allowed in the current context or should it just stay inactive with 0 priority?
/// Abandoned automatic objectives are removed and recreated automatically (when new orders are assigned or after a cooldown period).
/// Abandoned orders are removed, but the most recent order can be reissued by clicking the small order icon with the arrow in the crew manager panel.
/// </summary>
protected virtual bool AbandonIfDisallowed => true;
public virtual bool CanBeCompleted => !Abandon;
protected virtual float MaxDevotion => 10;
/// <summary>
/// Which event action (if any) created this objective
/// </summary>
public EventAction SourceEventAction;
/// <summary>
/// Which objective (if any) created this objective. When this is a subobjective, the parent objective is used by default.
/// </summary>
public AIObjective SourceObjective;
protected readonly List<AIObjective> subObjectives = new List<AIObjective>();
private float _cumulatedDevotion;
@@ -50,8 +78,6 @@ namespace Barotrauma
set { _cumulatedDevotion = MathHelper.Clamp(value, 0, MaxDevotion); }
}
protected virtual float MaxDevotion => 10;
/// <summary>
/// Final priority value after all calculations.
/// </summary>
@@ -100,17 +126,13 @@ namespace Barotrauma
}
}
}
public virtual bool CanBeCompleted => !Abandon;
/// <summary>
/// When true, the objective is never completed, unless CanBeCompleted returns false.
/// </summary>
public virtual bool IsLoop { get; set; }
public IEnumerable<AIObjective> SubObjectives => subObjectives;
public AIObjective CurrentSubObjective => subObjectives.FirstOrDefault();
private readonly List<AIObjective> all = new List<AIObjective>();
public IEnumerable<AIObjective> GetSubObjectivesRecursive(bool includingSelf = false)
{
all.Clear();
@@ -124,13 +146,11 @@ namespace Barotrauma
}
return all;
}
#pragma warning disable CS0649
/// <summary>
/// Aborts the objective when this condition is true.
/// </summary>
public Func<AIObjective, bool> AbortCondition;
#pragma warning restore CS0649
/// <summary>
/// A single shot event. Automatically cleared after launching. Use OnCompleted method for implementing (internal) persistent behavior.
@@ -186,6 +206,7 @@ namespace Barotrauma
public void AddSubObjective(AIObjective objective, bool addFirst = false)
{
var type = objective.GetType();
objective.SourceObjective = this;
subObjectives.RemoveAll(o => o.GetType() == type);
if (addFirst)
{
@@ -259,17 +280,21 @@ namespace Barotrauma
return character.Submarine.Info.IsOutpost && character.Submarine.TeamID == CharacterTeamType.FriendlyNPC;
}
protected void HandleNonAllowed()
protected void HandleDisallowed()
{
Priority = 0;
Abandon = !IsIgnoredAtOutpost();
if (AbandonIfDisallowed && !IsIgnoredAtOutpost())
{
// Never abandon objectives inside a friendly outpost, because otherwise we'd have to reassign most orders every round.
Abandon = true;
}
}
protected virtual float GetPriority()
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
if (objectiveManager.IsOrder(this))
@@ -360,7 +385,7 @@ namespace Barotrauma
/// <summary>
/// Checks if the subobjectives in the given collection are removed from the subobjectives. And if so, removes it also from the dictionary.
/// </summary>
protected void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
protected virtual void SyncRemovedObjectives<T1, T2>(Dictionary<T1, T2> dictionary, IEnumerable<T1> collection) where T2 : AIObjective
{
foreach (T1 key in collection)
{
@@ -398,6 +423,7 @@ namespace Barotrauma
{
if (objective.AllowMultipleInstances)
{
objective.SourceObjective = this;
subObjectives.Add(objective);
}
else
@@ -482,6 +508,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Check whether the objective should be aborted (and abandon if it should), and return whether the objective is completed or not.
/// </summary>
private bool Check()
{
if (AbortCondition != null && AbortCondition(this))
@@ -492,6 +521,9 @@ namespace Barotrauma
return CheckObjectiveSpecific();
}
/// <summary>
/// Should return whether the objective is completed or not.
/// </summary>
protected abstract bool CheckObjectiveSpecific();
private bool CheckState()
@@ -527,7 +559,7 @@ namespace Barotrauma
DebugConsole.NewMessage($"{character.Name}: Removing SUBobjective {subObjective.DebugTag} of {DebugTag}, because it cannot be completed.", Color.Red);
#endif
subObjectives.Remove(subObjective);
if (AbandonWhenCannotCompleteSubjectives)
if (AbandonWhenCannotCompleteSubObjectives)
{
if (objectiveManager.IsOrder(this))
{
@@ -16,7 +16,7 @@ namespace Barotrauma
public AIObjectiveChargeBatteries(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier)
: base(character, objectiveManager, priorityModifier, option) { }
protected override bool Filter(PowerContainer battery)
protected override bool IsValidTarget(PowerContainer battery)
{
if (battery == null) { return false; }
if (battery.OutputDisabled) { return false; }
@@ -37,7 +37,7 @@ namespace Barotrauma
return true;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (Option == "charge")
@@ -80,7 +80,6 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(PowerContainer battery) =>
new AIObjectiveOperateItem(battery, character, objectiveManager, Option, false, priorityModifier: PriorityModifier)
{
IsLoop = false,
Override = !character.IsDismissed,
completionCondition = () => IsReady(battery)
};
@@ -8,8 +8,8 @@ namespace Barotrauma
class AIObjectiveCheckStolenItems : AIObjective
{
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
public override bool AllowOutsideSubmarine => false;
public override bool AllowInAnySub => false;
protected override bool AllowOutsideSubmarine => false;
protected override bool AllowInAnySub => false;
public float FindStolenItemsProbability = 1.0f;
@@ -21,36 +21,38 @@ namespace Barotrauma
Done
}
private float inspectDelay;
private float warnDelay;
private const float InspectTime = 5.0f;
private const float NormalWarnDelay = 5.0f;
private const float CriminalWarnDelay = 3.0f;
private float inspectTimer;
private float warnTimer;
private float currentWarnDelay;
private State currentState;
public readonly Character TargetCharacter;
public readonly Character Target;
private AIObjectiveGoTo? goToObjective;
private readonly List<Item> stolenItems = new List<Item>();
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
public AIObjectiveCheckStolenItems(Character character, Character target, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
base(character, objectiveManager, priorityModifier)
{
TargetCharacter = targetCharacter;
inspectDelay = 5.0f;
warnDelay = 5.0f;
}
public override bool IsLoop
{
get => false;
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
Target = target;
InitTimers();
}
protected override bool CheckObjectiveSpecific() => false;
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
if (character.IsClimbing)
{
// Target is climbing -> stop following the objective (soft abandon, without ignoring the target).
Priority = 0;
}
else if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
@@ -70,22 +72,32 @@ namespace Barotrauma
{
switch (currentState)
{
case State.Done:
IsCompleted = true;
break;
case State.GotoTarget:
TryAddSubObjective(ref goToObjective,
constructor: () =>
constructor: () => new AIObjectiveGoTo(Target, character, objectiveManager, repeat: false)
{
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
{
SpeakIfFails = false
};
SpeakIfFails = false
},
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
currentState = State.Inspect;
stolenItems.Clear();
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
if (character.IsClimbing)
{
// Shouldn't start inspecting characters when they climb, nor get here, because the priority should be 0,
// but if this still happens, we'll have to abandon the objective
// because it's not currently possible to hold to characters and ladders at the same time.
Abandon = true;
}
else
{
currentState = State.Inspect;
stolenItems.Clear();
Target.Inventory.FindAllItems(it => it.Illegitimate, recursive: true, stolenItems);
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.criminal" : "dialogcheckstolenitems").Value);
}
},
onAbandon: () =>
{
@@ -103,10 +115,23 @@ namespace Barotrauma
private void Inspect(float deltaTime)
{
if (inspectDelay > 0.0f)
if (inspectTimer > 0.0f)
{
character.SelectCharacter(TargetCharacter);
inspectDelay -= deltaTime;
character.SelectCharacter(Target);
inspectTimer -= deltaTime;
if (inspectTimer < InspectTime - 1)
{
if (Target.AnimController.IsMovingFast)
{
ArrestFleeing();
}
else if (Math.Abs(Target.AnimController.TargetMovement.X) > 1.0f)
{
// If the target moves, reset the inspect timer and tell to hold still
character.Speak(TextManager.Get("dialogcheckstolenitems.holdstill").Value, identifier: "holdstill".ToIdentifier(), minDurationBetweenSimilar: 3f);
inspectTimer = InspectTime;
}
}
return;
}
@@ -118,7 +143,7 @@ namespace Barotrauma
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
character.Speak(TextManager.Get(Target.IsCriminal ? "dialogcheckstolenitems.nostolenitems.criminal" : "dialogcheckstolenitems.nostolenitems").Value);
currentState = State.Done;
IsCompleted = true;
}
@@ -127,16 +152,23 @@ namespace Barotrauma
private void Warn(float deltaTime)
{
if (warnDelay > 0.0f)
if (warnTimer > 0.0f)
{
warnDelay -= deltaTime;
warnTimer -= deltaTime;
if (warnTimer < currentWarnDelay - 1)
{
if (Target.AnimController.IsMovingFast)
{
ArrestFleeing();
}
}
return;
}
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == Target);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
character.Speak(TextManager.Get(character.IsCriminal ? "dialogcheckstolenitems.arrest.criminal" : "dialogcheckstolenitems.arrest").Value);
Arrest(abortWhenItemsDropped: true, allowHoldFire: true);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
@@ -156,5 +188,44 @@ namespace Barotrauma
currentState = State.Done;
IsCompleted = true;
}
private void ArrestFleeing()
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
currentState = State.Done;
IsCompleted = true;
Arrest(abortWhenItemsDropped: false, allowHoldFire: false);
}
private void Arrest(bool abortWhenItemsDropped, bool allowHoldFire)
{
bool isCriminal = Target.IsCriminal;
Func<AIObjective, bool>? abortCondition = null;
if (abortWhenItemsDropped && !isCriminal)
{
abortCondition = obj => Target.Inventory.FindItem(it => it.Illegitimate, recursive: true) == null;
}
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, Target, allowHoldFire: allowHoldFire && !isCriminal, speakWarnings: !isCriminal, abortCondition: abortCondition);
}
public override void OnDeselected()
{
base.OnDeselected();
character.DeselectCharacter();
}
public override void Reset()
{
base.Reset();
currentState = State.GotoTarget;
InitTimers();
}
private void InitTimers()
{
inspectTimer = InspectTime;
currentWarnDelay = Target.IsCriminal ? CriminalWarnDelay : NormalWarnDelay;
warnTimer = currentWarnDelay;
}
}
}
@@ -12,7 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "cleanup item".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => false;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public readonly Item item;
public bool IsPriority { get; set; }
@@ -24,7 +24,7 @@ namespace Barotrauma
/// <summary>
/// Allows decontainObjective to be interrupted if this objective gets abandoned (e.g. due to the item no longer being eligible for cleanup)
/// </summary>
public override bool ConcurrentObjectives => true;
protected override bool ConcurrentObjectives => true;
public AIObjectiveCleanupItem(Item item, Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
@@ -36,7 +36,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
else
@@ -31,7 +31,7 @@ namespace Barotrauma
this.prioritizedItems.AddRange(prioritizedItems.Where(i => i != null));
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -47,7 +47,7 @@ namespace Barotrauma
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
System.Diagnostics.Debug.Assert(target.GetComponent<Pickable>() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items.");
System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined.");
@@ -100,7 +100,7 @@ namespace Barotrauma
{
if (item == null) { return false; }
if (item.DontCleanUp) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.Illegitimate == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
if (item.Container == null)
@@ -16,23 +16,23 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private readonly CombatMode initialMode;
private float checkWeaponsTimer;
private const float checkWeaponsInterval = 1;
private const float CheckWeaponsInterval = 1;
private float ignoreWeaponTimer;
private const float ignoredWeaponsClearTime = 10;
private const float IgnoredWeaponsClearTime = 10;
private const float goodWeaponPriority = 30;
private const float arrestHoldFireTime = 8;
private const float GoodWeaponPriority = 30;
private float holdFireTimer;
private bool hasAimed;
private bool isLethalWeapon;
private bool AllowCoolDown => !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
private bool AllowCoolDown => allowCooldown || !IsOffensiveOrArrest || Mode != initialMode || character.TeamID == Enemy.TeamID;
private bool allowCooldown;
public Character Enemy { get; private set; }
public bool HoldPosition { get; set; }
@@ -45,7 +45,6 @@ namespace Barotrauma
{
_weapon = value;
_weaponComponent = null;
hasAimed = false;
}
}
private ItemComponent _weaponComponent;
@@ -58,8 +57,8 @@ namespace Barotrauma
}
}
public override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
protected override bool ConcurrentObjectives => true;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
@@ -72,6 +71,9 @@ namespace Barotrauma
private Hull retreatTarget;
private float coolDownTimer;
private float pathBackTimer;
private const float DefaultCoolDown = 10.0f;
private const float PathBackCheckTime = 1.0f;
private IEnumerable<Body> myBodies;
private float aimTimer;
private float reloadTimer;
@@ -79,17 +81,25 @@ namespace Barotrauma
private bool canSeeTarget;
private float visibilityCheckTimer;
private const float visibilityCheckInterval = 0.2f;
private const float VisibilityCheckInterval = 0.2f;
private float sqrDistance;
private const float maxDistance = 2000;
private const float distanceCheckInterval = 0.2f;
private const float MaxDistance = 2000;
private const float DistanceCheckInterval = 0.2f;
private float distanceTimer;
private const float closeDistanceThreshold = 300;
private const float floorHeightApproximate = 100;
private const float CloseDistanceThreshold = 300;
private const float FloorHeightApproximate = 100;
public bool AllowHoldFire;
public bool SpeakWarnings;
private bool firstWarningTriggered;
private bool lastWarningTriggered;
public float ArrestHoldFireTime { get; init; } = 10;
private const float ArrestTargetDistance = 100;
private bool arrestingRegistered;
/// <summary>
/// Don't start using a weapon if this condition is true
@@ -123,7 +133,7 @@ namespace Barotrauma
public CombatMode Mode { get; private set; }
private bool IsOffensiveOrArrest => initialMode is CombatMode.Offensive or CombatMode.Arrest;
private bool TargetEliminated => IsEnemyDisabled || Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f || Enemy.IsArrested && !character.IsInstigator;
private bool TargetEliminated => IsEnemyDisabled || (Enemy.IsUnconscious && Enemy.Params.Health.ConstantHealthRegeneration <= 0.0f) || (!character.IsInstigator && Enemy.IsHandcuffed && Enemy.IsKnockedDown);
private bool IsEnemyDisabled => Enemy == null || Enemy.Removed || Enemy.IsDead;
private float AimSpeed => HumanAIController.AimSpeed;
@@ -141,7 +151,7 @@ namespace Barotrauma
if (character.CurrentHull != null && Enemy.CurrentHull != null && character.CurrentHull != Enemy.CurrentHull)
{
// Inside, not in the same hull with the enemy
if (Math.Abs(toEnemy.Y) > floorHeightApproximate)
if (Math.Abs(toEnemy.Y) > FloorHeightApproximate)
{
// Different floor
return false;
@@ -156,7 +166,7 @@ namespace Barotrauma
return Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition) < margin * margin;
}
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = 10.0f)
public AIObjectiveCombat(Character character, Character enemy, CombatMode mode, AIObjectiveManager objectiveManager, float priorityModifier = 1, float coolDown = DefaultCoolDown)
: base(character, objectiveManager, priorityModifier)
{
if (mode == CombatMode.None)
@@ -187,48 +197,31 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Enemy == null || Enemy.Removed)
{
Priority = 0;
Abandon = true;
return Priority;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine == null || (Enemy.Submarine.TeamID != character.TeamID && Enemy.Submarine != character.Submarine))
{
Priority = 0;
Abandon = true;
return Priority;
}
}
if (TargetEliminated)
{
Priority = 0;
return Priority;
}
else
// 91-100
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// 91-100
const float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
const float maxPriority = AIObjectiveManager.MaxObjectivePriority;
const float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
Priority = 0;
}
}
return Priority;
@@ -246,7 +239,7 @@ namespace Barotrauma
if (ignoreWeaponTimer < 0)
{
ignoredWeapons.Clear();
ignoreWeaponTimer = ignoredWeaponsClearTime;
ignoreWeaponTimer = IgnoredWeaponsClearTime;
}
bool isFightingIntruders = objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>();
if (findSafety != null && isFightingIntruders)
@@ -258,7 +251,7 @@ namespace Barotrauma
distanceTimer -= deltaTime;
if (distanceTimer < 0)
{
distanceTimer = distanceCheckInterval;
distanceTimer = DistanceCheckInterval;
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
}
}
@@ -266,16 +259,62 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific()
{
if (character.Submarine is not { TeamID: CharacterTeamType.FriendlyNPC })
if (character.Submarine is { TeamID: CharacterTeamType.FriendlyNPC } && character.Submarine == Enemy.Submarine)
{
// Can't lose the target in friendly outposts.
if (sqrDistance > maxDistance * maxDistance)
// Target still in the outpost
if (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsSecurity)
{
// The target escaped from us.
return true;
// Outpost guards shouldn't lose the target in friendly outposts,
// However, if we are not a guard, let's ensure that we allow the cooldown.
allowCooldown = true;
}
}
return IsEnemyDisabled || (AllowCoolDown && coolDownTimer <= 0);
else
{
if ((Enemy.Submarine == null && character.Submarine != null) || sqrDistance > MaxDistance * MaxDistance)
{
// The target escaped from us.
Abandon = true;
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest)
{
Enemy.IsCriminal = true;
}
return false;
}
if (Enemy.Submarine != null && character.Submarine != null && character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Enemy.Submarine.TeamID != character.TeamID)
{
allowCooldown = true;
// Target not in the outpost anymore.
if (character.CanSeeTarget(Enemy))
{
allowCooldown = false;
coolDownTimer = DefaultCoolDown;
}
else if (pathBackTimer <= 0)
{
// Check once per sec during the cooldown whether we can find a path back to the docking port
pathBackTimer = PathBackCheckTime;
foreach ((Submarine sub, DockingPort dockingPort) in character.Submarine.ConnectedDockingPorts)
{
if (sub.TeamID != character.TeamID) { continue; }
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(dockingPort.Item), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
allowCooldown = false;
coolDownTimer = DefaultCoolDown;
}
}
}
if (IsOffensiveOrArrest)
{
Enemy.IsCriminal = true;
}
}
}
}
return TargetEliminated || (AllowCoolDown && coolDownTimer <= 0);
}
protected override void Act(float deltaTime)
@@ -288,6 +327,10 @@ namespace Barotrauma
if (AllowCoolDown)
{
coolDownTimer -= deltaTime;
if (pathBackTimer > 0)
{
pathBackTimer -= deltaTime;
}
}
if (seekAmmunitionObjective == null && seekWeaponObjective == null)
{
@@ -303,27 +346,6 @@ namespace Barotrauma
{
Move(deltaTime);
}
switch (Mode)
{
case CombatMode.Offensive:
if (TargetEliminated && objectiveManager.IsCurrentOrder<AIObjectiveFightIntruders>())
{
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
}
break;
case CombatMode.Arrest:
if (HumanAIController.HasItem(Enemy, Tags.HandLockerItem, out _, requireEquipped: true))
{
IsCompleted = true;
}
else if (Enemy.IsKnockedDown &&
!objectiveManager.IsCurrentObjective<AIObjectiveFightIntruders>() &&
!HumanAIController.HasItem(character, Tags.HandLockerItem, out _, requireEquipped: false))
{
IsCompleted = true;
}
break;
}
}
}
@@ -389,7 +411,7 @@ namespace Barotrauma
&& !character.IsInstigator); // Instigators (= aggressive NPCs spawned with events) shouldn't seek new weapons, because we don't want them to grab e.g. an smg, if they spawn with a wrench or something.
if (checkWeaponsTimer < 0)
{
checkWeaponsTimer = checkWeaponsInterval;
checkWeaponsTimer = CheckWeaponsInterval;
// First go through all weapons and try to reload without seeking ammunition
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
@@ -412,7 +434,7 @@ namespace Barotrauma
// All good, the weapon is loaded
break;
}
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(closeDistanceThreshold);
bool seekAmmo = isAllowedToSeekWeapons && seekAmmunitionObjective == null && !IsEnemyClose(CloseDistanceThreshold);
if (Reload(seekAmmo: seekAmmo))
{
// All good, we can use the weapon.
@@ -458,7 +480,7 @@ namespace Barotrauma
Mode = CombatMode.Retreat;
}
}
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < goodWeaponPriority && !IsEnemyClose(closeDistanceThreshold))))
else if (seekAmmunitionObjective == null && (WeaponComponent == null || (WeaponComponent.CombatPriority < GoodWeaponPriority && !IsEnemyClose(CloseDistanceThreshold))))
{
// No weapon or only a poor weapon equipped -> try to find better.
RemoveSubObjective(ref retreatObjective);
@@ -485,7 +507,7 @@ namespace Barotrauma
if (range is > 0 and < float.PositiveInfinity)
{
// Y distance is irrelevant when we are on the same floor. If we are on a different floor, let's double it.
float yDiff = Math.Abs(toItem.Y) > floorHeightApproximate ? toItem.Y * 2 : 0;
float yDiff = Math.Abs(toItem.Y) > FloorHeightApproximate ? toItem.Y * 2 : 0;
Vector2 adjustedDiff = new Vector2(toItem.X, yDiff);
if (adjustedDiff.LengthSquared() > MathUtils.Pow2(range))
{
@@ -501,7 +523,7 @@ namespace Barotrauma
}
if (i.CurrentHull != null && !HumanAIController.VisibleHulls.Contains(i.CurrentHull))
{
if (Math.Abs(toItem.Y) > floorHeightApproximate && Math.Abs(toEnemy.Y) > floorHeightApproximate)
if (Math.Abs(toItem.Y) > FloorHeightApproximate && Math.Abs(toEnemy.Y) > FloorHeightApproximate)
{
if (Math.Sign(toItem.Y) == Math.Sign(toEnemy.Y))
{
@@ -522,7 +544,7 @@ namespace Barotrauma
SpeakNoWeapons();
Mode = CombatMode.Retreat;
}
else if (!objectiveManager.HasActiveObjective<AIObjectiveFightIntruders>())
else if (!objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>())
{
// Poor weapon equipped
Mode = CombatMode.Defensive;
@@ -642,7 +664,7 @@ namespace Barotrauma
priority /= 2;
}
}
else if (Enemy.IsKnockedDown)
else if (Enemy.IsKnockedDown && Mode != CombatMode.Arrest)
{
// Enemy is stunned, reduce the priority of stunner weapons.
Attack attack = GetAttackDefinition(weapon);
@@ -775,7 +797,7 @@ namespace Barotrauma
float bestPriority = 0;
float lethalDmg = -1;
bool prioritizeMelee = IsEnemyClose(50) || EnemyAIController.IsLatchedTo(Enemy, character);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(closeDistanceThreshold);
bool isCloseToEnemy = prioritizeMelee || IsEnemyClose(CloseDistanceThreshold);
foreach (var weapon in weaponList)
{
float priority = GetWeaponPriority(weapon, prioritizeMelee, canSeekAmmo: !isCloseToEnemy, out lethalDmg);
@@ -801,9 +823,28 @@ namespace Barotrauma
}
isLethalWeapon = lethalDmg > 1;
}
if (AllowHoldFire && !hasAimed && holdFireTimer <= 0)
if (AllowHoldFire)
{
holdFireTimer = arrestHoldFireTime * Rand.Range(0.75f, 1.25f);
if (!hasAimed && holdFireTimer <= 0)
{
holdFireTimer = ArrestHoldFireTime * Rand.Range(0.9f, 1.1f);
}
else
{
if (SpeakWarnings)
{
if (!lastWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.3f)
{
FriendlyGuardSpeak("dialogarrest.lastwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
lastWarningTriggered = true;
}
else if (!firstWarningTriggered && holdFireTimer < ArrestHoldFireTime * 0.8f)
{
FriendlyGuardSpeak("dialogarrest.firstwarning".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 0f);
firstWarningTriggered = true;
}
}
}
}
}
return weaponComponent.Item;
@@ -909,9 +950,10 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
if (!Enemy.IsHuman)
if (!Enemy.IsHuman && !character.IsInFriendlySub)
{
SpeakRetreating();
// Only relevant when we are retreating from monsters and are not inside a friendly sub.
PlayerCrewSpeak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDurationBetweenSimilar: 20);
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
@@ -931,7 +973,7 @@ namespace Barotrauma
{
RemoveSubObjective(ref retreatObjective);
}
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(maxDistance))
if (character.Submarine == null && sqrDistance < MathUtils.Pow2(MaxDistance))
{
// Swim away
SteeringManager.Reset();
@@ -1017,6 +1059,16 @@ namespace Barotrauma
}
return;
}
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.Submarine != null && character.Submarine.TeamID != character.TeamID)
{
// An outpost guard following the target (possibly a player) to another sub -> don't go further, unless can see the enemy.
if (!character.IsClimbing && !character.CanSeeTarget(Enemy))
{
SteeringManager.Reset();
RemoveFollowTarget();
return;
}
}
if (followTargetObjective != null && followTargetObjective.Target != Enemy)
{
RemoveFollowTarget();
@@ -1045,32 +1097,27 @@ namespace Barotrauma
}
});
if (followTargetObjective == null) { return; }
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown)
if (Mode == CombatMode.Arrest && Enemy.IsKnockedDown && !arrestingRegistered)
{
if (HumanAIController.HasItem(character, Tags.HandLockerItem, out _))
bool hasHandCuffs = HumanAIController.HasItem(character, Tags.HandLockerItem, out _);
if (!hasHandCuffs && character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (!arrestingRegistered)
// Spawn handcuffs
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
if (prefab != null)
{
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = 100;
}
}
else
{
if (character.TeamID == CharacterTeamType.FriendlyNPC)
{
ItemPrefab prefab = ItemPrefab.Find(null, "handcuffs".ToIdentifier());
if (prefab != null)
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: i =>
{
Entity.Spawner.AddItemToSpawnQueue(prefab, character.Inventory, onSpawned: (Item i) => i.SpawnedInCurrentOutpost = true);
}
i.SpawnedInCurrentOutpost = true;
i.AllowStealing = false;
});
}
RemoveFollowTarget();
SteeringManager.Reset();
}
arrestingRegistered = true;
followTargetObjective.Completed += OnArrestTargetReached;
followTargetObjective.CloseEnough = ArrestTargetDistance;
}
if (!arrestingRegistered && followTargetObjective != null)
if (!arrestingRegistered)
{
followTargetObjective.CloseEnough =
WeaponComponent switch
@@ -1083,8 +1130,6 @@ namespace Barotrauma
}
}
private bool arrestingRegistered;
private void RemoveFollowTarget()
{
if (followTargetObjective != null)
@@ -1110,9 +1155,9 @@ namespace Barotrauma
// Confiscate stolen goods and all weapons
foreach (var item in Enemy.Inventory.AllItemsMod)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && item.StolenDuringRound ||
item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) ||
GetWeaponComponent(item) is { CombatPriority: > 0 })
// Ignore handcuffs already on the target.
if (item.HasTag(Tags.HandLockerItem) && Enemy.HasEquippedItem(item)) { continue; }
if (item.Illegitimate || item.HasTag(Tags.Weapon) || item.HasTag(Tags.Poison) || GetWeaponComponent(item) is { CombatPriority: > 0 })
{
item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
@@ -1255,7 +1300,7 @@ namespace Barotrauma
if (visibilityCheckTimer <= 0.0f)
{
canSeeTarget = character.CanSeeTarget(Enemy);
visibilityCheckTimer = visibilityCheckInterval;
visibilityCheckTimer = VisibilityCheckInterval;
}
if (!canSeeTarget)
{
@@ -1267,7 +1312,7 @@ namespace Barotrauma
character.SetInput(InputType.Aim, hit: false, held: true);
}
hasAimed = true;
if (holdFireTimer > 0)
if (AllowHoldFire && holdFireTimer > 0)
{
holdFireTimer -= deltaTime;
return;
@@ -1280,7 +1325,7 @@ namespace Barotrauma
if (reloadTimer > 0) { return; }
if (holdFireCondition != null && holdFireCondition()) { return; }
sqrDistance = Vector2.DistanceSquared(character.WorldPosition, Enemy.WorldPosition);
distanceTimer = distanceCheckInterval;
distanceTimer = DistanceCheckInterval;
if (WeaponComponent is MeleeWeapon meleeWeapon)
{
bool closeEnough = true;
@@ -1354,8 +1399,8 @@ namespace Barotrauma
private void UseWeapon(float deltaTime)
{
// Never allow to attack characters with deadly weapons while trying to arrest.
if (Mode == CombatMode.Arrest && isLethalWeapon) { return; }
// Never allow friendly crew (bots) to attack with deadly weapons.
if (Mode == CombatMode.Arrest && isLethalWeapon && character.IsOnPlayerTeam && Enemy.IsOnPlayerTeam) { return; }
character.SetInput(InputType.Shoot, hit: false, held: true);
Weapon.Use(deltaTime, user: character);
SetReloadTime(WeaponComponent);
@@ -1408,6 +1453,36 @@ namespace Barotrauma
protected override void OnCompleted()
{
base.OnCompleted();
if (Enemy != null)
{
switch (Mode)
{
case CombatMode.Offensive when Enemy.IsUnconscious && objectiveManager.HasObjectiveOrOrder<AIObjectiveFightIntruders>():
character.Speak(TextManager.Get("DialogTargetDown").Value, null, 3.0f, "targetdown".ToIdentifier(), 30.0f);
break;
case CombatMode.Arrest when IsCompleted:
if (!HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
(bot != HumanAIController && bot.ObjectiveManager.CurrentObjective is AIObjectiveCombat { Mode: CombatMode.Arrest } combatObj && combatObj.Enemy == Enemy) ||
bot.ObjectiveManager.CurrentObjective is AIObjectiveGoTo { SourceObjective: AIObjectiveCombat combatObjective } && combatObjective.Enemy == Enemy))
{
// Go to the target and confiscate any stolen items, unless someone is already on it.
// Added on the root level, because the lifetime of the new objective exceeds the lifetime of this objective.
RemoveFollowTarget();
var approachArrestTarget = new AIObjectiveGoTo(Enemy, character, objectiveManager, repeat: false, getDivingGearIfNeeded: false, closeEnough: ArrestTargetDistance)
{
UsePathingOutside = false,
IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false,
SpeakIfFails = false,
SourceObjective = this
};
approachArrestTarget.Completed += OnArrestTargetReached;
objectiveManager.AddObjective(approachArrestTarget);
}
break;
}
}
if (ShouldUnequipWeapon)
{
Unequip();
@@ -1424,11 +1499,23 @@ namespace Barotrauma
}
SteeringManager?.Reset();
}
public override void OnDeselected()
{
base.OnDeselected();
if (character.TeamID == CharacterTeamType.FriendlyNPC && IsOffensiveOrArrest && (!AllowHoldFire || (hasAimed && holdFireTimer <= 0)))
{
// Remember that the target resisted or acted offensively (we've aimed or tried to arrest/attack)
Enemy.IsCriminal = true;
}
}
public override void Reset()
{
base.Reset();
hasAimed = false;
holdFireTimer = 0;
pathBackTimer = 0;
isLethalWeapon = false;
canSeeTarget = false;
seekWeaponObjective = null;
@@ -1436,20 +1523,43 @@ namespace Barotrauma
retreatObjective = null;
followTargetObjective = null;
retreatTarget = null;
firstWarningTriggered = false;
lastWarningTriggered = false;
}
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(Identifier textIdentifier, float delay, float minDuration)
/// <summary>
/// Speak that we don't have weapons. But only outside of friendly subs (not that relevant there, reduces spam).
/// </summary>
private void SpeakNoWeapons()
{
if (character.IsOnPlayerTeam && !character.IsInFriendlySub)
if (!character.IsInFriendlySub)
{
LocalizedString msg = TextManager.Get(textIdentifier);
if (!msg.IsNullOrEmpty())
{
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDuration);
}
PlayerCrewSpeak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDurationBetweenSimilar: 30);
}
}
private void PlayerCrewSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
if (character.IsOnPlayerTeam)
{
Speak(textIdentifier, delay, minDurationBetweenSimilar);
}
}
private void FriendlyGuardSpeak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
if (character.TeamID == CharacterTeamType.FriendlyNPC && character.IsSecurity)
{
Speak(textIdentifier, delay, minDurationBetweenSimilar);
}
}
private void Speak(Identifier textIdentifier, float delay, float minDurationBetweenSimilar)
{
LocalizedString msg = TextManager.Get(textIdentifier);
if (!msg.IsNullOrEmpty())
{
character.Speak(msg.Value, identifier: textIdentifier, delay: delay, minDurationBetweenSimilar: minDurationBetweenSimilar);
}
}
@@ -11,7 +11,7 @@ namespace Barotrauma
class AIObjectiveContainItem: AIObjective
{
public override Identifier Identifier { get; set; } = "contain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -6,9 +6,9 @@ namespace Barotrauma
class AIObjectiveDeconstructItem : AIObjective
{
public override Identifier Identifier { get; set; } = "deconstruct item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public readonly Item Item;
@@ -11,7 +11,7 @@ namespace Barotrauma
//Clear periodically, because we may ending up ignoring items when all deconstructors are full
protected override float IgnoreListClearInterval => 30;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
protected override int MaxTargets => 10;
@@ -47,7 +47,7 @@ namespace Barotrauma
checkedDeconstructorExists = false;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -57,7 +57,7 @@ namespace Barotrauma
return AIObjectiveManager.RunPriority - 0.5f;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
@@ -9,7 +9,7 @@ namespace Barotrauma
class AIObjectiveDecontainItem : AIObjective
{
public override Identifier Identifier { get; set; } = "decontain item".ToIdentifier();
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Func<Item, float> GetItemPriority;
@@ -8,8 +8,8 @@ namespace Barotrauma
// Used for prisoner escorts to allow them to escape their binds
public override Identifier Identifier { get; set; } = "escape handcuffs".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private int escapeProgress;
private bool isBeingWatched;
@@ -28,7 +28,6 @@ namespace Barotrauma
}
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
protected override bool CheckObjectiveSpecific() => false;
// escape timer is set to 60 by default to allow players to locate prisoners in time
@@ -10,12 +10,10 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "extinguish fire".ToIdentifier();
public override bool ForceRun => true;
public override bool ConcurrentObjectives => true;
protected override bool ConcurrentObjectives => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
private readonly Hull targetHull;
@@ -32,7 +30,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
bool isOrder = objectiveManager.HasOrder<AIObjectiveExtinguishFires>();
@@ -10,13 +10,13 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "extinguish fires".ToIdentifier();
public override bool ForceRun => true;
public override bool AllowInAnySub => true;
protected override bool AllowInAnySub => true;
public AIObjectiveExtinguishFires(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Hull hull) => IsValidTarget(hull, character);
protected override bool IsValidTarget(Hull hull) => IsValidTarget(hull, character);
protected override float TargetEvaluation() =>
protected override float GetTargetPriority() =>
// If any target is visible -> 100 priority
Targets.Any(t => t == character.CurrentHull || HumanAIController.VisibleHulls.Contains(t)) ? 100 :
// Else based on the fire severity
@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -9,19 +9,17 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fight intruders".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 0.2f;
public bool TargetCharactersInOtherSubs { get; init; }
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveFightIntruders(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override bool IsValidTarget(Character target) => IsValidTarget(target, character, TargetCharactersInOtherSubs);
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (!character.IsOnPlayerTeam && !character.IsOriginallyOnPlayerTeam) { return 100; }
@@ -68,14 +66,14 @@ namespace Barotrauma
if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs)
{
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; }
if (target.IsHandcuffed && target.IsKnockedDown) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
return true;
}
@@ -11,8 +11,8 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier} ({gearTag})";
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AllowWhileHandcuffed => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
protected override bool AllowWhileHandcuffed => false;
private readonly Identifier gearTag;
@@ -13,18 +13,16 @@ namespace Barotrauma
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool IgnoreUnsafeHulls => true;
public override bool ConcurrentObjectives => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
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.CleanupStackTrace()); }
protected override bool ConcurrentObjectives => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
// TODO: expose?
const float priorityIncrease = 100;
const float priorityDecrease = 10;
const float SearchHullInterval = 3.0f;
private const float PriorityIncrease = 100;
private const float PriorityDecrease = 10;
private const float SearchHullInterval = 3.0f;
private float currenthullSafety;
private float currentHullSafety;
private float searchHullTimer;
@@ -111,15 +109,15 @@ namespace Barotrauma
}
if (character.CurrentHull == null)
{
currenthullSafety = 0;
currentHullSafety = 0;
}
else
{
currenthullSafety = HumanAIController.CurrentHullSafety;
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
currentHullSafety = HumanAIController.CurrentHullSafety;
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
Priority -= PriorityDecrease * deltaTime;
if (currentHullSafety >= 100 && !character.IsLowInOxygen)
{
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0;
@@ -127,8 +125,8 @@ namespace Barotrauma
}
else
{
float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime;
float dangerFactor = (100 - currentHullSafety) / 100;
Priority += dangerFactor * PriorityIncrease * deltaTime;
}
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
}
@@ -192,7 +190,7 @@ namespace Barotrauma
}
if (divingGearObjective == null || !divingGearObjective.CanBeCompleted)
{
if (currenthullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
if (currentHullSafety < HumanAIController.HULL_SAFETY_THRESHOLD)
{
searchHullTimer = Math.Min(1, searchHullTimer);
}
@@ -231,7 +229,7 @@ namespace Barotrauma
},
onCompleted: () =>
{
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
if (currentHullSafety > HumanAIController.HULL_SAFETY_THRESHOLD ||
HumanAIController.NeedsDivingGear(currentHull, out bool needsSuit) && (needsSuit ? HumanAIController.HasDivingSuit(character) : HumanAIController.HasDivingMask(character)))
{
resetPriority = true;
@@ -299,7 +297,7 @@ namespace Barotrauma
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsHandcuffed) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
@@ -13,17 +14,33 @@ namespace Barotrauma
protected override float TargetUpdateTimeMultiplier => 1.0f;
const float DefaultInspectDistance = 200.0f;
/// <summary>
/// How long the round must have ran before NPCs can start doing inspections
/// (prevents "unfair" inspections you have no chance to react to if you happen to spawn right next to a security NPC with stolen items on you)
/// </summary>
private const float DelayOnRoundStart = 30.0f;
private const float DefaultInspectDistance = 200.0f;
/// <summary>
/// Used when something is stolen and when the guards decide to inspect everyone.
/// </summary>
private const float ExtendedInspectDistance = 400.0f;
/// <summary>
/// Used when the target is tagged as a criminal (= suspective).
/// </summary>
private const float CriminalInspectDistance = 500.0f;
private const float CriminalInspectProbability = 1.0f;
/// <summary>
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
/// </summary>
public float InspectDistance = DefaultInspectDistance;
private float inspectDistance = DefaultInspectDistance;
private float? overrideInspectProbability;
/// <summary>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="NormalInspectionInterval"/>
/// regardless if the target is inspected or not.
/// </summary>
public float InspectProbability
@@ -53,18 +70,25 @@ namespace Barotrauma
/// When did the character last inspect whether some other character has stolen items on them?
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private readonly float inspectionInterval = 120.0f;
private const float NormalInspectionInterval = 120.0f;
private const float CriminalInspectionInterval = 30.0f;
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
protected override bool IsValidTarget(Character target)
{
if (GameMain.GameSession is not { RoundDuration: > DelayOnRoundStart })
{
return false;
}
if (!IsValidTarget(target, character)) { return false; }
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
float inspectDist = target.IsCriminal ? CriminalInspectDistance : inspectDistance;
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > inspectDist * inspectDist) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
{
float inspectionInterval = target.IsCriminal ? CriminalInspectionInterval : NormalInspectionInterval;
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
{
return false;
@@ -75,8 +99,14 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (character.IsClimbing)
{
// Don't inspect while climbing, because cannot grab while holding the ladders.
// Can lead to abandoning the objective when we need to climb the ladders to get to the target, but I think that's acceptable.
return 0;
}
return subObjectives.Any() ? 50 : 0;
}
@@ -84,13 +114,14 @@ namespace Barotrauma
{
lastInspectionTimes.Clear();
overrideInspectProbability = 1.0f;
InspectDistance = DefaultInspectDistance * 2;
inspectDistance = ExtendedInspectDistance;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
float probabity = target.IsCriminal ? CriminalInspectProbability : InspectProbability;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= probabity)
{
checkStolenItemsObjective.ForceComplete();
lastInspectionTimes[target] = Timing.TotalTime;
@@ -104,26 +135,30 @@ namespace Barotrauma
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (checkVisibleStolenItemsTimer > 0.0f)
if (checkVisibleStolenItemsTimer > 0.0f || character.IsClimbing)
{
checkVisibleStolenItemsTimer -= deltaTime;
return;
}
if (character.SelectedSecondaryItem?.GetComponent<Controller>() != null)
{
// Might be e.g. sitting on a chair.
character.SelectedSecondaryItem = null;
}
foreach (var target in Character.CharacterList)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
if (target.Inventory.AllItems.Any(it => it.Illegitimate && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.Target == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
@@ -140,7 +175,17 @@ namespace Barotrauma
if (target.Submarine != character.Submarine) { return false; }
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsArrested) { return false; }
if (target.IsHandcuffed) { return false; }
// Ignore targets that are climbing, because might need to use ladders to get to them.
if (target.IsClimbing) { return false; }
if (HumanAIController.IsTrueForAnyBotInTheCrew(bot =>
bot != HumanAIController &&
((bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCheckStolenItems checkObj && checkObj.Target == target) ||
(bot.ObjectiveManager.GetActiveObjective() is AIObjectiveCombat combatObj && combatObj.Enemy == target))))
{
// Already being inspected by someone or fighting with someone in our team.
return false;
}
return true;
}
@@ -148,5 +193,11 @@ namespace Barotrauma
{
lastInspectionTimes[target] = Timing.TotalTime;
}
public override void OnDeselected()
{
base.OnDeselected();
character.DeselectCharacter();
}
}
}
@@ -12,9 +12,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leak".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInFriendlySubs => true;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
public Gap Leak { get; private set; }
@@ -37,7 +37,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
float coopMultiplier = 1;
@@ -9,7 +9,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "fix leaks".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
private Hull PrioritizedHull { get; set; }
@@ -18,7 +18,7 @@ namespace Barotrauma
PrioritizedHull = prioritizedHull;
}
protected override bool Filter(Gap gap) => IsValidTarget(gap, character);
protected override bool IsValidTarget(Gap gap) => IsValidTarget(gap, character);
public static float GetLeakSeverity(Gap leak)
{
@@ -37,7 +37,7 @@ namespace Barotrauma
}
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
int totalLeaks = Targets.Count;
if (totalLeaks == 0) { return 0; }
@@ -13,9 +13,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "get item".ToIdentifier();
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public HashSet<Item> ignoredItems = new HashSet<Item>();
@@ -444,7 +444,7 @@ namespace Barotrauma
}
if (!AllowStealing && character.IsOnPlayerTeam)
{
if (item.SpawnedInCurrentOutpost && !item.AllowStealing) { continue; }
if (item.Illegitimate) { continue; }
}
if (!CheckItem(item)) { continue; }
if (item.Container != null)
@@ -13,7 +13,7 @@ namespace Barotrauma
public override string DebugTag => $"{Identifier}";
public override bool KeepDivingGearOn => true;
public override bool AllowMultipleInstances => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public bool AllowStealing { get; set; }
public bool TakeWholeStack { get; set; }
@@ -16,7 +16,7 @@ namespace Barotrauma
private readonly bool repeat;
//how long until the path to the target is declared unreachable
private float waitUntilPathUnreachable;
private bool getDivingGearIfNeeded;
private readonly bool getDivingGearIfNeeded;
/// <summary>
/// Doesn't allow the objective to complete if this condition is false
@@ -34,11 +34,6 @@ namespace Barotrauma
public bool DebugLogWhenFails { get; set; } = true;
public bool UsePathingOutside { get; set; } = true;
/// <summary>
/// Which event action created this objective (if any)
/// </summary>
public EventAction SourceEventAction;
public float ExtraDistanceWhileSwimming;
public float ExtraDistanceOutsideSub;
private float _closeEnoughMultiplier = 1;
@@ -94,10 +89,10 @@ namespace Barotrauma
/// </summary>
public bool UseDistanceRelativeToAimSourcePos { get; set; } = false;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowOutsideSubmarine => AllowGoingOutside;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => AllowGoingOutside;
protected override bool AllowInAnySub => true;
public Identifier DialogueIdentifier { get; set; } = "dialogcannotreachtarget".ToIdentifier();
public LocalizedString TargetName { get; set; }
@@ -287,10 +282,12 @@ namespace Barotrauma
if (waitUntilPathUnreachable < 0)
{
waitUntilPathUnreachable = pathWaitingTime;
if (repeat)
if (repeat && !IsCompleted)
{
SpeakCannotReach();
return;
if (!IsDoneFollowing())
{
SpeakCannotReach();
}
}
else
{
@@ -374,16 +371,10 @@ namespace Barotrauma
return;
}
}
if (repeat && IsCloseEnough)
if (IsDoneFollowing())
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
OnCompleted();
return;
}
}
OnCompleted();
return;
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
@@ -653,6 +644,21 @@ namespace Barotrauma
character.SetInput(InputType.Aim, false, true);
character.SetInput(InputType.Shoot, false, true);
}
bool IsDoneFollowing()
{
if (repeat && IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
return true;
}
}
}
return false;
}
}
private bool useScooter;
@@ -12,7 +12,7 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "idle".ToIdentifier();
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowInAnySub => true;
protected override bool AllowInAnySub => true;
private BehaviorType behavior;
public BehaviorType Behavior
@@ -91,8 +91,6 @@ namespace Barotrauma
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace()); }
public readonly HashSet<Identifier> PreferredOutpostModuleTypes = new HashSet<Identifier>();
public void CalculatePriority(float max = 0)
@@ -266,7 +264,7 @@ namespace Barotrauma
if (node.Waypoint.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(node.Waypoint.CurrentHull)) { return false; }
return true;
//don't stop at ladders when idling
}, endNodeFilter: node => node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
}, endNodeFilter: node => node.Waypoint.Stairs == null && node.Waypoint.Ladders == null && (!isCurrentHullAllowed || !IsForbidden(node.Waypoint.CurrentHull)));
if (path.Unreachable)
{
//can't go to this room, remove it from the list and try another room
@@ -299,7 +297,7 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1,
nodeFilter: node => node.Waypoint.CurrentHull != null,
endNodeFilter: node => node.Waypoint.Ladders == null);
endNodeFilter: node => node.Waypoint.Ladders == null && node.Waypoint.Stairs == null);
}
else
{
@@ -0,0 +1,126 @@
using System;
namespace Barotrauma
{
class AIObjectiveInspectNoises : AIObjective
{
public override Identifier Identifier { get; set; } = "inspect noises".ToIdentifier();
private AIObjectiveGoTo inspectNoiseObjective;
/// <summary>
/// Initial priority of the objective to check noises made by enemies
/// </summary>
const float InspectNoisePriority = 10.0f;
/// <summary>
/// How much the priority of the objective to check noises made by enemies increases per noise
/// </summary>
const float InspectNoisePriorityIncrease = 10.0f;
private const float InspectNoiseInterval = 1.0f;
private float inspectNoiseTimer;
/// <summary>
/// If the character is not currently inspecting the noise (= if some other objective is taking priority)
/// it forgets about it after this delay runs out. Otherwise they might unnecessarily go and inspect some
/// noise that was emitted a long time ago once done with the higher-prio objective.
/// </summary>
private const float InspectNoiseExpirationDelay = 60.0f;
private float inspectNoiseExpirationTimer = 0.0f;
protected override float GetPriority() => inspectNoiseObjective?.Priority ?? 0.0f;
public AIObjectiveInspectNoises(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
inspectNoiseTimer = Rand.Range(0.0f, InspectNoiseInterval);
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
inspectNoiseTimer -= deltaTime;
if (inspectNoiseTimer <= 0.0f)
{
CheckEnemyNoises();
inspectNoiseTimer = InspectNoiseInterval;
}
//if we're not currently inspecting the noise (something else taking priority), forget about it after a while
if (inspectNoiseObjective != null && objectiveManager.GetActiveObjective() != inspectNoiseObjective)
{
inspectNoiseExpirationTimer += deltaTime;
if (inspectNoiseExpirationTimer > InspectNoiseExpirationDelay)
{
inspectNoiseObjective.Abandon = true;
}
}
}
/// <summary>
/// Check if there's any loud provocative items used by enemies nearby (= if someone fired a gun somewhere), and go inspect them
/// </summary>
private void CheckEnemyNoises()
{
if (character.CurrentHull == null) { return; }
//forget about inspecting if we're doing another subobjective (= fighting something)
if (inspectNoiseObjective != null &&
CurrentSubObjective != inspectNoiseObjective)
{
inspectNoiseObjective.Abandon = true;
}
foreach (var aiTarget in AITarget.List)
{
if (aiTarget.ShouldBeIgnored()) { continue; }
if (!aiTarget.IsWithinSector(character.WorldPosition)) { continue; }
if (aiTarget.Entity is not Item item) { continue; }
if (!item.HasTag(Tags.ProvocativeToHumanAI)) { continue; }
if (item.GetRootInventoryOwner() is Character targetCharacter &&
AIObjectiveFightIntruders.IsValidTarget(targetCharacter, character, targetCharactersInOtherSubs: false))
{
float dist = character.CurrentHull.GetApproximateDistance(character.Position, targetCharacter.Position, targetCharacter.CurrentHull, aiTarget.SoundRange, distanceMultiplierPerClosedDoor: 2);
if (dist * HumanAIController.Hearing > aiTarget.SoundRange) { continue; }
character.Speak(TextManager.Get("dialogheardenemy").Value, identifier: "heardenemy".ToIdentifier(), minDurationBetweenSimilar: 30.0f);
if (inspectNoiseObjective != null && subObjectives.Contains(inspectNoiseObjective))
{
//priority of inspecting noises increases with each noise
//but orders still remain a higher priority
inspectNoiseObjective.Priority = Math.Min(inspectNoiseObjective.Priority + InspectNoisePriorityIncrease, AIObjectiveManager.LowestOrderPriority - 1);
//only refresh the target if the character hasn't yet started inspecting the noise
//(if it has, it should not switch the target, otherwise you could e.g. bounce an NPC back and forth by firing guns at different sides of an outpost)
if (objectiveManager.GetActiveObjective() != inspectNoiseObjective &&
inspectNoiseObjective.Target != targetCharacter.CurrentHull)
{
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: inspectNoiseObjective.Priority);
}
}
else
{
CreateInspectNoiseObjective(targetCharacter.CurrentHull, priority: InspectNoisePriority);
}
}
}
void CreateInspectNoiseObjective(ISpatialEntity target, float priority)
{
RemoveSubObjective(ref inspectNoiseObjective);
inspectNoiseObjective = new AIObjectiveGoTo(target, character, objectiveManager)
{
Priority = priority,
SourceObjective = this
};
inspectNoiseObjective.Completed += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
inspectNoiseObjective.Abandoned += () => { inspectNoiseObjective = null; inspectNoiseExpirationTimer = 0.0f; };
AddSubObjective(inspectNoiseObjective);
}
}
protected override void Act(float deltaTime)
{
}
protected override bool CheckObjectiveSpecific() => false;
}
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -11,13 +11,8 @@ namespace Barotrauma
class AIObjectiveLoadItem : AIObjective
{
public override Identifier Identifier { get; set; } = "load item".ToIdentifier();
public override bool IsLoop
{
get => true;
set => throw new Exception("Trying to set the value for AIObjectiveLoadItem.IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private AIObjectiveLoadItems.ItemCondition TargetItemCondition { get; }
private Item Container { get; }
@@ -163,7 +158,7 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
else if (!AIObjectiveLoadItems.IsValidTarget(Container, character, targetCondition: TargetItemCondition))
@@ -298,12 +293,14 @@ namespace Barotrauma
if (item.Removed) { return false; }
if (!ValidContainableItemIdentifiers.Contains(item.Prefab.Identifier)) { return false; }
if (ignoredItems.Contains(item)) { return false; }
if ((item.Illegitimate) == character.IsOnPlayerTeam) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
var rootInventoryOwner = item.GetRootInventoryOwner();
if (rootInventoryOwner is Character owner && owner != character) { return false; }
if (rootInventoryOwner is Item parentItem)
if (item.GetRootInventoryOwner() is Character owner && owner != character) { return false; }
Item parentItem = item.Container;
while (parentItem != null)
{
if (parentItem.HasTag(Tags.DontTakeItems)) { return false; }
parentItem = parentItem.Container;
}
if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
@@ -50,7 +50,7 @@ namespace Barotrauma
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
}
protected override bool Filter(Item target)
protected override bool IsValidTarget(Item target)
{
//don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers)
if (!IsValidTarget(target, character, null, TargetCondition)) { return false; }
@@ -104,7 +104,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Item target)
=> HumanAIController.RemoveTargets<AIObjectiveLoadItems, Item>(character, target);
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (objectiveManager.IsOrder(this))
@@ -4,21 +4,34 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
/// <summary>
/// An objective that creates specific kinds of subobjectives for specific types of targets, and loops through those targets.
/// For example, a cleanup objective that loops through items that need to be cleaned up, or a "fix leaks" objective that loops through leaks that need welding.
/// </summary>
abstract class AIObjectiveLoop<T> : AIObjective
{
public HashSet<T> Targets { get; private set; } = new HashSet<T>();
public Dictionary<T, AIObjective> Objectives { get; private set; } = new Dictionary<T, AIObjective>();
protected HashSet<T> ignoreList = new HashSet<T>();
private float ignoreListTimer;
private float ignoreListClearTimer;
protected float targetUpdateTimer;
protected virtual float TargetUpdateTimeMultiplier { get; } = 1;
/// <summary>
/// How often are the subobjectives synced based on the available targets?
/// </summary>
private float syncTimer;
private readonly float syncTime = 1;
// By default, doesn't clear the list automatically
/// <summary>
/// By default, doesn't clear the list automatically
/// </summary>
protected virtual float IgnoreListClearInterval => 0;
/// <summary>
/// Contains targets that anyone in the same crew has reported about. Used for automatic the target has to be reported before it can be can be targeted, so characters don't magically know where e.g. enemies are.
/// Ignored on orders: a bot explicitly ordered to repair leaks or fight intruders can find targets that haven't been reported.
/// </summary>
public HashSet<T> ReportedTargets { get; private set; } = new HashSet<T>();
public bool AddTarget(T target)
@@ -28,7 +41,7 @@ namespace Barotrauma
{
return false;
}
if (Filter(target))
if (IsValidTarget(target))
{
ReportedTargets.Add(target);
return true;
@@ -42,24 +55,27 @@ namespace Barotrauma
protected override void Act(float deltaTime) { }
protected override bool CheckObjectiveSpecific() => false;
public override bool CanBeCompleted => true;
public override bool AbandonWhenCannotCompleteSubjectives => false;
public override bool AbandonWhenCannotCompleteSubObjectives => false;
public override bool AllowSubObjectiveSorting => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
protected override bool AbandonIfDisallowed => false;
public virtual bool InverseTargetEvaluation => false;
/// <summary>
/// Makes the priority inversely proportional to the value returned by <see cref="GetTargetPriority"/>.
/// In other words, gives this objective a high priority when priority of the targets is low.
/// </summary>
public virtual bool InverseTargetPriority => false;
protected virtual bool ResetWhenClearingIgnoreList => true;
protected virtual bool ForceOrderPriority => true;
protected virtual int MaxTargets => int.MaxValue;
public override bool IsLoop { get => true; set => throw new Exception("Trying to set the value for IsLoop from: " + System.Environment.StackTrace.CleanupStackTrace()); }
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (IgnoreListClearInterval > 0)
{
if (ignoreListTimer > IgnoreListClearInterval)
if (ignoreListClearTimer > IgnoreListClearInterval)
{
if (ResetWhenClearingIgnoreList)
{
@@ -68,12 +84,12 @@ namespace Barotrauma
else
{
ignoreList.Clear();
ignoreListTimer = 0;
ignoreListClearTimer = 0;
}
}
else
{
ignoreListTimer += deltaTime;
ignoreListClearTimer += deltaTime;
}
}
if (targetUpdateTimer <= 0)
@@ -104,14 +120,17 @@ namespace Barotrauma
}
}
// the timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
//
/// <summary>
/// The timer is set between 1 and 10 seconds, depending on the priority modifier and a random +-25%
/// </summary>
private float CalculateTargetUpdateTimer() => targetUpdateTimer = 1 / MathHelper.Clamp(PriorityModifier * Rand.Range(0.75f, 1.25f), 0.1f, 1) * TargetUpdateTimeMultiplier;
public override void Reset()
{
base.Reset();
ignoreList.Clear();
ignoreListTimer = 0;
ignoreListClearTimer = 0;
UpdateTargets();
}
@@ -119,25 +138,25 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
// Allow the target value to be more than 100.
float targetValue = TargetEvaluation();
if (InverseTargetEvaluation)
float targetPriority = GetTargetPriority();
if (InverseTargetPriority)
{
targetValue = 100 - targetValue;
targetPriority = 100 - targetPriority;
}
var currentSubObjective = CurrentSubObjective;
if (currentSubObjective != null && currentSubObjective.Priority > targetValue)
if (currentSubObjective != null && currentSubObjective.Priority > targetPriority)
{
// If the priority is higher than the target value, let's just use it.
// The priority calculation is more precise, but it takes into account things like distances,
// so it's better not to use it if it's lower than the rougher targetValue.
targetValue = currentSubObjective.Priority;
targetPriority = currentSubObjective.Priority;
}
// If the target value is less than 1% of the max value, let's just treat it as zero.
if (targetValue < 1)
if (targetPriority < 1)
{
Priority = 0;
}
@@ -145,7 +164,7 @@ namespace Barotrauma
{
if (objectiveManager.IsOrder(this))
{
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetValue;
Priority = ForceOrderPriority ? objectiveManager.GetOrderPriority(this) : targetPriority;
}
else
{
@@ -155,7 +174,7 @@ namespace Barotrauma
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
float value = MathHelper.Clamp((CumulatedDevotion + (targetPriority * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value);
}
}
@@ -181,7 +200,7 @@ namespace Barotrauma
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
if (!IsValidTarget(target)) { continue; }
if (!ignoreList.Contains(target))
{
Targets.Add(target);
@@ -228,9 +247,13 @@ namespace Barotrauma
/// </summary>
protected abstract IEnumerable<T> GetList();
protected abstract float TargetEvaluation();
/// <summary>
/// Returns a priority value based on the current targets (e.g. high prio when there's lots of severe fires or leaks).
/// The priority of this objective is based on the target priority.
/// </summary>
protected abstract float GetTargetPriority();
protected abstract AIObjective ObjectiveConstructor(T target);
protected abstract bool Filter(T target);
protected abstract bool IsValidTarget(T target);
}
}
@@ -20,10 +20,27 @@ namespace Barotrauma
MaxValue = 2
}
/// <summary>
/// Highest possible priority for any objective. Used in certain cases where the character needs to react immediately to survive,
/// such as finding a suit when under pressure or getting out of a burning room.
/// </summary>
public const float MaxObjectivePriority = 100;
/// <summary>
/// Priority of objectives such as finding safety, rescuing someone in a critical state or defending against an attacker
/// (= objectives that are critical for saving the character's or someone else's life)
/// </summary>
public const float EmergencyObjectivePriority = 90;
/// <summary>
/// Maximum priority of an order given to the character (forced order, or the leftmost order in the crew list)
/// </summary>
public const float HighestOrderPriority = 70;
/// <summary>
/// Maximum priority of an order given to the character (rightmost order in the crew list)
/// </summary>
public const float LowestOrderPriority = 60;
/// <summary>
/// Objectives with a priority equal to or higher than this make the character run.
/// </summary>
public const float RunPriority = 50;
// Constantly increases the priority of the selected objective, unless overridden
public const float baseDevotion = 5;
@@ -138,6 +155,7 @@ namespace Barotrauma
return;
#endif
}
foreach (var delayedObjective in DelayedObjectives)
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
@@ -159,36 +177,46 @@ namespace Barotrauma
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
if (character.Info?.Job != null)
{
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier];
if (orderPrefab == null) { throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'"); }
Item item = null;
if (orderPrefab.MustSetTarget)
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
var orderPrefab = OrderPrefab.Prefabs[autonomousObjective.Identifier] ?? throw new Exception($"Could not find a matching prefab by the identifier: '{autonomousObjective.Identifier}'");
Item item = null;
if (orderPrefab.MustSetTarget)
{
item = orderPrefab.GetMatchingItems(character.Submarine, mustBelongToPlayerSub: false, requiredTeam: character.Info.TeamID, interactableFor: character)?.GetRandomUnsynced();
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) &&
Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC && !character.IsFriendlyNPCTurnedHostile)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
continue;
}
}
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
{
continue;
}
}
if (autonomousObjective.IgnoreAtNonOutpost && !Level.IsLoadedFriendlyOutpost)
{
continue;
}
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
var objective = CreateObjective(order, autonomousObjective.PriorityModifier);
if (objective != null && objective.CanBeCompleted)
{
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
else
{
string warningMsg = character.Info == null ?
$"The character {character.DisplayName} has been set to use human ai, but has no {nameof(CharacterInfo)}. This may cause issues with the AI. Consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config." :
$"The character {character.DisplayName} has been set to use human ai, but has no job. This may cause issues with the AI. Consider configuring some jobs for the character type.";
DebugConsole.AddWarning(warningMsg, character.Prefab.ContentPackage);
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
@@ -501,7 +529,6 @@ namespace Barotrauma
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(targetPump, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = order.OrderGiver is { IsCommanding: true }
};
newObjective.Completed += () => DismissSelf(order);
@@ -531,7 +558,7 @@ namespace Barotrauma
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
@@ -539,7 +566,6 @@ namespace Barotrauma
case "setchargepct":
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option, false, priorityModifier: priorityModifier)
{
IsLoop = false,
Override = !character.IsDismissed,
completionCondition = () =>
{
@@ -603,7 +629,6 @@ namespace Barotrauma
{
prepareObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
KeepActiveWhenReady = false,
CheckInventory = false,
EvaluateCombatPriority = true,
FindAllItems = false,
@@ -621,13 +646,16 @@ namespace Barotrauma
case "deconstructitems":
newObjective = new AIObjectiveDeconstructItems(character, this, priorityModifier);
break;
case "inspectnoises":
newObjective = new AIObjectiveInspectNoises(character, this, priorityModifier);
break;
default:
if (order.TargetItemComponent == null) { return null; }
if (!order.TargetItemComponent.Item.IsInteractable(character)) { return null; }
newObjective = new AIObjectiveOperateItem(order.TargetItemComponent, character, this, order.Option,
requireEquip: false, useController: order.UseController, controller: order.ConnectedController, priorityModifier: priorityModifier)
{
IsLoop = true,
Repeat = true,
// Don't override unless it's an order by a player
Override = order.OrderGiver != null && order.OrderGiver.IsCommanding
};
@@ -689,10 +717,16 @@ namespace Barotrauma
/// Only checks the current order. Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentOrder<T>() where T : AIObjective => CurrentOrder is T;
/// <summary>
/// Checks the current objective (which can be an order too). Deprecated, use pattern matching instead.
/// </summary>
public bool IsCurrentObjective<T>() where T : AIObjective => CurrentObjective is T;
/// <summary>
/// Checks if any objectives or orders are of the specified type. Regardless of whether the objective is active or inactive.
/// </summary>
public bool HasObjectiveOrOrder<T>() where T : AIObjective => Objectives.Any(o => o is T) || HasOrder<T>();
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
@@ -705,10 +739,23 @@ namespace Barotrauma
/// Return the first order with the specified objective. Can return null.
/// </summary>
public Order GetOrder(AIObjective objective) => CurrentOrders.FirstOrDefault(o => o.Objective == objective);
/// <summary>
/// Returns the last active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that don't sort their subobjectives by priority (see <see cref="AIObjective.AllowSubObjectiveSorting"/>.
/// </summary>
/// <returns>The last active objective of the specified type if found.
/// </returns>
public T GetLastActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// <summary>
/// Returns the first active objective of the specified objective type.
/// Should generally be used to get the active objective (or subobjective) of objectives that sort their subobjectives by priority, such as those that inherit <see cref="AIObjectiveLoop"/>.
/// </summary>
/// <returns>
/// The first active objective of the specified type if found.
/// </returns>
public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
@@ -13,8 +13,8 @@ namespace Barotrauma
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowMultipleInstances => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
public override bool PrioritizeIfSubObjectivesActive => component != null && (component is Reactor || component is Turret);
private readonly ItemComponent component, controller;
@@ -29,7 +29,12 @@ namespace Barotrauma
/// </summary>
public Func<PathNode, bool> EndNodeFilter;
public bool Override { get; set; } = true;
public bool Override { get; init; } = true;
/// <summary>
/// When true, the operate objective is never completed, unless it's abandoned.
/// </summary>
public bool Repeat { get; init; }
public override bool CanBeCompleted => base.CanBeCompleted && (!useController || controller != null);
@@ -50,7 +55,7 @@ namespace Barotrauma
bool isOrder = objectiveManager.IsOrder(this);
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
if (!isOrder && component.Item.ConditionPercentage <= 0)
@@ -307,7 +312,7 @@ namespace Barotrauma
}
}
protected override bool CheckObjectiveSpecific() => isDoneOperating && !IsLoop;
protected override bool CheckObjectiveSpecific() => isDoneOperating && !Repeat;
public override void Reset()
{
@@ -13,7 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool KeepDivingGearOnAlsoWhenInactive => true;
public override bool PrioritizeIfSubObjectivesActive => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private AIObjectiveGetItem getSingleItemObjective;
private AIObjectiveGetItems getAllItemsObjective;
@@ -22,7 +22,6 @@ namespace Barotrauma
private readonly Item targetItem;
private readonly ImmutableArray<Identifier> requiredItems;
private readonly ImmutableArray<Identifier> optionalItems;
private readonly HashSet<Item> items = new HashSet<Item>();
public bool KeepActiveWhenReady { get; set; }
public bool CheckInventory { get; set; }
public bool FindAllItems { get; set; }
@@ -61,12 +60,12 @@ namespace Barotrauma
{
if (!IsAllowed)
{
HandleNonAllowed();
HandleDisallowed();
return Priority;
}
Priority = objectiveManager.GetOrderPriority(this);
var subObjective = GetSubObjective();
if (subObjective != null && subObjective.IsCompleted)
if (subObjective is { IsCompleted: true })
{
Priority = 0;
}
@@ -113,20 +112,7 @@ namespace Barotrauma
},
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (objectiveReference != null)
{
foreach (var item in objectiveReference.achievedItems)
{
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
}
else
if (!KeepActiveWhenReady)
{
IsCompleted = true;
}
@@ -165,22 +151,11 @@ namespace Barotrauma
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
onCompleted: () =>
{
if (KeepActiveWhenReady)
{
if (getSingleItemObjective != null)
{
var item = getSingleItemObjective?.TargetItem;
if (item?.IsOwnedBy(character) != null)
{
items.Add(item);
}
}
}
else
if (!KeepActiveWhenReady)
{
IsCompleted = true;
}
},
},
onAbandon: () => Abandon = true))
{
Abandon = true;
@@ -193,7 +168,6 @@ namespace Barotrauma
public override void Reset()
{
base.Reset();
items.Clear();
subObjectivesCreated = false;
getMultipleItemsObjective = null;
getSingleItemObjective = null;
@@ -12,7 +12,7 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "pump water".ToIdentifier();
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
private List<Pump> pumpList;
@@ -25,7 +25,7 @@ namespace Barotrauma
base.FindTargets();
}
protected override bool Filter(Pump pump)
protected override bool IsValidTarget(Pump pump)
{
if (pump?.Item == null || pump.Item.Removed) { return false; }
if (pump.Item.IgnoreByAI(character)) { return false; }
@@ -62,7 +62,7 @@ namespace Barotrauma
return pumpList;
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 0; }
if (Option == "stoppumping")
@@ -90,7 +90,6 @@ namespace Barotrauma
protected override AIObjective ObjectiveConstructor(Pump pump)
=> new AIObjectiveOperateItem(pump, character, objectiveManager, Option, false)
{
IsLoop = false,
completionCondition = () => IsReady(pump)
};
@@ -10,9 +10,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "repair item".ToIdentifier();
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public override bool KeepDivingGearOn => Item?.CurrentHull == null;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowWhileHandcuffed => false;
public Item Item { get; private set; }
@@ -37,7 +37,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (!IsAllowed) { HandleNonAllowed(); }
if (!IsAllowed) { HandleDisallowed(); }
if (Item.IgnoreByAI(character))
{
Abandon = true;
@@ -19,9 +19,9 @@ namespace Barotrauma
public Item PrioritizedItem { get; private set; }
public override bool AllowMultipleInstances => true;
public override bool AllowInFriendlySubs => true;
protected override bool AllowInFriendlySubs => true;
public readonly static float RequiredSuccessFactor = 0.4f;
public const float RequiredSuccessFactor = 0.4f;
public override bool IsDuplicate<T>(T otherObjective) => otherObjective is AIObjectiveRepairItems repairObjective && objectiveManager.IsOrder(repairObjective) == objectiveManager.IsOrder(this);
@@ -62,7 +62,7 @@ namespace Barotrauma
}
}
protected override bool Filter(Item item)
protected override bool IsValidTarget(Item item)
{
if (!ViableForRepair(item, character, HumanAIController)) { return false; };
if (!Objectives.ContainsKey(item))
@@ -94,7 +94,7 @@ namespace Barotrauma
return item.Repairables.All(r => !r.IsBelowRepairThreshold);
}
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
var selectedItem = character.SelectedItem;
if (selectedItem != null && AIObjectiveRepairItem.IsRepairing(character, selectedItem) && selectedItem.ConditionPercentage < 100)
@@ -13,10 +13,9 @@ namespace Barotrauma
public override Identifier Identifier { get; set; } = "rescue".ToIdentifier();
public override bool ForceRun => true;
public override bool KeepDivingGearOn => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool AllowWhileHandcuffed => false;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
protected override bool AllowWhileHandcuffed => false;
const float TreatmentDelay = 0.5f;
@@ -484,7 +483,7 @@ namespace Barotrauma
protected override float GetPriority()
{
if (Target == null) { Abandon = true; }
if (!IsAllowed) { HandleNonAllowed(); }
if (!IsAllowed) { HandleDisallowed(); }
if (Abandon)
{
return Priority;
@@ -531,8 +530,8 @@ namespace Barotrauma
public override void OnDeselected()
{
character.SelectedCharacter = null;
base.OnDeselected();
character.DeselectCharacter();
}
}
}
@@ -9,9 +9,9 @@ namespace Barotrauma
{
public override Identifier Identifier { get; set; } = "rescue all".ToIdentifier();
public override bool ForceRun => true;
public override bool InverseTargetEvaluation => true;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
public override bool InverseTargetPriority => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
@@ -32,7 +32,7 @@ namespace Barotrauma
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
protected override bool IsValidTarget(Character target)
{
if (!IsValidTarget(target, character, out bool ignoredasMinorWounds))
{
@@ -61,7 +61,7 @@ namespace Barotrauma
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
protected override float GetTargetPriority()
{
if (Targets.None()) { return 100; }
if (!objectiveManager.IsOrder(this))
@@ -12,8 +12,8 @@ namespace Barotrauma
private AIObjectiveGoTo moveInsideObjective, moveOutsideObjective;
private bool usingEscapeBehavior, isSteeringThroughGap;
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
protected override bool AllowOutsideSubmarine => true;
protected override bool AllowInAnySub => true;
public AIObjectiveReturn(Character character, Character orderGiver, AIObjectiveManager objectiveManager, float priorityModifier = 1.0f) : base(character, objectiveManager, priorityModifier)
{
@@ -178,7 +178,6 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController)
{
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat(nameof(MaxHappiness), 100.0f);
UnhappyThreshold = element.GetAttributeFloat(nameof(UnhappyThreshold), MaxHappiness * 0.25f);
@@ -75,7 +75,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
if (!Active || character.IsArrested) { return; }
if (!Active || character.IsHandcuffed) { return; }
decisionTimer -= deltaTime;
if (decisionTimer <= 0.0f)
{
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
@@ -109,7 +109,7 @@ namespace Barotrauma
private static IEnumerable<MapEntity> GetThalamusEntities(Submarine wreck, Identifier tag) => MapEntity.MapEntityList.Where(e => e.Submarine == wreck && e.Prefab != null && IsThalamus(e.Prefab, tag));
private static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public static bool IsThalamus(MapEntityPrefab entityPrefab, Identifier tag) => entityPrefab.HasSubCategory("thalamus") || entityPrefab.Tags.Contains(tag);
public static WreckAI Create(Submarine wreck)
{
@@ -213,7 +213,7 @@ namespace Barotrauma
foreach (Item item in thalamusItems)
{
// Ensure that thalamus items are visible
item.HiddenInGame = false;
item.IsLayerHidden = false;
if (item.HasTag(Config.Spawner))
{
if (!spawnOrgans.Contains(item))
@@ -154,7 +154,10 @@ namespace Barotrauma
Collider.SetTransformIgnoreContacts(mainLimb.SimPosition, mainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
if (!Draggable || character.SelectedBy == null)
{
ResetPullJoints();
}
}
if (character.IsDead && deathAnimTimer < deathAnimDuration)
{
@@ -309,7 +309,10 @@ namespace Barotrauma
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
//(except when dragging, then we need the pull joints)
if (!character.CanBeDragged || character.SelectedBy == null) { ResetPullJoints(); }
if (!Draggable || character.SelectedBy == null)
{
ResetPullJoints();
}
}
return;
}
@@ -1104,8 +1104,8 @@ namespace Barotrauma
if (newHull?.Submarine == null && currentHull?.Submarine != null)
{
//don't teleport out yet if the character is going through a gap
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine())) != null)) { return; }
if (Gap.FindAdjacent(Gap.GapList.Where(g => g.Submarine == currentHull.Submarine), findPos, 150.0f, allowRoomToRoom: true) != null) { return; }
if (Limbs.Any(l => Gap.FindAdjacent(currentHull.ConnectedGaps, l.WorldPosition, ConvertUnits.ToDisplayUnits(l.body.GetSize().Combine()), allowRoomToRoom: true) != null)) { return; }
character.MemLocalState?.Clear();
Teleport(ConvertUnits.ToSimUnits(currentHull.Submarine.Position), currentHull.Submarine.Velocity);
}
@@ -1,17 +1,17 @@
using Barotrauma.Networking;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using Barotrauma.IO;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using FarseerPhysics.Dynamics;
using Barotrauma.Extensions;
using System.Collections.Immutable;
using Barotrauma.Abilities;
using System.Diagnostics;
#if SERVER
using System.Text;
#endif
@@ -354,6 +354,15 @@ namespace Barotrauma
public bool IsInstigator => CombatAction is { IsInstigator: true };
/// <summary>
/// Do the outpost security officers treat the character as a criminal?
/// Triggers when the character has either committed a major crime or resisted being arrested (or fled).
/// Only affects the reactions of friendly NPCs in the outposts.
/// The NPCs still don't react immediately to "criminals", but take this into account when the character next time does something wrong.
/// The consequences are that the guards will not hold fire and will not give more warnings before attacking.
/// </summary>
public bool IsCriminal;
/// <summary>
/// Set true only, if the character is turned hostile from an escort mission (See <see cref="EscortMission"/>).
/// </summary>
@@ -798,15 +807,27 @@ namespace Barotrauma
}
private float obstructVisionAmount;
public float ObstructVisionAmount
{
get { return obstructVisionAmount; }
set
{
obstructVisionAmount = MathHelper.Clamp(value, 0.0f, 1.0f);
}
}
/// <summary>
/// Provided for backwards compatibility: use <see cref="ObstructVisionAmount"/> instead.
/// </summary>
public bool ObstructVision
{
get
{
return obstructVisionAmount > 0.5f;
return obstructVisionAmount > 0.01f;
}
set
{
obstructVisionAmount = value ? 1.0f : 0.0f;
obstructVisionAmount = value ? 0.5f : 0.0f;
}
}
@@ -860,7 +881,7 @@ namespace Barotrauma
get { return CharacterHealth.IsUnconscious; }
}
public bool IsArrested
public bool IsHandcuffed
{
get { return IsHuman && HasEquippedItem(Tags.HandLockerItem); }
}
@@ -958,6 +979,18 @@ namespace Barotrauma
}
}
private float textChatVolume;
/// <summary>
/// How "loud" the player is when they use text chat.
/// When the user speaks in text chat this gets set to 1 and then slowly decreases back to 0 over 5 seconds.
/// </summary>
public float TextChatVolume
{
get => textChatVolume;
set => textChatVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
}
public float PressureTimer
{
get;
@@ -1108,37 +1141,8 @@ namespace Barotrauma
return !Removed;
}
}
private bool canBeDragged = true;
public bool CanBeDragged
{
get
{
if (!canBeDragged) { return false; }
if (Removed || !AnimController.Draggable) { return false; }
return IsKnockedDown || LockHands || IsPet || CanInventoryBeAccessed;
}
set { canBeDragged = value; }
}
//can other characters access the inventory of this character
private bool canInventoryBeAccessed = true;
public bool CanInventoryBeAccessed
{
get
{
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive)
{
return IsDead;
}
else
{
return IsKnockedDown || LockHands || IsBot && IsOnPlayerTeam;
}
}
set { canInventoryBeAccessed = value; }
}
public bool IsDraggable => !Removed || AnimController.Draggable;
public bool CanAim
{
@@ -1328,6 +1332,10 @@ namespace Barotrauma
: base(null, id)
{
wallet = new Wallet(Option<Character>.Some(this));
if (GameMain.GameSession?.Campaign?.Bank is { } bank)
{
wallet.SetRewardDistribution(bank.RewardDistribution);
}
this.Seed = seed;
this.Prefab = prefab;
@@ -1363,6 +1371,8 @@ namespace Barotrauma
if (Info != null)
{
teamID = Info.TeamID;
//no longer a new hire after spawning (only displayed as a new hire at the end of the outpost round, when the character hasn't spawned yet)
Info.IsNewHire = false;
}
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
@@ -1880,7 +1890,7 @@ namespace Barotrauma
public bool CanRunWhileDragging()
{
if (selectedCharacter == null || !selectedCharacter.CanBeDragged) { return true; }
if (selectedCharacter is not { IsDraggable: true }) { return true; }
//if the dragged character is conscious, don't allow running (the dragged character won't keep up, and the dragging gets interrupted)
if (!selectedCharacter.IsIncapacitated && selectedCharacter.Stun <= 0.0f) { return false; }
return HasAbilityFlag(AbilityFlags.MoveNormallyWhileDragging);
@@ -2581,18 +2591,15 @@ namespace Barotrauma
return null;
}
public bool CanAccessInventory(Inventory inventory)
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
{
if (!CanInteract || inventory.Locked) { return false; }
//the inventory belongs to some other character
if (inventory.Owner is Character character && inventory.Owner != this)
if (inventory.Owner is Character inventoryOwner)
{
var owner = character;
//can only be accessed if the character is incapacitated and has been selected
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
return inventoryOwner.IsInventoryAccessibleTo(this, accessLevel) && (inventoryOwner == this || CanInteractWith(inventoryOwner));
}
if (inventory.Owner is Item item)
{
if (!CanInteractWith(item))
@@ -2613,7 +2620,47 @@ namespace Barotrauma
}
return true;
}
public bool CanBeHealedBy(Character character, bool checkFriendlyTeam = true) =>
!character.IsClimbing && !DisableHealthWindow &&
UseHealthWindow && character.CanInteract &&
(!checkFriendlyTeam || IsFriendly(character) || CanBeDraggedBy(character)) &&
character.CanInteractWith(this, 160f, false);
public bool CanBeDraggedBy(Character character)
{
if (!IsDraggable) { return false; }
return IsKnockedDown || LockHands || IsPet || (IsBot && character.TeamID == TeamID);
}
/// <summary>
/// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc).
/// </summary>
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
{
if (Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive && !IsDead)
{
if (character == this)
{
return Inventory.AccessibleByOwner;
}
return false;
}
if (character == this) { return true; }
if (IsKnockedDown || LockHands) { return true; }
return accessLevel switch
{
CharacterInventory.AccessLevel.Restricted => false,
CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
_ => throw new NotImplementedException()
};
bool IsOnSameTeam() => character.TeamID == teamID;
bool IsFriendlyPet() => IsPet && character.IsFriendly(this);
}
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private float _selectedItemPriority;
@@ -2701,7 +2748,7 @@ namespace Barotrauma
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
{
if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!c.CharacterHealth.UseHealthWindow && !c.IsDraggable && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
if (!skipDistanceCheck)
{
@@ -2725,7 +2772,7 @@ namespace Barotrauma
{
distanceToItem = -1.0f;
bool hidden = item.HiddenInGame;
bool hidden = item.IsHidden;
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
#endif
@@ -2834,23 +2881,7 @@ namespace Barotrauma
}
if (distanceToItem > interactDistance && item.InteractDistance > 0.0f) { return false; }
Vector2 itemPosition = item.SimPosition;
if (Submarine == null && item.Submarine != null)
{
//character is outside, item inside
itemPosition += item.Submarine.SimPosition;
}
else if (Submarine != null && item.Submarine == null)
{
//character is inside, item outside
itemPosition -= Submarine.SimPosition;
}
else if (Submarine != item.Submarine)
{
//character and the item are inside different subs
itemPosition += item.Submarine.SimPosition;
itemPosition -= Submarine.SimPosition;
}
Vector2 itemPosition = GetPosition(Submarine, item, item.SimPosition);
if (SelectedSecondaryItem != null && !item.IsSecondaryItem)
{
@@ -2871,21 +2902,79 @@ namespace Barotrauma
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
{
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
if (body != null)
bool itemCenterVisible = CheckBody(body, item);
if (!itemCenterVisible && item.Prefab.RequireCursorInsideTrigger)
{
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
foreach (Rectangle trigger in item.Prefab.Triggers)
{
Rectangle transformTrigger = item.TransformTrigger(trigger, world: false);
RectangleF simRect = new RectangleF(
x: ConvertUnits.ToSimUnits(transformTrigger.X),
y: ConvertUnits.ToSimUnits(transformTrigger.Y - transformTrigger.Height),
width: ConvertUnits.ToSimUnits(transformTrigger.Width),
height: ConvertUnits.ToSimUnits(transformTrigger.Height));
simRect.Location = GetPosition(Submarine, item, simRect.Location);
Vector2 closest = ToolBox.GetClosestPointOnRectangle(simRect, SimPosition);
var triggerBody = Submarine.CheckVisibility(SimPosition, closest, ignoreLevel: true);
if (CheckBody(triggerBody, item)) { return true; }
}
}
else
{
return itemCenterVisible;
}
}
return true;
static bool CheckBody(Body body, Item item)
{
if (body is null) { return true; }
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
}
return true;
}
static Vector2 GetPosition(Submarine submarine, Item item, Vector2 simPosition)
{
Vector2 position = simPosition;
Vector2 itemSubPos = item.Submarine?.SimPosition ?? Vector2.Zero;
Vector2 subPos = submarine?.SimPosition ?? Vector2.Zero;
if (submarine == null && item.Submarine != null)
{
//character is outside, item inside
position += itemSubPos;
}
else if (submarine != null && item.Submarine == null)
{
//character is inside, item outside
position -= subPos;
}
else if (submarine != item.Submarine && submarine != null)
{
//character and the item are inside different subs
position += itemSubPos;
position -= subPos;
}
return position;
}
}
/// <summary>
@@ -3058,15 +3147,15 @@ namespace Barotrauma
{
DeselectCharacter();
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && (CanInteract || FocusedCharacter.IsDead && CanEat))
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDraggedBy(this) && (CanInteract || FocusedCharacter.IsDead && CanEat))
{
SelectCharacter(FocusedCharacter);
}
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
else if (FocusedCharacter is { IsIncapacitated: false } && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
{
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
}
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CanBeHealedBy(this))
{
if (FocusedCharacter == SelectedCharacter)
{
@@ -3078,12 +3167,14 @@ namespace Barotrauma
}
#endif
}
else if (!IsClimbing)
else
{
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
{
HealingCooldown.PutOnCooldown();
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
}
#elif SERVER
if (GameMain.Server?.ConnectedClients is { } clients)
@@ -3096,13 +3187,6 @@ namespace Barotrauma
break;
}
}
#endif
SelectCharacter(FocusedCharacter);
#if CLIENT
if (Controlled == this)
{
CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
}
#endif
}
}
@@ -3251,6 +3335,11 @@ namespace Barotrauma
{
UpdateProjSpecific(deltaTime, cam);
if (TextChatVolume > 0)
{
TextChatVolume -= 0.2f * deltaTime;
}
if (InvisibleTimer > 0.0f)
{
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
@@ -3480,16 +3569,33 @@ namespace Barotrauma
DoInteractionUpdate(deltaTime, mouseSimPos);
}
if (SelectedItem != null && !CanInteractWith(SelectedItem))
if (MustDeselect(SelectedItem))
{
SelectedItem = null;
}
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
if (MustDeselect(SelectedSecondaryItem))
{
ReleaseSecondaryItem();
}
if (!IsDead) { LockHands = false; }
bool MustDeselect(Item item)
{
if (item == null) { return false; }
if (!CanInteractWith(item)) { return true; }
bool hasSelectableComponent = false;
foreach (var component in item.Components)
{
//the "selectability" of an item can change e.g. if the player unequips another item that's required to access it
if (component.CanBeSelected && component.HasRequiredItems(this, addMessage: false))
{
hasSelectableComponent = true;
break;
}
}
return !hasSelectableComponent;
}
}
partial void UpdateControlled(float deltaTime, Camera cam);
@@ -3805,6 +3911,9 @@ namespace Barotrauma
private void UpdateSoundRange(float deltaTime)
{
const float textChatVolumeMultiplier = 0.5f;
const float voiceChatVolumeMultiplier = 1.5f;
if (aiTarget == null) { return; }
if (IsDead)
{
@@ -3814,8 +3923,37 @@ namespace Barotrauma
{
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float speechImpedimentMultiplier = 1.0f - SpeechImpediment / 100.0f;
if (TextChatVolume > 0)
{
targetRange = Math.Max(targetRange, TextChatVolume * textChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
}
if (IsPlayer)
{
float voipAmplitude = 0.0f;
#if SERVER
foreach (var c in GameMain.Server.ConnectedClients)
{
if (c.Character != this) { continue; }
voipAmplitude = c.VoipServerDecoder.Amplitude;
break;
}
#elif CLIENT && DEBUG
if (Controlled == this && GameMain.Client != null)
{
voipAmplitude = GameMain.Client.DebugServerVoipAmplitude;
}
#endif
targetRange = Math.Max(targetRange, voipAmplitude * voiceChatVolumeMultiplier * ChatMessage.SpeakRange * speechImpedimentMultiplier);
}
targetRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
targetRange = Math.Min(targetRange, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -4712,7 +4850,31 @@ namespace Barotrauma
}
#endif
isDead = true;
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
AnimController.Frozen = false;
Character killer = causeOfDeathAffliction?.Source;
if (IsBot)
{
foreach (var item in Inventory.AllItems)
{
if (item.Equipper is { IsPlayer: true } &&
item.GetComponents<ItemContainer>().Any(ic => ic.BlameEquipperForDeath()))
{
killer = item.Equipper;
if (AIController is HumanAIController humanAi)
{
humanAi.OnAttacked(killer, new AttackResult(damage: MaxVitality));
}
break;
}
}
}
CauseOfDeath = new CauseOfDeath(
causeOfDeath, causeOfDeathAffliction?.Prefab,
killer, LastDamageSource);
// Save these resistances in the CharacterInfo object so that if they
// are needed for respawning, they will be available (because there
@@ -4723,13 +4885,25 @@ namespace Barotrauma
info.LastResistanceMultiplierSkillLossRespawn = GetAbilityResistance(Tags.SkillLossRespawnResistance);
}
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
isDead = true;
AnimController.Frozen = false;
#if CLIENT
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
// NOTE: The opposite is done in Revive
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath } &&
GameMain.Client.Character == this &&
GameMain.Client.CharacterInfo is CharacterInfo characterInfo)
{
characterInfo.PermanentlyDead = true;
}
#endif
CauseOfDeath = new CauseOfDeath(
causeOfDeath, causeOfDeathAffliction?.Prefab,
causeOfDeathAffliction?.Source, LastDamageSource);
#if SERVER
if (Info is not null)
{
Info.LastRewardDistribution = Option.Some(Wallet.RewardDistribution);
}
#endif
if (GameAnalyticsManager.SendUserStatistics && Prefab?.ContentPackage == ContentPackageManager.VanillaCorePackage)
{
@@ -4843,6 +5017,15 @@ namespace Barotrauma
if (info != null)
{
info.CauseOfDeath = null;
// Keep permadeath status in sync (to show it correctly in the UI, the server takes care of the actual logic)
// NOTE: The opposite is done in Kill
// FYI: In case you're wondering, it's alright to revive a "permanently" dead character here, because if
// this gets called, the character wasn't actually dead anyway (eg. returning to lobby without saving)
if (GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath })
{
info.PermanentlyDead = false;
}
}
foreach (LimbJoint joint in AnimController.LimbJoints)
@@ -5470,7 +5653,14 @@ namespace Barotrauma
{
statValue += wearableValue;
}
foreach (var heldItem in HeldItems)
{
if (heldItem.GetComponent<Holdable>() is Holdable holdable &&
holdable.HoldableStatValues.TryGetValue(statType, out float holdableValue))
{
statValue += holdableValue;
}
}
return statValue;
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
@@ -44,8 +44,13 @@ namespace Barotrauma
public readonly Identifier MenuCategoryVar;
public readonly Identifier Pronouns;
public CharacterInfoPrefab(ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
public CharacterInfoPrefab(CharacterPrefab characterPrefab, ContentXElement headsElement, XElement varsElement, XElement menuCategoryElement, XElement pronounsElement)
{
if (headsElement == null)
{
throw new Exception($"No heads configured for the character \"{characterPrefab.Identifier}\". Characters with CharacterInfo must have head sprites. Please add a <Heads> element to the character's config.");
}
Heads = headsElement.Elements().Select(e => new CharacterInfo.HeadPreset(this, e)).ToImmutableArray();
if (varsElement != null)
{
@@ -82,6 +87,10 @@ namespace Barotrauma
}
}
/// <summary>
/// Stores information about the Character that is needed between rounds in the
/// menu etc., whereas Character itself is the object actually spawned in-game.
/// </summary>
partial class CharacterInfo
{
public class HeadInfo
@@ -289,7 +298,9 @@ namespace Barotrauma
public XElement HealthData;
public XElement OrderData;
private static ushort idCounter;
public bool PermanentlyDead;
private static ushort idCounter = 1;
private const string disguiseName = "???";
public bool HasNickname => Name != OriginalName;
@@ -492,6 +503,9 @@ namespace Barotrauma
public bool StartItemsGiven;
/// <summary>
/// Newly hired bot that hasn't spawned yet
/// </summary>
public bool IsNewHire;
public CauseOfDeath CauseOfDeath;
@@ -642,6 +656,15 @@ namespace Barotrauma
=> element.GetAttributeBool("specifiertags",
element.GetAttributeBool("genders",
element.GetAttributeBool("races", false)));
/// <summary>
/// Keeps track of the last reward distribution that was set on the character's wallet.
/// Is used to keep salary when the character respawns since CharacterInfo is preserved between deaths.
/// </summary>
/// <remarks>
/// None means the salary has not been set yet, which is not always 0 if default salary is set.
/// </remarks>
public Option<int> LastRewardDistribution = Option.None;
// Used for creating the data
public CharacterInfo(
@@ -662,6 +685,7 @@ namespace Barotrauma
}
ID = idCounter;
idCounter++;
if (idCounter == 0) { idCounter++; }
SpeciesName = speciesName;
SpriteTags = new List<Identifier>();
CharacterConfigElement = CharacterPrefab.FindBySpeciesName(SpeciesName)?.ConfigElement;
@@ -699,6 +723,12 @@ namespace Barotrauma
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
}
private void SetPersonalityTrait()
@@ -771,6 +801,7 @@ namespace Barotrauma
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
SpeciesName = infoElement.GetAttributeIdentifier("speciesname", "");
PermanentlyDead = infoElement.GetAttributeBool("permanentlydead", false);
ContentXElement element;
if (!SpeciesName.IsEmpty)
{
@@ -951,7 +982,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the (current) Name, appearence, and job.
/// Returns a presumably (not guaranteed) unique and persistent hash using the (current) Name, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifier()
@@ -960,7 +991,7 @@ namespace Barotrauma
}
/// <summary>
/// Returns a presumably (not guaranteed) unique hash using the OriginalName, appearence, and job.
/// Returns a presumably (not guaranteed) unique hash and persistent using the OriginalName, appearence, and job.
/// So unless there's another character with the exactly same name, job, and appearance, the hash should be unique.
/// </summary>
public int GetIdentifierUsingOriginalName()
@@ -1466,7 +1497,10 @@ namespace Barotrauma
new XAttribute("haircolor", XMLExtensions.ColorToString(Head.HairColor)),
new XAttribute("facialhaircolor", XMLExtensions.ColorToString(Head.FacialHairColor)),
new XAttribute("startitemsgiven", StartItemsGiven),
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty));
new XAttribute("personality", PersonalityTrait?.Identifier ?? Identifier.Empty),
new XAttribute("lastrewarddistribution", LastRewardDistribution.Match(some: value => value, none: () => -1).ToString()),
new XAttribute("permanentlydead", PermanentlyDead)
);
if (HumanPrefabIds != default)
{
@@ -32,6 +32,8 @@ namespace Barotrauma
return speciesName;
}
public bool HasCharacterInfo { get; private set; }
public void InheritFrom(CharacterPrefab parent)
{
ConfigElement = CharacterParams.CreateVariantXml(originalElement, parent.ConfigElement).FromPackage(ConfigElement.ContentPackage);
@@ -45,9 +47,10 @@ namespace Barotrauma
var menuCategoryElement = ConfigElement.GetChildElement("MenuCategory");
var pronounsElement = ConfigElement.GetChildElement("Pronouns");
if (headsElement != null)
HasCharacterInfo = headsElement != null || ConfigElement.GetAttributeBool(nameof(HasCharacterInfo), false);
if (HasCharacterInfo)
{
CharacterInfoPrefab = new CharacterInfoPrefab(headsElement, varsElement, menuCategoryElement, pronounsElement);
CharacterInfoPrefab = new CharacterInfoPrefab(this, headsElement, varsElement, menuCategoryElement, pronounsElement);
}
}
@@ -100,6 +100,12 @@ namespace Barotrauma
[Serialize(AIObjectiveIdle.BehaviorType.Passive, IsPropertySaveable.No)]
public AIObjectiveIdle.BehaviorType Behavior { get; protected set; }
[Serialize(1.0f, IsPropertySaveable.No, description:
"Affects how far the character can hear sounds created by AI targets with the tag ProvocativeToHumanAI. "+
"Used as a multiplier on the sound range of the target, e.g. a value of 0.5 would mean a target with a sound range of 1000 would need to be within 500 units for this character to hear it. "+
"Only affects the \"fight intruders\" objective, which makes the character go and inspect noises.")]
public float Hearing { get; set; } = 1.0f;
[Serialize(float.PositiveInfinity, IsPropertySaveable.No)]
public float ReportRange { get; protected set; }
@@ -174,6 +180,7 @@ namespace Barotrauma
idleObjective.PreferredOutpostModuleTypes.Add(moduleType);
}
}
humanAI.ReportRange = Hearing;
humanAI.ReportRange = ReportRange;
humanAI.FindWeaponsRange = FindWeaponsRange;
humanAI.AimSpeed = AimSpeed;
@@ -10,15 +10,26 @@ namespace Barotrauma
private float level;
/// <summary>
/// The highest skill level during the round (before any death penalties were applied)
/// </summary>
public float HighestLevelDuringRound { get; private set; }
public float Level
{
get { return level; }
set { level = value; }
set
{
HighestLevelDuringRound = MathHelper.Max(value, HighestLevelDuringRound);
level = value;
}
}
public LocalizedString DisplayName { get; private set; }
public void IncreaseSkill(float value, bool increasePastMax)
{
level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
Level = MathHelper.Clamp(level + value, 0.0f, increasePastMax ? SkillSettings.Current.MaximumSkillWithTalents : MaximumSkill);
}
private readonly Identifier iconJobId;
@@ -32,16 +43,18 @@ namespace Barotrauma
public Skill(SkillPrefab prefab, Rand.RandSync randSync)
{
Identifier = prefab.Identifier;
level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
Level = Rand.Range(prefab.LevelRange.Start, prefab.LevelRange.End, randSync);
iconJobId = GetIconJobId();
PriceMultiplier = prefab.PriceMultiplier;
DisplayName = TextManager.Get("SkillName." + Identifier);
}
public Skill(Identifier identifier, float level)
{
Identifier = identifier;
this.level = level;
Level = level;
iconJobId = GetIconJobId();
DisplayName = TextManager.Get("SkillName." + Identifier);
}
private Identifier GetIconJobId()
@@ -22,6 +22,7 @@ namespace Barotrauma
abstract class GroundedMovementParams : AnimationParams
{
[Header("Legs")]
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSize
{
@@ -29,12 +30,14 @@ namespace Barotrauma
set;
}
[Header("Standing")]
[Serialize(0f, IsPropertySaveable.Yes, 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, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
public float TorsoPosition { get; set; }
[Header("Step lift")]
[Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
public float StepLiftHeadMultiplier { get; set; }
@@ -50,6 +53,7 @@ namespace Barotrauma
[Serialize(2f, IsPropertySaveable.Yes, 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)]
public float StepLiftFrequency { get; set; }
[Header("Movement")]
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
public float BackwardsMovementMultiplier { get; set; }
}
@@ -69,11 +73,15 @@ namespace Barotrauma
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
[Header("General")]
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
public virtual AnimationType AnimationType { get; protected set; }
/// <summary>
/// The cached animations of all the characters that have been loaded.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
[Header("Movement")]
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed { get; set; }
@@ -84,6 +92,7 @@ namespace Barotrauma
/// <summary>
/// In degrees.
/// </summary>
[Header("Standing")]
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
public float HeadAngle
{
@@ -122,12 +131,11 @@ namespace Barotrauma
[Serialize(50.0f, IsPropertySaveable.Yes, 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; }
[Header("Legs")]
[Serialize(25.0f, IsPropertySaveable.Yes, 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(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
public virtual AnimationType AnimationType { get; protected set; }
[Header("Arms")]
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float ArmIKStrength { get; set; }
@@ -69,21 +69,13 @@ namespace Barotrauma
abstract class HumanSwimParams : SwimParams, IHumanAnimation
{
[Header("Legs")]
[Serialize(0.5f, IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public float LegMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float LegCycleLength { get; set; }
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
/// <summary>
/// In degrees.
/// </summary>
@@ -96,20 +88,33 @@ namespace Barotrauma
FootAngleInRadians = MathHelper.ToRadians(value);
}
}
public float FootAngleInRadians { get; private set; }
[Header("Arms")]
[Serialize("0.5, 0.1", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
[Serialize(5.0f, IsPropertySaveable.Yes), Editable]
public float HandCycleSpeed { get; set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable(DecimalCount = 2)]
public Vector2 HandMoveOffset { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the arms."), Editable(MinValueFloat = 0, MaxValueFloat = 20, DecimalCount = 2)]
public float ArmMoveStrength { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Header("Other")]
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
{
[Header("Standing")]
[Serialize(0.3f, IsPropertySaveable.Yes, description: "How much force is used to force the character upright."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
public float GetUpForce { get; set; }
@@ -119,6 +124,7 @@ namespace Barotrauma
[Serialize(0.25f, IsPropertySaveable.Yes, description: "How much the character's torso leans forwards when moving."), Editable(DecimalCount = 2)]
public float TorsoLeanAmount { get; set; }
[Header("Legs")]
[Serialize(15.0f, IsPropertySaveable.Yes, description: "How much force is used to move the feet to the correct position."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float FootMoveStrength { get; set; }
@@ -152,6 +158,7 @@ namespace Barotrauma
[Serialize(10.0f, IsPropertySaveable.Yes, description: "How much torque is used to bend the characters legs when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
public float LegBendTorque { get; set; }
[Header("Arms")]
[Serialize("0.4, 0.15", IsPropertySaveable.Yes, description: "How much the hands move along each axis."), Editable(DecimalCount = 2)]
public Vector2 HandMoveAmount { get; set; }
@@ -167,6 +174,7 @@ namespace Barotrauma
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to move the hands."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
public float HandMoveStrength { get; set; }
[Header("Other")]
[Serialize(true, IsPropertySaveable.Yes, description: "Is the head angle fixed or does the angle follow the mouse position?"), Editable]
public bool FixedHeadAngle { get; set; }
}
@@ -87,9 +87,20 @@ namespace Barotrauma
set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
/// <summary>
/// Can be used for scaling the textures without having to readjust the entire ragdoll.
/// Note that we'll still have to readjust the source rects and the colliders sizes, unless we also adjust <see cref="SourceRectScale"/>.
/// E.g. for upscaling the textures 2x, set <see cref="TextureScale"/> to 0.5 and <see cref="SourceRectScale"/> to 2.
/// </summary>
[Serialize(1f, IsPropertySaveable.No)]
public float TextureScale { get; set; }
/// <summary>
/// Multiplies both the position and the size of the source rects.
/// Used for scaling the textures when we cannot/don't want to touch the source rect definitions (e.g. on variants).
/// </summary>
[Serialize(1f, IsPropertySaveable.No)]
public float SourceRectScale { get; set; }
[Serialize(45f, IsPropertySaveable.Yes, description: "How high from the ground the main collider levitates when the character is standing? Doesn't affect swimming."), Editable(0f, 1000f)]
public float ColliderHeightFromFloor { get; set; }
@@ -491,6 +502,18 @@ namespace Barotrauma
float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f);
JointScale *= scaleMultiplier;
LimbScale *= scaleMultiplier;
float textureScale = ragdollElement.GetAttributeFloat(nameof(TextureScale), 0f);
if (textureScale > 0)
{
// Override, if defined.
TextureScale = textureScale;
}
float sourceRectScale = ragdollElement.GetAttributeFloat(nameof(SourceRectScale), 0f);
if (sourceRectScale > 0)
{
// Override, if defined.
SourceRectScale = sourceRectScale;
}
}
}
isVariantScaleApplied = true;
@@ -71,9 +71,9 @@ namespace Barotrauma
private Vector2 position;
public List<CircuitBoxConnection> ExternallyConnectedFrom = new();
public readonly List<CircuitBoxConnection> ExternallyConnectedFrom = new();
public static float Size = CircuitBoxSizes.ConnectorSize;
public static readonly float Size = CircuitBoxSizes.ConnectorSize;
public Vector2 Position
{
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -8,7 +8,7 @@ using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal sealed class CircuitBoxInputOutputNode : CircuitBoxNode
internal sealed partial class CircuitBoxInputOutputNode : CircuitBoxNode
{
public enum Type
{
@@ -17,22 +17,104 @@ namespace Barotrauma
Output
}
public Type NodeType;
public readonly Type NodeType;
private const int MaxConnectionLabelLength = 32;
private const string ConnectionLabelOverrideElementName = "ConnectionLabelOverride";
public Dictionary<string, string> ConnectionLabelOverrides = new();
public CircuitBoxInputOutputNode(IReadOnlyList<CircuitBoxConnection> conns, Vector2 initialPosition, Type type, CircuitBox circuitBox): base(circuitBox)
{
Size = CalculateSize(conns);
InitSize(conns);
Connectors = conns.ToImmutableArray();
Position = initialPosition;
NodeType = type;
UpdatePositions();
}
public XElement Save() => new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position)));
public void ReplaceAllConnectionLabelOverrides(Dictionary<string, string> replace)
{
foreach (var (_, value) in replace)
{
if (value.Length > MaxConnectionLabelLength)
{
DebugConsole.ThrowError($"Label override value \"{value}\" is too long (max {MaxConnectionLabelLength} characters)");
return;
}
}
foreach (var (name, value) in replace)
{
if (string.IsNullOrWhiteSpace(value))
{
ConnectionLabelOverrides.Remove(name);
}
else
{
ConnectionLabelOverrides[name] = value;
}
}
InitSize(Connectors);
UpdatePositions();
}
private void InitSize(IReadOnlyList<CircuitBoxConnection> conns)
{
#if CLIENT
foreach (CircuitBoxConnection conn in conns)
{
if (ConnectionLabelOverrides.TryGetValue(conn.Name, out string? value))
{
LocalizedString newLabel =
string.IsNullOrWhiteSpace(value)
? conn.Connection.DisplayName
: TextManager.Get(value).Fallback(value);
conn.SetLabel(newLabel, this);
}
else
{
conn.SetLabel(conn.Connection.DisplayName, this);
}
}
#endif
Size = CalculateSize(conns);
}
public XElement Save()
{
XElement element = new XElement($"{NodeType}Node", new XAttribute("pos", XMLExtensions.Vector2ToString(Position)));
foreach (var (name, value) in ConnectionLabelOverrides)
{
element.Add(new XElement(ConnectionLabelOverrideElementName,
new XAttribute("name", name),
new XAttribute("value", value)));
}
return element;
}
public void Load(ContentXElement element)
{
Position = element.GetAttributeVector2("pos", Vector2.Zero);
Dictionary<string, string> loadedOverrides = new();
foreach (var subElement in element.Elements())
{
if (subElement.Name != ConnectionLabelOverrideElementName) { continue; }
string name = subElement.GetAttributeString("name", string.Empty);
string value = subElement.GetAttributeString("value", string.Empty);
loadedOverrides[name] = value;
}
ConnectionLabelOverrides = loadedOverrides;
InitSize(Connectors);
UpdatePositions();
}
}
}
@@ -37,6 +37,7 @@ namespace Barotrauma
AddLabel,
RemoveLabel,
ResizeLabel,
RenameConnections,
ServerInitialize
}
@@ -156,6 +157,10 @@ namespace Barotrauma
[NetworkSerialize]
internal readonly record struct CircuitBoxRenameLabelEvent(ushort LabelId, Color Color, NetLimitedString NewHeader, NetLimitedString NewBody) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxRenameConnectionLabelsEvent(CircuitBoxInputOutputNode.Type Type, NetDictionary<string, string> Override) : INetSerializableStruct;
[NetworkSerialize]
internal readonly record struct CircuitBoxErrorEvent(string Message) : INetSerializableStruct;
@@ -164,6 +169,7 @@ namespace Barotrauma
ImmutableArray<CircuitBoxServerCreateComponentEvent> Components,
ImmutableArray<CircuitBoxServerCreateWireEvent> Wires,
ImmutableArray<CircuitBoxServerAddLabelEvent> Labels,
ImmutableArray<CircuitBoxRenameConnectionLabelsEvent> LabelOverrides,
Vector2 InputPos,
Vector2 OutputPos) : INetSerializableStruct;
@@ -198,6 +204,8 @@ namespace Barotrauma
=> CircuitBoxOpcode.RemoveLabel,
CircuitBoxResizeLabelEvent
=> CircuitBoxOpcode.ResizeLabel,
CircuitBoxRenameConnectionLabelsEvent
=> CircuitBoxOpcode.RenameConnections,
_ => throw new ArgumentOutOfRangeException(nameof(Data))
};
}
@@ -14,6 +14,7 @@ using System.Threading.Tasks;
using Barotrauma.MapCreatures.Behavior;
using System.Text;
namespace Barotrauma
{
readonly struct ColoredText
@@ -249,7 +250,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() =>
{
string[] creatureAndJobNames =
@@ -261,7 +262,10 @@ namespace Barotrauma
return new string[][]
{
creatureAndJobNames.ToArray(),
new string[] { "near", "inside", "outside", "cursor" }
new string[] { "near", "inside", "outside", "cursor" },
new string[] { "0", "1", "2", "3" },
new string[] { "true", "false" },
};
}, isCheat: true));
@@ -567,11 +571,30 @@ namespace Barotrauma
commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null));
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
() =>
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name] [location]: Teleport the specified character to a location , or the position of the cursor if location is omitted. If the name parameter is omitted, the controlled character will be teleported.",
onExecute: null,
getValidArgs:() =>
{
return new string[][] { ListCharacterNames() };
var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty<string>();
var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty<string>();
return new string[][]
{
characterList.Concat(ListCharacterNames()).ToArray(),
subList.Concat(ListAvailableLocations()).ToArray()
};
}, isCheat: true));
commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.",
onExecute:(string[] args) =>
{
var availableLocations = ListAvailableLocations();
NewMessage("***************", Color.Cyan);
foreach (var location in availableLocations)
{
NewMessage(location, Color.Cyan);
}
NewMessage("***************", Color.Cyan);
}));
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) =>
@@ -782,6 +805,17 @@ namespace Barotrauma
{
if (c.Character != revivedCharacter) { continue; }
// If killed in ironman mode, the character has been wiped from the save mid-round, so its
// original data needs to be restored to the save file (without making a backup of the dead character)
if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
{
characterToRestore.CharacterInfo.PermanentlyDead = false;
mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
}
}
//clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter);
break;
@@ -1176,7 +1210,8 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.", (string[] args) =>
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
onExecute:(string[] args) =>
{
if (Submarine.MainSub == null) { return; }
@@ -1233,7 +1268,7 @@ namespace Barotrauma
}
}
},
() =>
getValidArgs:() =>
{
return new string[][]
{
@@ -1924,6 +1959,7 @@ namespace Barotrauma
}));
#if DEBUG
commands.Add(new Command("debugvoip", "Toggle the server writing VOIP into audio files.", null, isCheat: false));
commands.Add(new Command("simulatedlongloadingtime", "simulatedlongloadingtime [minimum loading time]: forces loading a round to take at least the specified amount of seconds.", (string[] args) =>
{
@@ -2047,6 +2083,11 @@ namespace Barotrauma
string[] validArgs = allArgs[autoCompletedArgIndex].Where(arg =>
currentAutoCompletedCommand.Trim().Length <= arg.Length &&
arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray();
// add all completions that contain the current argument, to the end of the list
validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg =>
arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) &&
!validArgs.Contains(arg))).ToArray();
if (validArgs.Length == 0) { return command; }
@@ -2095,95 +2136,219 @@ namespace Barotrauma
currentAutoCompletedIndex = 0;
}
public static void ExecuteCommand(string command)
/// <summary>
/// Executes the specific command or commands
/// </summary>
/// <param name="inputtedCommands">Command, or multiple commands separated by newlines.</param>
public static void ExecuteCommand(string inputtedCommands)
{
if (activeQuestionCallback != null)
if (string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands == "\\" || inputtedCommands == "\n") { return; }
string[] commandsToExecute = inputtedCommands.Split("\n");
foreach (string command in commandsToExecute)
{
#if CLIENT
activeQuestionText = null;
#endif
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
temp(command);
return;
}
if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") { return; }
string[] splitCommand = ToolBox.SplitCommand(command);
if (splitCommand.Length == 0)
{
ThrowError("Failed to execute command \"" + command + "\"!");
GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.ExecuteCommand:LengthZero",
GameAnalyticsManager.ErrorSeverity.Error,
"Failed to execute command \"" + command + "\"!");
return;
}
Identifier firstCommand = splitCommand[0].ToIdentifier();
if (firstCommand != "admin")
{
NewCommand(command);
}
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
if (matchingCommand == null)
if (activeQuestionCallback != null)
{
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
#if CLIENT
activeQuestionText = null;
#endif
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
temp(command);
return;
}
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
if (string.IsNullOrWhiteSpace(command) || command == "\\") { return; }
string[] splitCommand = ToolBox.SplitCommand(command);
if (splitCommand.Length == 0)
{
if (matchingCommand.RelayToServer)
ThrowError("Failed to execute command \"" + command + "\"!");
GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.ExecuteCommand:LengthZero",
GameAnalyticsManager.ErrorSeverity.Error,
"Failed to execute command \"" + command + "\"!");
return;
}
Identifier firstCommand = splitCommand[0].ToIdentifier();
if (firstCommand != "admin")
{
NewCommand(command);
}
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
if (matchingCommand == null)
{
//if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
return;
}
else
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
{
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
if (matchingCommand.RelayToServer)
{
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
}
else
{
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
}
return;
}
return;
}
if (!IsCommandPermitted(firstCommand, GameMain.Client))
{
if (!IsCommandPermitted(firstCommand, GameMain.Client))
{
#if DEBUG
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
#else
ThrowError($"You're not permitted to use the command \"{firstCommand}\"!");
return;
#endif
}
}
}
#endif
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.Names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
}
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.Names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
}
if (!commandFound)
{
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
if (!commandFound)
{
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
}
}
}
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray();
private static string[] ListAvailableLocations()
{
List<string> locationNames = new();
foreach(var submarine in Submarine.Loaded)
{
locationNames.Add(submarine.Info.Name);
}
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null)
if (Level.Loaded != null)
{
foreach (var cave in Level.Loaded.Caves)
{
string caveName = cave.CaveGenerationParams.Name;
// add index in case there are duplicate names
int index = 1;
while (locationNames.Contains($"{caveName}_{index}"))
{
index++;
}
locationNames.Add($"{caveName}_{index}");
}
}
return locationNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
{
var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:false));
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError("No waypoints found in the main sub!");
}
foreach (var submarine in Submarine.Loaded)
{
if (string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase))
{
var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:false));
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError($"No waypoints found in sub {submarine.Info.Name}!");
}
}
if (Level.Loaded is Level loadedLevel)
{
(string locationNameNoIndex, int locationIndex) = SplitIndex(locationName);
int caveIndex = 1;
foreach (var cave in loadedLevel.Caves)
{
if (string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase))
{
if (caveIndex != locationIndex)
{
caveIndex++;
continue;
}
var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints);
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError($"No waypoints found in cave {cave.CaveGenerationParams.Name}!");
}
}
}
teleportPosition = Vector2.Zero;
return false;
WayPoint GetRandomWaypoint(IReadOnlyList<WayPoint> waypoints)
{
if (waypoints.None())
{
return null;
}
if (waypoints.Any(point => point.SpawnType == SpawnType.Human))
{
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Human, Rand.RandSync.Unsynced);
}
if (waypoints.Any(point => point.SpawnType == SpawnType.Path))
{
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Path, Rand.RandSync.Unsynced);
}
return waypoints.GetRandom(Rand.RandSync.Unsynced);
}
(string, int) SplitIndex(string caveName)
{
string[] splitName = caveName.Split('_');
if (splitName.Length == 1)
{
return (splitName[0], -1);
}
else
{
return (splitName[0], int.Parse(splitName[1]));
}
}
}
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false)
{
if (args.Length == 0) return null;
@@ -2202,6 +2367,11 @@ namespace Barotrauma
c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
(!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
if (botsOnly)
{
matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter);
}
if (!matchingCharacters.Any())
{
NewMessage("Character \""+ characterName + "\" not found", Color.Red);
@@ -2232,6 +2402,68 @@ namespace Barotrauma
return null;
}
private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter, string[] args)
{
if (Screen.Selected != GameMain.GameScreen)
{
NewMessage("Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow);
return;
}
Character targetCharacter = controlledCharacter;
Vector2 worldPosition = cursorWorldPos;
string locationNameArgument = "";
var availableLocations = ListAvailableLocations();
if (args.Length > 0)
{
if (args.Length > 1)
{
// remove location name from args
if (availableLocations.Contains(args.Last())
|| string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase))
{
locationNameArgument = args.Last();
args = args.Take(args.Length - 1).ToArray();
}
else
{
NewMessage("Invalid arguments", color: Color.Yellow);
return;
}
}
// the remaining args should be the character name and a possible index
if (args[0].ToLowerInvariant() != "me")
{
Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false);
targetCharacter = match;
}
}
if (!string.IsNullOrWhiteSpace(locationNameArgument))
{
if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
{
worldPosition = teleportPosition;
}
else
{
ThrowError($"No teleport position for location \"{locationNameArgument}\" was found.");
return;
}
}
if (targetCharacter != null)
{
targetCharacter.TeleportTo(worldPosition);
}
else
{
NewMessage("Invalid arguments", color: Color.Yellow);
}
}
public static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
{
@@ -2253,8 +2485,8 @@ namespace Barotrauma
{
job = JobPrefab.Prefabs[characterLowerCase];
}
bool human = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
bool addToCrew = false;
if (args.Length > 1)
{
switch (args[1].ToLowerInvariant())
@@ -2288,13 +2520,17 @@ namespace Barotrauma
spawnPosition = cursorWorldPos;
break;
default:
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
break;
}
addToCrew =
args.Length > 3 ?
args[3].Equals("true", StringComparison.OrdinalIgnoreCase) :
isHuman;
}
else
{
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
}
if (string.IsNullOrWhiteSpace(args[0])) { return; }
@@ -2313,29 +2549,36 @@ namespace Barotrauma
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
if (human)
if (isHuman)
{
var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
if (GameMain.GameSession != null)
{
spawnedCharacter.TeamID = teamType;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
}
spawnedCharacter.GiveJobItems(spawnPoint);
spawnedCharacter.GiveIdCardTags(spawnPoint);
spawnedCharacter.Info.StartItemsGiven = true;
}
else
{
if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null)
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
if (prefab != null)
{
Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8));
CharacterInfo characterInfo = null;
if (prefab.HasCharacterInfo)
{
characterInfo = new CharacterInfo(prefab.Identifier);
}
spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
}
}
if (addToCrew && GameMain.GameSession != null)
{
spawnedCharacter.TeamID = teamType;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
}
}
public static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg)
@@ -253,6 +253,11 @@ namespace Barotrauma
/// </summary>
SwimmingSpeed,
/// <summary>
/// Increases the character's speed by a percentage when using an item that propels the character forwards (such as a diving scooter).
/// </summary>
PropulsionSpeed,
/// <summary>
/// Decreases how long it takes for buffs applied to the character decay over time by a percentage.
/// Buffs are afflictions that have isBuff set to true.
@@ -107,6 +107,11 @@ namespace Barotrauma
{
if (spawnPending)
{
if (itemPrefab == null)
{
isFinished = true;
return;
}
SpawnItem();
spawnPending = false;
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -30,7 +30,7 @@ namespace Barotrauma
{
if (TargetTag.IsEmpty)
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no target tag! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
var conditionalElements = element.GetChildElements("Conditional");
@@ -52,7 +52,7 @@ namespace Barotrauma
if (Conditionals.None())
{
DebugConsole.LogError($"CheckConditionalAction error: {GetEventName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
DebugConsole.LogError($"CheckConditionalAction error: {GetEventDebugName()} uses a CheckConditionalAction with no valid PropertyConditional! This will cause the check to automatically succeed.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
@@ -67,11 +67,6 @@ namespace Barotrauma
}
}
private string GetEventName()
{
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
}
protected override bool? DetermineSuccess()
{
IEnumerable<ISerializableEntity> targets = null;
@@ -82,7 +77,7 @@ namespace Barotrauma
if (targets.None())
{
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
DebugConsole.LogError($"{nameof(CheckConditionalAction)} error: {GetEventDebugName()} uses a {nameof(CheckConditionalAction)} but no valid target was found for tag \"{TargetTag}\"! This will cause the check to automatically succeed.",
contentPackage: ParentEvent.Prefab.ContentPackage);
}
@@ -0,0 +1,35 @@
#nullable enable
namespace Barotrauma;
/// <summary>
/// Check whether the difficulty of the current level is within some specific range.
/// </summary>
class CheckDifficultyAction : BinaryOptionAction
{
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Minimum difficulty of the current level for the check to succeed.")]
public float MinDifficulty { get; set; }
[Serialize(100.0f, IsPropertySaveable.Yes, description: "Maximum difficulty of the current level for the check to succeed.")]
public float MaxDifficulty { get; set; }
public CheckDifficultyAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (MaxDifficulty <= MinDifficulty)
{
DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: maximum difficulty ({MaxDifficulty}) is not larger than minimum difficulty ({MinDifficulty}) in {nameof(CheckDifficultyAction)}.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
}
protected override bool? DetermineSuccess()
{
if (Level.Loaded == null) { return false; }
return Level.Loaded.Difficulty >= MinDifficulty && Level.Loaded.Difficulty <= MaxDifficulty;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(DetermineFinished())} {nameof(CheckDifficultyAction)} -> (min: {MinDifficulty}, max: {MaxDifficulty}" +
$" Succeeded: {(succeeded.HasValue ? succeeded.Value.ToString() : "not determined").ColorizeObject()})";
}
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
@@ -36,6 +36,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be equipped for the check to succeed?")]
public bool RequireEquipped { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the item need to be worn for the check to succeed?")]
public bool RequireWorn { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "If enabled, the doesn't need to be directly inside the container/character we're checking, but can be nested inside multiple containers (e.g. in a toolbelt in a character's inventory).")]
public bool Recursive { get; set; }
@@ -256,6 +259,19 @@ namespace Barotrauma
if (character == null) { return false; }
return character.HasEquippedItem(item);
}
if (RequireWorn)
{
if (character == null) { return false; }
foreach (var wearable in item.GetComponents<Wearable>())
{
foreach (var allowedSlot in wearable.AllowedSlots)
{
if (allowedSlot == InvSlotType.Any) { continue; }
if (character.HasEquippedItem(item, allowedSlot)) { return true; }
}
}
return false;
}
return true;
}
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -134,6 +134,10 @@ namespace Barotrauma
else
{
text = TextManager.Get(Text).Fallback(Text);
if (text.Value.IsNullOrEmpty())
{
text = text.Fallback(Text);
}
}
return ParentEvent.ReplaceVariablesInEventText(text);
}
@@ -0,0 +1,55 @@
#nullable enable
namespace Barotrauma;
/// <summary>
/// Can be used to disconnect wires and break devices and walls in beacon stations. Useful if you want the beacon to be in tact by default, and use events to determine whether it should be e.g. manned by bandits, or destroyed and infested by monsters.
/// </summary>
class DamageBeaconStationAction : EventAction
{
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of disconnecting wires (0.5 = 50% chance of disconnecting any given wire, 1 = all wires disconnected).")]
public float DisconnectWireProbability { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of a wall sections leaking (0.5 = 50% creating a leak on any given wall section, 1 = all walls leak).")]
public float DamageWallProbability { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Probability of devices being damaged (0.5 = 50% chance of damaging any given devices, 1 = all devices are damaged).")]
public float DamageDeviceProbability { get; set; }
private bool isFinished;
public DamageBeaconStationAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
if (DisconnectWireProbability <= 0.0f && DamageWallProbability <= 0.0f && DamageDeviceProbability <= 0.0f)
{
DebugConsole.LogError($"Potential error in event {GetEventDebugName()}: {DisconnectWireProbability}, {DamageWallProbability} and {DamageDeviceProbability} are all set to 0 in {nameof(DamageBeaconStationAction)}, and the action will do nothing.",
contentPackage: parentEvent.Prefab.ContentPackage);
}
}
public override bool IsFinished(ref string goToLabel) => isFinished;
public override void Reset()
{
isFinished = false;
}
public override void Update(float deltaTime)
{
if (isFinished) { return; }
if (Level.Loaded != null)
{
Level.Loaded.DisconnectBeaconStationWires(DisconnectWireProbability);
Level.Loaded.DamageBeaconStationWalls(DamageWallProbability);
Level.Loaded.DamageBeaconStationDevices(DamageDeviceProbability);
}
isFinished = true;
}
public override string ToDebugString()
{
return $"{ToolBox.GetDebugSymbol(isFinished)} {nameof(DamageBeaconStationAction)}";
}
}
@@ -198,6 +198,12 @@ namespace Barotrauma
}
}
protected string GetEventDebugName()
{
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
}
/// <summary>
/// Rich test to display in debugdraw
/// </summary>
@@ -1,4 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
@@ -31,6 +31,9 @@ namespace Barotrauma
[Serialize(-1, IsPropertySaveable.Yes, description: "Maximum number of NPCs the action can target. For example, you could only make a specific number of security officers man a periscope.")]
public int MaxTargets { get; set; }
[Serialize(100, IsPropertySaveable.Yes, description: "Priority of operating the item (0-100). Higher values will make the AI prefer operating the item over other orders (priority 60-70) or e.g. reacting to emergencies (priority 90).")]
public int Priority { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop operating the item when the event resets?")]
public bool AbandonOnReset { get; set; }
@@ -72,7 +75,7 @@ namespace Barotrauma
{
var newObjective = new AIObjectiveOperateItem(itemComponent, npc, humanAiController.ObjectiveManager, OrderOption, RequireEquip)
{
OverridePriority = 100.0f
OverridePriority = Priority
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -68,6 +68,7 @@ namespace Barotrauma
("bot", v => TagBots(playerCrewOnly: false)),
("crew", v => TagCrew()),
("humanprefabidentifier", TagHumansByIdentifier),
("humanprefabtag", TagHumansByTag),
("jobidentifier", TagHumansByJobIdentifier),
("structureidentifier", TagStructuresByIdentifier),
("structurespecialtag", TagStructuresBySpecialTag),
@@ -153,6 +154,11 @@ namespace Barotrauma
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab?.Identifier == identifier));
}
private void TagHumansByTag(Identifier tag)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HasJob(jobIdentifier)));
@@ -217,6 +223,7 @@ namespace Barotrauma
private bool IsValidItem(Item it)
{
return
!it.IsLayerHidden && /*items in hidden layers are treated as if they didn't exist, regardless if hidden items should be allowed*/
(!it.HiddenInGame || AllowHiddenItems) &&
ModuleTagMatches(it) &&
//if the item has just spawned, it may be in a hull but not moved into the coordinate space of the hull yet
@@ -424,7 +424,7 @@ namespace Barotrauma
public void RegisterEventHistory(bool registerFinishedOnly = false)
{
if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = !registerFinishedOnly;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
@@ -433,15 +433,15 @@ namespace Barotrauma
{
foreach (var finishedEvent in finishedEvents)
{
var key = finishedEvent.ParentSet;
if (key == null) { continue; }
if (level.LevelData.FinishedEvents.ContainsKey(key))
EventSet parentSet = finishedEvent.ParentSet;
if (parentSet == null) { continue; }
if (parentSet.Exhaustible)
{
level.LevelData.FinishedEvents[key] += 1;
level.LevelData.EventsExhausted = true;
}
else
if (!level.LevelData.FinishedEvents.TryAdd(parentSet, 1))
{
level.LevelData.FinishedEvents.Add(key, 1);
level.LevelData.FinishedEvents[parentSet] += 1;
}
}
}
@@ -691,6 +691,7 @@ namespace Barotrauma
{
return
(e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
(e.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(e.RequiredLayer)) &&
!level.LevelData.NonRepeatableEvents.Contains(e.Identifier);
}
@@ -702,8 +703,9 @@ namespace Barotrauma
private static bool IsValidForLevel(EventSet eventSet, Level level)
{
return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.IsAllowedDifficulty(eventSet.MinLevelDifficulty, eventSet.MaxLevelDifficulty) &&
level.LevelData.Type == eventSet.LevelType &&
(eventSet.RequiredLayer.IsEmpty || Submarine.LayerExistsInAnySub(eventSet.RequiredLayer)) &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
}
@@ -953,7 +955,7 @@ namespace Barotrauma
monsterStrength = 0;
foreach (Character character in Character.CharacterList)
{
if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; }
if (character.IsIncapacitated || character.IsHandcuffed || !character.Enabled || character.IsPet) { continue; }
if (character.AIController is EnemyAIController enemyAI)
{
@@ -32,6 +32,11 @@ namespace Barotrauma
/// </summary>
public readonly Identifier BiomeIdentifier;
/// <summary>
/// If set, this layer must be present somewhere in the level.
/// </summary>
public readonly Identifier RequiredLayer;
/// <summary>
/// If set, the event set can only be chosen in locations that belong to this faction.
/// </summary>
@@ -94,6 +99,8 @@ namespace Barotrauma
Probability = Math.Clamp(element.GetAttributeFloat(1.0f, "probability", "spawnprobability"), 0, 1);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", EventType != typeof(ScriptedEvent));
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
UnlockPathEvent = element.GetAttributeBool("unlockpathevent", false);
UnlockPathTooltip = element.GetAttributeString("unlockpathtooltip", "lockedpathtooltip");
UnlockPathReputation = element.GetAttributeInt("unlockpathreputation", 0);
@@ -106,6 +106,11 @@ namespace Barotrauma
/// </summary>
public readonly LevelData.LevelType LevelType;
/// <summary>
/// If set, this layer must be present somewhere in the level.
/// </summary>
public readonly Identifier RequiredLayer;
/// <summary>
/// If set, the event set can only be chosen in locations of this type.
/// </summary>
@@ -368,7 +373,7 @@ namespace Barotrauma
ChooseRandom = element.GetAttributeBool("chooserandom", false);
eventCount = element.GetAttributeInt("eventcount", 1);
SubSetCount = element.GetAttributeInt("setcount", 1);
Exhaustible = element.GetAttributeBool("exhaustible", false);
Exhaustible = element.GetAttributeBool("exhaustible", parentSet?.Exhaustible ?? false);
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
@@ -386,6 +391,8 @@ namespace Barotrauma
ResetTime = element.GetAttributeFloat(nameof(ResetTime), parentSet?.ResetTime ?? 0);
CampaignTutorialOnly = element.GetAttributeBool(nameof(CampaignTutorialOnly), parentSet?.CampaignTutorialOnly ?? false);
RequiredLayer = element.GetAttributeIdentifier(nameof(RequiredLayer), Identifier.Empty);
ForceAtDiscoveredNr = element.GetAttributeInt(nameof(ForceAtDiscoveredNr), -1);
ForceAtVisitedNr = element.GetAttributeInt(nameof(ForceAtVisitedNr), -1);
if (ForceAtDiscoveredNr >= 0 && ForceAtVisitedNr >= 0)
@@ -7,7 +7,8 @@ using System.Linq;
namespace Barotrauma
{
partial class AlienRuinMission : Mission
[TypePreviouslyKnownAs("AlienRuinMission")]
partial class EliminateTargetsMission : Mission
{
private readonly Identifier[] targetItemIdentifiers;
private readonly Identifier[] targetEnemyIdentifiers;
@@ -16,7 +17,10 @@ namespace Barotrauma
private readonly HashSet<Character> spawnedTargets = new HashSet<Character>();
private readonly HashSet<Entity> allTargets = new HashSet<Entity>();
private Ruin TargetRuin { get; set; }
public readonly SubmarineType TargetSubType;
public readonly bool PrioritizeThalamus;
private Submarine TargetSub { get; set; }
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
@@ -35,11 +39,13 @@ namespace Barotrauma
}
}
public AlienRuinMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
public EliminateTargetsMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetitems", Array.Empty<Identifier>());
targetEnemyIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("targetenemies", Array.Empty<Identifier>());
minEnemyCount = prefab.ConfigElement.GetAttributeInt("minenemycount", 0);
TargetSubType = prefab.ConfigElement.GetAttributeEnum("targetsub", SubmarineType.Ruin);
PrioritizeThalamus = prefab.RequireThalamusWreck;
}
protected override void StartMissionSpecific(Level level)
@@ -48,23 +54,48 @@ namespace Barotrauma
spawnedTargets.Clear();
allTargets.Clear();
if (IsClient) { return; }
TargetRuin = Level.Loaded?.Ruins?.GetRandom(randSync: Rand.RandSync.ServerAndClient);
if (TargetRuin == null)
TargetSub = TargetSubType switch
{
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): level contains no alien ruins",
SubmarineType.Wreck => FindWreck(),
SubmarineType.Ruin => Level.Loaded?.Ruins?.GetRandom(Rand.RandSync.ServerAndClient).Submarine,
SubmarineType.BeaconStation => Level.Loaded?.BeaconStation,
_ => null
};
Submarine FindWreck()
{
var wrecks = Level.Loaded?.Wrecks;
if (wrecks == null || wrecks.None()) { return null; }
if (PrioritizeThalamus)
{
var thalamusWrecks = wrecks.Where(w => w.WreckAI != null).ToArray();
if (thalamusWrecks.Any())
{
return thalamusWrecks.GetRandom(Rand.RandSync.ServerAndClient);
}
}
return wrecks.GetRandom(Rand.RandSync.ServerAndClient);
}
if (TargetSub == null)
{
DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): level contains no submarines",
contentPackage: Prefab.ContentPackage);
return;
}
if (targetItemIdentifiers.Length < 1 && targetEnemyIdentifiers.Length < 1)
{
DebugConsole.ThrowError($"Failed to initialize an Alien Ruin mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition",
DebugConsole.ThrowError($"Failed to initialize an {nameof(EliminateTargetsMission)} mission (\"{Prefab.Identifier}\"): no target identifiers set in the mission definition",
contentPackage: Prefab.ContentPackage);
return;
}
foreach (var item in Item.ItemList)
{
if (!targetItemIdentifiers.Contains(item.Prefab.Identifier)) { continue; }
if (item.Submarine != TargetRuin.Submarine) { continue; }
if (item.Submarine != TargetSub) { continue; }
existingTargets.Add(item);
allTargets.Add(item);
}
@@ -73,7 +104,7 @@ namespace Barotrauma
{
if (character.SpeciesName.IsEmpty) { continue; }
if (!targetEnemyIdentifiers.Contains(character.SpeciesName)) { continue; }
if (character.Submarine != TargetRuin.Submarine) { continue; }
if (character.Submarine != TargetSub) { continue; }
existingTargets.Add(character);
allTargets.Add(character);
existingEnemyCount++;
@@ -103,7 +134,7 @@ namespace Barotrauma
for (int i = 0; i < (minEnemyCount - existingEnemyCount); i++)
{
var prefab = enemyPrefabs.GetRandomUnsynced();
var spawnPos = TargetRuin.Submarine.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition;
var spawnPos = TargetSub.GetWaypoints(false).GetRandomUnsynced(w => w.CurrentHull != null)?.WorldPosition;
if (!spawnPos.HasValue)
{
DebugConsole.ThrowError($"Error in an Alien Ruin mission (\"{Prefab.Identifier}\"): no valid spawn positions could be found for the additional ({minEnemyCount - existingEnemyCount}) enemies to be spawned",
@@ -211,12 +211,12 @@ namespace Barotrauma
public virtual void SetLevel(LevelData level) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel);
}
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
{
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
if (missionType == MissionType.None)
@@ -225,32 +225,20 @@ namespace Barotrauma
}
else
{
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => ((int)(missionType & m.Type)) != 0));
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType)));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
if (requireCorrectLocationType)
{
allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1]));
}
if (allowedMissions.Count == 0)
if (difficultyLevel.HasValue)
{
return null;
allowedMissions.RemoveAll(m => !m.IsAllowedDifficulty(difficultyLevel.Value));
}
int probabilitySum = allowedMissions.Sum(m => m.Commonness);
int randomNumber = rand.NextInt32() % probabilitySum;
foreach (MissionPrefab missionPrefab in allowedMissions)
{
if (randomNumber <= missionPrefab.Commonness)
{
return missionPrefab.Instantiate(locations, Submarine.MainSub);
}
randomNumber -= missionPrefab.Commonness;
}
return null;
if (allowedMissions.Count == 0) { return null; }
MissionPrefab missionPrefab = ToolBox.SelectWeightedRandom(allowedMissions, m => m.Commonness, rand);
return missionPrefab.Instantiate(locations, Submarine.MainSub);
}
/// <summary>
@@ -288,7 +276,7 @@ namespace Barotrauma
{
foreach (MapEntity entityToShow in MapEntity.MapEntityList.Where(me => me.Prefab?.HasSubCategory(categoryToShow) ?? false))
{
entityToShow.HiddenInGame = false;
entityToShow.IsLayerHidden = false;
}
}
this.level = level;
@@ -381,9 +369,12 @@ namespace Barotrauma
/// </summary>
public void End()
{
completed =
DetermineCompleted() &&
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
if (GameMain.NetworkMember is not { IsClient: true })
{
completed =
DetermineCompleted() &&
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
}
if (completed)
{
if (Prefab.LocationTypeChangeOnCompleted != null)
@@ -23,9 +23,9 @@ namespace Barotrauma
Pirate = 0x200,
GoTo = 0x400,
ScanAlienRuins = 0x800,
ClearAlienRuins = 0x1000,
EliminateTargets = 0x1000,
End = 0x2000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | ClearAlienRuins | End
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End
}
partial class MissionPrefab : PrefabWithUintIdentifier
@@ -45,7 +45,7 @@ namespace Barotrauma
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.ClearAlienRuins, typeof(AlienRuinMission) },
{ MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
{ MissionType.End, typeof(EndMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
@@ -122,7 +122,7 @@ namespace Barotrauma
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin;
public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
@@ -216,6 +216,10 @@ namespace Barotrauma
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
RequireRuin = element.GetAttributeBool("requireruin", false);
RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
if (RequireThalamusWreck) { RequireWreck = true; }
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt("commonness", 1);
@@ -360,11 +364,16 @@ namespace Barotrauma
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
//backwards compatibility
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
{
missionTypeName = "AbandonedOutpost".ToIdentifier();
}
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
{
missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
}
else if (missionTypeName == "clearalienruins")
{
missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
}
if (!Enum.TryParse(missionTypeName.Value, true, out Type))
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
@@ -434,19 +443,13 @@ namespace Barotrauma
}
}
if (Type == MissionType.Beacon)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || !connection.LevelData.HasBeaconStation || connection.LevelData.IsBeaconActive) { return false; }
}
else if (Type == MissionType.ScanAlienRuins || Type == MissionType.ClearAlienRuins)
{
var connection = from.Connections.Find(c => c.Locations.Contains(from) && c.Locations.Contains(to));
if (connection?.LevelData == null || connection.LevelData.GenerationParams.GetMaxRuinCount() < 1) { return false; }
}
return false;
}
/// <summary>
/// Inclusive (matching the min an max values is accepted).
/// </summary>
public bool IsAllowedDifficulty(float difficulty) => difficulty >= MinLevelDifficulty && difficulty <= MaxLevelDifficulty;
public Mission Instantiate(Location[] locations, Submarine sub)
{
@@ -518,7 +518,9 @@ namespace Barotrauma
contentPackage: Prefab.ContentPackage);
continue;
}
target.Item.DontCleanUp = true;
if (target.ParentTarget.Item.GetComponent<ItemContainer>() is ItemContainer container)
{
if (!container.Inventory.TryPutItem(target.Item, user: null))
@@ -533,7 +533,7 @@ namespace Barotrauma
.Distinct();
public static IEnumerable<Item> FilterCargoCrates(IEnumerable<Item> items, Func<Item, bool> conditional = null)
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.HiddenInGame && !it.Removed && (conditional == null || conditional(it)));
=> items.Where(it => it.HasTag(Tags.Crate) && !it.NonInteractable && !it.NonPlayerTeamInteractable && !it.IsHidden && !it.Removed && (conditional == null || conditional(it)));
public static IEnumerable<ItemContainer> FindReusableCargoContainers(IEnumerable<Submarine> subs, IEnumerable<Hull> cargoRooms = null) =>
FilterCargoCrates(Item.ItemList, it => subs.Contains(it.Submarine) && !it.HasTag(Tags.CargoMissionItem) && (cargoRooms == null || cargoRooms.Contains(it.CurrentHull)))
@@ -685,7 +685,14 @@ namespace Barotrauma
var idCard = item.GetComponent<IdCard>();
if (cargoManager != null && idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
cargoManager.purchasedIDCards.Add((purchased, idCard));
if (purchased.DeliverImmediately)
{
InitPurchasedIDCard(purchased, idCard);
}
else
{
cargoManager.purchasedIDCards.Add((purchased, idCard));
}
}
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
@@ -703,18 +710,23 @@ namespace Barotrauma
{
foreach ((PurchasedItem purchased, IdCard idCard) in purchasedIDCards)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
InitPurchasedIDCard(purchased, idCard);
}
}
private static void InitPurchasedIDCard(PurchasedItem purchased, IdCard idCard)
{
if (idCard != null && purchased.BuyerCharacterInfoIdentifier != 0)
{
var owner = Character.CharacterList.Find(c => c.Info?.GetIdentifier() == purchased.BuyerCharacterInfoIdentifier);
if (owner?.Info != null)
{
var mainSubSpawnPoints = WayPoint.SelectCrewSpawnPoints(new List<CharacterInfo>() { owner.Info }, Submarine.MainSub);
idCard.Initialize(mainSubSpawnPoints.FirstOrDefault(), owner);
}
}
}
public static Vector2 GetCargoPos(Hull hull, ItemPrefab itemPrefab)
{
float floorPos = hull.Rect.Y - hull.Rect.Height;
@@ -99,14 +99,20 @@ namespace Barotrauma
{
if (order.Identifier == Tags.DeconstructThis)
{
Item.DeconstructItems.Add(item);
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Add(stackedItem);
}
#if CLIENT
HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
#endif
}
else
{
Item.DeconstructItems.Remove(item);
foreach (var stackedItem in item.GetStackedItems())
{
Item.DeconstructItems.Remove(stackedItem);
}
}
}
}
@@ -180,6 +186,17 @@ namespace Barotrauma
DebugConsole.ThrowError("Tried to add a dead character to CrewManager!\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
if (character.Info == null)
{
if (character.Prefab.ContentPackage == GameMain.VanillaContent)
{
DebugConsole.ThrowError($"Added a character with no {nameof(CharacterInfo)} to the crew." + Environment.StackTrace.CleanupStackTrace());
}
else
{
DebugConsole.ThrowError($"Added add a character with no {nameof(CharacterInfo)} to the crew. This may lead to issues: consider adding {nameof(CharacterPrefab.HasCharacterInfo)}=\"True\" to the character config.");
}
}
if (!characters.Contains(character))
{
@@ -275,11 +292,7 @@ namespace Barotrauma
if (Level.Loaded != null && Level.Loaded.ShouldSpawnCrewInsideOutpost())
{
spawnWaypoints = WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
spawnWaypoints = GetOutpostSpawnpoints();
while (spawnWaypoints.Count > characterInfos.Count)
{
spawnWaypoints.RemoveAt(Rand.Int(spawnWaypoints.Count));
@@ -301,46 +314,8 @@ namespace Barotrauma
var info = characterInfos[i];
info.TeamID = CharacterTeamType.Team1;
Character character = Character.Create(info, spawnWaypoints[i].WorldPosition, info.Name);
if (character.Info != null)
{
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
{
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
}
if (character.Info.InventoryData != null)
{
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
}
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoints[i]);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (character.Info.HealthData != null)
{
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
}
InitializeCharacter(character, mainSubWaypoints[i], spawnWaypoints[i]);
character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoints[i]);
character.GiveIdCardTags(spawnWaypoints[i]);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
AddCharacter(character, sortCrewList: false);
#if CLIENT
if (IsSinglePlayer && (Character.Controlled == null || character.Info.LastControlled)) { Character.Controlled = character; }
@@ -355,6 +330,61 @@ namespace Barotrauma
conversationTimer = IsSinglePlayer ? Rand.Range(5.0f, 10.0f) : Rand.Range(45.0f, 60.0f);
}
/// <summary>
/// Returns the potential crew spawnpositions for the crew in the loaded outpost
/// </summary>
public List<WayPoint> GetOutpostSpawnpoints()
{
return WayPoint.WayPointList.FindAll(wp =>
wp.SpawnType == SpawnType.Human &&
wp.Submarine == Level.Loaded.StartOutpost &&
wp.CurrentHull != null &&
wp.CurrentHull.OutpostModuleTags.Contains("airlock".ToIdentifier()));
}
public void InitializeCharacter(Character character, WayPoint mainSubWaypoint, WayPoint spawnWaypoint)
{
if (character.Info != null)
{
if (!character.Info.StartItemsGiven && character.Info.InventoryData != null)
{
DebugConsole.AddWarning($"Error when initializing a round: character \"{character.Name}\" has not been given their initial items but has saved inventory data. Using the saved inventory data instead of giving the character new items.");
}
if (character.Info.InventoryData != null)
{
character.SpawnInventoryItems(character.Inventory, character.Info.InventoryData.FromPackage(null));
}
else if (!character.Info.StartItemsGiven)
{
character.GiveJobItems(mainSubWaypoint);
foreach (Item item in character.Inventory.AllItems)
{
//if the character is loaded from a human prefab with preconfigured items, its ID card gets assigned to the sub it spawns in
//we don't want that in this case, the crew's cards shouldn't be submarine-specific
var idCard = item.GetComponent<Items.Components.IdCard>();
if (idCard != null)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (character.Info.HealthData != null)
{
CharacterInfo.ApplyHealthData(character, character.Info.HealthData);
}
character.LoadTalents();
character.GiveIdCardTags(mainSubWaypoint);
character.GiveIdCardTags(spawnWaypoint);
character.Info.StartItemsGiven = true;
if (character.Info.OrderData != null)
{
character.Info.ApplyOrderData();
}
}
}
public void RenameCharacter(CharacterInfo characterInfo, string newName)
{
int identifier = characterInfo.GetIdentifierUsingOriginalName();
@@ -53,7 +53,7 @@ namespace Barotrauma
internal struct NetWalletSetSalaryUpdate : INetSerializableStruct
{
[NetworkSerialize]
public ushort Target;
public Option<ushort> Target;
[NetworkSerialize(MinValueInt = 0, MaxValueInt = 100)]
public int NewRewardDistribution;
@@ -117,13 +117,13 @@ namespace Barotrauma
public override int Balance
{
get => 0;
set => new InvalidOperationException("Tried to set the balance on an invalid wallet");
set => throw new InvalidOperationException("Tried to set the balance on an invalid wallet");
}
public override int RewardDistribution
{
get => 0;
set => new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
set => throw new InvalidOperationException("Tried to set the reward distribution on an invalid wallet");
}
}
@@ -134,7 +134,7 @@ namespace Barotrauma
public const string LowerCaseSaveElementName = "wallet";
private const string AttributeNameBalance = "balance",
AttrubuteNameRewardDistribution = "rewarddistribution",
AttributeNameRewardDistribution = "rewarddistribution",
SaveElementName = "Wallet";
public readonly Option<Character> Owner;
@@ -152,7 +152,15 @@ namespace Barotrauma
public virtual int RewardDistribution
{
get => rewardDistribution;
set => rewardDistribution = ClampRewardDistribution(value);
set
{
rewardDistribution = ClampRewardDistribution(value);
if (Owner.TryUnwrap(out var character) && character.Info is { } info)
{
info.LastRewardDistribution = Option.Some(rewardDistribution);
}
}
}
public Wallet(Option<Character> owner)
@@ -163,12 +171,14 @@ namespace Barotrauma
public Wallet(Option<Character> owner, XElement element): this(owner)
{
balance = ClampBalance(element.GetAttributeInt(AttributeNameBalance, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttrubuteNameRewardDistribution, 0));
rewardDistribution = ClampBalance(element.GetAttributeInt(AttributeNameRewardDistribution, 0));
}
public XElement Save()
{
XElement element = new XElement(SaveElementName, new XAttribute(AttributeNameBalance, Balance), new XAttribute(AttrubuteNameRewardDistribution, RewardDistribution));
XElement element = new XElement(SaveElementName,
new XAttribute(AttributeNameBalance, Balance),
new XAttribute(AttributeNameRewardDistribution, RewardDistribution));
return element;
}
@@ -195,6 +205,11 @@ namespace Barotrauma
SettingsChanged(balanceChanged: Option<int>.Some(-price), rewardChanged: Option<int>.None());
}
/// <summary>
/// Sets how much salary the wallet owner should receive from mission rewards.
/// Bank's salary determines the default salary for new characters.
/// </summary>
/// <param name="value"></param>
public void SetRewardDistribution(int value)
{
int oldValue = RewardDistribution;
@@ -347,6 +347,10 @@ namespace Barotrauma
/// </summary>
public event Action BeforeLevelLoading;
/// <summary>
/// Triggers when saving and quitting mid-round (as in, not just transferring to a new level). Automatically cleared after triggering -> no need to unregister
/// </summary>
public event Action OnSaveAndQuit;
public override void AddExtraMissions(LevelData levelData)
{
@@ -472,7 +476,7 @@ namespace Barotrauma
var missionPrefabs = MissionPrefab.Prefabs.Where(m => m.Tags.Any(t => t == automaticMission.MissionTag)).OrderBy(m => m.UintIdentifier);
if (missionPrefabs.Any())
{
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => (float)p.Commonness, rand);
var missionPrefab = ToolBox.SelectWeightedRandom(missionPrefabs, p => p.Commonness, rand);
if (missionPrefab.Type == MissionType.Pirate && Missions.Any(m => m.Prefab.Type == MissionType.Pirate))
{
continue;
@@ -515,8 +519,8 @@ namespace Barotrauma
if (endLevelMissionPrefabs.Any())
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => (float)p.Commonness, rand);
if (!Missions.Any(m => m.Prefab.Type == endLevelMissionPrefab.Type))
var endLevelMissionPrefab = ToolBox.SelectWeightedRandom(endLevelMissionPrefabs, p => p.Commonness, rand);
if (Missions.All(m => m.Prefab.Type != endLevelMissionPrefab.Type))
{
if (levelData.Type == LevelData.LevelType.LocationConnection)
{
@@ -913,6 +917,20 @@ namespace Barotrauma
}
}
/// <summary>
/// Handles updating store stock, registering event history and relocating items (i.e. things that need to be done when saving and quitting mid-round)
/// </summary>
public void HandleSaveAndQuit()
{
OnSaveAndQuit?.Invoke();
OnSaveAndQuit = null;
if (Level.IsLoadedFriendlyOutpost)
{
UpdateStoreStock();
}
GameMain.GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
}
/// <summary>
/// Updates store stock before saving the game
/// </summary>
@@ -1368,13 +1386,13 @@ namespace Barotrauma
{
if (item.Removed) { continue; }
if (item.NonInteractable || item.NonPlayerTeamInteractable) { continue; }
if (item.HiddenInGame) { continue; }
if (item.IsHidden) { continue; }
if (!connectedSubs.Contains(item.Submarine)) { continue; }
if (item.Prefab.DontTransferBetweenSubs) { continue; }
if (AnyParentInventoryDisableTransfer(item)) { continue; }
var rootOwner = item.GetRootInventoryOwner();
if (rootOwner is Character) { continue; }
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.HiddenInGame)) { continue; }
if (rootOwner is Item ownerItem && (ownerItem.NonInteractable || item.NonPlayerTeamInteractable || ownerItem.IsHidden)) { continue; }
if (item.GetComponent<Door>() != null) { continue; }
if (item.Components.None(c => c is Pickable)) { continue; }
if (item.Components.Any(c => c is Pickable p && p.IsAttached)) { continue; }
@@ -29,7 +29,7 @@ namespace Barotrauma
: base(preset)
{
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
var mission = Mission.LoadRandom(locations, seed, false, missionType);
var mission = Mission.LoadRandom(locations, seed, requireCorrectLocationType: false, missionType, difficultyLevel: GameMain.NetworkMember.ServerSettings.SelectedLevelDifficulty);
if (mission != null)
{
missions.Add(mission);
@@ -231,6 +231,12 @@ namespace Barotrauma
{
campaign.Bank.Deduct(selectedSub.Price);
campaign.Bank.Balance = Math.Max(campaign.Bank.Balance, 0);
#if SERVER
if (GameMain.Server?.ServerSettings?.NewCampaignDefaultSalary is { } salary)
{
campaign.Bank.SetRewardDistribution((int)Math.Round(salary, digits: 0));
}
#endif
}
return campaign;
}
@@ -970,6 +976,8 @@ namespace Barotrauma
{
ToggleTabMenu();
}
DeathPrompt?.Close();
DeathPrompt.CloseBotPanel();
GUI.PreventPauseMenuToggle = true;
@@ -42,16 +42,25 @@ namespace Barotrauma
{
AvailableCharacters.ForEach(c => c.Remove());
AvailableCharacters.Clear();
foreach (var missingJob in location.Type.GetHireablesMissingFromCrew())
{
AddCharacter(missingJob);
amount--;
}
for (int i = 0; i < amount; i++)
{
JobPrefab job = location.Type.GetRandomHireable();
if (job == null) { return; }
var variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
AddCharacter(location.Type.GetRandomHireable());
}
if (location.Faction != null) { GenerateFactionCharacters(location.Faction.Prefab); }
if (location.SecondaryFaction != null) { GenerateFactionCharacters(location.SecondaryFaction.Prefab); }
void AddCharacter(JobPrefab job)
{
if (job == null) { return; }
int variant = Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient);
AvailableCharacters.Add(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant));
}
}
private void GenerateFactionCharacters(FactionPrefab faction)
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -125,7 +125,7 @@ namespace Barotrauma
public NetCrewMember(CharacterInfo info)
{
CharacterInfoID = info.GetIdentifierUsingOriginalName();
CharacterInfoID = info.ID;
Afflictions = ImmutableArray<NetAffliction>.Empty;
}
@@ -138,7 +138,7 @@ namespace Barotrauma
{
foreach (CharacterInfo info in crew)
{
if (info.GetIdentifierUsingOriginalName() == CharacterInfoID)
if (info.ID == CharacterInfoID)
{
return info;
}
@@ -159,6 +159,14 @@ namespace Barotrauma
private readonly CampaignMode? campaign;
/// <summary>
/// Characters whose afflictions have changed (processed periodically, refreshing the UI client-side, sending updates to clients server-side
/// </summary>
private readonly HashSet<Character> charactersWithAfflictionChanges = new HashSet<Character>();
private float processAfflictionChangesTimer;
private const float ProcessAfflictionChangesInterval = 1.0f;
public MedicalClinic(CampaignMode campaign)
{
this.campaign = campaign;
@@ -305,35 +313,8 @@ namespace Barotrauma
private void OnAfflictionCountChangedPrivate(Character character)
{
if (character is not { CharacterHealth: { } health, Info: { } info }) { return; }
ImmutableArray<NetAffliction> afflictions = GetAllAfflictions(health);
#if CLIENT
if (GameMain.NetworkMember is null)
{
ui?.UpdateAfflictions(new NetCrewMember(info, afflictions));
}
ui?.UpdateCrewPanel();
#elif SERVER
foreach (AfflictionSubscriber sub in afflictionSubscribers.ToList())
{
if (sub.Expiry < DateTimeOffset.Now)
{
afflictionSubscribers.Remove(sub);
continue;
}
if (sub.Target == info)
{
ServerSend(new NetCrewMember(info, afflictions),
header: NetworkHeader.AFFLICTION_UPDATE,
deliveryMethod: DeliveryMethod.Unreliable,
targetClient: sub.Subscriber);
}
}
#endif
if (character?.Info == null) { return; }
charactersWithAfflictionChanges.Add(character);
}
public int GetTotalCost() => PendingHeals.SelectMany(static h => h.Afflictions).Aggregate(0, static (current, affliction) => current + affliction.Price);
@@ -295,7 +295,7 @@ namespace Barotrauma
/// <summary>
/// Purchases an item swap and handles logic for deducting the credit.
/// </summary>
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false, Client? client = null)
public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null)
{
if (!CanUpgradeSub())
{
@@ -343,12 +343,14 @@ namespace Barotrauma
price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count;
}
if (force)
if (isNetworkMessage)
{
price = 0;
}
if (Campaign.TryPurchase(client, price))
//do not try to purchase if this is a network message (if the server is telling us that an item swap was purchased)
//we want to do the purchase no matter what, and the server handles deducting the money
if (isNetworkMessage || Campaign.TryPurchase(client, price))
{
PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -433,8 +435,7 @@ namespace Barotrauma
{
if (itemToCancel.PendingItemSwap == null)
{
var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
if (replacement == null)
if (MapEntityPrefab.FindByIdentifier(swappableItem.ReplacementOnUninstall) is not ItemPrefab replacement)
{
DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
return;
@@ -786,11 +787,10 @@ namespace Barotrauma
private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
{
if (!(element is { HasElements: true })) { return; }
if (element is not { HasElements: true }) { return; }
List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (XElement upgrade in element.Elements())
{
Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty);
@@ -15,6 +15,19 @@ namespace Barotrauma
partial class CharacterInventory : Inventory
{
/// <summary>
/// How much access other characters have to the inventory?
/// <see cref="Restricted"/> = Only accessible when character is knocked down or handcuffed.
/// <see cref="Limited"/> = Can also access inventories of bots on the same team and friendly pets.
/// <see cref="Allowed"/> = Can also access other players in the same team (used for drag and drop give).
/// </summary>
public enum AccessLevel
{
Restricted,
Limited,
Allowed
}
private readonly Character character;
public InvSlotType[] SlotTypes
@@ -22,8 +35,7 @@ namespace Barotrauma
get;
private set;
}
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand);
@@ -546,6 +558,7 @@ namespace Barotrauma
{
item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
}
item.Equipper = user;
}
protected override void CreateNetworkEvent(Range slotRange)
@@ -658,7 +658,8 @@ namespace Barotrauma.Items.Components
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
AvoidStaying = true
AvoidStaying = true,
IsWetRoom = true
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -180,6 +180,22 @@ namespace Barotrauma.Items.Components
OpenState = isOpen ? 1.0f : 0.0f;
}
}
/// <summary>
/// Can be used by status effects to tell the door to open (setting IsOpen directly would make it immediately fully open)
/// </summary>
public bool ShouldBeOpen
{
get { return isOpen; }
set
{
if (isOpen != value)
{
ToggleState(ActionType.OnUse, user: null);
}
}
}
public bool IsClosed => !IsOpen;
public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
@@ -896,9 +896,9 @@ namespace Barotrauma.Items.Components
return element;
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap);
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
flowerTiles = componentElement.GetAttributeIntArray("flowertiles", Array.Empty<int>())!;
Decayed = componentElement.GetAttributeBool("decayed", false);
@@ -1,10 +1,12 @@
using Barotrauma.Networking;
using Barotrauma.Abilities;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
@@ -41,6 +43,9 @@ namespace Barotrauma.Items.Components
private bool attachable, attached, attachedByDefault;
private Voronoi2.VoronoiCell attachTargetCell;
private PhysicsBody body;
public readonly ImmutableDictionary<StatTypes, float> HoldableStatValues;
public PhysicsBody Pusher
{
get;
@@ -287,6 +292,22 @@ namespace Barotrauma.Items.Components
}
}
characterUsable = element.GetAttributeBool("characterusable", true);
Dictionary<StatTypes, float> statValues = new Dictionary<StatTypes, float>();
foreach (var subElement in element.GetChildElements("statvalue"))
{
StatTypes statType = CharacterAbilityGroup.ParseStatType(subElement.GetAttributeString("stattype", ""), Name);
float statValue = subElement.GetAttributeFloat("value", 0f);
if (statValues.ContainsKey(statType))
{
statValues[statType] += statValue;
}
else
{
statValues.TryAdd(statType, statValue);
}
}
HoldableStatValues = statValues.ToImmutableDictionary();
}
private bool OnPusherCollision(Fixture sender, Fixture other, Contact contact)
@@ -304,9 +325,9 @@ namespace Barotrauma.Items.Components
}
private bool loadedFromInstance;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap);
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
loadedFromInstance = true;
@@ -296,7 +296,7 @@ namespace Barotrauma.Items.Components
impactQueue.Clear();
item.body.FarseerBody.OnCollision -= OnCollision;
item.body.CollisionCategories = Physics.CollisionItem;
item.body.CollidesWith = Physics.CollisionWall;
item.body.CollidesWith = Physics.DefaultItemCollidesWith;
item.body.FarseerBody.IsBullet = false;
item.body.PhysEnabled = false;
}
@@ -251,6 +251,7 @@ namespace Barotrauma.Items.Components
foreach (ConnectionPanel connectionPanel in item.GetComponents<ConnectionPanel>())
{
connectionPanel.DisconnectedWires.Clear();
foreach (Connection c in connectionPanel.Connections)
{
foreach (Wire w in c.Wires.ToArray())
@@ -260,7 +261,7 @@ namespace Barotrauma.Items.Components
w.Item.SetTransform(pos, 0.0f);
}
}
}
}
}
public override void Drop(Character dropper, bool setTransform = true)
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
if (!MathUtils.IsValid(dir)) { return true; }
float length = 200;
dir = dir.ClampLength(length) / length;
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier;
Vector2 propulsion = dir * Force * character.PropulsionSpeedMultiplier * (1.0f + character.GetStatValue(StatTypes.PropulsionSpeed));
if (character.AnimController.InWater && Force > 0.0f) { character.AnimController.TargetMovement = dir; }
foreach (Limb limb in character.AnimController.Limbs)
@@ -272,22 +272,6 @@ namespace Barotrauma.Items.Components
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
}
ignoredBodies.Clear();
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
ignoredBodies.Add(l.body.FarseerBody);
}
foreach (Item heldItem in character.HeldItems)
{
var holdable = heldItem.GetComponent<Holdable>();
if (holdable?.Pusher != null)
{
ignoredBodies.Add(holdable.Pusher.FarseerBody);
}
}
float degreeOfFailure = 1.0f - DegreeOfSuccess(character);
degreeOfFailure *= degreeOfFailure;
if (degreeOfFailure > Rand.Range(0.0f, 1.0f))
@@ -311,6 +295,25 @@ namespace Barotrauma.Items.Components
}
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
projectile.Launcher = item;
ignoredBodies.Clear();
if (!projectile.DamageUser)
{
foreach (Limb l in character.AnimController.Limbs)
{
if (l.IsSevered) { continue; }
ignoredBodies.Add(l.body.FarseerBody);
}
foreach (Item heldItem in character.HeldItems)
{
var holdable = heldItem.GetComponent<Holdable>();
if (holdable?.Pusher != null)
{
ignoredBodies.Add(holdable.Pusher.FarseerBody);
}
}
}
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (projectile.Item.body != null)
@@ -158,7 +158,7 @@ namespace Barotrauma.Items.Components
else
{
throwAngle = ThrowAngleStart;
ac.HoldItem(deltaTime, item, handlePos, itemPos: aimPos, aim: false, holdAngle);
ac.HoldItem(deltaTime, item, handlePos, itemPos: holdPos, aim: false, holdAngle);
}
}
else
@@ -941,14 +941,16 @@ namespace Barotrauma.Items.Components
#endif
}
public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public virtual void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
if (componentElement != null)
{
foreach (XAttribute attribute in componentElement.Attributes())
{
if (!SerializableProperties.TryGetValue(attribute.NameAsIdentifier(), out SerializableProperty property)) { continue; }
if (property.OverridePrefabValues || !usePrefabValues)
if (property.OverridePrefabValues ||
!usePrefabValues ||
(isItemSwap && property.GetAttribute<Editable>() is { TransferToSwappedItem: true }))
{
property.TrySetValue(this, attribute.Value);
}
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
@@ -12,7 +13,7 @@ namespace Barotrauma.Items.Components
{
partial class ItemContainer : ItemComponent, IDrawableComponent
{
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition);
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition, bool BlameEquipperForDeath);
readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
@@ -252,6 +253,8 @@ namespace Barotrauma.Items.Components
private float autoInjectCooldown = 1.0f;
const float AutoInjectInterval = 1.0f;
private bool subContainersCanAutoInject;
public bool ShouldBeContained(string[] identifiersOrTags, out bool isRestrictionsDefined)
{
@@ -277,6 +280,11 @@ namespace Barotrauma.Items.Components
public readonly bool HasSubContainers;
public bool hasSignalConnections;
private string totalConditionValueString = "", totalConditionPercentageString = "", totalItemsString = "";
private float prevTotalConditionValue = 0, prevTotalConditionPercentage = 0; int prevTotalItems = 0;
public ItemContainer(Item item, ContentXElement element)
: base(item, element)
{
@@ -323,6 +331,8 @@ namespace Barotrauma.Items.Components
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
bool autoInject = subElement.GetAttributeBool("autoinject", false);
subContainersCanAutoInject |= autoInject;
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
{
@@ -411,7 +421,12 @@ namespace Barotrauma.Items.Components
relatedItem ??= containableItem;
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
activeContainedItems.Add(new ActiveContainedItem(
containedItem,
effect,
containableItem.ExcludeBroken,
containableItem.ExcludeFullCondition,
containableItem.BlameEquipperForDeath));
}
}
}
@@ -448,8 +463,8 @@ namespace Barotrauma.Items.Components
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":GardeningPlanted:" + containedItem.Prefab.Identifier);
}
//no need to Update() if this item has no statuseffects and no physics body
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
//no need to Update() if this item has no statuseffects and no physics body, and if there are no signal connections.
IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
if (IsActive && item.GetRootInventoryOwner() is Character owner &&
owner.HasEquippedItem(item, predicate: slot => slot.HasFlag(InvSlotType.LeftHand) || slot.HasFlag(InvSlotType.RightHand)))
@@ -481,11 +496,16 @@ namespace Barotrauma.Items.Components
containedItems.RemoveAll(i => i.Item == containedItem);
item.SetContainedItemPositions();
//deactivate if the inventory is empty
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
IsActive = hasSignalConnections || activeContainedItems.Count > 0 || Inventory.AllItems.Any(static it => it.body != null);
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
}
public bool BlameEquipperForDeath()
{
return activeContainedItems.Any(c => c.BlameEquipperForDeath);
}
public bool CanBeContained(Item item)
{
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
@@ -545,11 +565,47 @@ namespace Barotrauma.Items.Components
alwaysContainedItemsSpawned = true;
}
if (hasSignalConnections)
{
float totalConditionValue = 0, totalConditionPercentage = 0; int totalItems = 0;
foreach (var item in Inventory.AllItems)
{
if (!MathUtils.NearlyEqual(item.Condition, 0))
{
totalConditionValue += item.Condition;
totalConditionPercentage += item.ConditionPercentage;
totalItems++;
}
}
if (!MathUtils.NearlyEqual(totalConditionValue, prevTotalConditionValue))
{
totalConditionValueString = ((int)totalConditionValue).ToString(CultureInfo.InvariantCulture);
prevTotalConditionValue = totalConditionValue;
}
if (!MathUtils.NearlyEqual(totalConditionPercentage, prevTotalConditionPercentage))
{
totalConditionPercentageString = ((int)totalConditionPercentage).ToString(CultureInfo.InvariantCulture);
prevTotalConditionPercentage = totalConditionPercentage;
}
if (totalItems != prevTotalItems)
{
totalItemsString = totalItems.ToString(CultureInfo.InvariantCulture);
prevTotalItems = totalItems;
}
item.SendSignal(totalConditionValueString, "contained_conditions");
item.SendSignal(totalConditionPercentageString, "contained_conditions_percentage");
item.SendSignal(totalItemsString, "contained_items");
}
if (item.ParentInventory is CharacterInventory ownerInventory)
{
SetContainedItemPositionsIfNeeded();
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
if (AutoInject || subContainersCanAutoInject)
{
//normally autoinjection should delete the (medical) item, so it only gets applied once
//but in multiplayer clients aren't allowed to remove items themselves, so they may be able to trigger this dozens of times
@@ -595,7 +651,7 @@ namespace Barotrauma.Items.Components
SetContainedItemPositionsIfNeeded();
}
}
else if (activeContainedItems.Count == 0)
else if (!hasSignalConnections && activeContainedItems.Count == 0)
{
IsActive = false;
return;
@@ -987,6 +1043,7 @@ namespace Barotrauma.Items.Components
{
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
containableItemIdentifiers = slotRestrictions.SelectMany(s => s.ContainableItems?.SelectMany(ri => ri.Identifiers) ?? Enumerable.Empty<Identifier>()).ToImmutableHashSet();
hasSignalConnections = item.Connections?.Any(c => c.Name is "contained_conditions" or "contained_conditions_percentage" or "contained_items") ?? false;
if (item.Submarine == null || !item.Submarine.Loading)
{
SpawnAlwaysContainedItems();
@@ -1087,9 +1144,9 @@ namespace Barotrauma.Items.Components
}
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap);
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
string containedString = componentElement.GetAttributeString("contained", "");
string[] itemIdStrings = containedString.Split(',');
@@ -589,9 +589,9 @@ namespace Barotrauma.Items.Components
return SaveLimbPositions(base.Save(parentElement));
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap);
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode)
{
LoadLimbPositions(componentElement);
@@ -6,7 +6,7 @@ using Barotrauma.Networking;
namespace Barotrauma.Items.Components
{
partial class Engine : Powered, IServerSerializable, IClientSerializable
partial class Engine : Powered, IServerSerializable, IClientSerializable, IDeteriorateUnderStress
{
private float force;
@@ -76,10 +76,7 @@ namespace Barotrauma.Items.Components
set { force = MathHelper.Clamp(value, -100.0f, 100.0f); }
}
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); }
}
public float CurrentVolume => CurrentStress;
public float CurrentBrokenVolume
{
@@ -90,6 +87,8 @@ namespace Barotrauma.Items.Components
}
}
public float CurrentStress => Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f)));
private const float TinkeringForceIncrease = 1.5f;
public Engine(Item item, ContentXElement element)
@@ -707,9 +707,15 @@ namespace Barotrauma.Items.Components
private static bool AnyOneHasRecipeForItem(Character user, ItemPrefab item)
{
CharacterType mustHaveRecipe = GameMain.GameSession?.GameMode is { IsSinglePlayer: true } ?
//in single player it doesn't matter if it's a bot or a player who has the recipe
//(the bots can turn into a "player" when switching characters, and that could interrupt the fabrication)
CharacterType.Both :
//in MP the recipes other players have don't cound
CharacterType.Bot;
return
(user != null && user.HasRecipeForItem(item.Identifier)) ||
GameSession.GetSessionCrewCharacters(CharacterType.Bot).Any(c => c.HasRecipeForItem(item.Identifier));
GameSession.GetSessionCrewCharacters(mustHaveRecipe).Any(c => c.HasRecipeForItem(item.Identifier));
}
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
@@ -986,9 +992,9 @@ namespace Barotrauma.Items.Components
return componentElement;
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap);
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
savedFabricatedItem = componentElement.GetAttributeString("fabricateditemidentifier", "");
savedTimeUntilReady = componentElement.GetAttributeFloat("savedtimeuntilready", 0.0f);
savedRequiredTime = componentElement.GetAttributeFloat("savedrequiredtime", 0.0f);

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