This commit is contained in:
EvilFactory
2023-06-15 12:13:50 -03:00
210 changed files with 4491 additions and 2580 deletions
@@ -95,10 +95,7 @@ namespace Barotrauma
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
visibleHulls ??= Character.GetVisibleHulls();
return visibleHulls;
}
private set
@@ -425,14 +422,9 @@ namespace Barotrauma
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
if (!pathSteering.CanAccessDoor(door))
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
continue;
}
}
else
@@ -473,7 +465,7 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
if (Character.CurrentHull == null || (isClose && !isClosedDoor) || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{
// Very close to the target, outside, or at the end of the path -> try to steer through the gap
Character.ReleaseSecondaryItem();
@@ -369,7 +369,13 @@ namespace Barotrauma
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet")))
{
// Pets see other pets as pets by default.
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
targetingTag = "pet";
}
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
@@ -695,6 +701,9 @@ namespace Barotrauma
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
//don't try to attack targets in a sub that belongs to a different team
//(for example, targets in an outpost if we're in the main sub)
if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
@@ -894,7 +903,7 @@ namespace Barotrauma
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
var location = memory.Location;
@@ -981,7 +990,7 @@ namespace Barotrauma
}
else
{
PathSteering.SetPath(path);
PathSteering.SetPath(patrolTarget.SimPosition, path);
patrolTimerMargin = 0;
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
searchingNewHull = false;
@@ -1088,13 +1097,13 @@ namespace Barotrauma
Character owner = GetOwner(item);
if (owner != null)
{
if (Character.IsFriendly(owner))
if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
ResetAITarget();
State = AIState.Idle;
return;
}
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
else
{
SelectedAiTarget = owner.AiTarget;
}
@@ -2186,7 +2195,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -2531,8 +2540,10 @@ namespace Barotrauma
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
Vector2 velocity = limbDiff;
if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
@@ -2924,7 +2935,8 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
//no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
@@ -3021,7 +3033,7 @@ namespace Barotrauma
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
@@ -3090,12 +3102,20 @@ namespace Barotrauma
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == Character) { continue; }
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
if (owner != null)
{
continue;
if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
if (Character.IsFriendly(owner))
{
// Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
continue;
}
if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
// ignore if owner is tagged to be explicitly ignored (Feign Death)
continue;
}
}
}
if (targetCharacter != null)
@@ -3418,7 +3438,7 @@ namespace Barotrauma
return false;
}
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
@@ -3428,9 +3448,8 @@ namespace Barotrauma
targetMemories.Add(target, memory);
}
}
if (addIfNotFound)
if (keepAlive)
{
// Keep the memory alive.
memory.Priority = Math.Max(memory.Priority, minPriority);
}
return memory;
@@ -3446,7 +3465,7 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
var memory = GetTargetMemory(_selectedAiTarget);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
@@ -3504,10 +3523,10 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10;
private float stateResetTimer;
private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>();
private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
public void LaunchTrigger(AITrigger trigger)
public void LaunchTrigger(StatusEffect.AITrigger trigger)
{
if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; }
@@ -3527,7 +3546,7 @@ namespace Barotrauma
{
foreach (var triggerObject in activeTriggers)
{
AITrigger trigger = triggerObject.Key;
StatusEffect.AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive)
@@ -3537,7 +3556,7 @@ namespace Barotrauma
inactiveTriggers.Add(trigger);
}
}
foreach (AITrigger trigger in inactiveTriggers)
foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
{
activeTriggers.Remove(trigger);
}
@@ -3643,7 +3662,11 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (!Character.IsPet || !target.IsHuman)
{
//don't turn pets hostile to all humans when attacked by one
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
}
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -42,6 +42,8 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
@@ -436,6 +438,7 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
dirtyHullSafetyCalculations.Remove(h);
}
}
else
@@ -443,9 +446,15 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
dirtyHullSafetyCalculations.Remove(h);
}
}
foreach (Hull h in dirtyHullSafetyCalculations)
{
RefreshHullSafety(h);
}
}
dirtyHullSafetyCalculations.Clear();
if (reportProblemsTimer <= 0.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
@@ -615,7 +624,7 @@ namespace Barotrauma
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
@@ -900,7 +909,7 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
var rootContainer = container.Item.RootContainer ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
@@ -1145,7 +1154,7 @@ namespace Barotrauma
string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
if (Character.Bleeding > 2.0f && !Character.IsMedic)
if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
{
string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
@@ -1658,7 +1667,7 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1891,7 +1900,7 @@ namespace Barotrauma
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
@@ -1900,9 +1909,13 @@ namespace Barotrauma
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
if (hullSafety > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
@@ -22,7 +22,10 @@ namespace Barotrauma
private readonly Character character;
private Vector2 currentTarget;
/// <summary>
/// In sim units.
/// </summary>
private Vector2 currentTargetPos;
private float findPathTimer;
@@ -40,11 +43,6 @@ namespace Barotrauma
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
@@ -54,9 +52,9 @@ namespace Barotrauma
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
public bool IsNextNodeLadder => GetNextLadder() != null;
@@ -64,14 +62,9 @@ namespace Barotrauma
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
var currentLadder = GetCurrentLadder();
if (currentLadder == null) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
return currentLadder == GetNextLadder();
}
}
@@ -107,13 +100,10 @@ namespace Barotrauma
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
public void SetPath(Vector2 targetPos, SteeringPath path)
{
currentTargetPos = targetPos;
currentPath = path;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -136,46 +126,17 @@ namespace Barotrauma
steering += addition;
}
/// <summary>
/// Seeks the ladder from the next and next + 1 nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
}
}
public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
}
return null;
public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
private Ladder GetLadder(WayPoint wp)
{
if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
{
return wp.Ladders;
}
return null;
}
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
@@ -183,19 +144,10 @@ namespace Barotrauma
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{
Vector2 targetDiff = target - currentTarget;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
{
//target in a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
// If the target has moved, we need a new path.
// Different subs are already taken into account before setting the target.
// Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
Vector2 targetDiff = target - currentTargetPos;
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
@@ -205,14 +157,14 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (!needsNewPath && findPathTimer < -1)
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
{
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null)
if (character.Submarine == null && wp.CurrentHull != null)
{
// Current node inside, while we are outside
// -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
@@ -226,7 +178,7 @@ namespace Barotrauma
if (findPathTimer < 0)
{
SkipCurrentPathNodes();
currentTarget = target;
currentTargetPos = target;
Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
@@ -252,6 +204,14 @@ namespace Barotrauma
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
{
// If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
// because it's possible that there's e.g. a closed door between us and the current node,
// and in that case we'd want to use the new path instead of the old.
// There's visibility checks in the pathfinder calls, so the new path should always be ok.
useNewPath = true;
}
bool IsIdenticalPath()
{
@@ -330,6 +290,7 @@ namespace Barotrauma
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
// TODO: might cause some edge cases -> do we need this?
diff.Y = 0.0f;
}
if (diff == Vector2.Zero) { return Vector2.Zero; }
@@ -346,12 +307,12 @@ namespace Barotrauma
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
Vector2 hostPosition = host.SimPosition;
if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
return currentTargetPos - hostPosition;
}
bool doorsChecked = false;
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
@@ -371,14 +332,46 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
Ladder currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
bool useLadders = canClimb && ladders != null;
var collider = character.AnimController.Collider;
Vector2 colliderSize = collider.GetSize();
if (useLadders)
{
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
{
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
useLadders = false;
}
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
{
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
{
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
useLadders = false;
}
}
else if (isDiving && steering.Y < 1)
{
// When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
useLadders = false;
}
}
if (character.IsClimbing && !useLadders)
{
if (currentPath.IsAtEndNode && canClimb && ladders != null)
{
// Don't release the ladders when ending a path in ladders.
useLadders = true;
}
else
{
character.StopClimbing();
}
}
if (useLadders && character.SelectedSecondaryItem != ladders.Item)
{
if (character.CanInteractWith(ladders.Item))
@@ -398,40 +391,28 @@ namespace Barotrauma
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !useLadders)
{
character.StopClimbing();
}
if (character.IsClimbing && useLadders)
{
if (currentLadder == null && nextLadder != null)
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
{
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
}
else
{
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
if (currentLadder != null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
}
//at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
float colliderHeight = collider.Height / 2 + collider.Radius;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
character.StopClimbing();
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
@@ -442,12 +423,36 @@ namespace Barotrauma
}
}
}
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
bool isAboveFloor;
if (diff.Y < 0)
{
NextNode(!doorsChecked);
// When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
float floorY = character.AnimController.FloorY;
isAboveFloor = colliderBottom > floorY;
}
else
{
// When climbing up, let's use the lowest collider (feet).
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
// when a foot is still below the platform.
float heightFromFloor = character.AnimController.GetHeightFromFloor();
isAboveFloor = heightFromFloor > -0.1f;
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
{
NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
character.StopClimbing();
}
}
}
else if (nextLadder != null)
else if (currentLadder != null && currentPath.NextNode != null)
{
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
@@ -466,7 +471,6 @@ namespace Barotrauma
if (door == null || door.CanBeTraversed)
{
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
Vector2 colliderSize = collider.GetSize();
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
@@ -485,7 +489,6 @@ namespace Barotrauma
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
@@ -512,9 +515,12 @@ namespace Barotrauma
}
}
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed))
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow)
{
NextNode(!doorsChecked);
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
{
NextNode(!doorsChecked);
}
}
}
if (currentPath.CurrentNode == null)
@@ -533,9 +539,9 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (door.CanBeTraversed) { return true; }
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
@@ -631,10 +637,12 @@ namespace Barotrauma
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
{
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
if (currentWaypoint.Submarine == null ||
currentWaypoint.Submarine.Info is { IsPlayer: false } ||
!linkedGap.IsRoomToRoom ||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
@@ -213,7 +213,7 @@ namespace Barotrauma
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
@@ -506,6 +506,8 @@ namespace Barotrauma
}
}
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing)
{
if (item == null) { return false; }
@@ -14,6 +14,10 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
@@ -81,8 +85,8 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading &&
container.HasTag(AllowCleanupTag) &&
container.HasAccess(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character) &&
@@ -91,7 +95,6 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{
if (item == null) { return false; }
if (!item.HasAccess(character)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
{
@@ -102,6 +105,7 @@ namespace Barotrauma
}
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
}
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var wire = item.GetComponent<Wire>();
@@ -995,10 +995,18 @@ namespace Barotrauma
}
}
}
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
}
if (matchingItems.Any() &&
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
{
var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true))
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -198,7 +198,7 @@ namespace Barotrauma
TargetName = container.Item.Name,
AbortCondition = obj =>
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) ||
(container.Item.RootContainer?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
@@ -30,7 +30,8 @@ namespace Barotrauma
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head);
protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{
@@ -51,7 +52,7 @@ namespace Barotrauma
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
}
if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) &&
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{
TryAddSubObjective(ref getDivingGear, () =>
@@ -65,7 +66,7 @@ namespace Barotrauma
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true
};
},
@@ -406,7 +406,7 @@ namespace Barotrauma
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
{
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, potentialHull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(potentialHull), character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
hullSafety = 0;
@@ -376,7 +376,7 @@ namespace Barotrauma
// Otherwise it will take some time for us to find a valid item when there are multiple items that we can't reach and some that we can.
// This is relatively expensive, so let's do this only when it significantly improves the behavior.
// Only allow one path find call per frame.
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrder);
}
bool checkPath = CheckPathForEachItem;
// Reset if the character has switched subs.
@@ -536,7 +536,7 @@ namespace Barotrauma
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, character.GetRelativeSimPosition(itemCandidate.item), character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
@@ -26,7 +26,8 @@ namespace Barotrauma
public Func<float> PriorityGetter;
public bool IsFollowOrderObjective;
public bool IsFollowOrder;
public bool IsWaitOrder;
public bool Mimic;
public bool SpeakIfFails { get; set; } = true;
@@ -59,7 +60,7 @@ namespace Barotrauma
{
get
{
if (IsFollowOrderObjective && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
if (IsFollowOrder && Target is Character targetCharacter && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null))
{
// Keep close when the target is going inside/outside
return minDistance;
@@ -220,15 +221,45 @@ namespace Barotrauma
}
}
Hull targetHull = GetTargetHull();
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
// Abandon if going through unsafe paths. Note ignores unsafe nodes when following an order or when the objective is set to ignore unsafe hulls.
bool containsUnsafeNodes = character.IsDismissed && !HumanAIController.ObjectiveManager.CurrentObjective.IgnoreUnsafeHulls
&& PathSteering != null && PathSteering.CurrentPath != null
&& PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
if (containsUnsafeNodes || HumanAIController.UnreachableHulls.Contains(targetHull))
bool isUnreachable = HumanAIController.UnreachableHulls.Contains(targetHull);
if (!objectiveManager.CurrentObjective.IgnoreUnsafeHulls)
{
Abandon = true;
if (HumanAIController.UnsafeHulls.Contains(targetHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(targetHull);
}
else if (PathSteering?.CurrentPath != null)
{
foreach (WayPoint wp in PathSteering.CurrentPath.Nodes)
{
if (wp.CurrentHull == null) { continue; }
if (HumanAIController.UnsafeHulls.Contains(wp.CurrentHull))
{
isUnreachable = true;
HumanAIController.AskToRecalculateHullSafety(wp.CurrentHull);
}
}
}
}
if (isUnreachable)
{
SteeringManager.Reset();
if (PathSteering?.CurrentPath != null)
{
PathSteering.CurrentPath.Unreachable = true;
}
if (repeat)
{
SpeakCannotReach();
}
else
{
Abandon = true;
}
return;
}
}
bool insideSteering = SteeringManager == PathSteering && PathSteering.CurrentPath != null && !PathSteering.IsPathDirty;
@@ -250,6 +281,7 @@ namespace Barotrauma
if (repeat)
{
SpeakCannotReach();
return;
}
else
{
@@ -262,310 +294,285 @@ namespace Barotrauma
waitUntilPathUnreachable = pathWaitingTime;
}
}
if (!Abandon)
if (Abandon) { return; }
if (getDivingGearIfNeeded)
{
if (getDivingGearIfNeeded)
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
{
Character followTarget = Target as Character;
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
if (Mimic && !character.IsImmuneToPressure)
if (HumanAIController.HasDivingSuit(followTarget))
{
if (HumanAIController.HasDivingSuit(followTarget))
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat)
{
if (IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
if (needsDivingSuit)
{
if (character.CanSeeTarget(Target))
{
OnCompleted();
return;
}
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrderObjective &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
// Try again without requiring the diving suit
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: false, objectiveManager),
onAbandon: () =>
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
else
{
TargetGap = null;
}
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
if (repeat && IsCloseEnough)
{
if (requiredCondition == null || requiredCondition())
{
if (character.CanSeeTarget(Target) && (!character.IsClimbing || IsFollowOrder))
{
OnCompleted();
return;
}
}
}
float maxGapDistance = 500;
Character targetCharacter = Target as Character;
if (character.AnimController.InWater)
{
if (character.CurrentHull == null ||
IsFollowOrder &&
targetCharacter != null && (targetCharacter.CurrentHull == null) != (character.CurrentHull == null) &&
Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) < maxGapDistance * maxGapDistance)
{
if (seekGapsTimer > 0)
{
seekGapsTimer -= deltaTime;
}
else
{
TargetGap = null;
}
if (TargetGap != null)
{
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrderObjective ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
if (TargetGap != null)
{
// Check that nothing is blocking the way
Vector2 rayStart = character.SimPosition;
Vector2 rayEnd = TargetGap.SimPosition;
if (TargetGap.Submarine != null && character.Submarine == null)
{
rayStart -= TargetGap.Submarine.SimPosition;
}
else if (TargetGap.Submarine == null && character.Submarine != null)
{
rayEnd -= character.Submarine.SimPosition;
}
var closestBody = Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true);
if (closestBody != null)
{
TargetGap = null;
}
}
}
else
{
TargetGap = null;
}
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
float threshold = 500;
if (isInside)
{
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
if (scooter != null && character.HasEquippedItem(scooter))
{
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
if (TargetGap != null)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
if (TargetGap.FlowTargetHull != null && HumanAIController.SteerThroughGap(TargetGap, IsFollowOrder ? Target.WorldPosition : TargetGap.FlowTargetHull.WorldPosition, deltaTime))
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside)
{
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 1);
return;
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: Target is Item || Target is Character);
TargetGap = null;
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
}
if (checkScooterTimer <= 0)
{
useScooter = false;
checkScooterTimer = checkScooterTime * Rand.Range(0.75f, 1.25f);
Identifier scooterTag = "scooter".ToIdentifier();
Identifier batteryTag = "mobilebattery".ToIdentifier();
Item scooter = null;
bool shouldUseScooter = Mimic && targetCharacter != null && targetCharacter.HasEquippedItem(scooterTag, allowBroken: false);
if (!shouldUseScooter)
{
if (useScooter)
float threshold = 500;
if (isInside)
{
UseScooter(Target.WorldPosition);
Vector2 diff = Target.WorldPosition - character.WorldPosition;
shouldUseScooter = Math.Abs(diff.X) > threshold || Math.Abs(diff.Y) > 150;
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
shouldUseScooter = Vector2.DistanceSquared(character.WorldPosition, Target.WorldPosition) > threshold * threshold;
}
}
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> equippedScooters, recursive: false, requireEquipped: true))
{
// Currently equipped scooter
scooter = equippedScooters.FirstOrDefault();
}
else if (shouldUseScooter)
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> nonEquippedScooters, containedTag: batteryTag, conditionPercentage: 1, requireEquipped: false))
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
// Non-equipped scooter with a battery
scooter = nonEquippedScooters.FirstOrDefault();
hasBattery = true;
}
else if (HumanAIController.HasItem(character, scooterTag, out IEnumerable<Item> _nonEquippedScooters, requireEquipped: false))
{
// Non-equipped scooter without a battery
scooter = _nonEquippedScooters.FirstOrDefault();
// Non-recursive so that the bots won't take batteries from other items. Also means that they can't find batteries inside containers. Not sure how to solve this.
hasBattery = HumanAIController.HasItem(character, batteryTag, out _, requireEquipped: false, conditionPercentage: 1, recursive: false);
}
if (scooter != null && hasBattery)
{
// Equip only if we have a battery available
HumanAIController.TakeItem(scooter, character.Inventory, equip: true, dropOtherIfCannotMove: false, allowSwapping: true, storeUnequipped: false);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
if (scooter != null && character.HasEquippedItem(scooter))
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
if (shouldUseScooter)
{
useScooter = true;
// Check the battery
if (scooter.ContainedItems.None(i => i.Condition > 0))
{
// Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{
useScooter = false;
}
}
else
{
useScooter = false;
}
}
}
if (!useScooter)
{
// Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
}
}
}
else
{
checkScooterTimer -= deltaTime;
}
}
else
{
TargetGap = null;
useScooter = false;
checkScooterTimer = 0;
}
if (SteeringManager == PathSteering)
{
Vector2 targetPos = character.GetRelativeSimPosition(Target);
Func<PathNode, bool> nodeFilter = null;
if (isInside && !AllowGoingOutside)
{
nodeFilter = n => n.Waypoint.CurrentHull != null;
}
else if (!isInside)
{
if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
}
if (!isInside && !UsePathingOutside)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
else
{
PathSteering.SteeringSeek(targetPos, weight: 1,
startNodeFilter: n => (n.Waypoint.CurrentHull == null) == (character.CurrentHull == null),
endNodeFilter: endNodeFilter,
nodeFilter: nodeFilter,
checkVisiblity: Target is Item || Target is Character);
}
if (!isInside && (PathSteering.CurrentPath == null || PathSteering.IsPathDirty || PathSteering.CurrentPath.Unreachable))
{
if (useScooter)
{
@@ -574,13 +581,33 @@ namespace Barotrauma
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 2);
}
}
}
else if (useScooter && PathSteering.CurrentPath?.CurrentNode != null)
{
UseScooter(PathSteering.CurrentPath.CurrentNode.WorldPosition);
}
}
else
{
if (useScooter)
{
UseScooter(Target.WorldPosition);
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
SteeringManager.SteeringAvoid(deltaTime, avoidLookAheadDistance, weight: 15);
}
}
}
void UseScooter(Vector2 targetWorldPos)
@@ -595,7 +622,7 @@ namespace Barotrauma
}
Vector2 diff = character.CursorPosition - character.Position;
Vector2 dir = Vector2.Normalize(diff);
if (character.CurrentHull == null && IsFollowOrderObjective)
if (character.CurrentHull == null && IsFollowOrder)
{
float sqrDist = diff.LengthSquared();
if (sqrDist > MathUtils.Pow2(CloseEnough * 1.5f))
@@ -674,7 +701,7 @@ namespace Barotrauma
{
if (gap.Open < 1) { continue; }
if (gap.Submarine == null) { continue; }
if (!IsFollowOrderObjective)
if (!IsFollowOrder)
{
if (gap.FlowTargetHull == null) { continue; }
if (gap.Submarine != Target.Submarine) { continue; }
@@ -792,8 +819,10 @@ namespace Barotrauma
// Release ladders when ordered to wait at a spawnpoint.
// This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
// Otherwise they might keep holding to the ladders when the target is just next to it.
// Releasing too early should be handled inside the IsCloseEnough property.
character.ReleaseSecondaryItem();
if (character.IsClimbing && character.AnimController.IsAboveFloor)
{
character.StopClimbing();
}
}
base.OnCompleted();
}
@@ -168,7 +168,7 @@ namespace Barotrauma
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null && !IsForbidden(character.CurrentHull))
{
TargetHull = character.CurrentHull;
}
@@ -178,7 +178,7 @@ namespace Barotrauma
IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
{
currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -258,7 +258,8 @@ namespace Barotrauma
currentTarget = ToolBox.SelectWeightedRandom(targetHulls, hullWeights, Rand.RandSync.Unsynced);
bool isInWrongSub = (character.TeamID == CharacterTeamType.FriendlyNPC && !character.IsEscorted) && character.Submarine.TeamID != character.TeamID;
bool isCurrentHullAllowed = !isInWrongSub && !IsForbidden(character.CurrentHull);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, currentTarget.SimPosition, character.Submarine, nodeFilter: node =>
Vector2 targetPos = character.GetRelativeSimPosition(currentTarget);
var path = PathSteering.PathFinder.FindPath(character.SimPosition, targetPos, character.Submarine, nodeFilter: node =>
{
if (node.Waypoint.CurrentHull == null) { return false; }
// Check that there is no unsafe hulls on the way to the target
@@ -278,7 +279,7 @@ namespace Barotrauma
return;
}
character.AIController.SelectTarget(currentTarget.AiTarget);
PathSteering.SetPath(path);
PathSteering.SetPath(targetPos, path);
SetTargetTimerNormal();
searchingNewHull = false;
}
@@ -98,7 +98,7 @@ namespace Barotrauma
{
foreach (var item in itemContainer.ContainableItems)
{
if (CheckStatusEffects(item.statusEffects) == CheckStatus.Finished)
if (CheckStatusEffects(item.StatusEffects) == CheckStatus.Finished)
{
return CheckStatus.Finished;
}
@@ -48,6 +48,8 @@ namespace Barotrauma
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)
@@ -188,6 +190,10 @@ namespace Barotrauma
if (!ignoreList.Contains(target))
{
Targets.Add(target);
if (Targets.Count > MaxTargets)
{
break;
}
}
}
}
@@ -436,7 +436,7 @@ namespace Barotrauma
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
IgnoreIfTargetDead = true,
IsFollowOrderObjective = true,
IsFollowOrder = true,
Mimic = character.IsOnPlayerTeam,
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
};
@@ -444,7 +444,11 @@ namespace Barotrauma
case "wait":
newObjective = new AIObjectiveGoTo(order.TargetSpatialEntity ?? character, character, this, repeat: true, priorityModifier: priorityModifier)
{
AllowGoingOutside = true
AllowGoingOutside = true,
IsWaitOrder = true,
DebugLogWhenFails = false,
SpeakIfFails = false,
CloseEnough = 100
};
break;
case "return":
@@ -13,6 +13,8 @@ namespace Barotrauma
public override bool AllowOutsideSubmarine => true;
public override bool AllowInAnySub => true;
private readonly HashSet<Character> charactersWithMinorInjuries = new HashSet<Character>();
private const float vitalityThreshold = 75;
private const float vitalityThresholdForOrders = 90;
public static float GetVitalityThreshold(AIObjectiveManager manager, Character character, Character target)
@@ -23,17 +25,34 @@ namespace Barotrauma
}
else
{
// When targeting player characters, always treat them when ordered, else use the threshold so that minor/non-severe damage is ignored.
// If we ignore any damage when the player orders a bot to do healings, it's observed to cause confusion among the players.
// On the other hand, if the bots too eagerly heal characters when it's not necessary, it's inefficient and can feel frustrating, because it can't be controlled.
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? (target.IsPlayer && target.HealthPercentage < 100 ? 100 : vitalityThresholdForOrders) : vitalityThreshold;
return character == target || manager.HasOrder<AIObjectiveRescueAll>() ? vitalityThresholdForOrders : vitalityThreshold;
}
}
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
public AIObjectiveRescueAll(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target) => IsValidTarget(target, character);
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character, requireTreatableAfflictions: false)) { return false; }
if (GetTreatableAfflictions(target).Any())
{
return true;
}
else
{
//the target might be at a low enough health to be considered a valid target,
//but if all afflictions are below treatment thresholds, the bot won't (and shouldn't) treat them
// -> make the bot speak to make it clear the bot intentionally ignores very minor injuries
if (!charactersWithMinorInjuries.Contains(character))
{
character.Speak(TextManager.GetWithVariable("dialogignoreminorinjuries", "[targetname]", target.Name).Value,
null, 1.0f, $"notreatableafflictions{target.Name}".ToIdentifier(), 10.0f);
charactersWithMinorInjuries.Add(character);
}
return false;
}
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
@@ -68,7 +87,7 @@ namespace Barotrauma
float vitality = 100;
vitality -= character.Bleeding * 2;
vitality += Math.Min(character.Oxygen, 0);
foreach (Affliction affliction in GetTreatableAfflictions(character))
foreach (Affliction affliction in GetTreatableAfflictions(character, ignoreTreatmentThreshold: true))
{
float strength = character.CharacterHealth.GetPredictedStrength(affliction, predictFutureDuration: 10.0f);
vitality -= affliction.GetVitalityDecrease(character.CharacterHealth, strength) / character.MaxVitality * 100;
@@ -84,12 +103,19 @@ namespace Barotrauma
return Math.Clamp(vitality, 0, 100);
}
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character)
public static IEnumerable<Affliction> GetTreatableAfflictions(Character character, bool ignoreTreatmentThreshold = false)
{
var allAfflictions = character.CharacterHealth.GetAllAfflictions();
foreach (Affliction affliction in allAfflictions)
{
if (affliction.Prefab.IsBuff || affliction.Strength < affliction.Prefab.TreatmentThreshold) { continue; }
if (affliction.Prefab.IsBuff) { continue; }
if (!ignoreTreatmentThreshold)
{
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = character.CharacterHealth.GetTotalAdjustedAfflictionStrength(affliction);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
}
if (affliction.Prefab.TreatmentSuitability.None(kvp => kvp.Value > 0)) { continue; }
if (allAfflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Identifier))) { continue; }
yield return affliction;
@@ -102,7 +128,7 @@ namespace Barotrauma
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
=> HumanAIController.RemoveTargets<AIObjectiveRescueAll, Character>(character, target);
public static bool IsValidTarget(Character target, Character character)
public static bool IsValidTarget(Character target, Character character, bool requireTreatableAfflictions = true)
{
if (target == null || target.IsDead || target.Removed) { return false; }
if (target.IsInstigator) { return false; }
@@ -112,7 +138,7 @@ namespace Barotrauma
{
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
{
return false;
return false;
}
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
{
@@ -127,6 +153,10 @@ namespace Barotrauma
return false;
}
}
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
{
return false;
}
}
else
{
@@ -159,5 +189,11 @@ namespace Barotrauma
}
return character.GetDamageDoneByAttacker(target) <= 0;
}
public override void Reset()
{
base.Reset();
charactersWithMinorInjuries.Clear();
}
}
}
@@ -51,7 +51,7 @@ namespace Barotrauma
return;
}
if (!IsRemotePlayer && !(AIController is HumanAIController))
if (!IsRemotePlayer && AIController is not HumanAIController)
{
float characterDistSqr = GetDistanceSqrToClosestPlayer();
if (characterDistSqr > MathUtils.Pow2(Params.DisableDistance * 0.5f))
@@ -63,6 +63,10 @@ namespace Barotrauma
AnimController.SimplePhysicsEnabled = false;
}
}
else
{
AnimController.SimplePhysicsEnabled = false;
}
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
if (Controlled == this) { return; }
@@ -1,10 +1,10 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
@@ -1550,6 +1550,10 @@ namespace Barotrauma
target.AnimController.ResetPullJoints();
}
bool targetPoseControlled =
target.SelectedItem?.GetComponent<Controller>() is { ControlCharacterPose: true } ||
target.SelectedSecondaryItem?.GetComponent<Controller>() is { ControlCharacterPose: true };
if (IsClimbing)
{
//cannot drag up ladders if the character is conscious
@@ -1725,13 +1729,12 @@ namespace Barotrauma
targetForce = 5000.0f;
}
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
if (diff.LengthSquared() > 0.1f)
if (!targetPoseControlled)
{
targetLimb.PullJointEnabled = true;
targetLimb.PullJointMaxForce = targetForce;
targetLimb.PullJointWorldAnchorB = targetAnchor;
targetLimb.Disabled = true;
target.AnimController.movement = -diff;
}
}
@@ -1757,7 +1760,7 @@ namespace Barotrauma
target.AnimController.IgnorePlatforms = IgnorePlatforms;
target.AnimController.TargetMovement = TargetMovement;
}
else if (target is AICharacter && target != Character.Controlled)
else if (target is AICharacter && target != Character.Controlled && !targetPoseControlled)
{
if (target.AnimController.Dir > 0 == WorldPosition.X > target.WorldPosition.X)
{
@@ -1205,7 +1205,7 @@ namespace Barotrauma
{
inWater = false;
headInWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
}
//ragdoll isn't in any room -> it's in the water
else if (currentHull == null)
@@ -1217,7 +1217,7 @@ namespace Barotrauma
{
headInWater = false;
inWater = false;
RefreshFloorY(ignoreStairs: Stairs == null);
RefreshFloorY(deltaTime, ignoreStairs: Stairs == null);
if (currentHull.WaterPercentage > 0.001f)
{
(float waterSurfaceDisplayUnits, float ceilingDisplayUnits) = GetWaterSurfaceAndCeilingY();
@@ -1562,15 +1562,24 @@ namespace Barotrauma
lastFloorCheckPos = Vector2.Zero;
}
private void RefreshFloorY(Limb refLimb = null, bool ignoreStairs = false)
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
if (Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f || lastFloorCheckIgnoreStairs != ignoreStairs || lastFloorCheckIgnorePlatforms != IgnorePlatforms)
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
Vector2.DistanceSquared(lastFloorCheckPos, refBody.SimPosition) > 0.1f * 0.1f)
{
floorY = GetFloorY(refBody.SimPosition, ignoreStairs);
lastFloorCheckPos = refBody.SimPosition;
lastFloorCheckIgnoreStairs = ignoreStairs;
lastFloorCheckIgnorePlatforms = IgnorePlatforms;
// Add some randomness to prevent all stationary characters doing the checks at the same frame.
floorYCheckTimer = FloorYStaleTime * Rand.Range(0.9f, 1.1f);
}
}
@@ -1854,6 +1863,7 @@ namespace Barotrauma
private bool collisionsDisabled;
private double lastObstacleRayCastTime;
protected void CheckDistFromCollider()
{
@@ -1861,15 +1871,28 @@ namespace Barotrauma
allowedDist = Math.Max(allowedDist, 1.0f);
float resetDist = allowedDist * 5.0f;
float obstacleCheckDist = 0.3f;
Vector2 diff = Collider.SimPosition - MainLimb.SimPosition;
float distSqrd = diff.LengthSquared();
if (distSqrd > resetDist * resetDist)
bool shouldReset = distSqrd > resetDist * resetDist;
if (!shouldReset && distSqrd > obstacleCheckDist * obstacleCheckDist)
{
if (Timing.TotalTime > lastObstacleRayCastTime + 1 &&
Submarine.PickBody(Collider.SimPosition, MainLimb.SimPosition, collisionCategory: Physics.CollisionWall) != null)
{
shouldReset = true;
lastObstacleRayCastTime = Timing.TotalTime;
}
}
if (shouldReset)
{
//ragdoll way too far, reset position
SetPosition(Collider.SimPosition, lerp: true, forceMainLimbToCollider: true);
}
if (distSqrd > allowedDist * allowedDist)
else if (distSqrd > allowedDist * allowedDist)
{
//ragdoll too far from the collider, disable collisions until it's close enough
//(in case the ragdoll has gotten stuck somewhere)
@@ -1891,7 +1914,7 @@ namespace Barotrauma
collisionsDisabled = false;
//force collision categories to be updated
prevCollisionCategory = Category.None;
}
}
}
partial void UpdateNetPlayerPositionProjSpecific(float deltaTime, float lowestSubPos);
@@ -1068,7 +1068,7 @@ namespace Barotrauma
{
get
{
return SelectedItem == null || (SelectedItem.GetComponent<Controller>()?.AllowAiming ?? false);
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
}
}
@@ -1131,6 +1131,7 @@ namespace Barotrauma
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public bool IsInPlayerSub => Submarine != null && Submarine.Info.IsPlayer;
public float AITurretPriority
{
@@ -2710,27 +2711,6 @@ namespace Barotrauma
CustomInteractHUDText = hudText;
}
private void TransformCursorPos()
{
if (Submarine == null)
{
//character is outside but cursor position inside
if (cursorPosition.Y > Level.Loaded.Size.Y)
{
var sub = Submarine.FindContaining(cursorPosition);
if (sub != null) cursorPosition += sub.Position;
}
}
else
{
//character is inside but cursor position is outside
if (cursorPosition.Y < Level.Loaded.Size.Y)
{
cursorPosition -= Submarine.Position;
}
}
}
public void SelectCharacter(Character character)
{
if (character == null || character == this) { return; }
@@ -4335,7 +4315,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (LastAttacker == null || !LastAttacker.IsPlayer)
{
@@ -4393,6 +4373,10 @@ namespace Barotrauma
{
statusEffect.Apply(actionType, deltaTime, this, this);
}
if (statusEffect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
{
statusEffect.Apply(actionType, deltaTime, this, CurrentHull);
}
}
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
@@ -4670,6 +4654,13 @@ namespace Barotrauma
CharacterList.Remove(this);
foreach (var attachedProjectile in AttachedProjectiles.ToList())
{
attachedProjectile.Unstick();
}
Latchers.ForEachMod(l => l?.DeattachFromBody(reset: true));
Latchers.Clear();
if (Inventory != null)
{
foreach (Item item in Inventory.AllItems)
@@ -4756,6 +4747,8 @@ namespace Barotrauma
}
#if SERVER
newItem.GetComponent<Terminal>()?.SyncHistory();
if (newItem.GetComponent<WifiComponent>() is WifiComponent wifiComponent) { newItem.CreateServerEvent(wifiComponent); }
if (newItem.GetComponent<GeneticMaterial>() is GeneticMaterial geneticMaterial) { newItem.CreateServerEvent(geneticMaterial); }
#endif
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
if (!slotIndices.Any())
@@ -386,6 +386,8 @@ namespace Barotrauma
{
foreach (AfflictionPrefab.PeriodicEffect periodicEffect in Prefab.PeriodicEffects)
{
if (Strength <= periodicEffect.MinStrength) { continue; }
if (periodicEffect.MaxStrength > 0 && Strength > periodicEffect.MaxStrength) { continue; }
PeriodicEffectTimers[periodicEffect] -= deltaTime;
if (PeriodicEffectTimers[periodicEffect] <= 0.0f)
{
@@ -498,6 +500,13 @@ namespace Barotrauma
/// </summary>
public void SetStrength(float strength)
{
if (!MathUtils.IsValid(strength))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({strength})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
_nonClampedStrength = strength;
_strength = _nonClampedStrength;
activeEffectDirty |= !MathUtils.NearlyEqual(_strength, prevActiveEffectStrength);
@@ -730,13 +730,13 @@ namespace Barotrauma
/// <summary>
/// How high the strength has to be for the affliction icon to be shown with a health scanner
/// </summary>
public readonly float ShowInHealthScannerThreshold = 0.05f;
public readonly float ShowInHealthScannerThreshold;
/// <summary>
/// How strong the affliction needs to be before bots attempt to treat it.
/// Also effects when the affliction is shown in the suitable treatments list.
/// </summary>
public readonly float TreatmentThreshold = 5.0f;
public readonly float TreatmentThreshold;
/// <summary>
/// Bots will not try to treat the affliction if the character has any of these afflictions
@@ -847,7 +847,7 @@ namespace Barotrauma
{
foreach (var itemPrefab in ItemPrefab.Prefabs)
{
float suitability = Math.Max(itemPrefab.GetTreatmentSuitability(Identifier), itemPrefab.GetTreatmentSuitability(AfflictionType));
float suitability = itemPrefab.GetTreatmentSuitability(Identifier) + itemPrefab.GetTreatmentSuitability(AfflictionType);
if (!MathUtils.NearlyEqual(suitability, 0.0f))
{
yield return new KeyValuePair<Identifier, float>(itemPrefab.Identifier, suitability);
@@ -915,7 +915,7 @@ namespace Barotrauma
ShowInHealthScannerThreshold = element.GetAttributeFloat(nameof(ShowInHealthScannerThreshold),
Math.Max(ActivationThreshold, AfflictionType == "talentbuff" ? float.MaxValue : ShowIconToOthersThreshold));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 5.0f));
TreatmentThreshold = element.GetAttributeFloat(nameof(TreatmentThreshold), Math.Max(ActivationThreshold, 10.0f));
DamageOverlayAlpha = element.GetAttributeFloat(nameof(DamageOverlayAlpha), 0.0f);
BurnOverlayAlpha = element.GetAttributeFloat(nameof(BurnOverlayAlpha), 0.0f);
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
@@ -422,9 +423,13 @@ namespace Barotrauma
return strength;
}
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true)
public void ApplyAffliction(Limb targetLimb, Affliction affliction, bool allowStacking = true, bool ignoreUnkillability = false)
{
if (!affliction.Prefab.IsBuff && Unkillable || Character.GodMode) { return; }
if (Character.GodMode) { return; }
if (!ignoreUnkillability)
{
if (!affliction.Prefab.IsBuff && Unkillable) { return; }
}
if (affliction.Prefab.LimbSpecific)
{
if (targetLimb == null)
@@ -455,11 +460,7 @@ namespace Barotrauma
var affliction = kvp.Key;
resistance += affliction.GetResistance(afflictionPrefab.Identifier);
}
resistance = 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
if (resistance > 1f) { resistance = 1f; }
return resistance;
return 1 - ((1 - resistance) * Character.GetAbilityResistance(afflictionPrefab));
}
public float GetStatValue(StatTypes statType)
@@ -990,7 +991,7 @@ namespace Barotrauma
#endif
}
private float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
private static float GetVitalityMultiplier(Affliction affliction, LimbHealth limbHealth)
{
float multiplier = 1.0f;
if (limbHealth.VitalityMultipliers.TryGetValue(affliction.Prefab.Identifier, out float vitalityMultiplier))
@@ -1132,7 +1133,11 @@ namespace Barotrauma
strength = GetPredictedStrength(affliction, predictFutureDuration, limb);
}
if (strength <= affliction.Prefab.TreatmentThreshold) { continue; }
//other afflictions of the same type increase the "treatability"
// e.g. we might want to ignore burns below 5%, but not if the character has them on all limbs
float totalAfflictionStrength = strength + GetTotalAdjustedAfflictionStrength(affliction, includeSameAffliction: false);
if (totalAfflictionStrength < affliction.Prefab.TreatmentThreshold) { continue; }
if (afflictions.Any(otherAffliction => affliction.Prefab.IgnoreTreatmentIfAfflictedBy.Contains(otherAffliction.Key.Identifier))) { continue; }
if (ignoreHiddenAfflictions)
@@ -1149,13 +1154,20 @@ namespace Barotrauma
foreach (KeyValuePair<Identifier, float> treatment in affliction.Prefab.TreatmentSuitability)
{
float suitability = treatment.Value * strength;
if (treatment.Value > strength)
{
//avoid using very effective meds on small injuries
float overtreatmentFactor = MathHelper.Clamp(treatment.Value / strength, 1.0f, 10.0f);
suitability /= overtreatmentFactor;
}
if (!treatmentSuitability.ContainsKey(treatment.Key))
{
treatmentSuitability[treatment.Key] = treatment.Value * strength;
treatmentSuitability[treatment.Key] = suitability;
}
else
{
treatmentSuitability[treatment.Key] += treatment.Value * strength;
treatmentSuitability[treatment.Key] += suitability;
}
minSuitability = Math.Min(treatmentSuitability[treatment.Key], minSuitability);
maxSuitability = Math.Max(treatmentSuitability[treatment.Key], maxSuitability);
@@ -1171,6 +1183,28 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns the total strength of instances of the same affliction on all the characters limbs,
/// with a smaller weight given to the other afflictions on other limbs
/// </summary>
/// <param name="otherAfflictionMultiplier">Multiplier on the strengths of the afflictions on other limbs.</param>
/// <param name="includeSameAffliction">Should the strength of the provided affliction be included too?</param>
public float GetTotalAdjustedAfflictionStrength(Affliction affliction, float otherAfflictionMultiplier = 0.3f, bool includeSameAffliction = true)
{
float totalAfflictionStrength = includeSameAffliction ? affliction.Strength : 0;
if (affliction.Prefab.LimbSpecific)
{
foreach (Affliction otherAffliction in afflictions.Keys)
{
if (affliction.Prefab == otherAffliction.Prefab && affliction != otherAffliction)
{
totalAfflictionStrength += otherAffliction.Strength * otherAfflictionMultiplier;
}
}
}
return totalAfflictionStrength;
}
private readonly HashSet<Identifier> afflictionTags = new HashSet<Identifier>();
public IEnumerable<Identifier> GetActiveAfflictionTags()
{
@@ -182,7 +182,9 @@ namespace Barotrauma
{
humanAI.ObjectiveManager.SetForcedOrder(new AIObjectiveGoTo(positionToStayIn, npc, humanAI.ObjectiveManager, repeat: true, getDivingGearIfNeeded: false, closeEnough: 200)
{
DebugLogWhenFails = false
DebugLogWhenFails = false,
IsWaitOrder = true,
CloseEnough = 100
});
}
}
@@ -1226,7 +1226,7 @@ namespace Barotrauma
if (statusEffect.type == ActionType.OnDamaged)
{
if (!statusEffect.HasRequiredAfflictions(character.LastDamage)) { continue; }
if (statusEffect.OnlyPlayerTriggered)
if (statusEffect.OnlyWhenDamagedByPlayer)
{
if (character.LastAttacker == null || !character.LastAttacker.IsPlayer)
{
@@ -231,12 +231,17 @@ namespace Barotrauma
protected void CreateSubParams()
{
SubParams.Clear();
var health = MainElement.GetChildElement("health");
if (health != null)
var healthElement = MainElement.GetChildElement("health");
if (healthElement != null)
{
Health = new HealthParams(health, this);
SubParams.Add(Health);
Health = new HealthParams(healthElement, this);
}
else
{
DebugConsole.ThrowError($"No health parameters defined for character \"{(SpeciesName)}\".");
Health = new HealthParams(null, this);
}
SubParams.Add(Health);
// TODO: support for multiple ai elements?
var ai = MainElement.GetChildElement("ai");
if (ai != null)
@@ -29,7 +29,7 @@ namespace Barotrauma.Abilities
nearbyCharactersAppliesToEnemies = abilityElement.GetAttributeBool("nearbycharactersappliestoenemies", true);
}
protected void ApplyEffectSpecific(Character targetCharacter)
protected void ApplyEffectSpecific(Character targetCharacter, Limb targetLimb = null)
{
//prevent an infinite loop if an effect triggers itself
//(e.g. a talent that triggers when an affliction is applied, and applies that same affliction)
@@ -66,6 +66,11 @@ namespace Barotrauma.Abilities
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb) && targetLimb != null)
{
statusEffect.SetUser(Character);
statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetLimb);
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.SetUser(Character);
@@ -99,7 +104,7 @@ namespace Barotrauma.Abilities
{
if ((abilityObject as IAbilityCharacter)?.Character is Character targetCharacter && !applyToSelf)
{
ApplyEffectSpecific(targetCharacter);
ApplyEffectSpecific(targetCharacter, targetLimb: (abilityObject as AbilityApplyTreatment)?.TargetLimb);
}
else
{
@@ -22,15 +22,13 @@
{
foreach (Identifier afflictionIdentifier in afflictionIdentifiers)
{
if (affliction.Identifier != afflictionIdentifier) { continue; }
affliction.Strength *= 1 + addedMultiplier;
if (affliction.Identifier != afflictionIdentifier) { continue; }
AfflictionPrefab afflictionPrefab = affliction.Prefab;
if (!replaceWith.IsEmpty)
{
if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab))
{
abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength);
}
}
AfflictionPrefab.Prefabs.TryGet(replaceWith, out afflictionPrefab);
}
abilityAffliction.Affliction = new Affliction(afflictionPrefab, affliction.Strength * (1 + addedMultiplier));
}
}
else
@@ -1,12 +1,11 @@
#nullable enable
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
@@ -64,9 +63,14 @@ namespace Barotrauma
public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def);
public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def);
public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return:NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier>? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
[return: NotNullIfNotNull("def")]
public Identifier[] GetAttributeIdentifierArray(Identifier[] def, params string[] keys) => Element.GetAttributeIdentifierArray(def, keys);
[return: NotNullIfNotNull("def")]
public Identifier[] GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim);
[return: NotNullIfNotNull("def")]
public ImmutableHashSet<Identifier> GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet<Identifier>? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim);
public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def);
public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def);
public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant);
@@ -1879,7 +1879,9 @@ namespace Barotrauma
commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("debugwiring", "Toggle the wiring debug mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false));
commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true));
commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null));
@@ -12,21 +12,112 @@ namespace Barotrauma
Exponential
}
/// <summary>
/// ActionTypes define when a <see cref="StatusEffect"/> is executed.
/// </summary>
public enum ActionType
{
Always = 0, OnPicked = 1, OnUse = 2, OnSecondaryUse = 3,
OnWearing = 4, OnContaining = 5, OnContained = 6, OnNotContained = 7,
OnActive = 8, OnFailure = 9, OnBroken = 10,
OnFire = 11, InWater = 12, NotInWater = 13,
/// <summary>
/// Executes every frame regardless of the state of the entity.
/// </summary>
Always = 0,
/// <summary>
/// Executes when the item is picked up. Only valid for items.
/// </summary>
OnPicked = 1,
/// <summary>
/// Executes when the item is used. The meaning of "using" an item depends on the item, but generally it means the action that happens when holding the item and clicking LMB. Only valid for items.
/// </summary>
OnUse = 2,
/// <summary>
/// Executes when an item is held and the aim key is held. Only valid for items.
/// </summary>
OnSecondaryUse = 3,
/// <summary>
/// Executes continuously while the item is being worn. Only valid for wearable items.
/// </summary>
OnWearing = 4,
/// <summary>
/// Executes continuously when a specific Containable is inside an ItemContainer. Only valid for Containables defined in an ItemContainer component.
/// </summary>
OnContaining = 5,
/// <summary>
/// Executes continuously when the item is contained in some inventory. Only valid for items.
/// </summary>
OnContained = 6,
/// <summary>
/// Executes continuously when the item is NOT contained in an inventory. Only valid for items.
/// </summary>
OnNotContained = 7,
/// <summary>
/// Executes continuously when the item is active. The meaning of "active" depends on the item, but generally means the item is on, powered, and doing the thing it's intended for. Only valid for items.
/// </summary>
OnActive = 8,
/// <summary>
/// Executes when using the item fails due to a failed skill check. Only valid for items.
/// </summary>
OnFailure = 9,
/// <summary>
/// Executes when using the item's condition drops to 0. Only valid for items.
/// </summary>
OnBroken = 10,
/// <summary>
/// Executes continuously when the entity is within the damage range of fire. Valid for items and characters.
/// </summary>
OnFire = 11,
/// <summary>
/// Executes continuously when the entity is submerged. Valid for items and characters.
/// </summary>
InWater = 12,
/// <summary>
/// Executes continuously when the entity is NOT submerged. Valid for items and characters.
/// </summary>
NotInWater = 13,
/// <summary>
/// Executes when the entity hits something hard enough. For items, the threshold is determined by <see cref="ItemPrefab.ImpactTolerance"/>,
/// for characters by <see cref="Ragdoll.ImpactTolerance"/>. Valid for items and characters.
/// </summary>
OnImpact = 14,
/// <summary>
/// Executes continuously when the character is eating another character. Only valid for characters.
/// </summary>
OnEating = 15,
/// <summary>
/// Executes when the entity receives damage from an external source (i.e. an affliction that increases in severity, or an item degrading by itself don't count).
/// Valid for items and characters.
/// </summary>
OnDamaged = 16,
/// <summary>
/// Executes when the limb gets severed. Only valid for limbs.
/// </summary>
OnSevered = 17,
/// <summary>
/// Executes when a <see cref="Items.Components.Growable"/> produces an item (e.g. when a plant grows a fruit). Only valid for Growable items.
/// </summary>
OnProduceSpawned = 18,
OnOpen = 19, OnClose = 20,
/// <summary>
/// Executes when a <see cref="Items.Components.Door"/> is opened. Only valid for doors.
/// </summary>
OnOpen = 19,
/// <summary>
/// Executes when a <see cref="Items.Components.Door"/> is closed. Only valid for doors.
/// </summary>
OnClose = 20,
/// <summary>
/// Executes when the entity spawns. Valid for items and characters.
/// </summary>
OnSpawn = 21,
/// <summary>
/// Executes when using the item succeeds based on a skill check. Only valid for items.
/// </summary>
OnSuccess = 22,
/// <summary>
/// Executes when an Ability (an effect from a talent) triggers the status effect. Only valid in Abilities, the target can be either a character or an item depending on the type of Ability.
/// </summary>
OnAbility = 23,
/// <summary>
/// Executes when the character dies. Only valid for characters.
/// </summary>
OnDeath = OnBroken
}
@@ -49,7 +49,7 @@ namespace Barotrauma
var limb = character.AnimController.GetLimb(LimbType);
if (Strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength));
character.CharacterHealth.ApplyAffliction(limb, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
{
@@ -60,7 +60,7 @@ namespace Barotrauma
{
if (Strength > 0.0f)
{
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength));
character.CharacterHealth.ApplyAffliction(null, afflictionPrefab.Instantiate(Strength), ignoreUnkillability: true);
}
else if (Strength < 0.0f)
{
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Barotrauma
{
class CheckSelectedItemAction : BinaryOptionAction
class CheckSelectedAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
@@ -16,7 +16,7 @@ namespace Barotrauma
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
public SelectedItemType ItemType { get; set; }
public CheckSelectedItemAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
@@ -34,7 +34,7 @@ namespace Barotrauma
}
if (character == null)
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
return false;
}
if (!TargetTag.IsEmpty)
@@ -42,11 +42,16 @@ namespace Barotrauma
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (targets.None())
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
Error($"{nameof(CheckSelectedAction)} error: {GetEventName()} uses a {nameof(CheckSelectedAction)} but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
foreach (var target in targets)
{
if (target is Character targetCharacter)
{
if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; }
continue;
}
if (target is not Item targetItem)
{
continue;
@@ -79,6 +84,18 @@ namespace Barotrauma
_ => false
};
}
#if DEBUG
void Error(string errorMsg)
{
DebugConsole.ThrowError(errorMsg);
}
#else
void Error(string errorMsg)
{
DebugConsole.LogError(errorMsg);
}
#endif
}
private string GetEventName()
@@ -1,94 +0,0 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
{
class CheckSelectedAction : BinaryOptionAction
{
public enum SelectedItemType { Primary, Secondary, Any };
[Serialize("", IsPropertySaveable.Yes)]
public Identifier CharacterTag { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier TargetTag { get; set; }
[Serialize(SelectedItemType.Any, IsPropertySaveable.Yes)]
public SelectedItemType ItemType { get; set; }
public CheckSelectedAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
protected override bool? DetermineSuccess()
{
Character character = null;
if (!CharacterTag.IsEmpty)
{
foreach (var t in ParentEvent.GetTargets(CharacterTag))
{
if (t is Character c)
{
character = c;
break;
}
}
}
if (character == null)
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid character was found for tag \"{CharacterTag}\"! This will cause the check to automatically fail.");
return false;
}
if (!TargetTag.IsEmpty)
{
IEnumerable<Entity> targets = ParentEvent.GetTargets(TargetTag);
if (targets.None())
{
DebugConsole.LogError($"CheckSelectedItemAction error: {GetEventName()} uses a CheckSelectedItemAction but no valid targets were found for tag \"{TargetTag}\"! This will cause the check to automatically fail.");
return false;
}
foreach (var target in targets)
{
if (target is Character targetCharacter)
{
if (ItemType == SelectedItemType.Any && character.SelectedCharacter == targetCharacter) { return true; }
continue;
}
if (target is not Item targetItem)
{
continue;
}
if (IsSelected(targetItem))
{
return true;
}
}
return false;
bool IsSelected(Item item)
{
return ItemType switch
{
SelectedItemType.Any => character.IsAnySelectedItem(item),
SelectedItemType.Primary => character.SelectedItem == item,
SelectedItemType.Secondary => character.SelectedSecondaryItem == item,
_ => false
};
}
}
else
{
return ItemType switch
{
SelectedItemType.Any => !character.HasSelectedAnyItem,
SelectedItemType.Primary => character.SelectedItem == null,
SelectedItemType.Secondary => character.SelectedSecondaryItem == null,
_ => false
};
}
}
private string GetEventName()
{
return ParentEvent?.Prefab?.Identifier is { IsEmpty: false } identifier ? $"the event \"{identifier}\"" : "an unknown event";
}
}
}
@@ -60,18 +60,42 @@ namespace Barotrauma
{
if (isFinished) { return; }
Identifier missionDebugId = (MissionIdentifier.IsEmpty ? MissionTag : MissionIdentifier);
if (GameMain.GameSession.GameMode is CampaignMode campaign)
{
Mission unlockedMission = null;
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes);
var unlockLocation = FindUnlockLocation(MinLocationDistance, UnlockFurtherOnMap, LocationTypes, mustAllowLocationTypeChanges: false);
if (unlockLocation == null && UnlockFurtherOnMap)
{
DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" further on the map. Attempting to find a location earlier on the map...");
unlockLocation = FindUnlockLocation(MinLocationDistance, unlockFurtherOnMap: false, LocationTypes, mustAllowLocationTypeChanges: false);
}
if (unlockLocation == null && CreateLocationIfNotFound)
{
DebugConsole.NewMessage($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\". Attempting to change the type of an empty location to create a suitable location...");
//find an empty location at least 3 steps away, further on the map
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable());
var emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: true, "none".ToIdentifier().ToEnumerable(),
mustAllowLocationTypeChanges: true,
requireCorrectFaction: false);
if (emptyLocation == null)
{
DebugConsole.NewMessage($"Failed to find a suitable empty location further on the map. Attempting to find a location earlier on the map...");
emptyLocation = FindUnlockLocation(Math.Max(MinLocationDistance, 3), unlockFurtherOnMap: false, "none".ToIdentifier().ToEnumerable(),
mustAllowLocationTypeChanges: true,
requireCorrectFaction: false);
}
if (emptyLocation != null)
{
System.Diagnostics.Debug.Assert(!emptyLocation.LocationTypeChangesBlocked);
emptyLocation.ChangeType(campaign, LocationType.Prefabs[LocationTypes[0]]);
unlockLocation = emptyLocation;
if (!RequiredFaction.IsEmpty)
{
emptyLocation.Faction = campaign.Factions.Find(f => f.Prefab.Identifier == RequiredFaction);
}
}
}
@@ -115,13 +139,13 @@ namespace Barotrauma
}
else
{
DebugConsole.AddWarning($"Failed to find a suitable location to unlock a mission in (LocationType: {LocationTypes}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
DebugConsole.AddWarning($"Failed to find a suitable location to unlock the mission \"{missionDebugId}\" (LocationType: {string.Join(", ", LocationTypes)}, MinLocationDistance: {MinLocationDistance}, UnlockFurtherOnMap: {UnlockFurtherOnMap})");
}
}
isFinished = true;
}
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes)
private Location FindUnlockLocation(int minDistance, bool unlockFurtherOnMap, IEnumerable<Identifier> locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction = true)
{
var campaign = GameMain.GameSession.GameMode as CampaignMode;
if (LocationTypes.Length == 0 && minDistance <= 1)
@@ -140,7 +164,7 @@ namespace Barotrauma
foreach (var location in currentLocations)
{
checkedLocations.Add(location);
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes))
if (IsLocationValid(currentLocation, location, unlockFurtherOnMap, distance, minDistance, locationTypes, mustAllowLocationTypeChanges, requireCorrectFaction))
{
return location;
}
@@ -160,9 +184,13 @@ namespace Barotrauma
return null;
}
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes)
private bool IsLocationValid(Location currLocation, Location location, bool unlockFurtherOnMap, int distance, int minDistance, IEnumerable<Identifier> locationTypes, bool mustAllowLocationTypeChanges, bool requireCorrectFaction)
{
if (!RequiredFaction.IsEmpty)
if (mustAllowLocationTypeChanges && location.LocationTypeChangesBlocked)
{
return false;
}
if (requireCorrectFaction && !RequiredFaction.IsEmpty)
{
if (location.Faction?.Prefab.Identifier != RequiredFaction &&
location.SecondaryFaction?.Prefab.Identifier != RequiredFaction)
@@ -15,6 +15,8 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes)]
public bool AddToCrew { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool RemoveFromCrew { get; set; }
@@ -38,6 +40,8 @@ namespace Barotrauma
{
if (isFinished) { return; }
bool isPlayerTeam = TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
affectedNpcs = ParentEvent.GetTargets(NPCTag).Where(c => c is Character).Select(c => c as Character).ToList();
foreach (var npc in affectedNpcs)
{
@@ -49,9 +53,13 @@ namespace Barotrauma
if (idCard != null)
{
idCard.TeamID = TeamID;
if (isPlayerTeam)
{
idCard.SubmarineSpecificID = 0;
}
}
}
if (AddToCrew && (TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2))
if (AddToCrew && isPlayerTeam)
{
npc.Info.StartItemsGiven = true;
GameMain.GameSession.CrewManager.AddCharacter(npc);
@@ -46,7 +46,7 @@ namespace Barotrauma
var newObjective = new AIObjectiveGoTo(target, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f,
IsFollowOrderObjective = true
IsFollowOrder = true
};
humanAiController.ObjectiveManager.AddObjective(newObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -35,7 +35,9 @@ namespace Barotrauma
AIObjectiveGoTo.GetTargetHull(npc) as ISpatialEntity ?? npc, npc, humanAiController.ObjectiveManager, repeat: true)
{
OverridePriority = 100.0f,
SourceEventAction = this
SourceEventAction = this,
IsWaitOrder = true,
CloseEnough = 100
};
humanAiController.ObjectiveManager.AddObjective(gotoObjective);
humanAiController.ObjectiveManager.WaitTimer = 0.0f;
@@ -1,12 +1,13 @@
using System.Xml.Linq;
namespace Barotrauma
namespace Barotrauma
{
class TriggerEventAction : EventAction
{
[Serialize("", IsPropertySaveable.Yes)]
public Identifier Identifier { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool NextRound { get; set; }
private bool isFinished;
public TriggerEventAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -26,17 +27,24 @@ namespace Barotrauma
if (GameMain.GameSession?.EventManager != null)
{
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
if (NextRound)
{
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
GameMain.GameSession.EventManager.QueuedEventsForNextRound.Enqueue(Identifier);
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
var eventPrefab = EventSet.GetEventPrefab(Identifier);
if (eventPrefab == null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
DebugConsole.ThrowError($"Error in TriggerEventAction - could not find an event with the identifier {Identifier}.");
}
else
{
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
GameMain.GameSession.EventManager.QueuedEvents.Enqueue(ev);
}
}
}
}
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -75,7 +76,6 @@ namespace Barotrauma
private readonly HashSet<Event> finishedEvents = new HashSet<Event>();
private readonly HashSet<Identifier> nonRepeatableEvents = new HashSet<Identifier>();
private readonly HashSet<EventSet> usedUniqueSets = new HashSet<EventSet>();
#if DEBUG && SERVER
@@ -102,7 +102,9 @@ namespace Barotrauma
public readonly Queue<Event> QueuedEvents = new Queue<Event>();
private struct TimeStamp
public readonly Queue<Identifier> QueuedEventsForNextRound = new Queue<Identifier>();
private readonly struct TimeStamp
{
public readonly double Time;
public readonly Event Event;
@@ -223,6 +225,21 @@ namespace Barotrauma
}
}
while (QueuedEventsForNextRound.TryDequeue(out var id))
{
var eventPrefab = EventSet.GetEventPrefab(id);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Error in EventManager.StartRound - could not find an event with the identifier {id}.");
continue;
}
var ev = eventPrefab.CreateInstance();
if (ev != null)
{
QueuedEvents.Enqueue(ev);
}
}
PreloadContent(GetFilesToPreload());
roundDuration = 0.0f;
@@ -355,7 +372,6 @@ namespace Barotrauma
QueuedEvents.Clear();
finishedEvents.Clear();
nonRepeatableEvents.Clear();
usedUniqueSets.Clear();
preloadedSprites.ForEach(s => s.Remove());
preloadedSprites.Clear();
@@ -1153,5 +1169,20 @@ namespace Barotrauma
return false;
}
public void Load(XElement element)
{
foreach (var id in element.GetAttributeIdentifierArray(nameof(QueuedEventsForNextRound), Array.Empty<Identifier>()))
{
QueuedEventsForNextRound.Enqueue(id);
}
}
public XElement Save()
{
return new XElement("eventmanager",
new XAttribute(nameof(QueuedEventsForNextRound),
string.Join(',', QueuedEventsForNextRound)));
}
}
}
@@ -91,7 +91,7 @@ namespace Barotrauma
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
{
List<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
IEnumerable<Submarine> connectedSubs = level.BeaconStation.GetConnectedSubs();
foreach (Item item in Item.ItemList)
{
if (!connectedSubs.Contains(item.Submarine) || item.Submarine?.Info is { IsPlayer: true }) { continue; }
@@ -320,7 +320,7 @@ namespace Barotrauma
private static bool IsItemDelivered(Item item)
{
if (item.Removed || item.Condition <= 0.0f || Submarine.MainSub == null) { return false; }
var submarine = item.Submarine ?? item.GetRootContainer()?.Submarine;
var submarine = item.Submarine ?? item.RootContainer?.Submarine;
return submarine == Submarine.MainSub || Submarine.MainSub.GetConnectedSubs().Contains(submarine);
}
}
@@ -1,3 +1,4 @@
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -11,20 +11,7 @@ namespace Barotrauma
{
partial class MineralMission : Mission
{
private struct ResourceCluster
{
public int Amount;
public float Rotation;
public ResourceCluster(int amount, float rotation)
{
Amount = amount;
Rotation = rotation;
}
public static implicit operator ResourceCluster((int amount, float rotation) tuple) => new ResourceCluster(tuple.amount, tuple.rotation);
}
private readonly Dictionary<Identifier, ResourceCluster> resourceClusters = new Dictionary<Identifier, ResourceCluster>();
private readonly Dictionary<Identifier, int> resourceAmounts = new Dictionary<Identifier, int>();
private readonly Dictionary<Identifier, List<Item>> spawnedResources = new Dictionary<Identifier, List<Item>>();
private readonly Dictionary<Identifier, Item[]> relevantLevelResources = new Dictionary<Identifier, Item[]>();
private readonly List<(Identifier Identifier, Vector2 Position)> missionClusterPositions = new List<(Identifier Identifier, Vector2 Position)>();
@@ -81,13 +68,13 @@ namespace Barotrauma
{
var identifier = c.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty) { continue; }
if (resourceClusters.ContainsKey(identifier))
if (resourceAmounts.ContainsKey(identifier))
{
resourceClusters[identifier] = (resourceClusters[identifier].Amount + 1, resourceClusters[identifier].Rotation);
resourceAmounts[identifier]++;
}
else
{
resourceClusters.Add(identifier, (1, 0.0f));
resourceAmounts.Add(identifier, 1);
}
}
}
@@ -128,7 +115,7 @@ namespace Barotrauma
if (IsClient) { return; }
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
if (MapEntityPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab)
{
@@ -136,10 +123,10 @@ namespace Barotrauma
continue;
}
var spawnedResources = level.GenerateMissionResources(prefab, cluster.Amount, positionType, out float rotation, caves);
if (spawnedResources.Count < cluster.Amount)
var spawnedResources = level.GenerateMissionResources(prefab, amount, positionType, caves);
if (spawnedResources.Count < amount)
{
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{cluster.Amount} of {prefab.Name}");
DebugConsole.ThrowError($"Error in MineralMission: spawned only {spawnedResources.Count}/{amount} of {prefab.Name}");
}
if (spawnedResources.None()) { continue; }
@@ -194,7 +181,7 @@ namespace Barotrauma
{
// When mission is completed successfully, half of the resources will be removed from the player (i.e. given to the outpost as a part of the mission)
var handoverResources = new List<Item>();
foreach (Identifier identifier in resourceClusters.Keys)
foreach (Identifier identifier in resourceAmounts.Keys)
{
if (relevantLevelResources.TryGetValue(identifier, out var availableResources))
{
@@ -231,11 +218,11 @@ namespace Barotrauma
private void FindRelevantLevelResources()
{
relevantLevelResources.Clear();
foreach (var identifier in resourceClusters.Keys)
foreach (var identifier in resourceAmounts.Keys)
{
var items = Item.ItemList.Where(i => i.Prefab.Identifier == identifier &&
i.Submarine == null && i.ParentInventory == null &&
(!(i.GetComponent<Holdable>() is Holdable h) || (h.Attachable && h.Attached)))
(i.GetComponent<Holdable>() is not Holdable h || (h.Attachable && h.Attached)))
.ToArray();
relevantLevelResources.Add(identifier, items);
}
@@ -243,12 +230,12 @@ namespace Barotrauma
private bool EnoughHaveBeenCollected()
{
foreach (var kvp in resourceClusters)
foreach (var kvp in resourceAmounts)
{
if (relevantLevelResources.TryGetValue(kvp.Key, out var availableResources))
{
var collected = availableResources.Count(HasBeenCollected);
var needed = kvp.Value.Amount;
var needed = kvp.Value;
if (collected < needed) { return false; }
}
else
@@ -299,10 +286,10 @@ namespace Barotrauma
protected override LocalizedString ModifyMessage(LocalizedString message, bool color = true)
{
int i = 1;
foreach ((Identifier identifier, ResourceCluster cluster) in resourceClusters)
foreach ((Identifier identifier, int amount) in resourceAmounts)
{
Replace($"[resourcename{i}]", ItemPrefab.FindByIdentifier(identifier)?.Name.Value ?? "");
Replace($"[resourcequantity{i}]", cluster.Amount.ToString());
Replace($"[resourcequantity{i}]", amount.ToString());
i++;
}
Replace("[handoverpercentage]", ToolBox.GetFormattedPercentage(resourceHandoverAmount));
@@ -363,7 +363,7 @@ namespace Barotrauma
//make body dynamic when picked up
foreach (var target in targets)
{
var root = target.Item?.GetRootContainer() ?? target.Item;
var root = target.Item?.RootContainer ?? target.Item;
if (root == null) { continue; }
if (target.Item.ParentInventory != null && target.Item.body != null) { target.Item.body.FarseerBody.BodyType = BodyType.Dynamic; }
}
@@ -389,7 +389,7 @@ namespace Barotrauma
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.GetRootContainer() ?? target.Item;
var root = target.Item?.RootContainer ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
@@ -335,7 +335,7 @@ namespace Barotrauma
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
SpawnedInCurrentOutpost = validContainer.Key.Item.SpawnedInCurrentOutpost,
AllowStealing = validContainer.Key.Item.AllowStealing,
AllowStealing = validContainer.Key.Item.AllowStealing || validContainer.Key.Item.Prefab.AllowStealingContainedItems,
Quality = quality,
OriginalModuleIndex = validContainer.Key.Item.OriginalModuleIndex,
OriginalContainerIndex =
@@ -9,6 +9,7 @@ using System.Text;
using System.Xml.Linq;
using Barotrauma.Networking;
using System.Collections;
using System.Collections.Immutable;
#if SERVER
using Barotrauma.Networking;
#endif
@@ -415,14 +416,31 @@ namespace Barotrauma
}
}
#endif
return Submarine.MainSub.GetItems(true).FindAll(item =>
return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList();
}
public static IReadOnlyCollection<Item> FindAllItemsOnPlayerAndSub(Character character)
{
List<Item> allItems = new();
if (character?.Inventory is { } inv)
{
allItems.AddRange(inv.FindAllItems(recursive: true));
}
allItems.AddRange(FindAllSellableItems());
return allItems;
}
public static IEnumerable<Item> FindAllSellableItems()
{
if (Submarine.MainSub is null) { return Enumerable.Empty<Item>(); }
return Submarine.MainSub.GetItems(true).FindAll(static item =>
{
if (!IsItemSellable(item, confirmedSoldEntities)) { return false; }
if (item.GetRootInventoryOwner() is Character) { return false; }
if (!item.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { return false; }
if (!item.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { return false; }
if (!item.Components.All(static c => c is not Holdable { Attachable: true, Attached: true })) { return false; }
if (!item.Components.All(static c => c is not Wire w || w.Connections.All(static c => c is null))) { return false; }
if (!ItemAndAllContainersInteractable(item)) { return false; }
if (item.GetRootContainer() is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
return true;
}).Distinct();
@@ -592,7 +610,7 @@ namespace Barotrauma
(itemContainer?.Item ?? item).CampaignInteractionType = CampaignMode.InteractionType.Cargo;
static void itemSpawned(PurchasedItem purchased, Item item)
{
Submarine sub = item.Submarine ?? item.GetRootContainer()?.Submarine;
Submarine sub = item.Submarine ?? item.RootContainer?.Submarine;
if (sub != null)
{
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
@@ -143,6 +143,8 @@ namespace Barotrauma
public virtual bool PurchasedLostShuttles { get; set; }
public virtual bool PurchasedItemRepairs { get; set; }
public bool DivingSuitWarningShown;
private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions)
{
if (GameMain.NetworkMember == null) { return true; }
@@ -738,9 +740,11 @@ namespace Barotrauma
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.StartOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.StartOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
foreach (var dockedSub in Level.Loaded.StartOutpost.DockedTo)
{
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
@@ -776,9 +780,11 @@ namespace Barotrauma
//if there's a sub docked to the outpost, we can leave the level
if (Level.Loaded.EndOutpost.DockedTo.Any())
{
var dockedSub = Level.Loaded.EndOutpost.DockedTo.FirstOrDefault();
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { return null; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
foreach (var dockedSub in Level.Loaded.EndOutpost.DockedTo)
{
if (dockedSub == GameMain.NetworkMember?.RespawnManager?.RespawnShuttle || dockedSub.TeamID != submarineTeam) { continue; }
return dockedSub.DockedTo.Contains(Submarine.MainSub) ? Submarine.MainSub : dockedSub;
}
}
//nothing docked, check if there's a sub close enough to the outpost and someone inside the outpost
@@ -930,6 +936,9 @@ namespace Barotrauma
CampaignMetadata.SetValue("campaign.endings".ToIdentifier(), loops + 1);
}
//no tutorials after finishing the campaign once
Settings.TutorialEnabled = false;
GameAnalyticsManager.AddProgressionEvent(
GameAnalyticsManager.ProgressionStatus.Complete,
Preset?.Identifier.Value ?? "none");
@@ -985,7 +994,7 @@ namespace Barotrauma
if (characterInfo == null) { return false; }
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
{
if (GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation)
if (MathF.Round(GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
{
return false;
}
@@ -1204,15 +1213,17 @@ namespace Barotrauma
{
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
DivingSuitWarningShown = element.GetAttributeBool(nameof(DivingSuitWarningShown).ToLowerInvariant(), false);
}
protected XElement SaveStats()
{
return new XElement("stats",
new XAttribute(nameof(TotalPlayTime).ToLowerInvariant(), TotalPlayTime),
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels));
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels),
new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown));
}
public void LogState()
{
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
@@ -223,6 +223,9 @@ namespace Barotrauma
case "stats":
LoadStats(subElement);
break;
case "eventmanager":
GameMain.GameSession.EventManager.Load(subElement);
break;
case Wallet.LowerCaseSaveElementName:
Bank = new Wallet(Option<Character>.None(), subElement);
break;
@@ -272,29 +275,35 @@ namespace Barotrauma
bool isSubmarineVisible(SubmarineInfo s)
=> !GameMain.NetworkMember.ServerSettings.HiddenSubs.Any(h
=> s.Name.Equals(h, StringComparison.OrdinalIgnoreCase));
List<SubmarineInfo> availableSubs =
SubmarineInfo.SavedSubmarines
var availableSubs = SubmarineInfo.SavedSubmarines;
#if CLIENT
if (GameMain.Client != null)
{
availableSubs = GameMain.Client.ServerSubmarines;
}
#endif
List<SubmarineInfo> campaignSubs =
availableSubs
.Where(s =>
s.IsCampaignCompatible
&& isSubmarineVisible(s))
.ToList();
if (!availableSubs.Any())
if (!campaignSubs.Any())
{
//None of the available subs were marked as campaign-compatible, just include all visible subs
availableSubs.AddRange(
SubmarineInfo.SavedSubmarines
.Where(isSubmarineVisible));
campaignSubs.AddRange(availableSubs.Where(isSubmarineVisible));
}
if (!availableSubs.Any())
if (!campaignSubs.Any())
{
//No subs are visible at all! Just make the selected one available
availableSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
campaignSubs.Add(GameMain.NetLobbyScreen.SelectedSub);
}
return availableSubs;
return campaignSubs;
}
private static void WriteItems(IWriteMessage msg, Dictionary<Identifier, List<PurchasedItem>> purchasedItems)
@@ -328,11 +328,11 @@ namespace Barotrauma
Campaign!.TransferItemsOnSubSwitch = transferItems;
}
public void PurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
public bool TryPurchaseSubmarine(SubmarineInfo newSubmarine, Client? client = null)
{
if (Campaign is null) { return; }
if (Campaign is null) { return false; }
int price = newSubmarine.GetPrice();
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return; }
if ((GameMain.NetworkMember is null || GameMain.NetworkMember is { IsServer: true }) && !Campaign.TryPurchase(client, price)) { return false; }
if (!OwnedSubmarines.Any(s => s.Name == newSubmarine.Name))
{
GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarinePurchase, newSubmarine.Name);
@@ -341,6 +341,7 @@ namespace Barotrauma
(Campaign as MultiPlayerCampaign)?.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.SubList);
#endif
}
return true;
}
public bool IsSubmarineOwned(SubmarineInfo query)
@@ -504,7 +505,8 @@ namespace Barotrauma
}
GameAnalyticsManager.AddDesignEvent($"{eventId}HintManager:{(HintManager.Enabled ? "Enabled" : "Disabled")}");
#endif
if (GameMode is CampaignMode campaignMode)
var campaignMode = GameMode as CampaignMode;
if (campaignMode != null)
{
if (campaignMode.Map?.Radiation != null && campaignMode.Map.Radiation.Enabled)
{
@@ -532,7 +534,7 @@ namespace Barotrauma
}
#endif
#if CLIENT
if (GameMode is CampaignMode && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
if (campaignMode != null && levelData != null) { SteamAchievementManager.OnBiomeDiscovered(levelData.Biome); }
var existingRoundSummary = GUIMessageBox.MessageBoxes.Find(mb => mb.UserData is RoundSummary)?.UserData as RoundSummary;
if (existingRoundSummary?.ContinueButton != null)
@@ -577,6 +579,14 @@ namespace Barotrauma
GameMain.LuaCs.Hook.Call("roundStart");
#endif
if (campaignMode is { DivingSuitWarningShown: false } &&
Level.Loaded != null && Level.Loaded.GetRealWorldDepth(0) > 4000)
{
#if CLIENT
CoroutineManager.Invoke(() => new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("hint.upgradedivingsuits")), delay: 5.0f);
#endif
campaignMode.DivingSuitWarningShown = true;
}
}
private void InitializeLevel(Level? level)
@@ -693,7 +703,7 @@ namespace Barotrauma
if (port.IsHorizontal || port.Docked) { continue; }
if (port.Item.Submarine == level.StartOutpost)
{
if (port.DockingTarget == null)
if (port.DockingTarget == null || (outPostPort != null && !outPostPort.MainDockingPort && port.MainDockingPort))
{
outPostPort = port;
}
@@ -982,7 +992,7 @@ namespace Barotrauma
Dictionary<ItemPrefab, int> submarineInventory = new Dictionary<ItemPrefab, int>();
foreach (Item item in Item.ItemList)
{
var rootContainer = item.GetRootContainer() ?? item;
var rootContainer = item.RootContainer ?? item;
if (rootContainer.Submarine?.Info == null || rootContainer.Submarine.Info.Type != SubmarineType.Player) { continue; }
if (rootContainer.Submarine != Submarine.MainSub && !Submarine.MainSub.DockedTo.Contains(rootContainer.Submarine)) { continue; }
@@ -85,6 +85,22 @@ namespace Barotrauma
InitProjSpecific(element);
var itemElements = element.Elements().Where(e => e.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase));
int itemCount = itemElements.Count();
if (itemCount > capacity)
{
DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for.");
}
#if DEBUG
else if (itemCount > capacity - 2)
{
DebugConsole.ThrowError(
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
"This can cause issues with talents that spawn extra loot in monsters' inventories."
+ " Consider increasing the inventory size.");
}
#endif
if (!spawnInitialItems) { return; }
#if CLIENT
@@ -92,10 +108,8 @@ namespace Barotrauma
if (GameMain.Client != null) { return; }
#endif
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("item", StringComparison.OrdinalIgnoreCase)) { continue; }
foreach (var subElement in itemElements)
{
string itemIdentifier = subElement.GetAttributeString("identifier", "");
if (!ItemPrefab.Prefabs.TryGet(itemIdentifier, out var itemPrefab))
{
@@ -1,13 +1,15 @@
using Barotrauma.Networking;
using Barotrauma.IO;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Joints;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
#if CLIENT
using Barotrauma.Lights;
#endif
namespace Barotrauma.Items.Components
{
@@ -243,10 +245,12 @@ namespace Barotrauma.Items.Components
if (!target.item.Submarine.DockedTo.Contains(item.Submarine))
{
target.item.Submarine.ConnectedDockingPorts.Add(item.Submarine, target);
target.item.Submarine.RefreshConnectedSubs();
}
if (!item.Submarine.DockedTo.Contains(target.item.Submarine))
{
item.Submarine.ConnectedDockingPorts.Add(target.item.Submarine, this);
item.Submarine.RefreshConnectedSubs();
}
DockingTarget = target;
@@ -507,9 +511,9 @@ namespace Barotrauma.Items.Components
wire.RemoveConnection(DockingTarget.item);
powerConnection.TryAddLink(wire);
wire.Connect(powerConnection, false, false);
wire.TryConnect(powerConnection, addNode: false);
recipient.TryAddLink(wire);
wire.Connect(recipient, false, false);
wire.TryConnect(recipient, addNode: false);
//Flag connections to be updated
Powered.ChangedConnections.Add(powerConnection);
@@ -558,6 +562,7 @@ namespace Barotrauma.Items.Components
var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
bodies = new Body[4];
RemoveConvexHulls();
if (DockingTarget.Door != null)
{
@@ -648,8 +653,10 @@ namespace Barotrauma.Items.Components
hullRects[i].X -= expand;
hullRects[i].Width += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch"
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -661,6 +668,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point((int)item.Position.X, item.Rect.Y - item.Rect.Height * i),
new Point((int)(DockingTarget.item.WorldPosition.X - item.WorldPosition.X), 0)), IsHorizontal, item);
}
#endif
if (rightHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -764,15 +780,17 @@ namespace Barotrauma.Items.Components
hullRects[1].Height += midHullDiff / 2 + 1;
}
int expand = 5;
for (int i = 0; i < 2; i++)
{
hullRects[i].Y += expand;
hullRects[i].Height += expand * 2;
hullRects[i].Location -= MathUtils.ToPoint(subs[i].WorldPosition - subs[i].HiddenSubPosition);
hulls[i] = new Hull(hullRects[i], subs[i]);
hulls[i].RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch";
hulls[i] = new Hull(hullRects[i], subs[i])
{
RoomName = IsHorizontal ? "entityname.dockingport" : "entityname.dockinghatch",
AvoidStaying = true
};
hulls[i].AddToGrid(subs[i]);
hulls[i].FreeID();
@@ -784,6 +802,15 @@ namespace Barotrauma.Items.Components
BodyType.Static);
}
}
#if CLIENT
for (int i = 0; i < 2; i++)
{
convexHulls[i] =
new ConvexHull(new Rectangle(
new Point(item.Rect.X + item.Rect.Width * i, (int)item.Position.Y),
new Point(0, (int)(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y))), IsHorizontal, item);
}
#endif
if (midHullDiff <= 100 && hulls[0].Submarine != null)
{
@@ -822,6 +849,8 @@ namespace Barotrauma.Items.Components
}
}
partial void RemoveConvexHulls();
private void LinkHullsToGaps()
{
if (gap == null || hulls == null || hulls[0] == null || hulls[1] == null)
@@ -916,7 +945,9 @@ namespace Barotrauma.Items.Components
}
DockingTarget.item.Submarine.ConnectedDockingPorts.Remove(item.Submarine);
DockingTarget.item.Submarine.RefreshConnectedSubs();
item.Submarine.ConnectedDockingPorts.Remove(DockingTarget.item.Submarine);
item.Submarine.RefreshConnectedSubs();
if (Door != null && DockingTarget.Door != null)
{
@@ -976,6 +1007,8 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
RemoveConvexHulls();
if (gap != null)
{
gap.Remove();
@@ -1091,6 +1124,7 @@ namespace Barotrauma.Items.Components
hulls[0]?.Remove(); hulls[0] = null;
hulls[1]?.Remove(); hulls[1] = null;
gap?.Remove(); gap = null;
RemoveConvexHulls();
overlaySprite?.Remove();
overlaySprite = null;
@@ -69,7 +69,7 @@ namespace Barotrauma.Items.Components
private bool isBroken;
public bool CanBeTraversed => (IsOpen || IsBroken) && !IsJammed && !IsStuck && !Impassable;
public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
public bool IsBroken
{
@@ -186,13 +186,19 @@ namespace Barotrauma.Items.Components
{
get { return openState; }
set
{
{
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
UpdateConvexHulls();
lastConvexHullState = openState;
//refresh convex hulls if the body of the door has moved by 5 pixels,
//or if it becomes fully closed or fully open
if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
(openState <= 0.0f && lastConvexHullState > 0.0f) ||
(openState >= 1.0f && lastConvexHullState < 1.0f))
{
UpdateConvexHulls();
lastConvexHullState = openState;
}
#endif
}
}
@@ -523,11 +529,11 @@ namespace Barotrauma.Items.Components
{
RefreshLinkedGap();
#if CLIENT
Vector2[] corners = GetConvexHullCorners(Rectangle.Empty);
convexHull = new ConvexHull(corners, Color.Black, item);
if (Window != Rectangle.Empty) convexHull2 = new ConvexHull(corners, Color.Black, item);
convexHull = new ConvexHull(doorRect, IsHorizontal, item);
if (Window != Rectangle.Empty)
{
convexHull2 = new ConvexHull(doorRect, IsHorizontal, item);
}
UpdateConvexHulls();
#endif
}
@@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -96,11 +95,16 @@ namespace Barotrauma.Items.Components
if (selectedEffect != null)
{
targetCharacter = character;
ApplyStatusEffects(ActionType.OnWearing, 1.0f);
ApplyStatusEffects(ActionType.OnWearing, 1.0f, targetCharacter);
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = selectedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedEffectStrength);
}
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -110,7 +114,12 @@ namespace Barotrauma.Items.Components
float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (affliction != null) { affliction.Strength = selectedTaintedEffectStrength; }
if (affliction != null)
{
affliction.Strength = selectedTaintedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedTaintedEffectStrength);
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
@@ -127,7 +136,7 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
var rootContainer = item.GetRootContainer();
var rootContainer = item.RootContainer;
if (!targetCharacter.HasEquippedItem(item) &&
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
@@ -220,7 +229,7 @@ namespace Barotrauma.Items.Components
return MathHelper.Clamp(probability, 0.0f, 1.0f);
}
private float GetTaintedProbabilityOnCombine(Character user)
private static float GetTaintedProbabilityOnCombine(Character user)
{
if (user == null) { return 1.0f; }
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
@@ -409,6 +409,7 @@ namespace Barotrauma.Items.Components
private int leafVariants;
private int[] flowerTiles;
[Serialize(100.0f, IsPropertySaveable.Yes)]
public float Health
{
get => health;
@@ -321,12 +321,12 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Drop(true, dropper);
Drop(true, dropper, setTransform);
}
private void Drop(bool dropConnectedWires, Character dropper)
private void Drop(bool dropConnectedWires, Character dropper, bool setTransform = true)
{
GetRope()?.Snap();
if (dropConnectedWires)
@@ -343,8 +343,11 @@ namespace Barotrauma.Items.Components
DeattachFromWall();
}
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
if (setTransform)
{
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
}
IsActive = false;
attachTargetCell = null;
@@ -357,7 +360,7 @@ namespace Barotrauma.Items.Components
item.Submarine = picker.Submarine;
if (item.body != null)
if (item.body != null && setTransform)
{
if (item.body.Removed)
{
@@ -599,6 +602,10 @@ namespace Barotrauma.Items.Components
throw new InvalidOperationException($"Tried to attach an item with no physics body to a wall ({item.Prefab.Identifier}).");
}
body.Enabled = false;
body.SetTransformIgnoreContacts(body.SimPosition, rotation: 0.0f);
item.body = null;
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
if (item.CurrentHull == null && item.Submarine == null)
{
@@ -638,9 +645,6 @@ namespace Barotrauma.Items.Components
}
}
body.Enabled = false;
item.body = null;
DisplayMsg = prevMsg;
PickKey = prevPickKey;
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
@@ -812,7 +816,7 @@ namespace Barotrauma.Items.Components
foreach (var edge in cell.Edges)
{
if (!edge.IsSolid) { continue; }
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, user.WorldPosition, attachPos, out Vector2 intersection))
{
attachPos = intersection;
edgeFound = true;
@@ -97,11 +97,18 @@ namespace Barotrauma.Items.Components
{
if (holdable != null && !holdable.Attached)
{
trigger.Enabled = false;
if (trigger != null)
{
trigger.Enabled = false;
}
IsActive = false;
}
else
{
if (trigger == null)
{
CreateTriggerBody();
}
if (trigger != null && Vector2.DistanceSquared(item.SimPosition, trigger.SimPosition) > 0.01f)
{
trigger.SetTransform(item.SimPosition, 0.0f);
@@ -123,12 +130,15 @@ namespace Barotrauma.Items.Components
{
holdable.PickingTime = float.MaxValue;
}
}
private void CreateTriggerBody()
{
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
var body = item.body ?? holdable.Body;
if (body != null)
if (body != null && Attached)
{
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
body.Density,
BodyType.Static,
Physics.CollisionWall,
@@ -143,7 +153,6 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
if (trigger != null)
{
trigger.Remove();
@@ -170,9 +170,9 @@ namespace Barotrauma.Items.Components
return characterUsable || character == null;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
hitting = false;
hitPos = 0.0f;
}
@@ -241,12 +241,9 @@ namespace Barotrauma.Items.Components
}
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
if (picker == null)
{
picker = dropper;
}
picker ??= dropper;
Vector2 bodyDropPos = Vector2.Zero;
@@ -255,8 +252,7 @@ namespace Barotrauma.Items.Components
if (item.ParentInventory != null && item.ParentInventory.Owner != null && !item.ParentInventory.Owner.Removed)
{
bodyDropPos = item.ParentInventory.Owner.SimPosition;
if (item.body != null) item.body.ResetDynamics();
item.body?.ResetDynamics();
}
}
else if (!picker.Removed)
@@ -270,7 +266,7 @@ namespace Barotrauma.Items.Components
picker = null;
}
if (item.body != null && !item.body.Enabled)
if (item.body != null && !item.body.Enabled && setTransform)
{
if (item.body.Removed)
{
@@ -912,17 +912,16 @@ namespace Barotrauma.Items.Components
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in currentTargets)
{
if (!(target is Door door)) { continue; }
if (target is not Door door) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
for (int i = 0; i < effect.propertyNames.Length; i++)
foreach (var propertyEffect in effect.PropertyEffects)
{
Identifier propertyName = effect.propertyNames[i];
if (propertyName != "stuck") { continue; }
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
if (propertyEffect.propertyName != "stuck") { continue; }
if (door.SerializableProperties == null || !door.SerializableProperties.TryGetValue(propertyEffect.propertyName, out SerializableProperty property)) { continue; }
object value = property.GetValue(target);
if (door.Stuck > 0)
{
bool isCutting = effect.propertyEffects[i].GetType() == typeof(float) && (float)effect.propertyEffects[i] < 0;
bool isCutting = propertyEffect.value is float and < 0;
var progressBar = user.UpdateHUDProgressBar(door, door.Item.WorldPosition, door.Stuck / 100, Color.DarkGray * 0.5f, Color.White,
textTag: isCutting ? "progressbar.cutting" : "progressbar.welding");
if (progressBar != null) { progressBar.Size = new Vector2(60.0f, 20.0f); }
@@ -56,9 +56,9 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
base.Drop(dropper);
base.Drop(dropper, setTransform);
throwState = ThrowState.None;
throwAngle = ThrowAngleStart;
Item.ResetWaterDragCoefficient();
@@ -442,7 +442,7 @@ namespace Barotrauma.Items.Components
}
/// <summary>a Character has dropped the item</summary>
public virtual void Drop(Character dropper) { }
public virtual void Drop(Character dropper, bool setTransform = true) { }
/// <returns>true if the operation was completed</returns>
public virtual bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
@@ -724,20 +724,50 @@ namespace Barotrauma.Items.Components
{
if (character.IsBot && item.IgnoreByAI(character)) { return false; }
if (!item.IsInteractable(character)) { return false; }
if (requiredItems.None()) { return true; }
if (character.Inventory != null)
if (requiredItems.Count == 0) { return true; }
if (character.Inventory != null && requiredItems.TryGetValue(RelatedItem.RelationType.Picked, out List<RelatedItem> relatedItems))
{
foreach (Item item in character.Inventory.AllItems)
foreach (RelatedItem relatedItem in relatedItems)
{
if (requiredItems.Any(ri => ri.Value.Any(r => r.Type == RelatedItem.RelationType.Picked && r.MatchesItem(item))))
foreach (Item otherItem in character.Inventory.AllItems)
{
return true;
}
if (relatedItem.MatchesItem(otherItem))
{
if (otherItem.GetComponent<IdCard>() is IdCard idCard)
{
if (!CheckIdCardAccess(relatedItem, idCard))
{
continue;
}
}
return true;
}
}
}
}
return false;
}
/// <summary>
/// Presumes that matching is already checked.
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null)
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
}
return true;
}
public virtual bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
if (requiredItems.None()) { return true; }
@@ -773,23 +803,14 @@ namespace Barotrauma.Items.Components
bool CheckItems(RelatedItem relatedItem, IEnumerable<Item> itemList)
{
bool Predicate(Item it)
bool Predicate(Item it)
{
if (it == null || it.Condition <= 0.0f || !relatedItem.MatchesItem(it)) { return false; }
if (item.Submarine != null)
if (it.GetComponent<IdCard>() is IdCard idCard)
{
var idCard = it.GetComponent<IdCard>();
if (idCard != null)
if (!CheckIdCardAccess(relatedItem, idCard))
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
return false;
}
return false;
}
}
return true;
@@ -1029,7 +1050,7 @@ namespace Barotrauma.Items.Components
prevRequiredItems[newRequiredItem.Type].Find(ri => ri.JoinedIdentifiers == newRequiredItem.JoinedIdentifiers) : null;
if (prevRequiredItem != null)
{
newRequiredItem.statusEffects = prevRequiredItem.statusEffects;
newRequiredItem.StatusEffects = prevRequiredItem.StatusEffects;
newRequiredItem.Msg = prevRequiredItem.Msg;
newRequiredItem.IsOptional = prevRequiredItem.IsOptional;
newRequiredItem.IgnoreInEditor = prevRequiredItem.IgnoreInEditor;
@@ -20,11 +20,13 @@ namespace Barotrauma.Items.Components
{
public readonly int MaxStackSize;
public List<RelatedItem> ContainableItems;
public readonly bool AutoInject;
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems)
public SlotRestrictions(int maxStackSize, List<RelatedItem> containableItems, bool autoInject)
{
MaxStackSize = maxStackSize;
ContainableItems = containableItems;
AutoInject = autoInject;
}
public bool MatchesItem(Item item)
@@ -269,7 +271,7 @@ namespace Barotrauma.Items.Components
List<SlotRestrictions> newSlotRestrictions = new List<SlotRestrictions>(totalCapacity);
for (int i = 0; i < capacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(maxStackSize, ContainableItems, autoInject: false));
}
int subContainerIndex = capacity;
@@ -279,6 +281,7 @@ namespace Barotrauma.Items.Components
int subCapacity = subElement.GetAttributeInt("capacity", 1);
int subMaxStackSize = subElement.GetAttributeInt("maxstacksize", maxStackSize);
bool autoInject = subElement.GetAttributeBool("autoinject", false);
var subContainableItems = new List<RelatedItem>();
foreach (var subSubElement in subElement.Elements())
@@ -298,7 +301,7 @@ namespace Barotrauma.Items.Components
for (int i = subContainerIndex; i < subContainerIndex + subCapacity; i++)
{
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems));
newSlotRestrictions.Add(new SlotRestrictions(subMaxStackSize, subContainableItems, autoInject));
}
subContainerIndex += subCapacity;
}
@@ -351,7 +354,7 @@ namespace Barotrauma.Items.Components
foreach (var containableItem in slotRestrictions[index].ContainableItems)
{
if (!containableItem.MatchesItem(containedItem)) { continue; }
foreach (StatusEffect effect in containableItem.statusEffects)
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition));
}
@@ -466,7 +469,7 @@ namespace Barotrauma.Items.Components
prevContainedItemPositions = item.Position;
}
if (AutoInject)
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
{
//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
@@ -480,7 +483,21 @@ namespace Barotrauma.Items.Components
ownerCharacter.HealthPercentage / 100f <= AutoInjectThreshold &&
ownerCharacter.HasEquippedItem(item))
{
foreach (Item item in Inventory.AllItemsMod)
if (AutoInject)
{
Inventory.AllItemsMod.ForEach(i => Inject(i));
}
else
{
for (int i = 0; i < slotRestrictions.Length; i++)
{
if (slotRestrictions[i].AutoInject)
{
Inventory.GetItemsAt(i).ForEachMod(i => Inject(i));
}
}
}
void Inject(Item item)
{
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter, useTarget: ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter, useTarget: ownerCharacter);
@@ -632,7 +649,7 @@ namespace Barotrauma.Items.Components
return false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
IsActive = true;
SetContainedActive(false);
@@ -98,6 +98,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
public bool AllowSelectingWhenSelectedByOther
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
public bool AllowSelectingWhenSelectedByBot
{
get;
set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
@@ -466,8 +480,18 @@ namespace Barotrauma.Items.Components
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
else if (user.IsBot && !activator.IsBot)
{
if (AllowSelectingWhenSelectedByBot)
{
CancelUsing(user);
user = activator;
IsActive = true;
return true;
}
}
return AllowSelectingWhenSelectedByOther;
}
else
{
@@ -10,6 +10,18 @@ namespace Barotrauma.Items.Components
{
private float force;
/// <summary>
/// Latest signal the set_force connection received, used to set <see cref="targetForce"/> in the Update method.
/// We use a separate variable, because otherwise specific item update orders and sending multiple signals to set_force would lead to bugs:
/// targetForce could be set to 0, then a power grid might update as if the engine was off and mark the voltage of the grid as 1,
/// then another item could set the targetForce to 100 and make it run without power.
/// </summary>
private float? lastReceivedTargetForce;
/// <summary>
/// The amount of force the engine is aiming for (the actual force may be less than this,
/// depending on the amount of power, the condition of the engine or boosts from talents)
/// </summary>
private float targetForce;
private float maxForce;
@@ -58,7 +70,7 @@ namespace Barotrauma.Items.Components
public float CurrentVolume
{
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage / MinVoltage, 1.0f))); }
get { return Math.Abs((force / 100.0f) * (MinVoltage <= 0.0f ? 1.0f : Math.Min(prevVoltage, 1.0f))); }
}
public float CurrentBrokenVolume
@@ -110,7 +122,10 @@ namespace Barotrauma.Items.Components
hasPower = Voltage > MinVoltage;
}
if (lastReceivedTargetForce.HasValue)
{
targetForce = lastReceivedTargetForce.Value;
}
Force = MathHelper.Lerp(force, (Voltage < MinVoltage) ? 0.0f : targetForce, deltaTime * 10.0f);
if (Math.Abs(Force) > 1.0f)
{
@@ -254,7 +269,7 @@ namespace Barotrauma.Items.Components
if (float.TryParse(signal.value, NumberStyles.Float, CultureInfo.InvariantCulture, out float tempForce))
{
controlLockTimer = 0.1f;
targetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
lastReceivedTargetForce = MathHelper.Clamp(tempForce, -100.0f, 100.0f);
User = signal.sender;
}
}
@@ -784,8 +784,10 @@ namespace Barotrauma.Items.Components
}
else
{
float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0;
float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0;
//prefer items in worse condition
return Math.Sign(item2.Condition - item1.Condition);
return Math.Sign(condition2 - condition1);
}
}
@@ -775,7 +775,6 @@ namespace Barotrauma.Items.Components
if (shutDown)
{
PowerOn = false;
AutoTemp = false;
TargetFissionRate = 0.0f;
TargetTurbineOutput = 0.0f;
unsentChanges = true;
@@ -68,7 +68,6 @@ namespace Barotrauma.Items.Components
public bool UseDirectionalPing => useDirectionalPing;
private bool useDirectionalPing = false;
private Vector2 pingDirection = new Vector2(1.0f, 0.0f);
private bool useMineralScanner;
private bool aiPingCheckPending;
@@ -133,6 +132,9 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool UseMineralScanner { get; set; }
public float Zoom
{
get { return zoom; }
@@ -366,7 +368,7 @@ namespace Barotrauma.Items.Components
bool isActive = msg.ReadBoolean();
bool directionalPing = useDirectionalPing;
float zoomT = zoom, pingDirectionT = 0.0f;
bool mineralScanner = useMineralScanner;
bool mineralScanner = UseMineralScanner;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
@@ -391,13 +393,13 @@ namespace Barotrauma.Items.Components
float pingAngle = MathHelper.Lerp(0.0f, MathHelper.TwoPi, pingDirectionT);
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
useMineralScanner = mineralScanner;
UseMineralScanner = mineralScanner;
#if CLIENT
zoomSlider.BarScroll = zoomT;
directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = useMineralScanner;
mineralScannerSwitch.Selected = UseMineralScanner;
}
#endif
}
@@ -418,7 +420,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
msg.WriteBoolean(useMineralScanner);
msg.WriteBoolean(UseMineralScanner);
}
}
}
@@ -444,7 +444,7 @@ namespace Barotrauma.Items.Components
if (connectedSubUpdateTimer <= 0.0f)
{
connectedSubs.Clear();
connectedSubs = controlledSub?.GetConnectedSubs();
connectedSubs.AddRange(controlledSub.GetConnectedSubs());
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
@@ -535,7 +535,7 @@ namespace Barotrauma.Items.Components
foreach (GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, controlledSub.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 diff = controlledSub.WorldPosition - intersection;
//far enough -> ignore
@@ -435,7 +435,7 @@ namespace Barotrauma.Items.Components
{
//other junction boxes don't need to receive the signal in the pass-through signal connections
//because we relay it straight to the connected items without going through the whole chain of junction boxes
if (ic is PowerTransfer && !(ic is RelayComponent)) { continue; }
if (ic is PowerTransfer && ic is not RelayComponent) { continue; }
ic.ReceiveSignal(signal, recipient);
}
@@ -709,7 +709,7 @@ namespace Barotrauma.Items.Components
return hits;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Item.ResetWaterDragCoefficient();
if (dropper != null)
@@ -717,7 +717,7 @@ namespace Barotrauma.Items.Components
DisableProjectileCollisions();
Unstick();
}
base.Drop(dropper);
base.Drop(dropper, setTransform);
}
public override void Update(float deltaTime, Camera cam)
@@ -939,7 +939,7 @@ namespace Barotrauma.Items.Components
Character character = null;
if (target.Body.UserData is Submarine submarine && target.UserData is not Barotrauma.Item)
{
item.Move(-submarine.Position);
item.Move(-submarine.Position, ignoreContacts: false);
item.Submarine = submarine;
item.body.Submarine = submarine;
return !Hitscan;
@@ -339,18 +339,19 @@ namespace Barotrauma.Items.Components
if (Math.Abs(TargetPullForce) > 0.001f)
{
var targetBody = GetBodyToPull(target);
if (user != null && targetCharacter != null && !user.AnimController.InWater)
bool lerpForces = LerpForces;
if (!lerpForces && user != null && targetCharacter != null && !user.AnimController.InWater)
{
// Prevents rubberbanding horizontally when dragging a corpse.
if ((forceDir.X < 0) != (user.AnimController.Dir < 0))
{
forceDir.X = Math.Clamp(forceDir.X, -0.1f, 0.1f);
// Prevents rubberbanding horizontally when dragging a corpse.
lerpForces = true;
}
}
float force = LerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
float force = lerpForces ? MathHelper.Lerp(0, TargetPullForce, MathUtils.InverseLerp(0, MaxLength / 3, distance - 50)) : TargetPullForce;
targetBody?.ApplyForce(-forceDir * force);
var targetRagdoll = targetCharacter?.AnimController;
if (targetRagdoll != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
if (targetRagdoll?.Collider != null && (targetRagdoll.InWater || targetRagdoll.OnGround))
{
targetRagdoll.Collider.ApplyForce(-forceDir * force * 3);
}
@@ -29,10 +29,10 @@ namespace Barotrauma.Items.Components
private readonly Item item;
public readonly bool IsOutput;
public readonly List<StatusEffect> Effects;
public readonly List<ushort> LoadedWireIds;
public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;
//The grid the connection is a part of
public GridInfo Grid;
@@ -40,6 +40,9 @@ namespace Barotrauma.Items.Components
//Priority in which power output will be handled - load is unaffected
public PowerPriority Priority = PowerPriority.Default;
public Signal LastSentSignal { get; private set; }
public Signal LastReceivedSignal {get; private set;}
public bool IsPower
{
get;
@@ -151,16 +154,20 @@ namespace Barotrauma.Items.Components
IsPower = Name == "power_in" || Name == "power" || Name == "power_out";
LoadedWireIds = new List<ushort>();
LoadedWires = new List<(ushort wireId, int? connectionIndex)>();
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "link":
int id = subElement.GetAttributeInt("w", 0);
int? i = null;
if (subElement.GetAttribute("i") != null)
{
i = subElement.GetAttributeInt("i", 0);
}
if (id < 0) { id = 0; }
if (LoadedWireIds.Count < MaxWires) { LoadedWireIds.Add(idRemap.GetOffsetId(id)); }
if (LoadedWires.Count < MaxWires) { LoadedWires.Add((idRemap.GetOffsetId(id), i)); }
break;
case "statuseffect":
Effects ??= new List<StatusEffect>();
@@ -288,6 +295,7 @@ namespace Barotrauma.Items.Components
public void SendSignal(Signal signal)
{
LastSentSignal = signal;
enumeratingWires = true;
foreach (var wire in wires)
{
@@ -298,6 +306,10 @@ namespace Barotrauma.Items.Components
signal.source?.LastSentSignalRecipients.Add(recipient);
Connection connection = recipient;
connection.LastReceivedSignal = signal;
#if CLIENT
wire.RegisterSignal(signal, source: this);
#endif
object[] obj = new object[] { signal, connection };
GameMain.LuaCs.Hook.Call("signalReceived", obj);
@@ -355,22 +367,29 @@ namespace Barotrauma.Items.Components
public void InitializeFromLoaded()
{
if (LoadedWireIds.Count == 0) { return; }
if (LoadedWires.Count == 0) { return; }
for (int i = 0; i < LoadedWireIds.Count; i++)
foreach ((ushort wireId, int? connectionIndex) in LoadedWires)
{
if (!(Entity.FindEntityByID(LoadedWireIds[i]) is Item wireItem)) { continue; }
if (Entity.FindEntityByID(wireId) is not Item wireItem) { continue; }
var wire = wireItem.GetComponent<Wire>();
if (wire != null && TryAddLink(wire))
{
if (wire.Item.body != null) wire.Item.body.Enabled = false;
wire.Connect(this, false, false);
if (wire.Item.body != null) { wire.Item.body.Enabled = false; }
if (connectionIndex.HasValue)
{
wire.Connect(this, connectionIndex.Value, addNode: false, sendNetworkEvent: false);
}
else
{
wire.TryConnect(this, addNode: false, sendNetworkEvent: false);
}
wire.FixNodeEnds();
recipientsDirty = true;
}
}
LoadedWireIds.Clear();
LoadedWires.Clear();
}
@@ -381,7 +400,8 @@ namespace Barotrauma.Items.Components
foreach (var wire in wires.OrderBy(w => w.Item.ID))
{
newElement.Add(new XElement("link",
new XAttribute("w", wire.Item.ID.ToString())));
new XAttribute("w", wire.Item.ID.ToString()),
new XAttribute("i", wire.Connections[0] == this ? 0 : 1)));
}
parentElement.Add(newElement);
@@ -295,8 +295,8 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < loadedConnections.Count && i < Connections.Count; i++)
{
Connections[i].LoadedWireIds.Clear();
Connections[i].LoadedWireIds.AddRange(loadedConnections[i].LoadedWireIds);
Connections[i].LoadedWires.Clear();
Connections[i].LoadedWires.AddRange(loadedConnections[i].LoadedWires);
}
disconnectedWireIds = element.GetAttributeUshortArray("disconnectedwires", Array.Empty<ushort>()).ToList();
@@ -82,8 +82,10 @@ namespace Barotrauma.Items.Components
signalOut.SendDuration -= 1;
item.SendSignal(new Signal(signalOut.Signal.value, sender: signalOut.Signal.sender, strength: signalOut.Signal.strength), "signal_out");
if (signalOut.SendDuration <= 0)
{
signalQueue.Dequeue();
{
//check the queue isn't empty again, because sending the signal may empty it
//if this component is set to reset when it receives a signal and the signal is routed back to this component
signalQueue.TryDequeue(out _);
}
else
{
@@ -173,6 +173,8 @@ namespace Barotrauma.Items.Components
set
{
lightColor = value;
//reset previously received signal to force updating the color if we receive a set_color signal after the color has been modified manually
prevColorSignal = string.Empty;
#if CLIENT
if (Light != null)
{
@@ -249,6 +251,11 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
SetLightSourceState(IsActive, lightBrightness);
turret = item.GetComponent<Turret>();
if (item.body != null)
{
item.body.FarseerBody.OnEnabled += CheckIfNeedsUpdate;
item.body.FarseerBody.OnDisabled += CheckIfNeedsUpdate;
}
#if CLIENT
Drawable = AlphaBlend && Light.LightSprite != null;
if (Screen.Selected.IsEditor)
@@ -277,15 +284,24 @@ namespace Barotrauma.Items.Components
return;
}
if (item.body == null && powerConsumption <= 0.0f && Parent == null && turret == null &&
if ((item.body == null || !item.body.Enabled) &&
powerConsumption <= 0.0f && Parent == null && turret == null &&
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
if (item.body != null && !item.body.Enabled)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
isOn = true;
#if CLIENT
Light.ParentSub = item.Submarine;
#endif
@@ -222,10 +222,10 @@ namespace Barotrauma.Items.Components
{
Vector2 e1 = edge.Point1 + cell.Translation;
Vector2 e2 = edge.Point2 + cell.Translation;
if (MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
MathUtils.LinesIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
if (MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.Right, detectRect.Y)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Bottom), new Vector2(detectRect.Right, detectRect.Bottom)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.X, detectRect.Y), new Vector2(detectRect.X, detectRect.Bottom)) ||
MathUtils.LineSegmentsIntersect(e1, e2, new Vector2(detectRect.Right, detectRect.Y), new Vector2(detectRect.Right, detectRect.Bottom)))
{
MotionDetected = true;
return;
@@ -8,6 +8,9 @@ namespace Barotrauma.Items.Components
public Item source;
public float power;
public float strength;
public readonly double CreationTime;
public double TimeSinceCreated => Timing.TotalTimeUnpaused - CreationTime;
public Signal(string value, int stepsTaken = 0, Character sender = null,
Item source = null, float power = 0.0f, float strength = 1.0f)
@@ -18,6 +21,7 @@ namespace Barotrauma.Items.Components
this.source = source;
this.power = power;
this.strength = strength;
CreationTime = Timing.TotalTimeUnpaused;
}
internal Signal WithStepsTaken(int stepsTaken)
@@ -64,9 +64,7 @@ namespace Barotrauma.Items.Components
set;
}
private bool linkToChat = false;
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.AllowLinkingWifiToChat, onlyInEditors: false)]
[Serialize(false, IsPropertySaveable.No, description: "If enabled, any signals received from another chat-linked wifi component are displayed " +
"as chat messages in the chatbox of the player holding the item.", alwaysUseInstanceValues: true)]
public bool LinkToChat
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
private Vector2 end;
private readonly float angle;
private readonly float length;
public readonly float Length;
public Vector2 Start
{
@@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components
this.end = end;
angle = MathUtils.VectorToAngle(end - start);
length = Vector2.Distance(start, end);
Length = Vector2.Distance(start, end);
}
}
@@ -52,7 +52,7 @@ namespace Barotrauma.Items.Components
private List<Vector2> nodes;
private readonly List<WireSection> sections;
private Connection[] connections;
private readonly Connection[] connections;
private bool canPlaceNode;
private Vector2 newNodePos;
@@ -81,6 +81,8 @@ namespace Barotrauma.Items.Components
get { return connections; }
}
public float Length { get; private set; }
[Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")]
public float MaxLength
{
@@ -162,14 +164,50 @@ namespace Barotrauma.Items.Components
SetConnectedDirty();
}
public bool Connect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false)
/// <summary>
/// Tries to add the given connection to this wire. Note that this only affects the wire -
/// adding the wire to the connection is done in <see cref="Connection.ConnectWire(Wire)"/>
/// </summary>
public bool TryConnect(Connection newConnection, bool addNode = true, bool sendNetworkEvent = false)
{
if (connections[0] == null)
{
return Connect(newConnection, 0, addNode, sendNetworkEvent);
}
else if (connections[1] == null)
{
return Connect(newConnection, 1, addNode, sendNetworkEvent);
}
return false;
}
/// <summary>
/// Tries to add the given connection to this wire. Note that this only affects the wire -
/// adding the wire to the connection is done in <see cref="Connection.ConnectWire(Wire)"/>
/// </summary>
/// <param name="connectionIndex">Which end of the wire to add the connection to? 0 or 1.
/// Normally doesn't make a difference, but matters if we're copying/loading a wire,
/// in which case the 1st node should be located at the same item as the 1st connection.</param>
/// <returns></returns>
public bool Connect(Connection newConnection, int connectionIndex, bool addNode = true, bool sendNetworkEvent = false)
{
for (int i = 0; i < 2; i++)
{
if (connections[i] == newConnection) { return false; }
}
if (!connections.Any(c => c == null)) { return false; }
if (connectionIndex < 0 || connectionIndex > 1)
{
DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: {connectionIndex} is not a valid index.");
return false;
}
if (connections[connectionIndex] != null)
{
DebugConsole.ThrowError($"Error while connecting a wire to {newConnection.Item}: a wire is already connected to the index {connectionIndex}.");
return false;
}
for (int i = 0; i < 2; i++)
{
@@ -183,70 +221,12 @@ namespace Barotrauma.Items.Components
newConnection.ConnectionPanel.DisconnectedWires.Remove(this);
for (int i = 0; i < 2; i++)
connections[connectionIndex] = newConnection;
FixNodeEnds();
if (addNode)
{
if (connections[i] != null) { continue; }
connections[i] = newConnection;
FixNodeEnds();
if (!addNode) { break; }
Submarine refSub = newConnection.Item.Submarine;
if (refSub == null)
{
Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition);
if (attachTarget == null && !(newConnection.Item.GetComponent<Holdable>()?.Attached ?? false))
{
connections[i] = null;
continue;
}
refSub = attachTarget?.Submarine;
}
Vector2 nodePos = refSub == null ?
newConnection.Item.Position :
newConnection.Item.Position - refSub.HiddenSubPosition;
if (nodes.Count > 0 && nodes[0] == nodePos) { break; }
if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { break; }
//make sure we place the node at the correct end of the wire (the end that's closest to the new node pos)
int newNodeIndex = 0;
if (nodes.Count > 1)
{
if (connections[0] != null && connections[0] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) <
Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)))
{
newNodeIndex = nodes.Count;
}
}
else if (connections[1] != null && connections[1] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) <
Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)))
{
newNodeIndex = nodes.Count;
}
}
else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos))
{
newNodeIndex = nodes.Count;
}
}
if (newNodeIndex == 0 && nodes.Count > 1)
{
nodes.Insert(0, nodePos);
}
else
{
nodes.Add(nodePos);
}
break;
AddNode(newConnection, connectionIndex);
}
SetConnectedDirty();
@@ -258,7 +238,7 @@ namespace Barotrauma.Items.Components
if (ic == this) { continue; }
ic.Drop(null);
}
if (item.Container != null) { item.Container.RemoveContained(this.item); }
item.Container?.RemoveContained(item);
if (item.body != null) { item.body.Enabled = false; }
IsActive = false;
@@ -286,6 +266,63 @@ namespace Barotrauma.Items.Components
return true;
}
private void AddNode(Connection newConnection, int selectedIndex)
{
Submarine refSub = newConnection.Item.Submarine;
if (refSub == null)
{
Structure attachTarget = Structure.GetAttachTarget(newConnection.Item.WorldPosition);
if (attachTarget == null && !(newConnection.Item.GetComponent<Holdable>()?.Attached ?? false))
{
connections[selectedIndex] = null;
return;
}
refSub = attachTarget?.Submarine;
}
Vector2 nodePos = refSub == null ?
newConnection.Item.Position :
newConnection.Item.Position - refSub.HiddenSubPosition;
if (nodes.Count > 0 && nodes[0] == nodePos) { return; }
if (nodes.Count > 1 && nodes[nodes.Count - 1] == nodePos) { return; }
//make sure we place the node at the correct end of the wire (the end that's closest to the new node pos)
int newNodeIndex = 0;
if (nodes.Count > 1)
{
if (connections[0] != null && connections[0] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) <
Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[0].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)))
{
newNodeIndex = nodes.Count;
}
}
else if (connections[1] != null && connections[1] != newConnection)
{
if (Vector2.DistanceSquared(nodes[0], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)) <
Vector2.DistanceSquared(nodes[nodes.Count - 1], connections[1].Item.Position - (refSub?.HiddenSubPosition ?? Vector2.Zero)))
{
newNodeIndex = nodes.Count;
}
}
else if (Vector2.DistanceSquared(nodes[nodes.Count - 1], nodePos) < Vector2.DistanceSquared(nodes[0], nodePos))
{
newNodeIndex = nodes.Count;
}
}
if (newNodeIndex == 0 && nodes.Count > 1)
{
nodes.Insert(0, nodePos);
}
else
{
nodes.Add(nodePos);
}
}
public override void Equip(Character character)
{
if (shouldClearConnections) { ClearConnections(character); }
@@ -298,7 +335,7 @@ namespace Barotrauma.Items.Components
IsActive = false;
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
if (shouldClearConnections) { ClearConnections(dropper); }
IsActive = false;
@@ -528,6 +565,7 @@ namespace Barotrauma.Items.Components
sections.Add(new WireSection(nodes[i], nodes[i + 1]));
}
Drawable = IsActive || sections.Count > 0;
Length = sections.Count > 0 ? sections.Sum(s => s.Length) : 0;
CalculateExtents();
}
@@ -845,8 +845,13 @@ namespace Barotrauma.Items.Components
{
public readonly Item Projectile;
public EventData(Item projectile)
public EventData(Item projectile, Turret turret)
{
System.Diagnostics.Debug.Assert(projectile != null, $"Tried to create Turret {nameof(EventData)} with no projectile.");
GameAnalyticsManager.AddErrorEventOnce(
"Turret.EventData:entitynull"+ turret.Item.Prefab.Identifier,
GameAnalyticsManager.ErrorSeverity.Error,
$"Turret \"{turret.Item.Prefab.Identifier}\" tried to create {nameof(EventData)} with no projectile.");
Projectile = projectile;
}
}
@@ -918,7 +923,7 @@ namespace Barotrauma.Items.Components
projectile.Container?.RemoveContained(projectile);
}
#if SERVER
item.CreateServerEvent(this, new EventData(projectile));
item.CreateServerEvent(this, new EventData(projectile, this));
#endif
ApplyStatusEffects(ActionType.OnUse, 1.0f, user: user);
@@ -1314,7 +1319,9 @@ namespace Barotrauma.Items.Components
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
// Don't shoot at captured enemies.
if (enemy.LockHands) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
if (dist > closestDistance) { continue; }
if (dist < shootDistance * shootDistance)
@@ -1413,7 +1420,7 @@ namespace Barotrauma.Items.Components
{
// The closest point can't be targeted -> get a point directly in front of the turret
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (MathUtils.GetLineIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
{
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
@@ -1889,22 +1896,27 @@ namespace Barotrauma.Items.Components
{
if (TryExtractEventData(extraData, out EventData eventData))
{
msg.WriteUInt16(eventData.Projectile.ID);
msg.WriteRangedSingle(MathHelper.Clamp(rotation, minRotation, maxRotation), minRotation, maxRotation, 16);
msg.WriteUInt16(eventData.Projectile?.ID ?? Entity.NullEntityID);
msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(rotation), minRotation, maxRotation), minRotation, maxRotation, 16);
}
else
{
msg.WriteUInt16((ushort)0);
float wrappedTargetRotation = targetRotation;
while (wrappedTargetRotation < minRotation && MathUtils.IsValid(wrappedTargetRotation))
msg.WriteRangedSingle(MathHelper.Clamp(wrapAngle(targetRotation), minRotation, maxRotation), minRotation, maxRotation, 16);
}
float wrapAngle(float angle)
{
float wrappedAngle = angle;
while (wrappedAngle < minRotation && MathUtils.IsValid(wrappedAngle))
{
wrappedTargetRotation += MathHelper.TwoPi;
wrappedAngle += MathHelper.TwoPi;
}
while (wrappedTargetRotation > maxRotation && MathUtils.IsValid(wrappedTargetRotation))
while (wrappedAngle > maxRotation && MathUtils.IsValid(wrappedAngle))
{
wrappedTargetRotation -= MathHelper.TwoPi;
wrappedAngle -= MathHelper.TwoPi;
}
msg.WriteRangedSingle(MathHelper.Clamp(wrappedTargetRotation, minRotation, maxRotation), minRotation, maxRotation, 16);
return wrappedAngle;
}
}
}
@@ -482,11 +482,11 @@ namespace Barotrauma.Items.Components
character.OnWearablesChanged();
}
public override void Drop(Character dropper)
public override void Drop(Character dropper, bool setTransform = true)
{
Character previousPicker = picker;
Unequip(picker);
base.Drop(dropper);
base.Drop(dropper, setTransform);
previousPicker?.OnWearablesChanged();
picker = null;
IsActive = false;
@@ -580,8 +580,8 @@ namespace Barotrauma
if (removeItem)
{
item.Drop(user);
if (item.ParentInventory != null) { item.ParentInventory.RemoveItem(item); }
item.Drop(user, setTransform: false);
item.ParentInventory?.RemoveItem(item);
}
slots[i].Add(item);
@@ -845,13 +845,31 @@ namespace Barotrauma
if (otherIsEquipped)
{
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
TryPutAndForce(existingItems, this, index);
TryPutAndForce(stackedItems, otherInventory, otherIndex);
}
else
{
stackedItems.ForEach(stackedItem => otherInventory.TryPutItem(stackedItem, otherIndex, false, false, user, createNetworkEvent, ignoreCondition: true));
existingItems.ForEach(existingItem => TryPutItem(existingItem, index, false, false, user, createNetworkEvent, ignoreCondition: true));
TryPutAndForce(stackedItems, otherInventory, otherIndex);
TryPutAndForce(existingItems, this, index);
}
void TryPutAndForce(IEnumerable<Item> items, Inventory inventory, int slotIndex)
{
foreach (var item in items)
{
if (!inventory.TryPutItem(item, slotIndex, false, false, user, createNetworkEvent, ignoreCondition: true) &&
!inventory.GetItemsAt(slotIndex).Contains(item))
{
inventory.ForceToSlot(item, slotIndex);
}
}
}
if (createNetworkEvent)
{
CreateNetworkEvent();
otherInventory.CreateNetworkEvent();
}
#if CLIENT
@@ -44,6 +44,13 @@ namespace Barotrauma
/// </summary>
public static IReadOnlyCollection<Item> CleanableItems => cleanableItems;
private static readonly List<Item> sonarVisibleItems = new List<Item>();
/// <summary>
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
/// </summary>
public static IReadOnlyCollection<Item> SonarVisibleItems => sonarVisibleItems;
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
public static bool ShowLinks = true;
@@ -127,7 +134,8 @@ namespace Barotrauma
private float condition;
private bool inWater;
private readonly bool hasWaterStatusEffects;
private readonly bool hasInWaterStatusEffects;
private readonly bool hasNotInWaterStatusEffects;
private Inventory parentInventory;
private readonly ItemInventory ownInventory;
@@ -207,6 +215,10 @@ namespace Barotrauma
}
}
public Item RootContainer { get; private set; }
private bool inWaterProofContainer;
private Item container;
public Item Container
{
@@ -218,6 +230,8 @@ namespace Barotrauma
container = value;
CheckCleanable();
SetActiveSprite();
RefreshRootContainer();
}
}
}
@@ -409,14 +423,15 @@ namespace Barotrauma
set { spriteColor = value; }
}
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)]
public Color InventoryIconColor
{
get;
protected set;
}
[Editable, Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true.")]
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, description: "Changes the color of the item this item is contained inside. Only has an effect if either of the UseContainedSpriteColor or UseContainedInventoryIconColor property of the container is set to true."),
ConditionallyEditable(ConditionallyEditable.ConditionType.Pickable)]
public Color ContainerColor
{
get;
@@ -700,14 +715,26 @@ namespace Barotrauma
}
}
[Serialize(false, IsPropertySaveable.No)]
public bool FireProof
{
get { return Prefab.FireProof; }
get; private set;
}
private bool waterProof;
[Serialize(false, IsPropertySaveable.No)]
public bool WaterProof
{
get { return Prefab.WaterProof; }
get { return waterProof; }
private set
{
if (waterProof == value) { return; }
waterProof = value;
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshInWaterProofContainer();
}
}
}
public bool UseInHealthInterface
@@ -736,7 +763,7 @@ namespace Barotrauma
{
//if the item has an active physics body, inWater is updated in the Update method
if (body != null && body.Enabled) { return inWater; }
if (hasWaterStatusEffects) { return inWater; }
if (hasInWaterStatusEffects) { return inWater; }
//if not, we'll just have to check
return IsInWater();
@@ -1068,7 +1095,8 @@ namespace Barotrauma
}
}
hasWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater] || hasStatusEffectsOfType[(int)ActionType.NotInWater];
hasInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.InWater];
hasNotInWaterStatusEffects = hasStatusEffectsOfType[(int)ActionType.NotInWater];
if (body != null)
{
@@ -1124,6 +1152,7 @@ namespace Barotrauma
ItemList.Add(this);
if (Prefab.IsDangerous) { dangerousItems.Add(this); }
if (Repairables.Any()) { repairableItems.Add(this); }
if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); }
CheckCleanable();
DebugConsole.Log("Created " + Name + " (" + ID + ")");
@@ -1419,7 +1448,7 @@ namespace Barotrauma
}
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
public override void Move(Vector2 amount, bool ignoreContacts = true)
{
if (!MathUtils.IsValid(amount))
{
@@ -1427,7 +1456,7 @@ namespace Barotrauma
return;
}
base.Move(amount);
base.Move(amount, ignoreContacts);
if (ItemList != null && body != null)
{
@@ -1511,17 +1540,51 @@ namespace Barotrauma
return CurrentHull;
}
public Item GetRootContainer()
private void RefreshRootContainer()
{
if (Container == null) { return null; }
Item rootContainer = Container;
while (rootContainer.Container != null)
Item newRootContainer = null;
inWaterProofContainer = false;
if (Container != null)
{
rootContainer = rootContainer.Container;
Item rootContainer = Container;
inWaterProofContainer |= Container.WaterProof;
while (rootContainer.Container != null)
{
rootContainer = rootContainer.Container;
inWaterProofContainer |= rootContainer.WaterProof;
}
newRootContainer = rootContainer;
}
if (newRootContainer != RootContainer)
{
RootContainer = newRootContainer;
isActive = true;
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshRootContainer();
}
}
return rootContainer;
}
private void RefreshInWaterProofContainer()
{
inWaterProofContainer = false;
if (container == null) { return; }
if (container.WaterProof || container.inWaterProofContainer)
{
inWaterProofContainer = true;
}
foreach (Item containedItem in ContainedItems)
{
containedItem.RefreshInWaterProofContainer();
}
}
/// <summary>
/// Used by the AI to check whether they can (in principle) and are allowed (in practice) to interact with an object or not.
/// Unlike CanInteractWith(), this method doesn't check the distance, the triggers, or anything like that.
/// </summary>
public bool HasAccess(Character character)
{
if (character.IsBot && IgnoreByAI(character)) { return false; }
@@ -1529,6 +1592,7 @@ namespace Barotrauma
var itemContainer = GetComponent<ItemContainer>();
if (itemContainer != null && !itemContainer.HasAccess(character)) { return false; }
if (Container != null && !Container.HasAccess(character)) { return false; }
if (GetComponent<Pickable>() is { CanBePicked: false }) { return false; }
return true;
}
@@ -1538,9 +1602,8 @@ namespace Barotrauma
{
if (ParentInventory == null) { return this; }
if (ParentInventory.Owner is Character) { return ParentInventory.Owner; }
var rootContainer = GetRootContainer();
if (rootContainer?.ParentInventory?.Owner is Character) { return rootContainer.ParentInventory.Owner; }
return rootContainer ?? this;
if (RootContainer?.ParentInventory?.Owner is Character) { return RootContainer.ParentInventory.Owner; }
return RootContainer ?? this;
}
public Inventory FindParentInventory(Func<Inventory, bool> predicate)
@@ -1784,8 +1847,23 @@ namespace Barotrauma
bool wasInFullCondition = IsFullCondition;
float diff = value - condition;
if (GetComponent<Door>() is Door door && door.IsStuck && diff < 0)
{
float dmg = -diff;
// When the door is fully welded shut, reduce the welded state instead of the condition.
float prevStuck = door.Stuck;
door.Stuck -= dmg;
if (door.IsStuck) { return; }
// Reduce the damage by the amount we just adjusted the welded state by.
float damageReduction = dmg - prevStuck;
if (damageReduction < 0) { return; }
value -= damageReduction;
}
condition = MathHelper.Clamp(value, 0.0f, MaxCondition);
if (MathUtils.NearlyEqual(prevCondition, condition, epsilon: 0.000001f)) { return; }
if (MathUtils.NearlyEqual(prevCondition, value, epsilon: 0.000001f)) { return; }
RecalculateConditionValues();
@@ -2008,7 +2086,7 @@ namespace Barotrauma
if (Removed) { return; }
bool needsWaterCheck = hasWaterStatusEffects;
bool needsWaterCheck = hasInWaterStatusEffects || hasNotInWaterStatusEffects;
if (body != null && body.Enabled)
{
System.Diagnostics.Debug.Assert(body.FarseerBody.FixtureList != null);
@@ -2037,7 +2115,7 @@ namespace Barotrauma
if (needsWaterCheck)
{
bool wasInWater = inWater;
inWater = IsInWater() && !WaterProof;
inWater = !inWaterProofContainer && IsInWater() && !WaterProof;
if (inWater)
{
//the item has gone through the surface of the water
@@ -2050,36 +2128,29 @@ namespace Barotrauma
body.LinearVelocity *= 0.2f;
}
}
Item container = this.Container;
while (container != null)
{
if (container.WaterProof)
{
inWater = false;
break;
}
container = container.Container;
}
}
if (hasWaterStatusEffects && condition > 0.0f)
if ((hasInWaterStatusEffects || hasNotInWaterStatusEffects) && condition > 0.0f)
{
ApplyStatusEffects(inWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
}
}
else
{
if (updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
(body == null || !body.Enabled))
if (inWaterProofContainer && !hasNotInWaterStatusEffects)
{
#if CLIENT
positionBuffer.Clear();
#endif
isActive = false;
needsWaterCheck = false;
}
}
if (!needsWaterCheck &&
updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
(body == null || !body.Enabled))
{
#if CLIENT
positionBuffer.Clear();
#endif
isActive = false;
}
}
partial void Splash();
@@ -2877,7 +2948,7 @@ namespace Barotrauma
if (user != null)
{
var abilityItem = new AbilityApplyTreatment(user, character, this);
var abilityItem = new AbilityApplyTreatment(user, character, this, targetLimb);
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
}
@@ -2938,7 +3009,7 @@ namespace Barotrauma
}
}
foreach (ItemComponent ic in components) { ic.Drop(dropper); }
foreach (ItemComponent ic in components) { ic.Drop(dropper, setTransform); }
if (Container != null)
{
@@ -3577,7 +3648,7 @@ namespace Barotrauma
element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture)));
}
Item rootContainer = GetRootContainer() ?? this;
Item rootContainer = RootContainer ?? this;
System.Diagnostics.Debug.Assert(Submarine != null || rootContainer.ParentInventory?.Owner is Character);
Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
@@ -3758,6 +3829,7 @@ namespace Barotrauma
ItemList.Remove(this);
dangerousItems.Remove(this);
repairableItems.Remove(this);
sonarVisibleItems.Remove(this);
cleanableItems.Remove(this);
}
@@ -3781,12 +3853,14 @@ namespace Barotrauma
public Character Character { get; set; }
public Character User { get; set; }
public Item Item { get; set; }
public Limb TargetLimb { get; set; }
public AbilityApplyTreatment(Character user, Character target, Item item)
public AbilityApplyTreatment(Character user, Character target, Item item, Limb limb)
{
Character = target;
User = user;
Item = item;
TargetLimb = limb;
}
}
}
@@ -88,12 +88,15 @@ namespace Barotrauma
public abstract ItemPrefab FirstMatchingPrefab { get; }
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition)
public LocalizedString OverrideDescription { get; }
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
{
Amount = amount;
MinCondition = minCondition;
MaxCondition = maxCondition;
UseCondition = useCondition;
OverrideDescription = overrideDescription;
}
public readonly int Amount;
public readonly float MinCondition;
@@ -129,12 +132,14 @@ namespace Barotrauma
public override ItemPrefab FirstMatchingPrefab => ItemPrefab;
public override bool MatchesItem(Item item)
{
return item?.Prefab.Identifier == ItemPrefabIdentifier;
}
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition)
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) :
base(amount, minCondition, maxCondition, useCondition, overrideDescription)
{
ItemPrefabIdentifier = itemPrefab;
using MD5 md5 = MD5.Create();
@@ -163,7 +168,8 @@ namespace Barotrauma
return item.HasTag(Tag);
}
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition) : base(amount, minCondition, maxCondition, useCondition)
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
: base(amount, minCondition, maxCondition, useCondition, overrideDescription)
{
Tag = tag;
using MD5 md5 = MD5.Create();
@@ -260,6 +266,12 @@ namespace Barotrauma
bool useCondition = subElement.GetAttributeBool("usecondition", true);
int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
LocalizedString description = string.Empty;
if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty())
{
description = TextManager.Get(texTag);
}
if (requiredItemIdentifier != Identifier.Empty)
{
var existing = requiredItems.FindIndex(r =>
@@ -272,7 +284,7 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition));
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description));
}
else
{
@@ -286,7 +298,7 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition));
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description));
}
break;
}
@@ -669,8 +681,19 @@ namespace Barotrauma
[Serialize(0.0f, IsPropertySaveable.No)]
public float OffsetOnSelected { get; private set; }
private float health;
[Serialize(100.0f, IsPropertySaveable.No)]
public float Health { get; private set; }
public float Health
{
get { return health; }
private set
{
//don't allow health values higher than this, because they lead to various issues:
//e.g. integer overflows when we're casting to int to display a health value, value being set to float.Infinity if it's high enough
health = Math.Min(value, 1000000.0f);
}
}
[Serialize(false, IsPropertySaveable.No)]
public bool AllowSellingWhenBroken { get; private set; }
@@ -702,12 +725,6 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool DamagedByMonsters { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool FireProof { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool WaterProof { get; private set; }
private float impactTolerance;
[Serialize(0.0f, IsPropertySaveable.No)]
public float ImpactTolerance
@@ -813,10 +830,13 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
public float AISlowTurretPriority { get; private set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
public float AITurretTargetingMaxDistance { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")]
public bool AllowStealingContainedItems { get; private set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -1437,6 +1457,22 @@ namespace Barotrauma
"Specify the amount in the variant to fix this.");
}
}
if (originalElement?.Name.ToIdentifier() == "Deconstruct" &&
variantElement?.Name.ToIdentifier() == "Deconstruct")
{
if (originalElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
variantElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
{
DebugConsole.AddWarning($"Potential error in item variant \"{Identifier}\": " +
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. Overriding the base recipe may not work correctly.");
}
if (variantElement.Elements().Any(e => e.Name.ToIdentifier() == "Item") &&
originalElement.Elements().Any(e => e.Name.ToIdentifier() == "RequiredItem"))
{
DebugConsole.AddWarning($"Potential error in item \"{parent.Identifier}\": " +
$"the item defines deconstruction recipes using 'RequiredItem' instead of 'Item'. The item variant \"{Identifier}\" may not override the base recipe correctly.");
}
}
}
}
@@ -35,7 +35,11 @@ namespace Barotrauma
/// The item this relation is defined in must be inside a specific kind of container.
/// Can for example by used to make an item do something when it's inside some other type of item.
/// </summary>
Container
Container,
/// <summary>
/// Signifies an error (type could not be parsed)
/// </summary>
Invalid
}
/// <summary>
@@ -60,9 +64,9 @@ namespace Barotrauma
/// </summary>
public ImmutableHashSet<Identifier> ExcludedIdentifiers { get; private set; }
private RelationType type;
private readonly RelationType type;
public List<StatusEffect> statusEffects;
public List<StatusEffect> StatusEffects = new List<StatusEffect>();
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. A message displayed if the required item isn't found (e.g. a notification about lack of ammo or fuel).
@@ -198,8 +202,121 @@ namespace Barotrauma
{
this.Identifiers = identifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
this.ExcludedIdentifiers = excludedIdentifiers.Select(id => id.Value.Trim().ToIdentifier()).ToImmutableHashSet();
}
public RelatedItem(ContentXElement element, string parentDebugName)
{
Identifier[] identifiers;
if (element.GetAttribute("name") != null)
{
//backwards compatibility + a console warning
DebugConsole.ThrowError($"Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.");
Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty<Identifier>());
//attempt to convert to identifiers and tags
List<Identifier> convertedIdentifiers = new List<Identifier>();
foreach (Identifier itemName in itemNames)
{
var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value);
if (matchingItem != null)
{
convertedIdentifiers.Add(matchingItem.Identifier);
}
else
{
//no matching item found, this must be a tag
convertedIdentifiers.Add(itemName);
}
}
identifiers = convertedIdentifiers.ToArray();
}
else
{
identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty<Identifier>());
}
}
}
this.Identifiers = identifiers.ToImmutableHashSet();
Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty<Identifier>());
}
}
this.ExcludedIdentifiers = excludedIdentifiers.ToImmutableHashSet();
ExcludeBroken = element.GetAttributeBool("excludebroken", true);
RequireEmpty = element.GetAttributeBool("requireempty", false);
ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false);
AllowVariants = element.GetAttributeBool("allowvariants", true);
Rotation = element.GetAttributeFloat("rotation", 0f);
SetActive = element.GetAttributeBool("setactive", false);
if (element.GetAttribute(nameof(Hide)) != null)
{
Hide = element.GetAttributeBool(nameof(Hide), false);
}
if (element.GetAttribute(nameof(ItemPos)) != null)
{
ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero);
}
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "containable":
typeStr = "Contained";
break;
case "suitablefertilizer":
case "suitableseed":
typeStr = "None";
break;
}
}
if (!Enum.TryParse(typeStr, true, out type))
{
DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.");
type = RelationType.Invalid;
}
MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty);
LocalizedString msg = TextManager.Get(MsgTag);
if (!msg.Loaded)
{
Msg = MsgTag.Value;
}
else
{
#if CLIENT
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
{
msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType));
}
Msg = msg;
#endif
}
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; }
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
IsOptional = element.GetAttributeBool("optional", false);
IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false);
MatchOnEmpty = element.GetAttributeBool("matchonempty", false);
TargetSlot = element.GetAttributeInt("targetslot", -1);
statusEffects = new List<StatusEffect>();
}
public bool CheckRequirements(Character character, Item parentItem)
@@ -301,120 +418,10 @@ namespace Barotrauma
}
public static RelatedItem Load(ContentXElement element, bool returnEmpty, string parentDebugName)
{
Identifier[] identifiers;
if (element.GetAttribute("name") != null)
{
//backwards compatibility + a console warning
DebugConsole.ThrowError("Error in RelatedItem config (" + (string.IsNullOrEmpty(parentDebugName) ? element.ToString() : parentDebugName) + ") - use item tags or identifiers instead of names.");
Identifier[] itemNames = element.GetAttributeIdentifierArray("name", Array.Empty<Identifier>());
//attempt to convert to identifiers and tags
List<Identifier> convertedIdentifiers = new List<Identifier>();
foreach (Identifier itemName in itemNames)
{
var matchingItem = ItemPrefab.Prefabs.Find(me => me.Name == itemName.Value);
if (matchingItem != null)
{
convertedIdentifiers.Add(matchingItem.Identifier);
}
else
{
//no matching item found, this must be a tag
convertedIdentifiers.Add(itemName);
}
}
identifiers = convertedIdentifiers.ToArray();
}
else
{
identifiers = element.GetAttributeIdentifierArray("items", null) ?? element.GetAttributeIdentifierArray("item", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifiers", null) ?? element.GetAttributeIdentifierArray("tags", null);
if (identifiers == null)
{
identifiers = element.GetAttributeIdentifierArray("identifier", null) ?? element.GetAttributeIdentifierArray("tag", Array.Empty<Identifier>());
}
}
}
Identifier[] excludedIdentifiers = element.GetAttributeIdentifierArray("excludeditems", null) ?? element.GetAttributeIdentifierArray("excludeditem", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifiers", null) ?? element.GetAttributeIdentifierArray("excludedtags", null);
if (excludedIdentifiers == null)
{
excludedIdentifiers = element.GetAttributeIdentifierArray("excludedidentifier", null) ?? element.GetAttributeIdentifierArray("excludedtag", Array.Empty<Identifier>());
}
}
if (identifiers.Length == 0 && excludedIdentifiers.Length == 0 && !returnEmpty) { return null; }
RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers)
{
ExcludeBroken = element.GetAttributeBool("excludebroken", true),
RequireEmpty = element.GetAttributeBool("requireempty", false),
ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false),
AllowVariants = element.GetAttributeBool("allowvariants", true),
Rotation = element.GetAttributeFloat("rotation", 0f),
SetActive = element.GetAttributeBool("setactive", false)
};
if (element.GetAttribute(nameof(Hide)) != null)
{
ri.Hide = element.GetAttributeBool(nameof(Hide), false);
}
if (element.GetAttribute(nameof(ItemPos)) != null)
{
ri.ItemPos = element.GetAttributeVector2(nameof(ItemPos), Vector2.Zero);
}
string typeStr = element.GetAttributeString("type", "");
if (string.IsNullOrEmpty(typeStr))
{
switch (element.Name.ToString().ToLowerInvariant())
{
case "containable":
typeStr = "Contained";
break;
case "suitablefertilizer":
case "suitableseed":
typeStr = "None";
break;
}
}
if (!Enum.TryParse(typeStr, true, out ri.type))
{
DebugConsole.ThrowError("Error in RelatedItem config (" + parentDebugName + ") - \"" + typeStr + "\" is not a valid relation type.");
return null;
}
ri.MsgTag = element.GetAttributeIdentifier("msg", Identifier.Empty);
LocalizedString msg = TextManager.Get(ri.MsgTag);
if (!msg.Loaded)
{
ri.Msg = ri.MsgTag.Value;
}
else
{
#if CLIENT
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
{
msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType));
}
ri.Msg = msg;
#endif
}
foreach (var subElement in element.Elements())
{
if (!subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase)) { continue; }
ri.statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
}
ri.IsOptional = element.GetAttributeBool("optional", false);
ri.IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false);
ri.MatchOnEmpty = element.GetAttributeBool("matchonempty", false);
ri.TargetSlot = element.GetAttributeInt("targetslot", -1);
{
RelatedItem ri = new RelatedItem(element, parentDebugName);
if (ri.Type == RelationType.Invalid) { return null; }
if (ri.Identifiers.None() && ri.ExcludedIdentifiers.None() && !returnEmpty) { return null; }
return ri;
}
}
@@ -326,7 +326,9 @@ namespace Barotrauma
var lightComponent = item.GetComponent<LightComponent>();
if (lightComponent != null)
{
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor, 10.0f);
//multiply by 10 to make the effect more noticeable
//(a strength of 1 is already enough to kill power and shut down the lights, but we want weaker EMPs to make the lights flicker noticeably)
lightComponent.TemporaryFlickerTimer = Math.Min(EmpStrength * distFactor * 10.0f, 10.0f);
}
//discharge batteries
@@ -254,7 +254,7 @@ namespace Barotrauma
d.ForceRefreshFadeTimer(Math.Min(d.FadeTimer, d.FadeInTime));
}
UpdateProjSpecific(growModifier);
UpdateProjSpecific(growModifier, deltaTime);
if (size.X < 1.0f && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
@@ -273,7 +273,7 @@ namespace Barotrauma
position.X -= GrowSpeed * growModifier * 0.5f * deltaTime;
}
partial void UpdateProjSpecific(float growModifier);
partial void UpdateProjSpecific(float growModifier, float deltaTime);
private void OnChangeHull(Vector2 pos, Hull particleHull)
{
@@ -33,6 +33,8 @@ namespace Barotrauma
/// </summary>
public bool IsDiagonal { get; }
public readonly float GlowEffectT;
//a value between 0.0f-1.0f (0.0 = closed, 1.0f = open)
private float open;
@@ -194,6 +196,8 @@ namespace Barotrauma
GapList.Add(this);
InsertToList();
GlowEffectT = Rand.Range(0.0f, 1.0f);
float blockerSize = ConvertUnits.ToSimUnits(Math.Max(rect.Width, rect.Height)) / 2;
outsideCollisionBlocker = GameMain.World.CreateEdge(-Vector2.UnitX * blockerSize, Vector2.UnitX * blockerSize,
BodyType.Static,
@@ -216,7 +220,7 @@ namespace Barotrauma
return new Gap(rect, IsHorizontal, Submarine);
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
public override void Move(Vector2 amount, bool ignoreContacts = true)
{
if (!MathUtils.IsValid(amount))
{
@@ -224,7 +228,7 @@ namespace Barotrauma
return;
}
base.Move(amount);
base.Move(amount, ignoreContacts);
if (!DisableHullRechecks) { FindHulls(); }
}
@@ -337,8 +341,28 @@ namespace Barotrauma
}
}
private int updateCount;
public override void Update(float deltaTime, Camera cam)
{
int updateInterval = 4;
float flowMagnitude = flowForce.LengthSquared();
if (flowMagnitude < 1.0f)
{
//very sparse updates if there's practically no water moving
updateInterval = 8;
}
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
{
//frequent updates if water is moving between hulls
updateInterval = 1;
}
updateCount++;
if (updateCount < updateInterval) { return; }
deltaTime *= updateCount;
updateCount = 0;
flowForce = Vector2.Zero;
outsideColliderRaycastTimer -= deltaTime;
@@ -590,7 +590,7 @@ namespace Barotrauma
return index;
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
public override void Move(Vector2 amount, bool ignoreContacts = true)
{
if (!MathUtils.IsValid(amount))
{
@@ -851,7 +851,21 @@ namespace Barotrauma
{
decal.Update(deltaTime);
}
decals.RemoveAll(d => d.FadeTimer >= d.LifeTime || d.BaseAlpha <= 0.001f);
//clients don't remove decals unless the server says so
if (GameMain.NetworkMember is not { IsClient: true })
{
for (int i = decals.Count - 1; i >= 0; i--)
{
var decal = decals[i];
if (decal.FadeTimer >= decal.LifeTime || decal.BaseAlpha <= 0.001f)
{
decals.RemoveAt(i);
#if SERVER
decalUpdatePending = true;
#endif
}
}
}
if (aiTarget != null)
{
@@ -1509,9 +1523,8 @@ namespace Barotrauma
public void CleanSection(BackgroundSection section, float cleanVal, bool updateRequired)
{
bool decalsCleaned = false;
for (int i = 0; i < decals.Count; i++)
foreach (Decal decal in decals)
{
Decal decal = decals[i];
if (decal.AffectsSection(section))
{
decal.Clean(cleanVal);
@@ -1672,5 +1685,9 @@ namespace Barotrauma
return element;
}
public override string ToString()
{
return $"{base.ToString()} ({Name ?? "unnamed"})";
}
}
}
@@ -136,7 +136,7 @@ namespace Barotrauma
{
me.Move(position);
me.Submarine = sub;
if (!(me is Item item)) { continue; }
if (me is not Item item) { continue; }
Wire wire = item.GetComponent<Wire>();
//Vector2 subPosition = Submarine == null ? Vector2.Zero : Submarine.HiddenSubPosition;
if (wire != null)
@@ -296,9 +296,9 @@ namespace Barotrauma
Vector2 triangleCenter = (edge.Point1 + edge.Point2 + extrudedPoint) / 3;
foreach (GraphEdge nearbyEdge in nearbyCell.Edges)
{
if (!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) &&
!MathUtils.LinesIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2))
if (!MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, extrudedPoint) &&
!MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point2, extrudedPoint) &&
!MathUtils.LineSegmentsIntersect(nearbyEdge.Point1, triangleCenter, edge.Point1, edge.Point2))
{
isInside = true;
break;
@@ -1454,7 +1454,7 @@ namespace Barotrauma
if (node2.X <= pathNodes.Last().X) { continue; }
if (MathUtils.NearlyEqual(node1.X, pathNodes.Last().X)) { continue; }
if (Math.Abs(node1.Y - nodePos.Y) > tunnel.MinWidth && Math.Abs(node2.Y - nodePos.Y) > tunnel.MinWidth &&
!MathUtils.LinesIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2()))
!MathUtils.LineSegmentsIntersect(node1.ToVector2(), node2.ToVector2(), pathNodes.Last().ToVector2(), nodePos.ToVector2()))
{
continue;
}
@@ -1550,7 +1550,7 @@ namespace Barotrauma
foreach (GraphEdge edge in tunnel.Cells[i].Edges)
{
if (edge.AdjacentCell(tunnel.Cells[i])?.CellType == CellType.Solid &&
MathUtils.LinesIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2))
MathUtils.LineSegmentsIntersect(newWaypoint.WorldPosition, prevWayPoint.WorldPosition, edge.Point1, edge.Point2))
{
solidCellBetween = true;
break;
@@ -2801,7 +2801,7 @@ namespace Barotrauma
if (Vector2.DistanceSquared(c.EdgeCenter, validLocation.EdgeCenter) > (intervalRange.X * intervalRange.X)) { return true; }
// If there is a line from a previous path point to one of its existing cluster locations
// which intersects with the line from this path point to the new possible cluster location
if (MathUtils.LinesIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; }
if (MathUtils.LineSegmentsIntersect(anotherPathPoint.Position, c.EdgeCenter, pathPoint.Position, validLocation.EdgeCenter)) { return true; }
return false;
}
}
@@ -2975,13 +2975,13 @@ namespace Barotrauma
}
/// <param name="rotation">Used by clients to set the rotation for the resources</param>
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, out float rotation, IEnumerable<Cave> targetCaves = null)
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable<Cave> targetCaves = null)
{
var allValidLocations = GetAllValidClusterLocations();
var placedResources = new List<Item>();
rotation = 0.0f;
if (allValidLocations.None()) { return placedResources; } // TODO: WHAT?!
// if there are no valid locations, don't place anything
if (allValidLocations.None()) { return placedResources; }
// Make sure not to pick a spot that already has other level resources
for (int i = allValidLocations.Count - 1; i >= 0; i--)
@@ -3077,7 +3077,6 @@ namespace Barotrauma
}
PlaceResources(prefab, requiredAmount, selectedLocation, out placedResources);
Vector2 edgeNormal = selectedLocation.Edge.GetNormal(selectedLocation.Cell);
rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
return placedResources;
static bool IsOnMainPath(ClusterLocation location) => location.Edge.NextToMainPath;
@@ -3182,13 +3181,11 @@ namespace Barotrauma
Vector2 edgeNormal = location.Edge.GetNormal(location.Cell);
float moveAmount = (item.body == null ? item.Rect.Height / 2 : ConvertUnits.ToDisplayUnits(item.body.GetMaxExtent() * 0.7f));
moveAmount += (item.GetComponent<LevelResource>()?.RandomOffsetFromWall ?? 0.0f) * Rand.Range(-0.5f, 0.5f, Rand.RandSync.ServerAndClient);
item.Move(edgeNormal * moveAmount, ignoreContacts: true);
item.Move(edgeNormal * moveAmount);
if (item.GetComponent<Holdable>() is Holdable h)
{
h.AttachToWall();
#if CLIENT
item.Rotation = MathHelper.ToDegrees(-MathUtils.VectorToAngle(edgeNormal) + MathHelper.PiOver2);
#endif
}
else if (item.body != null)
{
@@ -3509,7 +3506,7 @@ namespace Barotrauma
{
foreach (GraphEdge e in cell.Edges)
{
if (!MathUtils.LinesIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; }
if (!MathUtils.LineSegmentsIntersect(closestPathCell.Center, pos.ToVector2(), e.Point1, e.Point2)) { continue; }
cell.CellType = CellType.Removed;
for (int x = 0; x < cellGrid.GetLength(0); x++)
@@ -294,40 +294,37 @@ namespace Barotrauma
throw new Exception($"Generating a campaign map failed (no locations created). Width: {Width}, height: {Height}");
}
foreach (Location location in Locations)
{
if (location.Type.Identifier != "outpost") { continue; }
SetStartLocation(location);
}
FindStartLocation(l => l.Type.Identifier == "outpost");
//if no outpost was found (using a mod that replaces the outpost location type?), find any type of outpost
if (CurrentLocation == null)
{
FindStartLocation(l => l.Type.HasOutpost);
}
void FindStartLocation(Func<Location, bool> predicate)
{
foreach (Location location in Locations)
{
if (!location.Type.HasOutpost) { continue; }
SetStartLocation(location);
if (!predicate(location)) { continue; }
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
}
}
}
void SetStartLocation(Location location)
StartLocation.SecondaryFaction = null;
var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
if (startOutpostFaction != null)
{
if (CurrentLocation == null || location.MapPosition.X < CurrentLocation.MapPosition.X)
StartLocation.Faction = startOutpostFaction;
foreach (var connection in StartLocation.Connections)
{
CurrentLocation = StartLocation = furthestDiscoveredLocation = location;
StartLocation.SecondaryFaction = null;
var startOutpostFaction = campaign?.Factions.FirstOrDefault(f => f.Prefab.StartOutpost);
if (startOutpostFaction != null)
var otherLocation = connection.OtherLocation(StartLocation);
if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
StartLocation.Faction = startOutpostFaction;
foreach (var connection in StartLocation.Connections)
{
var otherLocation = connection.OtherLocation(StartLocation);
if (otherLocation.HasOutpost() && otherLocation.Type.OutpostTeam == CharacterTeamType.FriendlyNPC)
{
otherLocation.Faction = startOutpostFaction;
}
}
}
otherLocation.Faction = startOutpostFaction;
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma
public List<ushort> unresolvedLinkedToID;
public static int MapEntityUpdateInterval = 1;
public static int GapUpdateInterval = 4;
public static int GapUpdateInterval = 1;
public static int PoweredUpdateInterval = 1;
private static int mapEntityUpdateTick;
@@ -317,7 +317,7 @@ namespace Barotrauma
}
}
public virtual void Move(Vector2 amount, bool ignoreContacts = false)
public virtual void Move(Vector2 amount, bool ignoreContacts = true)
{
rect.X += (int)amount.X;
rect.Y += (int)amount.Y;
@@ -454,7 +454,7 @@ namespace Barotrauma
List<Wire> orphanedWires = new List<Wire>();
for (int i = 0; i < clones.Count; i++)
{
if (!(clones[i] is Item cloneItem)) { continue; }
if (clones[i] is not Item cloneItem) { continue; }
var door = cloneItem.GetComponent<Door>();
door?.RefreshLinkedGap();
@@ -509,10 +509,12 @@ namespace Barotrauma
}
(clones[itemIndex] as Item).Connections[connectionIndex].TryAddLink(cloneWire);
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], false);
cloneWire.Connect((clones[itemIndex] as Item).Connections[connectionIndex], n, addNode: false);
}
if ((cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) && cloneItem.GetComponent<DockingPort>() == null)
if (originalWire.Connections.Any(c => c != null) &&
(cloneWire.Connections[0] == null || cloneWire.Connections[1] == null) &&
cloneItem.GetComponent<DockingPort>() == null)
{
if (!clones.Any(c => (c as Item)?.GetComponent<ConnectionPanel>()?.DisconnectedWires.Contains(cloneWire) ?? false))
{
@@ -786,7 +786,7 @@ namespace Barotrauma
//check if the connection overlaps with this module's connection
if (selfGapPos1.HasValue && selfGapPos2.HasValue &&
!gapPos1.NearlyEquals(gapPos2) && !selfGapPos1.Value.NearlyEquals(selfGapPos2.Value) &&
MathUtils.LinesIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value))
MathUtils.LineSegmentsIntersect(gapPos1, gapPos2, selfGapPos1.Value, selfGapPos2.Value))
{
return true;
}
@@ -1105,8 +1105,8 @@ namespace Barotrauma
DebugConsole.AddWarning($"Failed to connect junction boxes between outpost modules (not enough free connections in module \"{module.PreviousModule.Info.Name}\")");
continue;
}
wire.Connect(thisJunctionBox.Connections[i], addNode: false);
wire.Connect(previousJunctionBox.Connections[i], addNode: false);
wire.TryConnect(thisJunctionBox.Connections[i], addNode: false);
wire.TryConnect(previousJunctionBox.Connections[i], addNode: false);
wire.SetNodes(new List<Vector2>());
}
}
@@ -1374,11 +1374,6 @@ namespace Barotrauma
endWaypoint.linkedTo.Add(prevWayPoint);
}
}
else
{
startWaypoint.linkedTo.Add(endWaypoint);
endWaypoint.linkedTo.Add(startWaypoint);
}
WayPoint closestWaypoint = null;
float closestDistSqr = 30.0f * 30.0f;
@@ -1595,10 +1590,11 @@ namespace Barotrauma
{
var startWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == bottomGap);
var endWaypoint = WayPoint.WayPointList.Find(wp => wp.ConnectedGap == topGap);
float margin = 100;
if (startWaypoint != null && endWaypoint != null)
{
WayPoint prevWaypoint = startWaypoint;
for (float y = startWaypoint.Position.Y + WayPoint.LadderWaypointInterval; y <= endWaypoint.Position.Y - WayPoint.LadderWaypointInterval; y += WayPoint.LadderWaypointInterval)
for (float y = bottomGap.Position.Y + margin; y <= topGap.Position.Y - margin; y += WayPoint.LadderWaypointInterval)
{
var wayPoint = new WayPoint(new Vector2(startWaypoint.Position.X, y), SpawnType.Path, ladder.Item.Submarine)
{
@@ -59,7 +59,7 @@ namespace Barotrauma
private static Explosion explosionOnBroken;
#if DEBUG
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)]
#else
[Serialize(false, IsPropertySaveable.Yes)]
#endif
@@ -104,9 +104,11 @@ namespace Barotrauma
public List<Body> Bodies { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody)]
public bool CastShadow
{
get { return Prefab.CastShadow; }
get;
set;
}
public bool IsHorizontal { get; private set; }
@@ -118,7 +120,7 @@ namespace Barotrauma
private float? maxHealth;
[Serialize(100.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0)]
[Serialize(100.0f, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody, MinValueFloat = 0)]
public float MaxHealth
{
get => maxHealth ?? Prefab.Health;
@@ -189,14 +191,14 @@ namespace Barotrauma
set { spriteColor = value; }
}
[Editable, Serialize(false, IsPropertySaveable.Yes)]
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody), Serialize(false, IsPropertySaveable.Yes)]
public bool UseDropShadow
{
get;
private set;
}
[Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")]
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasBody), Serialize("0,0", IsPropertySaveable.Yes, description: "The position of the drop shadow relative to the structure. If set to zero, the shadow is positioned automatically so that it points towards the sub's center of mass.")]
public Vector2 DropShadowOffset
{
get;
@@ -367,7 +369,7 @@ namespace Barotrauma
private set;
}
public override void Move(Vector2 amount, bool ignoreContacts = false)
public override void Move(Vector2 amount, bool ignoreContacts = true)
{
if (!MathUtils.IsValid(amount))
{
@@ -375,7 +377,7 @@ namespace Barotrauma
return;
}
base.Move(amount);
base.Move(amount, ignoreContacts);
for (int i = 0; i < Sections.Length; i++)
{
@@ -440,13 +442,11 @@ namespace Barotrauma
}
else
{
float width = BodyWidth > 0.0f ? BodyWidth : rect.Width;
float height = BodyHeight > 0.0f ? BodyHeight : rect.Height;
if (BodyWidth > 0.0f && BodyHeight > 0.0f)
{
IsHorizontal = BodyWidth > BodyHeight;
}
else
{
IsHorizontal = (rect.Width > rect.Height);
IsHorizontal = width > height;
}
}
@@ -455,29 +455,28 @@ namespace Barotrauma
InitProjSpecific();
if (!HiddenInGame)
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
if (element?.GetAttribute(nameof(CastShadow)) == null)
{
if (Prefab.Body)
{
Bodies = new List<Body>();
WallList.Add(this);
CreateSections();
UpdateSections();
}
else
{
Sections = new WallSection[1];
Sections[0] = new WallSection(rect, this);
if (StairDirection != Direction.None)
{
CreateStairBodies();
}
}
CastShadow = Prefab.CastShadow;
}
SerializableProperties = element != null ? SerializableProperty.DeserializeProperties(this, element) : SerializableProperty.GetProperties(this);
if (Prefab.Body)
{
Bodies = new List<Body>();
WallList.Add(this);
CreateSections();
UpdateSections();
}
else if (StairDirection != Direction.None)
{
CreateStairBodies();
}
if (Sections == null)
{
Sections = new WallSection[1];
Sections[0] = new WallSection(rect, this);
}
#if CLIENT
foreach (var subElement in sp.ConfigElement.Elements())
@@ -1546,16 +1545,16 @@ namespace Barotrauma
}
}
if (element.GetAttributeBool("flippedx", false)) { s.FlipX(false); }
if (element.GetAttributeBool("flippedy", false)) { s.FlipY(false); }
if (element.GetAttributeBool(nameof(FlippedX), false)) { s.FlipX(false); }
if (element.GetAttributeBool(nameof(FlippedY), false)) { s.FlipY(false); }
//structures with a body drop a shadow by default
if (element.GetAttribute("usedropshadow") == null)
if (element.GetAttribute(nameof(UseDropShadow)) == null)
{
s.UseDropShadow = prefab.Body;
}
if (element.GetAttribute("noaitarget") == null)
if (element.GetAttribute(nameof(NoAITarget)) == null)
{
s.NoAITarget = prefab.NoAITarget;
}
@@ -1604,12 +1603,12 @@ namespace Barotrauma
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
width + "," + height));
if (FlippedX) element.Add(new XAttribute("flippedx", true));
if (FlippedY) element.Add(new XAttribute("flippedy", true));
if (FlippedX) { element.Add(new XAttribute("flippedx", true)); }
if (FlippedY) { element.Add(new XAttribute("flippedy", true)); }
for (int i = 0; i < Sections.Length; i++)
{
if (Sections[i].damage == 0.0f) continue;
if (Sections[i].damage == 0.0f) { continue; }
var sectionElement =
new XElement("section",
new XAttribute("i", i),
@@ -1619,6 +1618,11 @@ namespace Barotrauma
SerializableProperty.SerializeProperties(this, element);
if (CastShadow == Prefab.CastShadow)
{
element.GetAttribute(nameof(CastShadow))?.Remove();
}
foreach (var upgrade in Upgrades)
{
upgrade.Save(element);
@@ -7,6 +7,7 @@ using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Voronoi2;
@@ -427,17 +428,20 @@ namespace Barotrauma
/// <summary>
/// Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
/// </summary>
public Rectangle GetDockedBorders()
public Rectangle GetDockedBorders(bool allowDifferentTeam = true)
{
checkSubmarineBorders.Clear();
return GetDockedBordersRecursive();
return GetDockedBordersRecursive(allowDifferentTeam);
}
private Rectangle GetDockedBordersRecursive()
private Rectangle GetDockedBordersRecursive(bool allowDifferentTeam)
{
Rectangle dockedBorders = Borders;
checkSubmarineBorders.Add(this);
var connectedSubs = DockedTo.Where(s => !checkSubmarineBorders.Contains(s) && !s.Info.IsOutpost);
var connectedSubs = DockedTo.Where(s =>
!checkSubmarineBorders.Contains(s) &&
!s.Info.IsOutpost &&
(allowDifferentTeam || s.TeamID == TeamID));
foreach (Submarine dockedSub in connectedSubs)
{
//use docking ports instead of world position to determine
@@ -446,7 +450,7 @@ namespace Barotrauma
Vector2? expectedLocation = CalculateDockOffset(this, dockedSub);
if (expectedLocation == null) { continue; }
Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive();
Rectangle dockedSubBorders = dockedSub.GetDockedBordersRecursive(allowDifferentTeam);
dockedSubBorders.Location += MathUtils.ToPoint(expectedLocation.Value);
dockedBorders.Y = -dockedBorders.Y;
@@ -458,23 +462,23 @@ namespace Barotrauma
return dockedBorders;
}
/// <summary>
/// Don't use this directly, because the list is updated only when GetConnectedSubs() is called. The method is called so frequently that we don't want to create new list here.
/// </summary>
private readonly List<Submarine> connectedSubs = new List<Submarine>(2);
private readonly HashSet<Submarine> connectedSubs;
/// <summary>
/// Returns a list of all submarines that are connected to this one via docking ports, including this sub.
/// </summary>
public List<Submarine> GetConnectedSubs()
public IEnumerable<Submarine> GetConnectedSubs()
{
return connectedSubs;
}
public void RefreshConnectedSubs()
{
connectedSubs.Clear();
connectedSubs.Add(this);
GetConnectedSubsRecursive(connectedSubs);
return connectedSubs;
}
private void GetConnectedSubsRecursive(List<Submarine> subs)
private void GetConnectedSubsRecursive(HashSet<Submarine> subs)
{
foreach (Submarine dockedSub in DockedTo)
{
@@ -1067,6 +1071,8 @@ namespace Barotrauma
public void Update(float deltaTime)
{
RefreshConnectedSubs();
if (Info.IsWreck)
{
WreckAI?.Update(deltaTime);
@@ -1393,6 +1399,13 @@ namespace Barotrauma
public Submarine(SubmarineInfo info, bool showErrorMessages = true, Func<Submarine, List<MapEntity>> loadEntities = null, IdRemap linkedRemap = null) : base(null, Entity.NullEntityID)
{
Stopwatch sw = Stopwatch.StartNew();
connectedSubs = new HashSet<Submarine>(2)
{
this
};
upgradeEventIdentifier = new Identifier($"Submarine{ID}");
Loading = true;
GameMain.World.Enabled = false;
@@ -1489,8 +1502,14 @@ namespace Barotrauma
if (me.Submarine != this) { continue; }
if (me is Item item)
{
item.SpawnedInCurrentOutpost = info.OutpostGenerationParams != null;
item.AllowStealing = info.OutpostGenerationParams?.AllowStealing ?? true;
item.AllowStealing = true;
if (info.OutpostGenerationParams != null)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing =
info.OutpostGenerationParams.AllowStealing ||
item.RootContainer is { Prefab: { AllowStealingContainedItems: true } };
}
if (item.GetComponent<Repairable>() != null && indestructible)
{
item.Indestructible = true;
@@ -1569,6 +1588,7 @@ namespace Barotrauma
#if CLIENT
GameMain.LightManager.OnMapLoaded();
Lights.ConvexHull.RecalculateAll(this);
#endif
//if the sub was made using an older version,
//halve the brightness of the lights to make them look (almost) right on the new lighting formula
@@ -1596,6 +1616,10 @@ namespace Barotrauma
Loading = false;
GameMain.World.Enabled = true;
}
sw.Stop();
string debugMsg = $"Loading {Info?.Name ?? "unknown"} took {sw.ElapsedMilliseconds} ms.";
DebugConsole.Log(debugMsg);
System.Diagnostics.Debug.WriteLine(debugMsg);
}
protected override ushort DetermineID(ushort id, Submarine submarine)
@@ -1745,8 +1769,7 @@ namespace Barotrauma
}
#endif
if (e.Submarine != this) { continue; }
var rootContainer = item.GetRootContainer();
if (rootContainer != null && rootContainer.Submarine != this) { continue; }
if (item.RootContainer != null && item.RootContainer.Submarine != this) { continue; }
}
else
{

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