Merge remote-tracking branch 'origin/master' into dev
# Conflicts: # Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs # Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs # Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs # Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs # Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs # Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs # Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs # Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs # Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs # Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs # Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs # Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs # Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs # Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs # Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs # Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs # Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs # Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs # Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs # Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs # Barotrauma/BarotraumaClient/LinuxClient.csproj # Barotrauma/BarotraumaClient/MacClient.csproj # Barotrauma/BarotraumaClient/WindowsClient.csproj # Barotrauma/BarotraumaServer/LinuxServer.csproj # Barotrauma/BarotraumaServer/MacServer.csproj # Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs # Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs # Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs # Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs # Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs # Barotrauma/BarotraumaServer/WindowsServer.csproj # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs # Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs # Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs # Barotrauma/BarotraumaShared/SharedSource/Enums.cs # Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs # Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs # Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs # Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs # Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs # Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs # Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs # Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs # Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs # Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs # Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs # Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs # Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs # Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs # Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs # Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs # Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs # Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs # Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs # Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs # Barotrauma/BarotraumaShared/changelog.txt
This commit is contained in:
@@ -95,10 +95,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (visibleHulls == null)
|
||||
{
|
||||
visibleHulls = Character.GetVisibleHulls();
|
||||
}
|
||||
visibleHulls ??= Character.GetVisibleHulls();
|
||||
return visibleHulls;
|
||||
}
|
||||
private set
|
||||
@@ -107,12 +104,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
|
||||
steeringManager is IndoorsSteeringManager pathSteering &&
|
||||
pathSteering.CurrentPath != null &&
|
||||
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
|
||||
!pathSteering.CurrentPath.Unreachable &&
|
||||
(!requireNonDirty || !pathSteering.IsPathDirty);
|
||||
/// <summary>
|
||||
/// Is the current path valid, using the provided parameters.
|
||||
/// </summary>
|
||||
/// <param name="requireNonDirty"></param>
|
||||
/// <param name="requireUnfinished"></param>
|
||||
/// <param name="nodePredicate"></param>
|
||||
/// <returns>When <paramref name="nodePredicate"/> is defined, returns false if any of the nodes fails to match the predicate.</returns>
|
||||
public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
|
||||
{
|
||||
if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
|
||||
if (pathSteering.CurrentPath == null) { return false; }
|
||||
if (pathSteering.CurrentPath.Unreachable) { return false; }
|
||||
if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
|
||||
if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
|
||||
if (nodePredicate != null)
|
||||
{
|
||||
return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
|
||||
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
|
||||
@@ -251,7 +262,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
|
||||
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false)
|
||||
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
|
||||
{
|
||||
var pickable = item.GetComponent<Pickable>();
|
||||
if (pickable == null) { return false; }
|
||||
@@ -265,23 +276,28 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
var holdable = item.GetComponent<Holdable>();
|
||||
if (holdable != null)
|
||||
{
|
||||
pickable = holdable;
|
||||
}
|
||||
// Not allowed to wear -> don't use the Wearable component even when it's found.
|
||||
pickable = item.GetComponent<Holdable>();
|
||||
}
|
||||
if (item.ParentInventory is ItemInventory itemInventory)
|
||||
{
|
||||
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
|
||||
}
|
||||
if (equip)
|
||||
if (equip && pickable != null)
|
||||
{
|
||||
int targetSlot = -1;
|
||||
//check if all the slots required by the item are free
|
||||
foreach (InvSlotType slots in pickable.AllowedSlots)
|
||||
{
|
||||
if (slots.HasFlag(InvSlotType.Any)) { continue; }
|
||||
if (!wear)
|
||||
{
|
||||
if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
|
||||
{
|
||||
// Don't allow other than hand slots if not allowed to wear.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < targetInventory.Capacity; i++)
|
||||
{
|
||||
if (targetInventory is CharacterInventory characterInventory)
|
||||
@@ -294,7 +310,7 @@ namespace Barotrauma
|
||||
var otherItem = targetInventory.GetItemAt(i);
|
||||
if (otherItem == null) { continue; }
|
||||
//try to move the existing item to LimbSlot.Any and continue if successful
|
||||
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot))
|
||||
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (storeUnequipped && targetInventory.Owner == Character)
|
||||
{
|
||||
@@ -304,6 +320,11 @@ namespace Barotrauma
|
||||
}
|
||||
if (dropOtherIfCannotMove)
|
||||
{
|
||||
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
|
||||
{
|
||||
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
|
||||
return false;
|
||||
}
|
||||
//if everything else fails, simply drop the existing item
|
||||
otherItem.Drop(Character);
|
||||
}
|
||||
@@ -314,7 +335,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
|
||||
return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +360,7 @@ namespace Barotrauma
|
||||
if (avoidDroppingInSea && !character.IsInFriendlySub)
|
||||
{
|
||||
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot))
|
||||
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
|
||||
{
|
||||
if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
|
||||
continue;
|
||||
@@ -401,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
|
||||
@@ -449,9 +465,10 @@ 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 || IsCurrentPathNullOrUnreachable || 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();
|
||||
SteeringManager.Reset();
|
||||
pathSteering?.ResetPath();
|
||||
Vector2 dir = Vector2.Normalize(diff);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -480,7 +486,7 @@ namespace Barotrauma
|
||||
if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
|
||||
{
|
||||
Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
|
||||
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
|
||||
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
|
||||
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -3934,7 +3957,7 @@ namespace Barotrauma
|
||||
{
|
||||
SteerAwayFromTheEnemy();
|
||||
}
|
||||
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true))
|
||||
else if (canAttackDoors && HasValidPath())
|
||||
{
|
||||
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
|
||||
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
{
|
||||
partial class HumanAIController : AIController
|
||||
{
|
||||
public static bool debugai;
|
||||
public static bool DebugAI;
|
||||
public static bool DisableCrewAI;
|
||||
|
||||
private readonly AIObjectiveManager objectiveManager;
|
||||
@@ -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;
|
||||
@@ -55,6 +57,7 @@ namespace Barotrauma
|
||||
|
||||
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
|
||||
private float obstacleRaycastTimer;
|
||||
private bool isBlocked;
|
||||
|
||||
private readonly float enemyCheckInterval = 0.2f;
|
||||
private readonly float enemySpotDistanceOutside = 800;
|
||||
@@ -92,7 +95,10 @@ namespace Barotrauma
|
||||
|
||||
private readonly SteeringManager outsideSteering, insideSteering;
|
||||
|
||||
public bool UseIndoorSteeringOutside { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Waypoints that are not linked to a sub (e.g. main path).
|
||||
/// </summary>
|
||||
public bool UseOutsideWaypoints { get; private set; }
|
||||
|
||||
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
|
||||
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
|
||||
@@ -225,14 +231,15 @@ namespace Barotrauma
|
||||
IgnoredItems.Clear();
|
||||
}
|
||||
|
||||
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true)
|
||||
// Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
|
||||
bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
|
||||
{
|
||||
Entity target = SelectedAiTarget?.Entity;
|
||||
if (target == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (useTargetSub)
|
||||
if (targetSub)
|
||||
{
|
||||
if (target.Submarine is Submarine sub)
|
||||
{
|
||||
@@ -244,62 +251,71 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2);
|
||||
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
|
||||
}
|
||||
|
||||
bool hasValidPath = HasValidPath();
|
||||
|
||||
if (Character.Submarine == null)
|
||||
bool isOutside = Character.Submarine == null;
|
||||
if (isOutside)
|
||||
{
|
||||
obstacleRaycastTimer -= deltaTime;
|
||||
if (obstacleRaycastTimer <= 0)
|
||||
{
|
||||
bool hasValidPath = HasValidPath();
|
||||
isBlocked = false;
|
||||
UseOutsideWaypoints = false;
|
||||
obstacleRaycastTimer = obstacleRaycastIntervalLong;
|
||||
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false))
|
||||
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
|
||||
if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
|
||||
{
|
||||
// If the target is behind a level wall, switch to the pathing to get around the obstacles.
|
||||
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity;
|
||||
if (spatialTarget == null)
|
||||
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
|
||||
Vector2 rayEnd = spatialTarget.SimPosition;
|
||||
Submarine targetSub = spatialTarget.Submarine;
|
||||
if (targetSub != null)
|
||||
{
|
||||
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
|
||||
spatialTarget = gotoObjective?.Target;
|
||||
rayEnd += targetSub.SimPosition;
|
||||
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
|
||||
}
|
||||
if (spatialTarget == null)
|
||||
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
|
||||
isBlocked = obstacle != null;
|
||||
// Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
|
||||
UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
|
||||
bool resetPath = false;
|
||||
if (UseOutsideWaypoints)
|
||||
{
|
||||
UseIndoorSteeringOutside = false;
|
||||
bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
|
||||
if (isUsingInsideWaypoints)
|
||||
{
|
||||
resetPath = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
|
||||
Vector2 rayEnd = spatialTarget.SimPosition;
|
||||
Submarine targetSub = spatialTarget.Submarine;
|
||||
if (targetSub != null)
|
||||
bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
|
||||
if (isUsingOutsideWaypoints)
|
||||
{
|
||||
rayEnd += targetSub.SimPosition;
|
||||
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
|
||||
resetPath = true;
|
||||
}
|
||||
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
|
||||
UseIndoorSteeringOutside = obstacle != null;
|
||||
}
|
||||
if (resetPath)
|
||||
{
|
||||
PathSteering.ResetPath();
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (hasValidPath)
|
||||
{
|
||||
UseIndoorSteeringOutside = false;
|
||||
if (hasValidPath)
|
||||
obstacleRaycastTimer = obstacleRaycastIntervalShort;
|
||||
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
|
||||
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
|
||||
{
|
||||
obstacleRaycastTimer = obstacleRaycastIntervalShort;
|
||||
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
|
||||
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
|
||||
if (connectedSub == Submarine.MainSub) { continue; }
|
||||
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
|
||||
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
|
||||
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
|
||||
{
|
||||
if (connectedSub == Submarine.MainSub) { continue; }
|
||||
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
|
||||
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
|
||||
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
|
||||
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
|
||||
{
|
||||
PathSteering.CurrentPath.Unreachable = true;
|
||||
break;
|
||||
}
|
||||
PathSteering.CurrentPath.Unreachable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,10 +323,11 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
UseIndoorSteeringOutside = false;
|
||||
UseOutsideWaypoints = false;
|
||||
isBlocked = false;
|
||||
}
|
||||
|
||||
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
|
||||
if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
|
||||
{
|
||||
// Spot enemies while staying outside or inside an enemy ship.
|
||||
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
|
||||
@@ -352,12 +369,13 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
|
||||
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
|
||||
if (useInsideSteering)
|
||||
{
|
||||
if (steeringManager != insideSteering)
|
||||
{
|
||||
insideSteering.Reset();
|
||||
PathSteering.ResetPath();
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
if (IsCloseEnoughToTarget(maxSteeringBuffer))
|
||||
@@ -420,6 +438,7 @@ namespace Barotrauma
|
||||
foreach (Hull h in VisibleHulls)
|
||||
{
|
||||
PropagateHullSafety(Character, h);
|
||||
dirtyHullSafetyCalculations.Remove(h);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -427,18 +446,33 @@ 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)
|
||||
{
|
||||
ReportProblems();
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Allows bots to heal targets autonomously while swimming outside of the sub.
|
||||
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
|
||||
{
|
||||
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
|
||||
}
|
||||
}
|
||||
reportProblemsTimer = reportProblemsInterval;
|
||||
}
|
||||
UpdateSpeaking();
|
||||
SpeakAboutIssues();
|
||||
UnequipUnnecessaryItems();
|
||||
reactTimer = GetReactionTime();
|
||||
}
|
||||
@@ -590,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))
|
||||
{
|
||||
@@ -875,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))
|
||||
{
|
||||
@@ -906,20 +940,40 @@ namespace Barotrauma
|
||||
}
|
||||
}))
|
||||
{
|
||||
suitableContainer = targetContainer;
|
||||
return true;
|
||||
if (targetContainer != null &&
|
||||
character.AIController is HumanAIController humanAI &&
|
||||
humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
|
||||
{
|
||||
ignoredItems.Add(targetContainer);
|
||||
itemIndex = 0;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
suitableContainer = targetContainer;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private float draggedTimer;
|
||||
private float refuseDraggingTimer;
|
||||
private const float RefuseDraggingAfter = 10.0f;
|
||||
/// <summary>
|
||||
/// The bot breaks free if being dragged by a human player from another team for longer than this
|
||||
/// </summary>
|
||||
private const float RefuseDraggingThresholdHigh = 10.0f;
|
||||
/// <summary>
|
||||
/// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster
|
||||
/// </summary>
|
||||
private const float RefuseDraggingThresholdLow = 0.5f;
|
||||
private const float RefuseDraggingDuration = 30.0f;
|
||||
|
||||
private void UpdateDragged(float deltaTime)
|
||||
{
|
||||
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
|
||||
if (Character.IsEscorted) { return; }
|
||||
if (Character.LockHands) { return; }
|
||||
|
||||
//don't allow player characters who aren't in the same team to drag us for more than x seconds
|
||||
if (Character.SelectedBy == null ||
|
||||
@@ -931,8 +985,8 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
draggedTimer += deltaTime;
|
||||
if (draggedTimer > RefuseDraggingAfter ||
|
||||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f))
|
||||
if (draggedTimer > RefuseDraggingThresholdHigh ||
|
||||
(refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
|
||||
{
|
||||
draggedTimer = 0.0f;
|
||||
refuseDraggingTimer = RefuseDraggingDuration;
|
||||
@@ -945,7 +999,8 @@ namespace Barotrauma
|
||||
{
|
||||
Order newOrder = null;
|
||||
Hull targetHull = null;
|
||||
bool speak = Character.SpeechImpediment < 100;
|
||||
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
|
||||
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
|
||||
@@ -1044,25 +1099,21 @@ namespace Barotrauma
|
||||
}
|
||||
if (newOrder != null && speak)
|
||||
{
|
||||
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
|
||||
if (!Character.IsEscorted)
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
|
||||
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
|
||||
minDurationBetweenSimilar: 60.0f);
|
||||
}
|
||||
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
|
||||
#if SERVER
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
|
||||
.WithTargetEntity(targetHull)
|
||||
.WithOrderGiver(Character), "", null, Character));
|
||||
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
|
||||
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
|
||||
.WithTargetEntity(targetHull)
|
||||
.WithOrderGiver(Character), "", null, Character));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1093,21 +1144,33 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpeaking()
|
||||
private void SpeakAboutIssues()
|
||||
{
|
||||
if (!Character.IsOnPlayerTeam) { return; }
|
||||
if (Character.SpeechImpediment >= 100) { return; }
|
||||
if (Character.Oxygen < 20.0f)
|
||||
float minDelay = 0.5f, maxDelay = 2f;
|
||||
if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f);
|
||||
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)
|
||||
if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f);
|
||||
string msgId = "DialogBleeding";
|
||||
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null)
|
||||
if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure)
|
||||
{
|
||||
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f);
|
||||
if (Character.PressureProtection > 0)
|
||||
{
|
||||
string msgId = "DialogInsufficientPressureProtection";
|
||||
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
else if (Character.CurrentHull?.DisplayName != null)
|
||||
{
|
||||
string msgId = "DialogPressure";
|
||||
Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1236,7 +1299,7 @@ namespace Barotrauma
|
||||
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
|
||||
if (isAccidental)
|
||||
{
|
||||
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
|
||||
if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
|
||||
{
|
||||
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
|
||||
}
|
||||
@@ -1402,7 +1465,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
|
||||
if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
|
||||
{
|
||||
// Already targeting the attacker -> treat as a more serious threat.
|
||||
cumulativeDamage *= 2;
|
||||
@@ -1587,7 +1650,7 @@ namespace Barotrauma
|
||||
hull.LethalPressure > 0 ||
|
||||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
|
||||
{
|
||||
needsSuit = !Character.IsProtectedFromPressure;
|
||||
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
|
||||
return needsAir || needsSuit;
|
||||
}
|
||||
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
|
||||
@@ -1604,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.
|
||||
@@ -1837,18 +1900,22 @@ 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>
|
||||
public static void PropagateHullSafety(Character character, Hull hull)
|
||||
{
|
||||
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
|
||||
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);
|
||||
}
|
||||
@@ -1916,7 +1983,7 @@ namespace Barotrauma
|
||||
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
|
||||
{
|
||||
bool targetAdded = false;
|
||||
DoForEachCrewMember(caller, humanAI =>
|
||||
DoForEachBot(caller, humanAI =>
|
||||
{
|
||||
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
|
||||
var objective = humanAI.ObjectiveManager.GetObjective<T1>();
|
||||
@@ -1933,7 +2000,7 @@ namespace Barotrauma
|
||||
|
||||
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
|
||||
{
|
||||
DoForEachCrewMember(caller, humanAI =>
|
||||
DoForEachBot(caller, humanAI =>
|
||||
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
|
||||
}
|
||||
|
||||
@@ -2108,7 +2175,7 @@ namespace Barotrauma
|
||||
if (other.IsPet)
|
||||
{
|
||||
// Hostile NPCs are hostile to all pets, unless they are in the same team.
|
||||
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; }
|
||||
return sameTeam || me.TeamID != CharacterTeamType.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2120,9 +2187,11 @@ namespace Barotrauma
|
||||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
|
||||
{
|
||||
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
|
||||
|
||||
//NPCs that allow some campaign interaction are not turned hostile by low reputation
|
||||
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
|
||||
if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI)
|
||||
|
||||
if (npc.AIController is HumanAIController npcAI)
|
||||
{
|
||||
return !npcAI.IsInHostileFaction();
|
||||
}
|
||||
@@ -2134,6 +2203,7 @@ namespace Barotrauma
|
||||
public bool IsInHostileFaction()
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
|
||||
if (Character.IsEscorted) { return false; }
|
||||
|
||||
Identifier npcFaction = Character.Faction;
|
||||
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
|
||||
@@ -2145,8 +2215,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
|
||||
if (campaign.CurrentLocation is { IsFactionHostile: true })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -2154,71 +2223,89 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
|
||||
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
|
||||
|
||||
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
|
||||
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (FilterCrewMember(character, c))
|
||||
if (!IsBotInTheCrew(character, c)) { continue; }
|
||||
if (!predicate(c.AIController as HumanAIController))
|
||||
{
|
||||
if (!predicate(c.AIController as HumanAIController))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
|
||||
public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (FilterCrewMember(character, c))
|
||||
if (!IsBotInTheCrew(character, c)) { continue; }
|
||||
if (predicate(c.AIController as HumanAIController))
|
||||
{
|
||||
if (predicate(c.AIController as HumanAIController))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false)
|
||||
public static int CountBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate = null)
|
||||
{
|
||||
if (character == null) { return 0; }
|
||||
int count = 0;
|
||||
foreach (var other in Character.CharacterList)
|
||||
{
|
||||
if (onlyActive && !IsActive(other))
|
||||
if (!IsBotInTheCrew(character, other)) { continue; }
|
||||
if (predicate == null || predicate(other.AIController as HumanAIController))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (onlyBots && other.IsPlayer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (FilterCrewMember(character, other))
|
||||
{
|
||||
if (predicate == null || predicate(other.AIController as HumanAIController))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
|
||||
/// <summary>
|
||||
/// Including the player characters in the same team.
|
||||
/// </summary>
|
||||
public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
|
||||
{
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (onlyActive && c.IsIncapacitated) { continue; }
|
||||
if (onlyConnectedSubs)
|
||||
{
|
||||
if (Character.Submarine == null)
|
||||
{
|
||||
if (c.Submarine != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (predicate(c))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
|
||||
{
|
||||
if (character == null) { return; }
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range))
|
||||
if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
|
||||
{
|
||||
action(c.AIController as HumanAIController);
|
||||
}
|
||||
@@ -2238,7 +2325,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self);
|
||||
private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
|
||||
|
||||
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
|
||||
{
|
||||
@@ -2283,10 +2370,9 @@ namespace Barotrauma
|
||||
bool isOrder = IsOrderedToOperateThis(Character.AIController);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.Removed) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
if (c.SelectedItem == target.Item)
|
||||
@@ -2354,9 +2440,9 @@ namespace Barotrauma
|
||||
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
if (!IsActive(c)) { continue; }
|
||||
if (c == Character) { continue; }
|
||||
if (c.TeamID != Character.TeamID) { continue; }
|
||||
if (c.IsIncapacitated) { continue; }
|
||||
other = c;
|
||||
if (c.IsPlayer)
|
||||
{
|
||||
@@ -2370,7 +2456,7 @@ namespace Barotrauma
|
||||
{
|
||||
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
|
||||
if (repairItemsObjective == null) { continue; }
|
||||
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target)
|
||||
if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
|
||||
{
|
||||
// Not targeting the same item.
|
||||
continue;
|
||||
@@ -2405,11 +2491,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
#region Wrappers
|
||||
public bool IsFriendly(Character other) => IsFriendly(Character, other);
|
||||
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
|
||||
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
|
||||
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
|
||||
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
|
||||
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
|
||||
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
|
||||
public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
|
||||
public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
+161
-127
@@ -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,66 +126,28 @@ 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)
|
||||
{
|
||||
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished;
|
||||
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,15 +157,33 @@ namespace Barotrauma
|
||||
if (needsNewPath || findPathTimer < -1.0f)
|
||||
{
|
||||
IsPathDirty = true;
|
||||
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
|
||||
{
|
||||
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 && 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)
|
||||
float maxDist = 200;
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
|
||||
{
|
||||
needsNewPath = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
|
||||
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0;
|
||||
bool useNewPath = needsNewPath;
|
||||
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
|
||||
{
|
||||
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
|
||||
@@ -234,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()
|
||||
{
|
||||
@@ -312,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; }
|
||||
@@ -328,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());
|
||||
@@ -353,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))
|
||||
@@ -380,56 +391,76 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
var collider = character.AnimController.Collider;
|
||||
if (character.IsClimbing && !useLadders)
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
if (character.IsClimbing && useLadders)
|
||||
{
|
||||
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
|
||||
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
|
||||
if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
|
||||
{
|
||||
//climbing ladders -> don't move horizontally
|
||||
diff.X = 0.0f;
|
||||
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
//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)
|
||||
else
|
||||
{
|
||||
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))
|
||||
bool nextLadderSameAsCurrent = currentLadder == nextLadder;
|
||||
if (currentLadder != null && nextLadder != null)
|
||||
{
|
||||
character.StopClimbing();
|
||||
//climbing ladders -> don't move horizontally
|
||||
diff.X = 0.0f;
|
||||
}
|
||||
else if (nextLadder != null && !nextLadderSameAsCurrent)
|
||||
//at the same height as the waypoint
|
||||
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
|
||||
float colliderHeight = collider.Height / 2 + collider.Radius;
|
||||
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
|
||||
if (heightDiff < colliderHeight * 1.25f)
|
||||
{
|
||||
// Try to change the ladder (hatches between two submarines)
|
||||
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
|
||||
if (nextLadder != null && !nextLadderSameAsCurrent)
|
||||
{
|
||||
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
|
||||
// Try to change the ladder (hatches between two submarines)
|
||||
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
|
||||
{
|
||||
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool isAboveFloor;
|
||||
if (diff.Y < 0)
|
||||
{
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10))
|
||||
else if (currentLadder != null && currentPath.NextNode != null)
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
else if (nextLadder != null)
|
||||
{
|
||||
//if the current node is below the character and the next one is above (or vice versa)
|
||||
//and both are on ladders, we can skip directly to the next one
|
||||
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
|
||||
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
{
|
||||
NextNode(!doorsChecked);
|
||||
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
{
|
||||
//if the current node is below the character and the next one is above (or vice versa)
|
||||
//and both are on ladders, we can skip directly to the next one
|
||||
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
|
||||
NextNode(!doorsChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConvertUnits.ToSimUnits(diff);
|
||||
@@ -440,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);
|
||||
@@ -459,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.
|
||||
@@ -486,9 +515,12 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
|
||||
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (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)
|
||||
@@ -507,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; }
|
||||
@@ -605,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,15 +506,31 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
protected static bool CanEquip(Character character, Item item)
|
||||
public virtual void SpeakAfterOrderReceived() { }
|
||||
|
||||
protected static bool CanEquip(Character character, Item item, bool allowWearing)
|
||||
{
|
||||
bool canEquip = item != null;
|
||||
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
if (item == null) { return false; }
|
||||
bool canEquip = false;
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
if (character.Inventory.IsAnySlotAvailable(item))
|
||||
{
|
||||
canEquip = true;
|
||||
}
|
||||
}
|
||||
if (!canEquip)
|
||||
{
|
||||
canEquip = false;
|
||||
var inv = character.Inventory;
|
||||
foreach (var allowedSlot in item.AllowedSlots)
|
||||
{
|
||||
if (!allowWearing)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
foreach (var slotType in inv.SlotTypes)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(slotType)) { continue; }
|
||||
@@ -530,18 +546,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
return canEquip;
|
||||
}
|
||||
protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet<Identifier> identifiersOrTags)
|
||||
{
|
||||
if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; }
|
||||
foreach (var identifier in identifiersOrTags)
|
||||
{
|
||||
if (item.HasTag(identifier)) { return true; }
|
||||
}
|
||||
return false;
|
||||
return canEquip && character.Inventory.CanBePut(item);
|
||||
}
|
||||
|
||||
protected bool CanEquip(Item item) => CanEquip(character, item);
|
||||
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -64,7 +64,6 @@ namespace Barotrauma
|
||||
if (subObjectives.Any()) { return; }
|
||||
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
|
||||
{
|
||||
itemIndex = 0;
|
||||
if (suitableContainer != null)
|
||||
{
|
||||
bool equip = item.GetComponent<Holdable>() != null ||
|
||||
@@ -112,10 +111,7 @@ namespace Barotrauma
|
||||
Abandon = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
|
||||
protected override bool CheckObjectiveSpecific()
|
||||
|
||||
+7
-3
@@ -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>();
|
||||
@@ -121,7 +125,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return CanEquip(character, item);
|
||||
return CanEquip(character, item, allowWearing: false);
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
|
||||
+64
-42
@@ -170,13 +170,33 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
|
||||
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
|
||||
if (Priority > 0)
|
||||
if (TargetEliminated)
|
||||
{
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
|
||||
Priority = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 91-100
|
||||
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
|
||||
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
|
||||
float priorityScale = maxPriority - minPriority;
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
|
||||
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
|
||||
{
|
||||
Priority = 0;
|
||||
xDist /= 2;
|
||||
yDist /= 2;
|
||||
}
|
||||
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
|
||||
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
|
||||
if (Priority > 0)
|
||||
{
|
||||
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
@@ -312,12 +332,10 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
AskHelp();
|
||||
Retreat(deltaTime);
|
||||
}
|
||||
break;
|
||||
case CombatMode.Retreat:
|
||||
AskHelp();
|
||||
Retreat(deltaTime);
|
||||
break;
|
||||
default:
|
||||
@@ -352,7 +370,7 @@ namespace Barotrauma
|
||||
Weapon = null;
|
||||
continue;
|
||||
}
|
||||
if (WeaponComponent.IsNotEmpty(character))
|
||||
if (!WeaponComponent.IsEmpty(character))
|
||||
{
|
||||
// All good, the weapon is loaded
|
||||
break;
|
||||
@@ -470,7 +488,7 @@ namespace Barotrauma
|
||||
// Not in the inventory anymore or cannot find the weapon component
|
||||
return false;
|
||||
}
|
||||
if (!WeaponComponent.IsNotEmpty(character))
|
||||
if (WeaponComponent.IsEmpty(character))
|
||||
{
|
||||
// Try reloading (and seek ammo)
|
||||
if (!Reload(seekAmmo))
|
||||
@@ -541,7 +559,7 @@ namespace Barotrauma
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
if (!weapon.IsNotEmpty(character))
|
||||
if (weapon.IsEmpty(character))
|
||||
{
|
||||
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
|
||||
{
|
||||
@@ -554,7 +572,6 @@ namespace Barotrauma
|
||||
priority /= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (Enemy.Params.Health.StunImmunity)
|
||||
{
|
||||
if (weapon.Item.HasTag("stunner"))
|
||||
@@ -750,7 +767,7 @@ namespace Barotrauma
|
||||
private bool Equip()
|
||||
{
|
||||
if (character.LockHands) { return false; }
|
||||
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
|
||||
if (WeaponComponent.IsEmpty(character))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -783,6 +800,10 @@ namespace Barotrauma
|
||||
|
||||
private void Retreat(float deltaTime)
|
||||
{
|
||||
if (!Enemy.IsHuman)
|
||||
{
|
||||
SpeakRetreating();
|
||||
}
|
||||
RemoveFollowTarget();
|
||||
RemoveSubObjective(ref seekAmmunitionObjective);
|
||||
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
|
||||
@@ -793,6 +814,7 @@ namespace Barotrauma
|
||||
{
|
||||
// Swim away
|
||||
SteeringManager.Reset();
|
||||
character.ReleaseSecondaryItem();
|
||||
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
|
||||
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
|
||||
return;
|
||||
@@ -819,7 +841,8 @@ namespace Barotrauma
|
||||
{
|
||||
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
|
||||
{
|
||||
UsePathingOutside = false
|
||||
UsePathingOutside = false,
|
||||
SpeakIfFails = false
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -861,6 +884,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
// Swim towards the target
|
||||
SteeringManager.Reset();
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
|
||||
@@ -882,7 +906,8 @@ namespace Barotrauma
|
||||
UsePathingOutside = false,
|
||||
IgnoreIfTargetDead = true,
|
||||
TargetName = Enemy.DisplayName,
|
||||
AlwaysUseEuclideanDistance = false
|
||||
AlwaysUseEuclideanDistance = false,
|
||||
SpeakIfFails = false
|
||||
},
|
||||
onAbandon: () =>
|
||||
{
|
||||
@@ -966,14 +991,22 @@ namespace Barotrauma
|
||||
item.GetComponent<RangedWeapon>() != null)
|
||||
{
|
||||
item.Drop(character);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot);
|
||||
character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
@@ -1028,54 +1061,43 @@ namespace Barotrauma
|
||||
if (Weapon.OwnInventory == null) { return true; }
|
||||
// Eject empty ammo
|
||||
HumanAIController.UnequipEmptyItems(Weapon);
|
||||
RelatedItem item = null;
|
||||
Item ammunition = null;
|
||||
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
|
||||
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it));
|
||||
if (ammunition != null)
|
||||
{
|
||||
// Ammunition still remaining
|
||||
return true;
|
||||
}
|
||||
item = requiredItem;
|
||||
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
|
||||
ammunitionIdentifiers = requiredItem.Identifiers;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (WeaponComponent is MeleeWeapon meleeWeapon)
|
||||
{
|
||||
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
|
||||
}
|
||||
|
||||
// No ammo
|
||||
if (ammunition == null)
|
||||
if (ammunitionIdentifiers != null)
|
||||
{
|
||||
if (ammunitionIdentifiers != null)
|
||||
// Try reload ammunition from inventory
|
||||
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
|
||||
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
|
||||
if (ammunition != null)
|
||||
{
|
||||
// Try reload ammunition from inventory
|
||||
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
|
||||
ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
|
||||
if (ammunition != null)
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
if (!container.Inventory.TryPutItem(ammunition, user: character))
|
||||
{
|
||||
var container = Weapon.GetComponent<ItemContainer>();
|
||||
if (!container.Inventory.TryPutItem(ammunition, null))
|
||||
if (ammunition.ParentInventory == character.Inventory)
|
||||
{
|
||||
if (ammunition.ParentInventory == character.Inventory)
|
||||
{
|
||||
ammunition.Drop(character);
|
||||
}
|
||||
ammunition.Drop(character);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
|
||||
if (!WeaponComponent.IsEmpty(character))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
|
||||
{
|
||||
SeekAmmunition(ammunitionIdentifiers);
|
||||
}
|
||||
@@ -1270,7 +1292,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
|
||||
private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
|
||||
private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
|
||||
|
||||
private void Speak(Identifier textIdentifier, float delay, float minDuration)
|
||||
{
|
||||
|
||||
+6
-6
@@ -109,7 +109,7 @@ namespace Barotrauma
|
||||
|
||||
private bool CheckItem(Item item)
|
||||
{
|
||||
return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
|
||||
return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
@@ -156,15 +156,15 @@ namespace Barotrauma
|
||||
Inventory originalInventory = ItemToContain.ParentInventory;
|
||||
var slots = originalInventory?.FindIndices(ItemToContain);
|
||||
|
||||
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
|
||||
bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
|
||||
{
|
||||
if (targetSlot.HasValue)
|
||||
{
|
||||
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null);
|
||||
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character);
|
||||
}
|
||||
else
|
||||
{
|
||||
return inventory.TryPutItem(itemToContain, user: null);
|
||||
return inventory.TryPutItem(itemToContain, user: character);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +198,11 @@ 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>(),
|
||||
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach)
|
||||
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach)
|
||||
},
|
||||
onAbandon: () => Abandon = true,
|
||||
onCompleted: () => RemoveSubObjective(ref goToObjective));
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
|
||||
+2
-3
@@ -28,7 +28,7 @@ namespace Barotrauma
|
||||
if (character.IsSecurity) { return 100; }
|
||||
if (objectiveManager.IsOrder(this)) { return 100; }
|
||||
// If there's any security officers onboard, leave fighting for them.
|
||||
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100;
|
||||
return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100;
|
||||
}
|
||||
|
||||
protected override AIObjective ObjectiveConstructor(Character target)
|
||||
@@ -37,8 +37,7 @@ namespace Barotrauma
|
||||
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
|
||||
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
|
||||
{
|
||||
var reputation = campaign.Map?.CurrentLocation?.Reputation;
|
||||
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
|
||||
if (campaign.CurrentLocation is { IsFactionHostile: true })
|
||||
{
|
||||
combatObjective.holdFireCondition = () =>
|
||||
{
|
||||
|
||||
+5
-4
@@ -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
|
||||
};
|
||||
},
|
||||
@@ -79,7 +80,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (mask != targetItem)
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-20
@@ -52,17 +52,14 @@ namespace Barotrauma
|
||||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
|
||||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
|
||||
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100;
|
||||
&& ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
|
||||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
|
||||
(needsSuit ?
|
||||
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
|
||||
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
|
||||
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
|
||||
{
|
||||
Priority = 100;
|
||||
Priority = AIObjectiveManager.MaxObjectivePriority;
|
||||
}
|
||||
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
|
||||
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
|
||||
@@ -75,11 +72,11 @@ namespace Barotrauma
|
||||
{
|
||||
Priority = 0;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, 100);
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
|
||||
{
|
||||
// Boost the priority while seeking the diving gear
|
||||
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100));
|
||||
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
@@ -111,7 +108,7 @@ namespace Barotrauma
|
||||
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
|
||||
{
|
||||
Priority -= priorityDecrease * deltaTime;
|
||||
if (currenthullSafety >= 100)
|
||||
if (currenthullSafety >= 100 && !character.IsLowInOxygen)
|
||||
{
|
||||
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
|
||||
Priority = 0;
|
||||
@@ -122,7 +119,7 @@ namespace Barotrauma
|
||||
float dangerFactor = (100 - currenthullSafety) / 100;
|
||||
Priority += dangerFactor * priorityIncrease * deltaTime;
|
||||
}
|
||||
Priority = MathHelper.Clamp(Priority, 0, 100);
|
||||
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +135,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (resetPriority) { return; }
|
||||
var currentHull = character.CurrentHull;
|
||||
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0);
|
||||
bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
|
||||
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
|
||||
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
|
||||
{
|
||||
@@ -200,16 +197,11 @@ namespace Barotrauma
|
||||
UpdateSimpleEscape(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
|
||||
previousSafeHull = currentSafeHull;
|
||||
currentSafeHull = potentialSafeHull;
|
||||
|
||||
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
|
||||
if (currentSafeHull == null)
|
||||
{
|
||||
currentSafeHull = previousSafeHull;
|
||||
}
|
||||
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
|
||||
currentSafeHull ??= previousSafeHull;
|
||||
if (currentSafeHull != null && currentSafeHull != currentHull)
|
||||
{
|
||||
if (goToObjective?.Target != currentSafeHull)
|
||||
@@ -219,6 +211,7 @@ namespace Barotrauma
|
||||
TryAddSubObjective(ref goToObjective,
|
||||
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
|
||||
{
|
||||
SpeakIfFails = false,
|
||||
AllowGoingOutside =
|
||||
character.IsProtectedFromPressure ||
|
||||
character.CurrentHull == null ||
|
||||
@@ -300,6 +293,7 @@ namespace Barotrauma
|
||||
//only move if we haven't reached the edge of the room
|
||||
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
else
|
||||
@@ -349,7 +343,6 @@ namespace Barotrauma
|
||||
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
|
||||
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
|
||||
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
|
||||
|
||||
//sort the hulls based on distance and which sub they're in
|
||||
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
|
||||
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
|
||||
@@ -413,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;
|
||||
@@ -493,5 +486,18 @@ namespace Barotrauma
|
||||
cannotFindDivingGear = false;
|
||||
cannotFindSafeHull = false;
|
||||
}
|
||||
|
||||
private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
|
||||
{
|
||||
if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
|
||||
if (needsSuit)
|
||||
{
|
||||
return !HumanAIController.HasDivingSuit(character, minOxygen);
|
||||
}
|
||||
else
|
||||
{
|
||||
return !HumanAIController.HasDivingGear(character, minOxygen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-27
@@ -37,40 +37,56 @@ namespace Barotrauma
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
else if (HumanAIController.IsTrueForAnyCrewMember(
|
||||
other => other != HumanAIController &&
|
||||
other.Character.IsBot &&
|
||||
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
|
||||
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
|
||||
float coopMultiplier = 1;
|
||||
foreach (var c in Character.CharacterList)
|
||||
{
|
||||
Priority = 0;
|
||||
if (!HumanAIController.IsActive(c)) { continue; }
|
||||
if (c.TeamID != character.TeamID) { continue; }
|
||||
if (c == character) { continue; }
|
||||
if (c.IsPlayer) { continue; }
|
||||
if (c.AIController is HumanAIController otherAI )
|
||||
{
|
||||
if (otherAI.ObjectiveManager.GetFirstActiveObjective<AIObjectiveFixLeak>() is AIObjectiveFixLeak fixLeak)
|
||||
{
|
||||
if (fixLeak.Leak == Leak)
|
||||
{
|
||||
// Ignore leaks that others are already targeting
|
||||
Priority = 0;
|
||||
return Priority;
|
||||
}
|
||||
if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull)
|
||||
{
|
||||
// Reduce the priority of leaks that others should be targeting
|
||||
coopMultiplier = 0.1f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
float reduction = isPriority ? 1 : 2;
|
||||
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
if (operateObjective != null && objectiveManager.GetFirstActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
|
||||
{
|
||||
// Prioritize leaks that we are already fixing
|
||||
Priority = maxPriority;
|
||||
}
|
||||
else
|
||||
{
|
||||
float reduction = isPriority ? 1 : 2;
|
||||
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
|
||||
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
|
||||
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
|
||||
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
|
||||
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
|
||||
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
|
||||
{
|
||||
// Prioritize leaks that we are already fixing
|
||||
Priority = maxPriority;
|
||||
}
|
||||
else
|
||||
{
|
||||
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
|
||||
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
|
||||
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
|
||||
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
|
||||
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
|
||||
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
|
||||
{
|
||||
// Double the distance when the leak can be accessed from the current hull.
|
||||
distanceFactor *= 2;
|
||||
}
|
||||
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
|
||||
// Double the distance when the leak can be accessed from the current hull.
|
||||
distanceFactor *= 2;
|
||||
}
|
||||
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1));
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
|
||||
+3
-3
@@ -38,9 +38,9 @@ namespace Barotrauma
|
||||
|
||||
protected override float TargetEvaluation()
|
||||
{
|
||||
int totalLeaks = Targets.Count();
|
||||
int totalLeaks = Targets.Count;
|
||||
if (totalLeaks == 0) { return 0; }
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true);
|
||||
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && c.Character.Submarine == character.Submarine);
|
||||
bool anyFixers = otherFixers > 0;
|
||||
if (objectiveManager.IsOrder(this))
|
||||
{
|
||||
@@ -52,7 +52,7 @@ namespace Barotrauma
|
||||
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
|
||||
int leaks = totalLeaks - secondaryLeaks;
|
||||
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
return 0;
|
||||
|
||||
+104
-33
@@ -1,9 +1,11 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -31,14 +33,15 @@ namespace Barotrauma
|
||||
private ISpatialEntity moveToTarget;
|
||||
private bool isDoneSeeking;
|
||||
public Item TargetItem => targetItem;
|
||||
private int currSearchIndex;
|
||||
private int currentSearchIndex;
|
||||
public ImmutableHashSet<Identifier> ignoredContainerIdentifiers;
|
||||
public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
private float currItemPriority;
|
||||
private readonly bool checkInventory;
|
||||
|
||||
public static float DefaultReach = 100;
|
||||
public const float DefaultReach = 100;
|
||||
public const float MaxReach = 150;
|
||||
|
||||
public bool AllowToFindDivingGear { get; set; } = true;
|
||||
public bool MustBeSpecificItem { get; set; }
|
||||
@@ -76,7 +79,7 @@ namespace Barotrauma
|
||||
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
currentSearchIndex = 0;
|
||||
Equip = equip;
|
||||
originalTarget = targetItem;
|
||||
this.targetItem = targetItem;
|
||||
@@ -89,7 +92,7 @@ namespace Barotrauma
|
||||
public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
|
||||
: base(character, objectiveManager, priorityModifier)
|
||||
{
|
||||
currSearchIndex = -1;
|
||||
currentSearchIndex = 0;
|
||||
Equip = equip;
|
||||
this.spawnItemIfNotFound = spawnItemIfNotFound;
|
||||
this.checkInventory = checkInventory;
|
||||
@@ -125,7 +128,7 @@ namespace Barotrauma
|
||||
|
||||
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
|
||||
{
|
||||
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach);
|
||||
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach);
|
||||
}
|
||||
|
||||
private bool CheckInventory()
|
||||
@@ -246,7 +249,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(c);
|
||||
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
|
||||
canInteract = character.CanInteractWith(c);
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
}
|
||||
@@ -268,7 +271,7 @@ namespace Barotrauma
|
||||
|
||||
Inventory itemInventory = targetItem.ParentInventory;
|
||||
var slots = itemInventory?.FindIndices(targetItem);
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true))
|
||||
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
|
||||
{
|
||||
if (TakeWholeStack && slots != null)
|
||||
{
|
||||
@@ -298,8 +301,11 @@ namespace Barotrauma
|
||||
if (!Equip)
|
||||
{
|
||||
// Try equipping and wearing the item
|
||||
Wear = true;
|
||||
Equip = true;
|
||||
if (!objectiveManager.HasActiveObjective<AIObjectiveCleanupItem>() && !objectiveManager.HasActiveObjective<AIObjectiveLoadItem>())
|
||||
{
|
||||
Wear = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#if DEBUG
|
||||
@@ -342,6 +348,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private Stopwatch sw;
|
||||
private Stopwatch StopWatch => sw ??= new Stopwatch();
|
||||
private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>();
|
||||
private List<Item> itemList;
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (IdentifiersOrTags == null)
|
||||
@@ -349,13 +359,16 @@ namespace Barotrauma
|
||||
if (targetItem == null)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red);
|
||||
DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.");
|
||||
#endif
|
||||
Abandon = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
StopWatch.Restart();
|
||||
}
|
||||
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
|
||||
if (!CheckPathForEachItem)
|
||||
{
|
||||
@@ -363,15 +376,25 @@ 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;
|
||||
bool hasCalledPathFinder = false;
|
||||
int itemsPerFrame = (int)priority;
|
||||
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
|
||||
// Reset if the character has switched subs.
|
||||
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
|
||||
{
|
||||
currSearchIndex++;
|
||||
var item = Item.ItemList[currSearchIndex];
|
||||
currentSearchIndex = 0;
|
||||
}
|
||||
if (currentSearchIndex == 0)
|
||||
{
|
||||
itemCandidates.Clear();
|
||||
itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true);
|
||||
}
|
||||
int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority));
|
||||
int checkedItems = 0;
|
||||
for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++)
|
||||
{
|
||||
checkedItems++;
|
||||
var item = itemList[currentSearchIndex];
|
||||
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
|
||||
if (itemSub == null) { continue; }
|
||||
Submarine mySub = character.Submarine;
|
||||
@@ -395,8 +418,6 @@ namespace Barotrauma
|
||||
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
|
||||
}
|
||||
}
|
||||
// Don't allow going into another sub, unless it's connected and of the same team and type.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
|
||||
if (character.IsItemTakenBySomeoneElse(item)) { continue; }
|
||||
if (item.ParentInventory is ItemInventory itemInventory)
|
||||
{
|
||||
@@ -411,11 +432,14 @@ namespace Barotrauma
|
||||
if (rootInventoryOwner is Item ownerItem)
|
||||
{
|
||||
if (!ownerItem.IsInteractable(character)) { continue; }
|
||||
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
|
||||
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
|
||||
if (ownerItem != item.Container)
|
||||
if (ownerItem != item)
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
|
||||
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
|
||||
if (ownerItem != item.Container)
|
||||
{
|
||||
itemPriority *= 0.1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
|
||||
@@ -463,22 +487,69 @@ namespace Barotrauma
|
||||
{
|
||||
itemPriority *= item.Condition / item.MaxCondition;
|
||||
}
|
||||
if (checkPath)
|
||||
{
|
||||
itemCandidates.Add((item, itemPriority));
|
||||
}
|
||||
// Ignore if the item has a lower priority than the currently selected one
|
||||
if (itemPriority < currItemPriority) { continue; }
|
||||
if (!hasCalledPathFinder && PathSteering != null && checkPath)
|
||||
if (EvaluateCombatPriority && itemPriority <= 0)
|
||||
{
|
||||
hasCalledPathFinder = true;
|
||||
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
|
||||
if (path.Unreachable) { continue; }
|
||||
// Not good enough
|
||||
continue;
|
||||
}
|
||||
currItemPriority = itemPriority;
|
||||
targetItem = item;
|
||||
moveToTarget = rootInventoryOwner ?? item;
|
||||
}
|
||||
if (currSearchIndex >= Item.ItemList.Count - 1)
|
||||
if (currentSearchIndex >= itemList.Count - 1)
|
||||
{
|
||||
isDoneSeeking = true;
|
||||
if (targetItem == null)
|
||||
}
|
||||
if (checkedItems > 0)
|
||||
{
|
||||
if (isDoneSeeking && itemCandidates.Any())
|
||||
{
|
||||
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
|
||||
}
|
||||
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
|
||||
{
|
||||
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
|
||||
if (StopWatch.ElapsedMilliseconds > 5)
|
||||
{
|
||||
DebugConsole.ThrowError(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
|
||||
DebugConsole.AddWarning(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isDoneSeeking)
|
||||
{
|
||||
if (PathSteering == null)
|
||||
{
|
||||
itemCandidates.Clear();
|
||||
}
|
||||
if (itemCandidates.Any())
|
||||
{
|
||||
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
|
||||
{
|
||||
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.
|
||||
itemCandidates.Remove(itemCandidate);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The path was valid -> we are done.
|
||||
itemCandidates.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetItem == null && itemCandidates.None())
|
||||
{
|
||||
if (spawnItemIfNotFound)
|
||||
{
|
||||
@@ -569,11 +640,11 @@ namespace Barotrauma
|
||||
{
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (ignoredItems.Contains(item)) { return false; };
|
||||
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; }
|
||||
if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
|
||||
if (item.Condition < TargetCondition) { return false; }
|
||||
if (ItemFilter != null && !ItemFilter(item)) { return false; }
|
||||
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
|
||||
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
|
||||
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
|
||||
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
@@ -591,7 +662,7 @@ namespace Barotrauma
|
||||
targetItem = originalTarget;
|
||||
moveToTarget = targetItem?.GetRootInventoryOwner();
|
||||
isDoneSeeking = false;
|
||||
currSearchIndex = 0;
|
||||
currentSearchIndex = 0;
|
||||
currItemPriority = 0;
|
||||
}
|
||||
|
||||
|
||||
+316
-260
@@ -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
|
||||
{
|
||||
@@ -257,302 +289,290 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
|
||||
else if (HumanAIController.HasValidPath(requireUnfinished: false))
|
||||
{
|
||||
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;
|
||||
if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true))
|
||||
{
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
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.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
|
||||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
|
||||
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 && HumanAIController.UseIndoorSteeringOutside)
|
||||
{
|
||||
nodeFilter = n => n.Waypoint.Submarine == null;
|
||||
}
|
||||
|
||||
if (!isInside && !UsePathingOutside)
|
||||
{
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -560,19 +580,41 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
|
||||
character.ReleaseSecondaryItem();
|
||||
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)
|
||||
{
|
||||
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
|
||||
SteeringManager.Reset();
|
||||
character.ReleaseSecondaryItem();
|
||||
character.CursorPosition = targetWorldPos;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
@@ -580,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))
|
||||
@@ -659,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; }
|
||||
@@ -772,6 +814,16 @@ namespace Barotrauma
|
||||
{
|
||||
StopMovement();
|
||||
HumanAIController.FaceTarget(Target);
|
||||
if (Target is WayPoint { Ladders: null })
|
||||
{
|
||||
// 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.
|
||||
if (character.IsClimbing && character.AnimController.IsAboveFloor)
|
||||
{
|
||||
character.StopClimbing();
|
||||
}
|
||||
}
|
||||
base.OnCompleted();
|
||||
}
|
||||
|
||||
@@ -781,6 +833,10 @@ namespace Barotrauma
|
||||
findDivingGear = null;
|
||||
seekGapsTimer = 0;
|
||||
TargetGap = null;
|
||||
if (SteeringManager is IndoorsSteeringManager pathSteering)
|
||||
{
|
||||
pathSteering.ResetPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-12
@@ -163,15 +163,22 @@ namespace Barotrauma
|
||||
|
||||
character.SelectedItem = null;
|
||||
|
||||
CleanupItems(deltaTime);
|
||||
if (!character.IsClimbing)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
|
||||
bool currentTargetIsInvalid =
|
||||
currentTarget == null ||
|
||||
IsForbidden(currentTarget) ||
|
||||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
|
||||
|
||||
if (behavior == BehaviorType.StayInHull && TargetHull != null && !IsForbidden(TargetHull) && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
|
||||
{
|
||||
currentTarget = TargetHull;
|
||||
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
|
||||
@@ -251,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
|
||||
@@ -271,7 +279,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
character.AIController.SelectTarget(currentTarget.AiTarget);
|
||||
PathSteering.SetPath(path);
|
||||
PathSteering.SetPath(targetPos, path);
|
||||
SetTargetTimerNormal();
|
||||
searchingNewHull = false;
|
||||
}
|
||||
@@ -305,12 +313,8 @@ namespace Barotrauma
|
||||
{
|
||||
if (character.IsClimbing)
|
||||
{
|
||||
if (character.AnimController.GetHeightFromFloor() < 0.1f)
|
||||
{
|
||||
character.AnimController.Anim = AnimController.Animation.None;
|
||||
character.SelectedSecondaryItem = null;
|
||||
}
|
||||
return;
|
||||
PathSteering.Reset();
|
||||
character.StopClimbing();
|
||||
}
|
||||
var currentHull = character.CurrentHull;
|
||||
if (!character.AnimController.InWater && currentHull != null)
|
||||
@@ -362,6 +366,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
character.ReleaseSecondaryItem();
|
||||
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
|
||||
}
|
||||
return;
|
||||
|
||||
+5
-13
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
|
||||
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
|
||||
|
||||
private int itemIndex = 0;
|
||||
private int itemIndex;
|
||||
private AIObjectiveDecontainItem decontainObjective;
|
||||
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
|
||||
private Item targetItem;
|
||||
@@ -219,9 +219,8 @@ namespace Barotrauma
|
||||
return Priority;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
if (targetItem == null)
|
||||
{
|
||||
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
|
||||
@@ -233,6 +232,7 @@ namespace Barotrauma
|
||||
}
|
||||
targetItem = item;
|
||||
}
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
float GetPriority(Item item)
|
||||
{
|
||||
try
|
||||
@@ -256,11 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (targetItem != null)
|
||||
else
|
||||
{
|
||||
if(decontainObjective == null && !IsValidContainable(targetItem))
|
||||
{
|
||||
@@ -290,10 +286,6 @@ namespace Barotrauma
|
||||
Reset();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidContainable(Item item)
|
||||
@@ -310,7 +302,7 @@ namespace Barotrauma
|
||||
if (parentItem.HasTag("donttakeitems")) { return false; }
|
||||
}
|
||||
if (!item.HasAccess(character)) { return false; }
|
||||
if (!character.HasItem(item) && !CanEquip(item)) { return false; }
|
||||
if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
|
||||
if (!ItemContainer.CanBeContained(item)) { return false; }
|
||||
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
|
||||
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (item == null || item.Removed) { return false; }
|
||||
if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
|
||||
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; }
|
||||
if ((item.GetComponent<ItemContainer>() is not ItemContainer container)) { return false; }
|
||||
if (container.Inventory == null) { return false; }
|
||||
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
|
||||
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
|
||||
|
||||
+12
-6
@@ -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)
|
||||
@@ -89,12 +91,7 @@ namespace Barotrauma
|
||||
var target = objective.Key;
|
||||
if (!Targets.Contains(target))
|
||||
{
|
||||
var subObjective = objective.Value;
|
||||
if (CurrentSubObjective == subObjective)
|
||||
{
|
||||
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
|
||||
}
|
||||
subObjectives.Remove(subObjective);
|
||||
subObjectives.Remove(objective.Value);
|
||||
}
|
||||
}
|
||||
SyncRemovedObjectives(Objectives, GetList());
|
||||
@@ -157,6 +154,11 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
float max = AIObjectiveManager.LowestOrderPriority - 1;
|
||||
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
|
||||
{
|
||||
// Allow higher prio
|
||||
max = AIObjectiveManager.EmergencyObjectivePriority;
|
||||
}
|
||||
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
|
||||
Priority = MathHelper.Lerp(0, max, value);
|
||||
}
|
||||
@@ -188,6 +190,10 @@ namespace Barotrauma
|
||||
if (!ignoreList.Contains(target))
|
||||
{
|
||||
Targets.Add(target);
|
||||
if (Targets.Count > MaxTargets)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-9
@@ -20,6 +20,8 @@ namespace Barotrauma
|
||||
MaxValue = 2
|
||||
}
|
||||
|
||||
public const float MaxObjectivePriority = 100;
|
||||
public const float EmergencyObjectivePriority = 90;
|
||||
public const float HighestOrderPriority = 70;
|
||||
public const float LowestOrderPriority = 60;
|
||||
public const float RunPriority = 50;
|
||||
@@ -125,11 +127,21 @@ namespace Barotrauma
|
||||
{
|
||||
CoroutineManager.StopCoroutines(delayedObjective.Value);
|
||||
}
|
||||
|
||||
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
|
||||
|
||||
DelayedObjectives.Clear();
|
||||
Objectives.Clear();
|
||||
FailedAutonomousObjectives = false;
|
||||
AddObjective(new AIObjectiveFindSafety(character, this));
|
||||
AddObjective(new AIObjectiveIdle(character, this));
|
||||
var newIdleObjective = new AIObjectiveIdle(character, this);
|
||||
if (prevIdleObjective != null)
|
||||
{
|
||||
newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
|
||||
newIdleObjective.Behavior = prevIdleObjective.Behavior;
|
||||
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
|
||||
}
|
||||
AddObjective(newIdleObjective);
|
||||
int objectiveCount = Objectives.Count;
|
||||
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
|
||||
{
|
||||
@@ -155,7 +167,7 @@ namespace Barotrauma
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
objectiveCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
|
||||
}
|
||||
|
||||
@@ -410,12 +422,12 @@ namespace Barotrauma
|
||||
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
|
||||
{
|
||||
CloseEnough = Rand.Range(80f, 100f),
|
||||
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4),
|
||||
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4),
|
||||
ExtraDistanceOutsideSub = 100,
|
||||
ExtraDistanceWhileSwimming = 100,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true,
|
||||
IsFollowOrderObjective = true,
|
||||
IsFollowOrder = true,
|
||||
Mimic = character.IsOnPlayerTeam,
|
||||
DialogueIdentifier = "dialogcannotreachplace".ToIdentifier()
|
||||
};
|
||||
@@ -423,7 +435,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":
|
||||
@@ -633,10 +649,11 @@ namespace Barotrauma
|
||||
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
|
||||
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last active objective of the specific type.
|
||||
/// </summary>
|
||||
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
public T GetLastActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
|
||||
|
||||
public T GetFirstActiveObjective<T>() where T : AIObjective
|
||||
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
|
||||
|
||||
+2
-2
@@ -122,7 +122,7 @@ namespace Barotrauma
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
Priority = 0;
|
||||
@@ -204,7 +204,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (operateTarget != null)
|
||||
{
|
||||
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
{
|
||||
// Another crew member is already targeting this entity (leak).
|
||||
Abandon = true;
|
||||
|
||||
+14
-24
@@ -69,16 +69,6 @@ namespace Barotrauma
|
||||
if (subObjective != null && subObjective.IsCompleted)
|
||||
{
|
||||
Priority = 0;
|
||||
items.RemoveWhere(i => i == null || i.Removed || !i.IsOwnedBy(character));
|
||||
if (items.None())
|
||||
{
|
||||
Abandon = true;
|
||||
|
||||
}
|
||||
else if (items.Any(i => i.Components.Any(i => !i.IsNotEmpty(character))))
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
return Priority;
|
||||
}
|
||||
@@ -162,25 +152,25 @@ namespace Barotrauma
|
||||
};
|
||||
}
|
||||
if (!TryAddSubObjective(ref getSingleItemObjective, getItemConstructor,
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (KeepActiveWhenReady)
|
||||
onCompleted: () =>
|
||||
{
|
||||
if (getSingleItemObjective != null)
|
||||
if (KeepActiveWhenReady)
|
||||
{
|
||||
var item = getSingleItemObjective?.TargetItem;
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
if (getSingleItemObjective != null)
|
||||
{
|
||||
items.Add(item);
|
||||
var item = getSingleItemObjective?.TargetItem;
|
||||
if (item?.IsOwnedBy(character) != null)
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
else
|
||||
{
|
||||
IsCompleted = true;
|
||||
}
|
||||
},
|
||||
onAbandon: () => Abandon = true))
|
||||
{
|
||||
Abandon = true;
|
||||
}
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ namespace Barotrauma
|
||||
// Don't stop fixing until completely done
|
||||
return 100;
|
||||
}
|
||||
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>() && !c.Character.IsIncapacitated, onlyBots: true);
|
||||
int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveRepairItems>());
|
||||
int items = Targets.Count;
|
||||
if (items == 0)
|
||||
{
|
||||
@@ -116,7 +116,7 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f))
|
||||
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
|
||||
{
|
||||
// Enough fixers
|
||||
return 0;
|
||||
|
||||
+32
-33
@@ -293,6 +293,11 @@ namespace Barotrauma
|
||||
currentTreatmentSuitabilities[treatmentSuitability.Key] > bestSuitability)
|
||||
{
|
||||
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key, true);
|
||||
//allow taking items from the target's inventory too if the target is unconscious
|
||||
if (matchingItem == null && targetCharacter.IsIncapacitated)
|
||||
{
|
||||
matchingItem ??= targetCharacter.Inventory?.FindItemByIdentifier(treatmentSuitability.Key, true);
|
||||
}
|
||||
if (matchingItem != null)
|
||||
{
|
||||
bestItem = matchingItem;
|
||||
@@ -379,24 +384,24 @@ namespace Barotrauma
|
||||
onAbandon: () =>
|
||||
{
|
||||
Abandon = true;
|
||||
if (character != targetCharacter && character.IsOnPlayerTeam)
|
||||
if (character.IsOnPlayerTeam)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
|
||||
SpeakCannotTreat();
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (cprSuitability <= 0)
|
||||
{
|
||||
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
|
||||
Abandon = true;
|
||||
SpeakCannotTreat();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!targetCharacter.IsUnconscious)
|
||||
{
|
||||
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
|
||||
character.Speak(TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, formatCapitals: FormatCapitals.No).Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
|
||||
Abandon = true;
|
||||
//no suitable treatments found, not inside our own sub (= can't search for more treatments), the target isn't unconscious (= can't give CPR)
|
||||
SpeakCannotTreat();
|
||||
return;
|
||||
}
|
||||
if (character != targetCharacter)
|
||||
@@ -414,6 +419,14 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private void SpeakCannotTreat()
|
||||
{
|
||||
LocalizedString msg = character == targetCharacter ?
|
||||
TextManager.Get("dialogcannottreatself") :
|
||||
TextManager.GetWithVariable("dialogcannottreatpatient", "[name]", targetCharacter.DisplayName, FormatCapitals.No);
|
||||
character.Speak(msg.Value, identifier: "cannottreatpatient".ToIdentifier(), minDurationBetweenSimilar: 20.0f);
|
||||
}
|
||||
|
||||
private void ApplyTreatment(Affliction affliction, Item item)
|
||||
{
|
||||
item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
|
||||
@@ -433,50 +446,36 @@ namespace Barotrauma
|
||||
|
||||
protected override float GetPriority()
|
||||
{
|
||||
if (!IsAllowed)
|
||||
if (!IsAllowed || targetCharacter == null)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
if (character.CurrentHull == null)
|
||||
if (character.CurrentHull != null)
|
||||
{
|
||||
if (!objectiveManager.HasOrder<AIObjectiveRescueAll>())
|
||||
if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
|
||||
{
|
||||
// Don't go into rooms that have enemies
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
}
|
||||
}
|
||||
else if (Character.CharacterList.Any(c => c.CurrentHull == targetCharacter.CurrentHull && !HumanAIController.IsFriendly(character, c) && HumanAIController.IsActive(c)))
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
|
||||
if (character.Submarine?.Info is { IsRuin: false })
|
||||
{
|
||||
// Don't go into rooms that have enemies
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
return Priority;
|
||||
verticalDistance *= 2;
|
||||
}
|
||||
if (targetCharacter == null)
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
|
||||
if (character.CurrentHull != null && targetCharacter.CurrentHull == character.CurrentHull)
|
||||
{
|
||||
Priority = 0;
|
||||
Abandon = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
float horizontalDistance = Math.Abs(character.WorldPosition.X - targetCharacter.WorldPosition.X);
|
||||
float verticalDistance = Math.Abs(character.WorldPosition.Y - targetCharacter.WorldPosition.Y);
|
||||
if (character.Submarine?.Info is { IsRuin: false })
|
||||
{
|
||||
verticalDistance *= 2;
|
||||
}
|
||||
float distanceFactor = MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 5000, horizontalDistance + verticalDistance));
|
||||
if (targetCharacter.CurrentHull == character.CurrentHull)
|
||||
{
|
||||
distanceFactor = 1;
|
||||
}
|
||||
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
|
||||
distanceFactor = 1;
|
||||
}
|
||||
float vitalityFactor = 1 - AIObjectiveRescueAll.GetVitalityFactor(targetCharacter) / 100;
|
||||
float devotion = CumulatedDevotion / 100;
|
||||
Priority = MathHelper.Lerp(0, AIObjectiveManager.EmergencyObjectivePriority, MathHelper.Clamp(devotion + (vitalityFactor * distanceFactor * PriorityModifier), 0, 1));
|
||||
return Priority;
|
||||
}
|
||||
|
||||
|
||||
+57
-20
@@ -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;
|
||||
|
||||
@@ -42,15 +61,16 @@ namespace Barotrauma
|
||||
if (Targets.None()) { return 100; }
|
||||
if (!objectiveManager.IsOrder(this))
|
||||
{
|
||||
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsMedic && !c.Character.IsUnconscious))
|
||||
if (!character.IsMedic && HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsMedic, onlyActive: true, onlyConnectedSubs: true))
|
||||
{
|
||||
// Don't do anything if there's a medic on board and we are not a medic
|
||||
// Don't do anything if there's a medic on board actively treating and we are not a medic
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
float worstCondition = Targets.Min(t => GetVitalityFactor(t));
|
||||
if (Targets.Contains(character))
|
||||
{
|
||||
// Targeting self -> higher prio
|
||||
if (character.Bleeding > 10)
|
||||
{
|
||||
// Enforce the highest priority when bleeding out.
|
||||
@@ -67,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;
|
||||
@@ -83,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;
|
||||
@@ -101,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; }
|
||||
@@ -111,13 +138,13 @@ namespace Barotrauma
|
||||
{
|
||||
if (GetVitalityFactor(target) >= GetVitalityThreshold(humanAI.ObjectiveManager, character, target))
|
||||
{
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
if (!humanAI.ObjectiveManager.HasOrder<AIObjectiveRescueAll>())
|
||||
{
|
||||
if (!character.IsMedic && target != character)
|
||||
{
|
||||
// Don't allow to treat others autonomously
|
||||
// Don't allow to treat others autonomously, unless we are a medic
|
||||
return false;
|
||||
}
|
||||
// Ignore unsafe hulls, unless ordered
|
||||
@@ -126,6 +153,10 @@ namespace Barotrauma
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (requireTreatableAfflictions && GetTreatableAfflictions(target).None())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -136,17 +167,17 @@ namespace Barotrauma
|
||||
// Don't allow going into another sub, unless it's connected and of the same team and type.
|
||||
if (!character.Submarine.IsEntityFoundOnThisSub(target.CurrentHull, includingConnectedSubs: true)) { return false; }
|
||||
}
|
||||
else
|
||||
else if (target.Submarine != null)
|
||||
{
|
||||
return target.Submarine == null;
|
||||
// We are outside, but the target is inside.
|
||||
return false;
|
||||
}
|
||||
if (target != character && target.IsBot && HumanAIController.IsActive(target) && target.AIController is HumanAIController targetAI)
|
||||
{
|
||||
// Ignore all concious targets that are currently fighting, fleeing, fixing, or treating characters
|
||||
// Ignore all concious targets that are currently fighting, fleeing, or treating characters
|
||||
if (targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveCombat>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFindSafety>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveFixLeak>())
|
||||
targetAI.ObjectiveManager.HasActiveObjective<AIObjectiveRescue>())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -158,5 +189,11 @@ namespace Barotrauma
|
||||
}
|
||||
return character.GetDamageDoneByAttacker(target) <= 0;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
charactersWithMinorInjuries.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,8 +228,8 @@ namespace Barotrauma
|
||||
{
|
||||
continue;
|
||||
}
|
||||
//optimization: node extremely close (< 1 m). If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
|
||||
if (node.TempDistance < 1.0f)
|
||||
//optimization: node close enough. If it's valid, choose it as the start node and skip the more exhaustive search for the closest one
|
||||
if (node.TempDistance < FarseerPhysics.ConvertUnits.ToSimUnits(AIObjectiveGetItem.DefaultReach))
|
||||
{
|
||||
if (IsValidStartNode(node))
|
||||
{
|
||||
|
||||
+30
-20
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace Barotrauma
|
||||
{
|
||||
class ShipIssueWorkerOperateWeapons : ShipIssueWorkerItem
|
||||
{
|
||||
public override float RedundantIssueModifier => 0.65f;
|
||||
public override float RedundantIssueModifier => 0.8f;
|
||||
private readonly List<float> targetingImportances = new List<float>();
|
||||
|
||||
public override bool AllowEasySwitching => true;
|
||||
@@ -17,43 +18,52 @@ namespace Barotrauma
|
||||
float GetTargetingImportance(Entity entity)
|
||||
{
|
||||
float currentDistanceToEnemy = Vector2.Distance(entity.WorldPosition, TargetItem.WorldPosition);
|
||||
|
||||
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MinImportance, MaxImportance * 0.5f);
|
||||
if (TargetItem.Submarine != null)
|
||||
if (currentDistanceToEnemy > Sonar.DefaultSonarRange) { return 0.0f; }
|
||||
float importance = MathHelper.Clamp(100 - (currentDistanceToEnemy / 100f), MaxImportance * 0.1f, MaxImportance * 0.5f);
|
||||
if (TargetItem.Submarine != null && importance > 0.0f)
|
||||
{
|
||||
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
|
||||
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
|
||||
if (Vector2.Dot(dir, submarineDir) < 0)
|
||||
if (TargetItemComponent is Turret turret)
|
||||
{
|
||||
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
|
||||
// = the turret is most likely on the wrong side of the sub, reduce importance
|
||||
importance *= 0.1f;
|
||||
if (!turret.CheckTurretAngle(entity.WorldPosition))
|
||||
{
|
||||
importance *= 0.1f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 dir = entity.WorldPosition - TargetItem.WorldPosition;
|
||||
Vector2 submarineDir = TargetItem.WorldPosition - TargetItem.Submarine.WorldPosition;
|
||||
if (Vector2.Dot(dir, submarineDir) < 0)
|
||||
{
|
||||
//direction from the weapon to the target is opposite to the direction from the sub to the weapon
|
||||
// = the turret is most likely on the wrong side of the sub, reduce importance
|
||||
importance *= 0.1f;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return importance;
|
||||
}
|
||||
|
||||
public override void CalculateImportanceSpecific()
|
||||
{
|
||||
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
|
||||
{
|
||||
//operate (= recharge the turrets) with low priority if they're out of power
|
||||
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
|
||||
Importance = ShipCommandManager.MinimumIssueThreshold * 1.05f;
|
||||
return;
|
||||
}
|
||||
|
||||
targetingImportances.Clear();
|
||||
foreach (Character character in shipCommandManager.EnemyCharacters)
|
||||
{
|
||||
targetingImportances.Add(GetTargetingImportance(character));
|
||||
}
|
||||
// there should maybe be additional logic for targeting and destroying spires, because they currently cause some issues with pathing
|
||||
|
||||
if (targetingImportances.Any(i => i > 0))
|
||||
{
|
||||
targetingImportances.Sort();
|
||||
Importance = targetingImportances.TakeLast(3).Sum();
|
||||
Importance = Math.Max(targetingImportances.TakeLast(3).Sum(), ShipCommandManager.MinimumIssueThreshold);
|
||||
}
|
||||
if (TargetItemComponent is Turret turret && !turret.HasPowerToShoot())
|
||||
{
|
||||
//operate (= recharge the turrets) with low priority if they're out of power
|
||||
//if something else (issues with reactor or the electrical grid) is preventing them from being charged, fixing those issues should take priority
|
||||
Importance = Math.Max(ShipCommandManager.MinimumIssueThreshold / RedundantIssueModifier, Importance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ namespace Barotrauma
|
||||
float bestValue = 0f;
|
||||
Character bestCharacter = null;
|
||||
|
||||
if (mostImportantIssue != null && mostImportantIssue.Importance > MinimumIssueThreshold)
|
||||
if (mostImportantIssue != null && mostImportantIssue.Importance >= MinimumIssueThreshold)
|
||||
{
|
||||
IEnumerable<Character> bestCharacters = CrewManager.GetCharactersSortedForOrder(mostImportantIssue.SuggestedOrder, AlliedCharacters, character, true);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+14
-11
@@ -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
|
||||
{
|
||||
@@ -1259,7 +1259,7 @@ namespace Barotrauma
|
||||
|
||||
if (ladder.Item.Prefab.Triggers.None())
|
||||
{
|
||||
character.SelectedSecondaryItem = null;
|
||||
character.ReleaseSecondaryItem();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1544,6 +1544,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
|
||||
@@ -1719,13 +1723,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;
|
||||
}
|
||||
}
|
||||
@@ -1751,7 +1754,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)
|
||||
{
|
||||
|
||||
@@ -78,6 +78,11 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attacks are used to deal damage to characters, structures and items.
|
||||
/// They can be defined in the weapon components of the items or the limb definitions of the characters.
|
||||
/// The limb attacks can also be used by the player, when they control a monster or have some appendage, like a husk stinger.
|
||||
/// </summary>
|
||||
partial class Attack : ISerializableEntity
|
||||
{
|
||||
[Serialize(AttackContext.Any, IsPropertySaveable.Yes, description: "The attack will be used only in this context."), Editable]
|
||||
@@ -123,7 +128,7 @@ namespace Barotrauma
|
||||
set => _damageRange = value;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the minimum range required for the attack to hit."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
|
||||
public float MinRange { get; private set; }
|
||||
|
||||
[Serialize(0.25f, IsPropertySaveable.Yes, description: "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2)]
|
||||
@@ -138,22 +143,22 @@ namespace Barotrauma
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "A random factor applied to all cooldowns. Example: 0.1 -> adds a random value between -10% and 10% of the cooldown. Min 0 (default), Max 1 (could disable or double the cooldown in extreme cases)."), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2)]
|
||||
public float CoolDownRandomFactor { get; private set; } = 0;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "When set to true, causes the enemy AI to use the fast movement animations when the attack is on cooldown."), Editable]
|
||||
public bool FullSpeedAfterAttack { get; private set; }
|
||||
|
||||
private float _structureDamage;
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to submarine walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
|
||||
public float StructureDamage
|
||||
{
|
||||
get => _structureDamage * DamageMultiplier;
|
||||
set => _structureDamage = value;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "Whether or not damaging structures with the attack causes damage particles to emit."), Editable]
|
||||
public bool EmitStructureDamageParticles { get; private set; }
|
||||
|
||||
private float _itemDamage;
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to items."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float ItemDamage
|
||||
{
|
||||
get =>_itemDamage * DamageMultiplier;
|
||||
@@ -178,16 +183,16 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public float ImpactMultiplier { get; set; } = 1;
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How much damage the attack does to level walls."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float LevelWallDamage { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Sets whether or not the attack is ranged or not."), Editable]
|
||||
public bool Ranged { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description:"Only affects ranged attacks."), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled the attack will not be launched when there's a friendly character in the way. Only affects ranged attacks."), Editable]
|
||||
public bool AvoidFriendlyFire { get; set; }
|
||||
|
||||
[Serialize(20f, IsPropertySaveable.Yes, description: "Only affects ranged attacks."), Editable]
|
||||
[Serialize(20f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine how accurately the attack needs to be aimed for the attack to trigger. Only affects ranged attacks."), Editable]
|
||||
public float RequiredAngle { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
|
||||
@@ -205,16 +210,13 @@ namespace Barotrauma
|
||||
[Serialize(5f, IsPropertySaveable.Yes, description: "How fast the held weapon is swayed back and forth while aiming. Only affects monsters using ranged weapons (items)."), Editable]
|
||||
public float SwayFrequency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy support. Use Afflictions.
|
||||
/// </summary>
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "Legacy support. Use Afflictions.")]
|
||||
public float Stun { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Can damage only Humans."), Editable]
|
||||
public bool OnlyHumans { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes), Editable]
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "List of limb indices to apply the force into."), Editable]
|
||||
public string ApplyForceOnLimbs
|
||||
{
|
||||
get
|
||||
@@ -240,20 +242,20 @@ namespace Barotrauma
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 RootForceWorldStart { get; private set; }
|
||||
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 RootForceWorldMiddle { get; private set; }
|
||||
|
||||
|
||||
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "Applied to the main limb. In world space coordinates(i.e. 0, 1 pushes the character upwards a bit). The attacker's facing direction is taken into account."), Editable]
|
||||
public Vector2 RootForceWorldEnd { get; private set; }
|
||||
|
||||
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:""), Editable]
|
||||
|
||||
[Serialize(TransitionMode.Linear, IsPropertySaveable.Yes, description:"Applied to the main limb. The transition smoothing of the applied force."), Editable]
|
||||
public TransitionMode RootTransitionEasing { get; private set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs)"), Editable(MinValueFloat = -10000.0f, MaxValueFloat = 10000.0f)]
|
||||
public float Torque { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Only apply the force once during the attacks lifetime."), Editable]
|
||||
public bool ApplyForcesOnlyOnce { get; private set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer)."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)]
|
||||
@@ -279,10 +281,10 @@ namespace Barotrauma
|
||||
//public float StickChance { get; set; }
|
||||
public float StickChance => 0f;
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: ""), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes, description: "Used by enemy AI to determine the priority when selecting attacks. When random attacks are disabled on the character it is multiplied with distance to determine the which attack to use. Only attacks that are currently valid are taken into consideration when making the decision."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float Priority { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: ""), Editable]
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "Triggers the 'blink' animation on the attacking limbs when the attack executes. Used e.g. by abyss monsters to make their jaws close when attacking."), Editable]
|
||||
public bool Blink { get; private set; }
|
||||
|
||||
public IEnumerable<StatusEffect> StatusEffects
|
||||
@@ -298,7 +300,7 @@ namespace Barotrauma
|
||||
private set;
|
||||
} = new Dictionary<Identifier, SerializableProperty>();
|
||||
|
||||
//the indices of the limbs Force is applied on
|
||||
//the indices of the limbs Force is applied on
|
||||
//(if none, force is applied only to the limb the attack is attached to)
|
||||
public readonly List<int> ForceOnLimbIndices = new List<int>();
|
||||
|
||||
@@ -309,6 +311,10 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
|
||||
|
||||
/// <summary>
|
||||
/// StatusEffects to apply when the attack triggers.
|
||||
/// StatusEffect types of 'OnUse' are executed always, 'OnFailure' only when the attack doesn't deal damage and 'OnSuccess' executes when some damage is dealt.
|
||||
/// </summary>
|
||||
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
|
||||
|
||||
public void SetUser(Character user)
|
||||
@@ -322,7 +328,7 @@ namespace Barotrauma
|
||||
|
||||
// used for talents/ability conditions
|
||||
public Item SourceItem { get; set; }
|
||||
|
||||
|
||||
public List<Affliction> GetMultipliedAfflictions(float multiplier)
|
||||
{
|
||||
List<Affliction> multipliedAfflictions = new List<Affliction>();
|
||||
@@ -428,7 +434,13 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case "conditional":
|
||||
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
|
||||
foreach (XAttribute attribute in subElement.Attributes())
|
||||
{
|
||||
if (PropertyConditional.IsValid(attribute))
|
||||
{
|
||||
Conditionals.Add(new PropertyConditional(attribute));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using FarseerPhysics.Dynamics;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Collections.Immutable;
|
||||
using Barotrauma.Abilities;
|
||||
using System.Diagnostics;
|
||||
#if SERVER
|
||||
using System.Text;
|
||||
#endif
|
||||
@@ -964,6 +965,7 @@ namespace Barotrauma
|
||||
/// The secondary selected item. It's an item other than a device (see <see cref="SelectedItem"/>), e.g. a ladder or a chair.
|
||||
/// </summary>
|
||||
public Item SelectedSecondaryItem { get; set; }
|
||||
public void ReleaseSecondaryItem() => SelectedSecondaryItem = null;
|
||||
/// <summary>
|
||||
/// Has the characters selected a primary or a secondary item?
|
||||
/// </summary>
|
||||
@@ -1063,7 +1065,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return SelectedItem == null || (SelectedItem.GetComponent<Controller>()?.AllowAiming ?? false);
|
||||
return (SelectedItem == null || SelectedItem.GetComponent<Controller>() is { AllowAiming: true }) && !IsIncapacitated && !IsRagdolled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,6 +1128,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
|
||||
{
|
||||
@@ -2432,6 +2435,8 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private Stopwatch sw;
|
||||
private Stopwatch StopWatch => sw ??= new Stopwatch();
|
||||
private float _selectedItemPriority;
|
||||
private Item _foundItem;
|
||||
/// <summary>
|
||||
@@ -2445,14 +2450,20 @@ namespace Barotrauma
|
||||
IEnumerable<Item> ignoredItems = null, IEnumerable<Identifier> ignoredContainerIdentifiers = null,
|
||||
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
|
||||
{
|
||||
if (HumanAIController.DebugAI)
|
||||
{
|
||||
StopWatch.Restart();
|
||||
}
|
||||
if (itemIndex == 0)
|
||||
{
|
||||
_foundItem = null;
|
||||
_selectedItemPriority = 0;
|
||||
}
|
||||
for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++)
|
||||
int itemsPerFrame = IsOnPlayerTeam ? 100 : 10;
|
||||
int checkedItemCount = 0;
|
||||
for (int i = 0; i < itemsPerFrame && itemIndex < Item.ItemList.Count; i++, itemIndex++)
|
||||
{
|
||||
itemIndex++;
|
||||
checkedItemCount++;
|
||||
var item = Item.ItemList[itemIndex];
|
||||
if (!item.IsInteractable(this)) { continue; }
|
||||
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
||||
@@ -2492,7 +2503,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
targetItem = _foundItem;
|
||||
return itemIndex >= Item.ItemList.Count - 1;
|
||||
bool completed = itemIndex >= Item.ItemList.Count - 1;
|
||||
if (HumanAIController.DebugAI && checkedItemCount > 0 && targetItem != null && StopWatch.ElapsedMilliseconds > 1)
|
||||
{
|
||||
var msg = $"Went through {checkedItemCount} of total {Item.ItemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {completed}";
|
||||
if (StopWatch.ElapsedMilliseconds > 5)
|
||||
{
|
||||
DebugConsole.ThrowError(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
|
||||
DebugConsole.AddWarning(msg);
|
||||
}
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
|
||||
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
|
||||
@@ -2512,7 +2537,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
return checkVisibility ? CanSeeCharacter(c) : true;
|
||||
return !checkVisibility || CanSeeCharacter(c);
|
||||
}
|
||||
|
||||
public bool CanInteractWith(Item item, bool checkLinked = true)
|
||||
@@ -2679,27 +2704,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.FindContainingInLocalCoordinates(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; }
|
||||
@@ -2863,7 +2867,7 @@ namespace Barotrauma
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (!IsClimbing)
|
||||
else
|
||||
{
|
||||
#if CLIENT
|
||||
if (Controlled == this)
|
||||
@@ -2906,14 +2910,14 @@ namespace Barotrauma
|
||||
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
|
||||
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
|
||||
{
|
||||
SelectedSecondaryItem = null;
|
||||
ReleaseSecondaryItem();
|
||||
#if CLIENT
|
||||
CharacterHealth.OpenHealthWindow = null;
|
||||
#endif
|
||||
}
|
||||
else if (IsKeyHit(InputType.Health) && SelectedItem != null)
|
||||
else if (IsKeyHit(InputType.Health) && (SelectedItem != null || SelectedSecondaryItem != null))
|
||||
{
|
||||
SelectedItem = null;
|
||||
SelectedItem = SelectedSecondaryItem = null;
|
||||
}
|
||||
else if (focusedItem != null)
|
||||
{
|
||||
@@ -3246,7 +3250,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (SelectedSecondaryItem != null && !CanInteractWith(SelectedSecondaryItem))
|
||||
{
|
||||
SelectedSecondaryItem = null;
|
||||
ReleaseSecondaryItem();
|
||||
}
|
||||
|
||||
if (!IsDead) { LockHands = false; }
|
||||
@@ -3460,14 +3464,19 @@ namespace Barotrauma
|
||||
despawnTimer += deltaTime * despawnPriority;
|
||||
if (despawnTimer < GameSettings.CurrentConfig.CorpseDespawnDelay) { return; }
|
||||
|
||||
if (IsHuman)
|
||||
Identifier despawnContainerId =
|
||||
IsHuman ?
|
||||
"despawncontainer".ToIdentifier() :
|
||||
Params.DespawnContainer;
|
||||
if (!despawnContainerId.IsEmpty)
|
||||
{
|
||||
var containerPrefab =
|
||||
ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ??
|
||||
MapEntityPrefab.FindByIdentifier(despawnContainerId) as ItemPrefab ??
|
||||
ItemPrefab.Prefabs.Find(me => me?.Tags != null && me.Tags.Contains(despawnContainerId)) ??
|
||||
(MapEntityPrefab.FindByIdentifier("metalcrate".ToIdentifier()) as ItemPrefab);
|
||||
if (containerPrefab == null)
|
||||
{
|
||||
DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red);
|
||||
DebugConsole.NewMessage($"Could not spawn a container for a despawned character's items. No item with the tag \"{despawnContainerId}\" or the identifier \"metalcrate\" found.", Color.Red);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3591,7 +3600,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (character == this) { continue; }
|
||||
if (character.TeamID != TeamID) { continue; }
|
||||
if (!(character.AIController is HumanAIController)) { continue; }
|
||||
if (character.AIController is not HumanAIController) { continue; }
|
||||
if (!HumanAIController.IsActive(character)) { continue; }
|
||||
foreach (var currentOrder in character.CurrentOrders)
|
||||
{
|
||||
@@ -4562,11 +4571,6 @@ namespace Barotrauma
|
||||
SetStun(0.0f, true);
|
||||
isDead = false;
|
||||
|
||||
if (info != null)
|
||||
{
|
||||
info.CauseOfDeath = null;
|
||||
}
|
||||
|
||||
foreach (LimbJoint joint in AnimController.LimbJoints)
|
||||
{
|
||||
var revoluteJoint = joint.revoluteJoint;
|
||||
@@ -4590,7 +4594,10 @@ namespace Barotrauma
|
||||
limb.IsSevered = false;
|
||||
}
|
||||
|
||||
GameMain.GameSession?.ReviveCharacter(this);
|
||||
if (GameMain.GameSession != null)
|
||||
{
|
||||
GameMain.GameSession.ReviveCharacter(this);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
@@ -4627,6 +4634,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)
|
||||
@@ -4713,6 +4727,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())
|
||||
@@ -4966,6 +4982,8 @@ namespace Barotrauma
|
||||
#region Talents
|
||||
private readonly List<CharacterTalent> characterTalents = new List<CharacterTalent>();
|
||||
|
||||
public IReadOnlyCollection<CharacterTalent> CharacterTalents => characterTalents;
|
||||
|
||||
public void LoadTalents()
|
||||
{
|
||||
List<Identifier> toBeRemoved = null;
|
||||
@@ -5062,7 +5080,7 @@ namespace Barotrauma
|
||||
|
||||
public void CheckTalents(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
|
||||
{
|
||||
foreach (var characterTalent in characterTalents)
|
||||
foreach (CharacterTalent characterTalent in CharacterTalents)
|
||||
{
|
||||
characterTalent.CheckTalent(abilityEffectType, abilityObject);
|
||||
}
|
||||
@@ -5358,7 +5376,7 @@ namespace Barotrauma
|
||||
public void StopClimbing()
|
||||
{
|
||||
AnimController.StopClimbing();
|
||||
SelectedSecondaryItem = null;
|
||||
ReleaseSecondaryItem();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -905,10 +905,19 @@ namespace Barotrauma
|
||||
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
|
||||
if (talentIdentifier == Identifier.Empty) { continue; }
|
||||
|
||||
if (TalentPrefab.TalentPrefabs.TryGet(talentIdentifier, out TalentPrefab prefab))
|
||||
{
|
||||
foreach (TalentMigration migration in prefab.Migrations)
|
||||
{
|
||||
migration.TryApply(version, this);
|
||||
}
|
||||
}
|
||||
|
||||
UnlockedTalents.Add(talentIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoadHeadAttachments();
|
||||
}
|
||||
|
||||
@@ -1886,6 +1895,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
|
||||
{
|
||||
float statValue = GetSavedStatValue(statType, statIdentifier);
|
||||
|
||||
if (GameMain.NetworkMember is null) { return statValue; }
|
||||
|
||||
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
|
||||
{
|
||||
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
|
||||
statValue = Math.Max(statValue, botStatValue);
|
||||
}
|
||||
|
||||
return statValue;
|
||||
}
|
||||
|
||||
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
|
||||
{
|
||||
if (!SavedStatValues.ContainsKey(statType))
|
||||
|
||||
+10
-1
@@ -179,7 +179,7 @@ namespace Barotrauma
|
||||
float currVitalityDecrease = MathHelper.Lerp(
|
||||
currentEffect.MinVitalityDecrease,
|
||||
currentEffect.MaxVitalityDecrease,
|
||||
currentEffect.GetStrengthFactor(this));
|
||||
currentEffect.GetStrengthFactor(strength));
|
||||
|
||||
if (currentEffect.MultiplyByMaxVitality)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -495,6 +497,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);
|
||||
|
||||
+34
-31
@@ -117,7 +117,7 @@ namespace Barotrauma
|
||||
public readonly bool CauseSpeechImpediment;
|
||||
|
||||
/// <summary>
|
||||
/// If not set to true, affected characters will no longer require air
|
||||
/// If set to false, affected characters will no longer require air
|
||||
/// once the affliction reaches the active stage.
|
||||
/// </summary>
|
||||
public readonly bool NeedsAir;
|
||||
@@ -247,7 +247,7 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description:
|
||||
"If set to true, MinVitalityDecrease and MaxVitalityDecrease represent a fraction of the affected character's maximum " +
|
||||
"vilatily, with 1 meaning 100%, instead of the same amount for all species.")]
|
||||
"vitality, with 1 meaning 100%, instead of the same amount for all species.")]
|
||||
public bool MultiplyByMaxVitality { get; private set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "Blur effect strength at this effect's lowest strength.")]
|
||||
@@ -457,16 +457,22 @@ namespace Barotrauma
|
||||
/// Returns 0 if affliction.Strength is MinStrength,
|
||||
/// 1 if affliction.Strength is MaxStrength
|
||||
/// </summary>
|
||||
public float GetStrengthFactor(Affliction affliction)
|
||||
public float GetStrengthFactor(Affliction affliction) => GetStrengthFactor(affliction.Strength);
|
||||
|
||||
/// <summary>
|
||||
/// Returns 0 if affliction.Strength is MinStrength,
|
||||
/// 1 if affliction.Strength is MaxStrength
|
||||
/// </summary>
|
||||
public float GetStrengthFactor(float strength)
|
||||
=> MathUtils.InverseLerp(
|
||||
MinStrength,
|
||||
MaxStrength,
|
||||
affliction.Strength);
|
||||
strength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Description element can be used to define descriptions for the affliction that are shown at specific conditions.
|
||||
/// For example a description that only shows to other players or only at certain strength levels.
|
||||
/// The description element can be used to define descriptions for the affliction which are shown under specific conditions;
|
||||
/// for example a description that only shows to other players or only at certain strength levels.
|
||||
/// </summary>
|
||||
/// <doc>
|
||||
/// <Field identifier="Text" type="string" defaultValue="""">
|
||||
@@ -692,8 +698,7 @@ namespace Barotrauma
|
||||
public readonly bool ShowBarInHealthMenu;
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, this affliction's icon will be hidden from the HUD
|
||||
/// after 5 seconds.
|
||||
/// If set to true, this affliction's icon will be hidden from the HUD after 5 seconds.
|
||||
/// </summary>
|
||||
public readonly bool HideIconAfterDelay;
|
||||
|
||||
@@ -701,37 +706,37 @@ namespace Barotrauma
|
||||
/// How high the strength has to be for the affliction to take effect
|
||||
/// </summary>
|
||||
public readonly float ActivationThreshold = 0.0f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// How high the strength has to be for the affliction icon to be shown in the UI
|
||||
/// </summary>
|
||||
public readonly float ShowIconThreshold = 0.05f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// How high the strength has to be for the affliction icon to be shown to others with a health scanner or via the health interface
|
||||
/// </summary>
|
||||
public readonly float ShowIconToOthersThreshold = 0.05f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The maximum strength this affliction can have.
|
||||
/// </summary>
|
||||
public readonly float MaxStrength = 100.0f;
|
||||
|
||||
/// <summary>
|
||||
/// The strength of the radiation grain effect to apply
|
||||
/// when the strength of this affliction increases.
|
||||
/// The strength of the radiation grain effect to apply when the strength of this affliction increases.
|
||||
/// </summary>
|
||||
public readonly float GrainBurst;
|
||||
|
||||
/// <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
|
||||
/// 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
|
||||
@@ -758,13 +763,12 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly float DamageOverlayAlpha;
|
||||
|
||||
/// <summary>
|
||||
/// Steam achievement given when the controlled character receives the affliction
|
||||
/// Steam achievement given when the controlled character receives the affliction.
|
||||
/// </summary>
|
||||
public readonly Identifier AchievementOnReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Steam achievement given when the affliction is removed from the controlled character
|
||||
/// Steam achievement given when the affliction is removed from the controlled character.
|
||||
/// </summary>
|
||||
public readonly Identifier AchievementOnRemoved;
|
||||
|
||||
@@ -775,10 +779,8 @@ namespace Barotrauma
|
||||
public readonly Color[] IconColors;
|
||||
|
||||
/// <summary>
|
||||
/// If set to true and the affliction has an AfflictionOverlay element,
|
||||
/// the overlay's opacity will be strictly proportional to its strength.
|
||||
/// Otherwise, the overlay's opacity will be determined based on its
|
||||
/// activation threshold and effects.
|
||||
/// If set to true and the affliction has an AfflictionOverlay element, the overlay's opacity will be strictly proportional to its strength.
|
||||
/// Otherwise, the overlay's opacity will be determined based on its activation threshold and effects.
|
||||
/// </summary>
|
||||
public readonly bool AfflictionOverlayAlphaIsLinear;
|
||||
|
||||
@@ -788,16 +790,18 @@ namespace Barotrauma
|
||||
public readonly bool ResetBetweenRounds;
|
||||
|
||||
/// <summary>
|
||||
/// Should damage particles be emitted when a character receives this affliction? Only relevant if the affliction is of the type "bleeding" or "damage".
|
||||
/// Should damage particles be emitted when a character receives this affliction?
|
||||
/// Only relevant if the affliction is of the type "bleeding" or "damage".
|
||||
/// </summary>
|
||||
public readonly bool DamageParticles;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary modifier that affects how much medical skill is increased when you apply the affliction on a target.
|
||||
/// If the affliction causes damage or is of type poison or paralysis, the skill is increased only when the target is hostile.
|
||||
/// If the affliction is of type buff, the skill is increased only when the target is friendly.
|
||||
/// If the affliction causes damage or is of the 'poison' or 'paralysis' type, the skill is increased only when the target is hostile.
|
||||
/// If the affliction is of the 'buff' type, the skill is increased only when the target is friendly.
|
||||
/// </summary>
|
||||
public readonly float MedicalSkillGain;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary modifier that affects how much weapons skill is increased when you apply the affliction on a target.
|
||||
/// The skill is increased only when the target is hostile.
|
||||
@@ -827,14 +831,13 @@ namespace Barotrauma
|
||||
private readonly ConstructorInfo constructor;
|
||||
|
||||
/// <summary>
|
||||
/// Icon that's used in UI to represent this affliction.
|
||||
/// An icon that’s used in the UI to represent this affliction.
|
||||
/// </summary>
|
||||
public readonly Sprite Icon;
|
||||
|
||||
/// <summary>
|
||||
/// A sprite that covers the affected player's entire screen when this affliction is active.
|
||||
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier
|
||||
/// and MaxAfflictionOverlayAlphaMultiplier
|
||||
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
|
||||
/// </summary>
|
||||
public readonly Sprite AfflictionOverlay;
|
||||
|
||||
@@ -844,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);
|
||||
@@ -912,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;
|
||||
@@ -417,9 +418,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)
|
||||
@@ -449,11 +454,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)
|
||||
@@ -974,7 +975,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))
|
||||
@@ -1116,7 +1117,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)
|
||||
@@ -1133,13 +1138,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);
|
||||
@@ -1155,6 +1167,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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,9 @@ namespace Barotrauma
|
||||
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons tagged as \"slowturret\", like railguns. The tag is arbitrary and can be added to any turrets, just like the priority. Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making."), Editable]
|
||||
public float AISlowTurretPriority { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, description: "Identifier or tag of the item the character's items are placed inside when the character despawns."), Editable]
|
||||
public Identifier DespawnContainer { get; private set; }
|
||||
|
||||
public readonly CharacterFile File;
|
||||
|
||||
public XDocument VariantFile { get; private set; }
|
||||
@@ -228,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)
|
||||
|
||||
+7
-2
@@ -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
|
||||
{
|
||||
|
||||
+3
-1
@@ -6,11 +6,13 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
private readonly ItemTalentStats stat;
|
||||
private readonly float value;
|
||||
private readonly bool stackable;
|
||||
|
||||
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
|
||||
value = abilityElement.GetAttributeFloat("value", 0f);
|
||||
stackable = abilityElement.GetAttributeBool("stackable", true);
|
||||
}
|
||||
|
||||
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
|
||||
@@ -25,7 +27,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (abilityObject is not IAbilityItem ability) { return; }
|
||||
|
||||
ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent);
|
||||
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -9,12 +9,14 @@ namespace Barotrauma.Abilities
|
||||
private readonly ItemTalentStats stat;
|
||||
private readonly float value;
|
||||
private readonly ImmutableHashSet<Identifier> tags;
|
||||
private readonly bool stackable;
|
||||
|
||||
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
|
||||
{
|
||||
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
|
||||
value = abilityElement.GetAttributeFloat("value", 0f);
|
||||
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
|
||||
stackable = abilityElement.GetAttributeBool("stackable", true);
|
||||
}
|
||||
|
||||
public override void InitializeAbility(bool addingFirstTime)
|
||||
@@ -41,7 +43,7 @@ namespace Barotrauma.Abilities
|
||||
{
|
||||
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
|
||||
{
|
||||
item.StatManager.ApplyStat(stat, value, CharacterTalent);
|
||||
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -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
|
||||
|
||||
+5
@@ -217,6 +217,11 @@ namespace Barotrauma.Abilities
|
||||
|
||||
public static StatTypes ParseStatType(string statTypeString, string debugIdentifier)
|
||||
{
|
||||
//backwards compatibility
|
||||
if (statTypeString.Equals("MedicalItemDurationMultiplier", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
statTypeString = "BuffItemApplyingMultiplier";
|
||||
}
|
||||
if (!Enum.TryParse(statTypeString, true, out StatTypes statType))
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid stat type type \"" + statTypeString + "\" in CharacterTalent (" + debugIdentifier + ")");
|
||||
|
||||
@@ -71,6 +71,29 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<Identifier> checkedNonStackableTalents = new();
|
||||
|
||||
/// <summary>
|
||||
/// Checks talents for a given AbilityObject taking into account non-stackable talents.
|
||||
/// </summary>
|
||||
public static void CheckTalentsForCrew(IEnumerable<Character> crew, AbilityEffectType type, AbilityObject abilityObject)
|
||||
{
|
||||
checkedNonStackableTalents.Clear();
|
||||
foreach (Character character in crew)
|
||||
{
|
||||
foreach (CharacterTalent characterTalent in character.CharacterTalents)
|
||||
{
|
||||
if (!characterTalent.Prefab.AbilityEffectsStackWithSameTalent)
|
||||
{
|
||||
if (checkedNonStackableTalents.Contains(characterTalent.Prefab.Identifier)) { continue; }
|
||||
checkedNonStackableTalents.Add(characterTalent.Prefab.Identifier);
|
||||
}
|
||||
|
||||
characterTalent.CheckTalent(type, abilityObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckTalent(AbilityEffectType abilityEffectType, AbilityObject abilityObject)
|
||||
{
|
||||
if (characterAbilityGroupEffectDictionary.TryGetValue(abilityEffectType, out var characterAbilityGroups))
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal abstract class TalentMigration
|
||||
{
|
||||
private readonly Version version;
|
||||
|
||||
private delegate TalentMigration TalentMigrationCtor(Version version, ContentXElement element);
|
||||
|
||||
private static readonly Dictionary<Identifier, TalentMigrationCtor> migrationTemplates =
|
||||
new()
|
||||
{
|
||||
[new Identifier("AddStat")] =
|
||||
static (version, element) => new TalentMigrationAddStat(version, element),
|
||||
|
||||
[new Identifier("UpdateStatIdentifier")] =
|
||||
static (version, element) => new TalentMigrationUpdateStatIdentifier(version, element)
|
||||
};
|
||||
|
||||
public bool TryApply(Version savedVersion, CharacterInfo info)
|
||||
{
|
||||
if (version <= savedVersion) { return false; }
|
||||
Apply(info);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void Apply(CharacterInfo info);
|
||||
|
||||
protected TalentMigration(Version targetVersion)
|
||||
{
|
||||
version = targetVersion;
|
||||
}
|
||||
|
||||
public static TalentMigration FromXML(ContentXElement element)
|
||||
{
|
||||
Version? version = XMLExtensions.GetAttributeVersion(element, "version", null);
|
||||
|
||||
if (version is null)
|
||||
{
|
||||
throw new Exception("Talent migration version not defined.");
|
||||
}
|
||||
|
||||
Identifier name = element.Name.ToString().ToIdentifier();
|
||||
|
||||
if (!migrationTemplates.TryGetValue(name, out TalentMigrationCtor? ctor))
|
||||
{
|
||||
throw new Exception($"Unknown talent migration type: {name}.");
|
||||
}
|
||||
|
||||
return ctor(version, element);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migration that adds a missing permanent stat to the character.
|
||||
/// </summary>
|
||||
internal sealed class TalentMigrationAddStat : TalentMigration
|
||||
{
|
||||
[Serialize(StatTypes.None, IsPropertySaveable.Yes)]
|
||||
public StatTypes StatType { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes)]
|
||||
public Identifier StatIdentifier { get; set; }
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.Yes)]
|
||||
public float Value { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes)]
|
||||
public bool RemoveOnDeath { get; set; }
|
||||
|
||||
public TalentMigrationAddStat(Version targetVersion, ContentXElement element) : base(targetVersion)
|
||||
=> SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
protected override void Apply(CharacterInfo info)
|
||||
{
|
||||
info.ChangeSavedStatValue(StatType, Value, StatIdentifier, RemoveOnDeath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migration that updates permanent stat identifiers.
|
||||
/// </summary>
|
||||
internal class TalentMigrationUpdateStatIdentifier : TalentMigration
|
||||
{
|
||||
[Serialize("", IsPropertySaveable.Yes, "The old identifier to update.")]
|
||||
public Identifier Old { get; set; }
|
||||
|
||||
[Serialize("", IsPropertySaveable.Yes, "What to change the old identifier to.")]
|
||||
public Identifier New { get; set; }
|
||||
|
||||
public TalentMigrationUpdateStatIdentifier(Version targetVersion, ContentXElement element) : base(targetVersion)
|
||||
=> SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
protected override void Apply(CharacterInfo info)
|
||||
{
|
||||
foreach (SavedStatValue statValue in info.SavedStatValues.Values.SelectMany(static s => s))
|
||||
{
|
||||
if (statValue.StatIdentifier != Old) { continue; }
|
||||
|
||||
statValue.StatIdentifier = New;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#if CLIENT
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Xml.Linq;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework;
|
||||
#endif
|
||||
|
||||
@@ -12,6 +15,11 @@ namespace Barotrauma
|
||||
|
||||
public LocalizedString Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set to false the AbilityEffects of multiple of the same talent will not be checked and only the first one.
|
||||
/// </summary>
|
||||
public bool AbilityEffectsStackWithSameTalent;
|
||||
|
||||
public readonly Sprite Icon;
|
||||
|
||||
#if CLIENT
|
||||
@@ -20,6 +28,8 @@ namespace Barotrauma
|
||||
|
||||
public static readonly PrefabCollection<TalentPrefab> TalentPrefabs = new PrefabCollection<TalentPrefab>();
|
||||
|
||||
public readonly ImmutableHashSet<TalentMigration> Migrations;
|
||||
|
||||
public ContentXElement ConfigElement
|
||||
{
|
||||
get;
|
||||
@@ -32,6 +42,8 @@ namespace Barotrauma
|
||||
|
||||
DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value);
|
||||
|
||||
AbilityEffectsStackWithSameTalent = element.GetAttributeBool("abilityeffectsstackwithsametalent", true);
|
||||
|
||||
Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty);
|
||||
if (!nameIdentifier.IsEmpty)
|
||||
{
|
||||
@@ -48,6 +60,8 @@ namespace Barotrauma
|
||||
: Option<Color>.None();
|
||||
#endif
|
||||
|
||||
var migrations = ImmutableHashSet.CreateBuilder<TalentMigration>();
|
||||
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -60,9 +74,25 @@ namespace Barotrauma
|
||||
TextManager.ConstructDescription(ref tempDescription, subElement);
|
||||
Description = tempDescription;
|
||||
break;
|
||||
case "migrations":
|
||||
foreach (var migrationElement in subElement.Elements())
|
||||
{
|
||||
try
|
||||
{
|
||||
var migration = TalentMigration.FromXML(migrationElement);
|
||||
migrations.Add(migration);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Migrations = migrations.ToImmutable();
|
||||
|
||||
if (element.GetAttribute("description") != null)
|
||||
{
|
||||
string description = element.GetAttributeString("description", string.Empty);
|
||||
|
||||
+11
-3
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -22,7 +23,7 @@ namespace Barotrauma
|
||||
: string.Empty);
|
||||
}
|
||||
|
||||
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 13, 0);
|
||||
public static readonly Version MinimumHashCompatibleVersion = new Version(1, 0, 13, 2);
|
||||
|
||||
public const string LocalModsDir = "LocalMods";
|
||||
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
|
||||
@@ -176,7 +177,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Md5Hash CalculateHash(bool logging = false)
|
||||
public Md5Hash CalculateHash(bool logging = false, string? name = null, string? modVersion = null)
|
||||
{
|
||||
using IncrementalHash incrementalHash = IncrementalHash.CreateHash(HashAlgorithmName.MD5);
|
||||
|
||||
@@ -203,7 +204,14 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string selectedName = name ?? Name;
|
||||
if (!selectedName.IsNullOrEmpty())
|
||||
{
|
||||
incrementalHash.AppendData(Encoding.UTF8.GetBytes(selectedName));
|
||||
}
|
||||
incrementalHash.AppendData(Encoding.UTF8.GetBytes(modVersion ?? ModVersion));
|
||||
|
||||
var md5Hash = Md5Hash.BytesAsHash(incrementalHash.GetHashAndReset());
|
||||
if (logging)
|
||||
{
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (contentPackages.AtLeast(2, cp2 => cp == cp2))
|
||||
{
|
||||
throw new InvalidOperationException($"Input contains duplicate packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
|
||||
throw new InvalidOperationException($"There are duplicates in the list of selected content packages (\"{cp.Name}\", hash: {cp.Hash?.ShortRepresentation ?? "none"})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1878,6 +1878,7 @@ 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));
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
OnClose = 20,
|
||||
/// <summary>
|
||||
/// Executes when the entity spawns. Only valid for doors.
|
||||
/// Executes when the entity spawns. Valid for items and characters.
|
||||
/// </summary>
|
||||
OnSpawn = 21,
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
+21
-4
@@ -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()
|
||||
|
||||
-94
@@ -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;
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
while (QueuedEventsForNextRound.Count > 0 && QueuedEventsForNextRound.Dequeue() is Identifier id)
|
||||
while (QueuedEventsForNextRound.TryDequeue(out var id))
|
||||
{
|
||||
var eventPrefab = EventSet.GetEventPrefab(id);
|
||||
if (eventPrefab == null)
|
||||
@@ -879,9 +879,9 @@ namespace Barotrauma
|
||||
monsterStrength = 0;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsIncapacitated || !character.Enabled || character.IsPet) { continue; }
|
||||
if (character.IsIncapacitated || character.IsArrested || !character.Enabled || character.IsPet) { continue; }
|
||||
|
||||
if (character.AIController is EnemyAIController enemyAI)
|
||||
if (character.AIController is EnemyAIController enemyAI)
|
||||
{
|
||||
if (!enemyAI.AIParams.StayInAbyss)
|
||||
{
|
||||
@@ -889,7 +889,7 @@ namespace Barotrauma
|
||||
monsterStrength += enemyAI.CombatStrength;
|
||||
}
|
||||
|
||||
if (character.CurrentHull?.Submarine?.Info != null &&
|
||||
if (character.CurrentHull?.Submarine?.Info != null &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)) &&
|
||||
character.CurrentHull.Submarine.Info.Type == SubmarineType.Player)
|
||||
{
|
||||
@@ -902,10 +902,11 @@ namespace Barotrauma
|
||||
// -> One Crawler adds 0.02, a Mudraptor 0.042, a Hammerhead 0.1, and a Moloch 0.25.
|
||||
enemyDanger += enemyAI.CombatStrength / 5000.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (character.AIController is HumanAIController humanAi && !character.IsOnFriendlyTeam(CharacterTeamType.Team1))
|
||||
{
|
||||
if (character.Submarine != null &&
|
||||
if (character.Submarine != null &&
|
||||
character.Submarine.PhysicsBody is { BodyType: BodyType.Dynamic } &&
|
||||
Vector2.DistanceSquared(character.Submarine.WorldPosition, Submarine.MainSub.WorldPosition) < Sonar.DefaultSonarRange * Sonar.DefaultSonarRange)
|
||||
{
|
||||
//we have no easy way to define the strength of a human enemy (depends more on the sub and it's state than the character),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -401,7 +401,7 @@ namespace Barotrauma
|
||||
int reward = GetReward(sub);
|
||||
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
|
||||
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
|
||||
CharacterTalent.CheckTalentsForCrew(crewCharacters, AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier);
|
||||
crewCharacters.ForEach(c => missionMoneyGainMultiplier.Value += c.GetStatValue(StatTypes.MissionMoneyGainMultiplier));
|
||||
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ namespace Barotrauma
|
||||
private float missionDifficulty;
|
||||
private int alternateReward;
|
||||
|
||||
private Identifier factionIdentifier;
|
||||
|
||||
private Submarine enemySub;
|
||||
private readonly List<Character> characters = new List<Character>();
|
||||
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
|
||||
@@ -140,8 +142,8 @@ namespace Barotrauma
|
||||
missionDifficulty = level?.Difficulty ?? 0;
|
||||
|
||||
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
|
||||
|
||||
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
|
||||
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
|
||||
|
||||
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
|
||||
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
|
||||
@@ -170,11 +172,11 @@ namespace Barotrauma
|
||||
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
|
||||
}
|
||||
|
||||
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
|
||||
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
|
||||
{
|
||||
return Math.Abs(levelDifficulty - preferredDifficulty + MathHelper.Lerp(-randomnessModifier, randomnessModifier, (float)rand.NextDouble()));
|
||||
}
|
||||
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
|
||||
private static int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty, Random rand)
|
||||
{
|
||||
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
|
||||
}
|
||||
@@ -254,6 +256,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
enemySub.ImmuneToBallastFlora = true;
|
||||
enemySub.EnableFactionSpecificEntities(factionIdentifier);
|
||||
}
|
||||
|
||||
private void InitPirates()
|
||||
@@ -444,7 +447,7 @@ namespace Barotrauma
|
||||
|
||||
private bool CheckWinState() => !IsClient && characters.All(m => DeadOrCaptured(m));
|
||||
|
||||
private bool DeadOrCaptured(Character character)
|
||||
private static bool DeadOrCaptured(Character character)
|
||||
{
|
||||
return character == null || character.Removed || character.Submarine == null || (character.LockHands && character.Submarine == Submarine.MainSub) || character.IsIncapacitated;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ namespace Barotrauma
|
||||
public Item Item;
|
||||
|
||||
/// <summary>
|
||||
/// Note that the integer values matter here: the state of the target can't go back to a smaller value,
|
||||
/// and a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
|
||||
/// Note that the integer values matter here:
|
||||
/// a larger or equal value than the <see href="RequiredRetrievalState">RequiredRetrievalState</see> means the item counts as retrieved
|
||||
/// (if the item needs to be picked up to be considered retrieved, it's also considered retrieved if it's in the sub)
|
||||
/// </summary>
|
||||
public enum RetrievalState
|
||||
@@ -167,6 +167,8 @@ namespace Barotrauma
|
||||
|
||||
private readonly List<Target> targets = new List<Target>();
|
||||
|
||||
public bool AnyTargetNeedsToBeRetrievedToSub => targets.Any(t => t.RequiredRetrievalState == Target.RetrievalState.RetrievedToSub && !t.Retrieved);
|
||||
|
||||
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
|
||||
{
|
||||
get
|
||||
@@ -361,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; }
|
||||
}
|
||||
@@ -382,23 +384,46 @@ namespace Barotrauma
|
||||
switch (target.State)
|
||||
{
|
||||
case Target.RetrievalState.None:
|
||||
if (target.Interacted)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.GetRootContainer() ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
if (target.Interacted)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.Interact);
|
||||
}
|
||||
var root = target.Item?.RootContainer ?? target.Item;
|
||||
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.PickedUp);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Target.RetrievalState.PickedUp:
|
||||
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? target.Item.GetRootInventoryOwner()?.Submarine;
|
||||
if (parentSub != null)
|
||||
case Target.RetrievalState.RetrievedToSub:
|
||||
{
|
||||
if (parentSub.Info.Type == SubmarineType.Player || Level.IsLoadedFriendlyOutpost)
|
||||
Entity rootInventoryOwner = target.Item.GetRootInventoryOwner();
|
||||
Submarine parentSub = target.Item.CurrentHull?.Submarine ?? rootInventoryOwner?.Submarine;
|
||||
|
||||
bool inPlayerSub = parentSub != null && parentSub.Info.Type == SubmarineType.Player;
|
||||
bool inPlayerInventory = false;
|
||||
bool playerInFriendlySub = false;
|
||||
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
inPlayerInventory = true;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
playerInFriendlySub =
|
||||
character.IsInFriendlySub ||
|
||||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
|
||||
{
|
||||
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.State = Target.RetrievalState.PickedUp;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -406,8 +431,8 @@ namespace Barotrauma
|
||||
|
||||
void TrySetRetrievalState(Target.RetrievalState retrievalState)
|
||||
{
|
||||
if (retrievalState < target.State) { return; }
|
||||
bool wasRetrieved = false;
|
||||
if (retrievalState < target.State || target.State == retrievalState) { return; }
|
||||
bool wasRetrieved = target.Retrieved;
|
||||
target.State = retrievalState;
|
||||
//increment the mission state if the target became retrieved
|
||||
if (!wasRetrieved && target.Retrieved) { State = i + 1; }
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -416,7 +416,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities));
|
||||
return FindAllSellableItems().Where(it => IsItemSellable(it, confirmedSoldEntities)).ToList();
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<Item> FindAllItemsOnPlayerAndSub(Character character)
|
||||
@@ -440,7 +440,7 @@ namespace Barotrauma
|
||||
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 { } rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
|
||||
if (item.RootContainer is Item rootContainer && rootContainer.HasTag("dontsellitems")) { return false; }
|
||||
return true;
|
||||
}).Distinct();
|
||||
|
||||
@@ -610,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>())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -15,7 +16,7 @@ namespace Barotrauma
|
||||
/// <summary>
|
||||
/// Maximum amount of reputation loss you can get from damaging outpost NPCs per round
|
||||
/// </summary>
|
||||
public const float MaxReputationLossFromNPCDamage = 10.0f;
|
||||
public const float MaxReputationLossFromNPCDamage = 20.0f;
|
||||
/// <summary>
|
||||
/// Maximum amount of reputation loss you can get from damaging outpost walls per round
|
||||
/// </summary>
|
||||
@@ -71,45 +72,50 @@ namespace Barotrauma
|
||||
|
||||
public float GetReputationChangeMultiplier(float reputationChange)
|
||||
{
|
||||
if (reputationChange > 0f)
|
||||
return reputationChange switch
|
||||
{
|
||||
float reputationGainMultiplier = 1f;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
reputationGainMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationGainMultiplier, includeSaved: false);
|
||||
reputationGainMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationGainMultiplier, Identifier) ?? 0;
|
||||
}
|
||||
return reputationGainMultiplier;
|
||||
}
|
||||
else if (reputationChange < 0f)
|
||||
> 0f => GetMultiplierForStatType(StatTypes.ReputationGainMultiplier, Identifier),
|
||||
< 0f => GetMultiplierForStatType(StatTypes.ReputationLossMultiplier, Identifier),
|
||||
_ => 1.0f
|
||||
};
|
||||
|
||||
static float GetMultiplierForStatType(StatTypes statTypes, Identifier identifier)
|
||||
{
|
||||
float reputationLossMultiplier = 1f;
|
||||
foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both))
|
||||
float multiplier = 1f;
|
||||
var crew = GameSession.GetSessionCrewCharacters(CharacterType.Both);
|
||||
if (crew != null && crew.Any())
|
||||
{
|
||||
reputationLossMultiplier *= 1f + character.GetStatValue(StatTypes.ReputationLossMultiplier, includeSaved: false);
|
||||
reputationLossMultiplier *= 1f + character.Info?.GetSavedStatValue(StatTypes.ReputationLossMultiplier, Identifier) ?? 0;
|
||||
multiplier *= 1f + crew.Max(c => c.GetStatValue(statTypes, includeSaved: false));
|
||||
multiplier *= 1f + crew.Max(c => c.Info?.GetSavedStatValue(statTypes, identifier) ?? 0);
|
||||
}
|
||||
return reputationLossMultiplier;
|
||||
return multiplier;
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
public void AddReputation(float reputationChange, float maxReputationChangePerRound = float.MaxValue)
|
||||
{
|
||||
float currentValue = Value;
|
||||
float currentReputationChange = currentValue - ReputationAtRoundStart;
|
||||
if (Math.Abs(currentReputationChange) >= maxReputationChangePerRound &&
|
||||
Math.Sign(currentReputationChange) == Math.Sign(reputationChange))
|
||||
{
|
||||
return;
|
||||
float prevValue = Value;
|
||||
//if we're already over the limit, do nothing (assuming the change is in the "same direction" that we've gone over the limit)
|
||||
if (doesReputationChangeGoOverLimit(prevValue, reputationChange))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float newValue = Value + reputationChange * GetReputationChangeMultiplier(reputationChange);
|
||||
if (Math.Abs(newValue - ReputationAtRoundStart) > maxReputationChangePerRound &&
|
||||
Math.Sign(newValue - currentValue) == Math.Sign(newValue - ReputationAtRoundStart))
|
||||
if (doesReputationChangeGoOverLimit(newValue, newValue - prevValue))
|
||||
{
|
||||
newValue = ReputationAtRoundStart + maxReputationChangePerRound * Math.Sign(reputationChange);
|
||||
}
|
||||
|
||||
Value = newValue;
|
||||
|
||||
bool doesReputationChangeGoOverLimit(float newValue, float change)
|
||||
{
|
||||
float totalReputationChange = newValue - ReputationAtRoundStart;
|
||||
return
|
||||
Math.Abs(totalReputationChange) > maxReputationChangePerRound &&
|
||||
Math.Sign(totalReputationChange) == Math.Sign(change);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly NamedEvent<Reputation> OnReputationValueChanged = new NamedEvent<Reputation>();
|
||||
|
||||
@@ -95,8 +95,6 @@ namespace Barotrauma
|
||||
public SubmarineInfo PendingSubmarineSwitch;
|
||||
public bool TransferItemsOnSubSwitch { get; set; }
|
||||
|
||||
public bool SwitchedSubsThisRound { get; private set; }
|
||||
|
||||
protected Map map;
|
||||
public Map Map
|
||||
{
|
||||
@@ -129,6 +127,8 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public Location CurrentLocation => Map?.CurrentLocation;
|
||||
|
||||
public Wallet Bank;
|
||||
|
||||
public LevelData NextLevel
|
||||
@@ -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; }
|
||||
@@ -291,7 +293,6 @@ namespace Barotrauma
|
||||
PurchasedLostShuttlesInLatestSave = PurchasedLostShuttles = false;
|
||||
var connectedSubs = Submarine.MainSub.GetConnectedSubs();
|
||||
wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost);
|
||||
SwitchedSubsThisRound = false;
|
||||
}
|
||||
|
||||
public static int GetHullRepairCost()
|
||||
@@ -739,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
|
||||
@@ -777,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
|
||||
@@ -931,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");
|
||||
@@ -986,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;
|
||||
}
|
||||
@@ -1205,26 +1213,15 @@ 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));
|
||||
}
|
||||
|
||||
protected void LoadEvents(XElement element)
|
||||
{
|
||||
TotalPlayTime = element.GetAttributeDouble(nameof(TotalPlayTime).ToLowerInvariant(), 0);
|
||||
TotalPassedLevels = element.GetAttributeInt(nameof(TotalPassedLevels).ToLowerInvariant(), 0);
|
||||
}
|
||||
|
||||
protected XElement SaveEvents()
|
||||
{
|
||||
return new XElement("events",
|
||||
new XAttribute(nameof(EventManager.QueuedEventsForNextRound).ToLowerInvariant(),
|
||||
string.Join(',', GameMain.GameSession.EventManager.QueuedEventsForNextRound)));
|
||||
new XAttribute(nameof(TotalPassedLevels).ToLowerInvariant(), TotalPassedLevels),
|
||||
new XAttribute(nameof(DivingSuitWarningShown).ToLowerInvariant(), DivingSuitWarningShown));
|
||||
}
|
||||
|
||||
public void LogState()
|
||||
@@ -1310,7 +1307,6 @@ namespace Barotrauma
|
||||
TransferItemsBetweenSubs();
|
||||
}
|
||||
RefreshOwnedSubmarines();
|
||||
SwitchedSubsThisRound = true;
|
||||
PendingSubmarineSwitch = null;
|
||||
}
|
||||
|
||||
|
||||
+16
-10
@@ -275,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)
|
||||
@@ -575,6 +577,14 @@ namespace Barotrauma
|
||||
|
||||
HintManager.OnRoundStarted();
|
||||
#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)
|
||||
@@ -691,7 +701,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;
|
||||
}
|
||||
@@ -972,7 +982,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; }
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace Barotrauma
|
||||
{
|
||||
internal sealed partial class MedicalClinic
|
||||
{
|
||||
private const int RateLimitMaxRequests = 20,
|
||||
RateLimitExpiry = 5;
|
||||
|
||||
public enum NetworkHeader
|
||||
{
|
||||
REQUEST_AFFLICTIONS,
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public static readonly List<InvSlotType> anySlot = new List<InvSlotType>() { InvSlotType.Any };
|
||||
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
|
||||
|
||||
protected bool[] IsEquipped;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
@@ -226,7 +240,7 @@ namespace Barotrauma
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
var wearable = item.GetComponent<Wearable>();
|
||||
if (wearable != null && !wearable.AutoEquipWhenFull && CheckIfAnySlotAvailable(item, false) == -1)
|
||||
if (wearable != null && !wearable.AutoEquipWhenFull && !IsAnySlotAvailable(item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -336,7 +350,7 @@ namespace Barotrauma
|
||||
//try to place the item in a LimbSlot.Any slot if that's allowed
|
||||
if (allowedSlots.Contains(InvSlotType.Any) && item.AllowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
int freeIndex = CheckIfAnySlotAvailable(item, inWrongSlot);
|
||||
int freeIndex = GetFreeAnySlot(item, inWrongSlot);
|
||||
if (freeIndex > -1)
|
||||
{
|
||||
PutItem(item, freeIndex, user, true, createNetworkEvent);
|
||||
@@ -393,7 +407,9 @@ namespace Barotrauma
|
||||
return placedInSlot > -1;
|
||||
}
|
||||
|
||||
public int CheckIfAnySlotAvailable(Item item, bool inWrongSlot)
|
||||
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
|
||||
|
||||
private int GetFreeAnySlot(Item item, bool inWrongSlot)
|
||||
{
|
||||
//attempt to stack first
|
||||
for (int i = 0; i < capacity; i++)
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -383,6 +389,8 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool isClosing = false;
|
||||
if ((!IsStuck && !IsJammed) || !isOpen)
|
||||
{
|
||||
@@ -521,8 +529,11 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
RefreshLinkedGap();
|
||||
#if CLIENT
|
||||
convexHull = new ConvexHull(Rectangle.Empty, !IsHorizontal, item);
|
||||
if (Window != Rectangle.Empty) { convexHull2 = new ConvexHull(Rectangle.Empty, !IsHorizontal, item); }
|
||||
convexHull = new ConvexHull(doorRect, IsHorizontal, item);
|
||||
if (Window != Rectangle.Empty)
|
||||
{
|
||||
convexHull2 = new ConvexHull(doorRect, IsHorizontal, item);
|
||||
}
|
||||
UpdateConvexHulls();
|
||||
#endif
|
||||
}
|
||||
@@ -556,6 +567,12 @@ namespace Barotrauma.Items.Components
|
||||
gap.ConnectedDoor = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (OutsideSubmarineFixture != null)
|
||||
{
|
||||
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
|
||||
OutsideSubmarineFixture = null;
|
||||
}
|
||||
|
||||
//no need to remove the gap if we're unloading the whole submarine
|
||||
//otherwise the gap will be removed twice and cause console warnings
|
||||
|
||||
@@ -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);
|
||||
@@ -700,7 +704,8 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
Vector2 attachPos = GetAttachPosition(character, useWorldCoordinates: true);
|
||||
Submarine attachSubmarine = Structure.GetAttachTarget(attachPos)?.Submarine ?? item.Submarine;
|
||||
int maxAttachableCount = (int)character.Info.GetSavedStatValue(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
|
||||
int maxAttachableCount = (int)character.Info.GetSavedStatValueWithBotsInMp(StatTypes.MaxAttachableCount, item.Prefab.Identifier);
|
||||
|
||||
int currentlyAttachedCount = Item.ItemList.Count(
|
||||
i => i.Submarine == attachSubmarine && i.GetComponent<Holdable>() is Holdable holdable && holdable.Attached && i.Prefab.Identifier == item.Prefab.Identifier);
|
||||
if (maxAttachableCount == 0)
|
||||
@@ -811,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;
|
||||
|
||||
+14
-5
@@ -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)
|
||||
{
|
||||
|
||||
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Vector2 barrelPos = TransformedBarrelPos + item.body.SimPosition;
|
||||
float rotation = (Item.body.Dir == 1.0f) ? Item.body.Rotation : Item.body.Rotation - MathHelper.Pi;
|
||||
float spread = GetSpread(character) * Projectile.GetSpreadFromPool();
|
||||
float spread = GetSpread(character) * projectile.GetSpreadFromPool();
|
||||
|
||||
var lastProjectile = LastProjectile;
|
||||
if (lastProjectile != projectile)
|
||||
@@ -277,7 +277,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Item.body.ApplyLinearImpulse(new Vector2((float)Math.Cos(projectile.Item.body.Rotation), (float)Math.Sin(projectile.Item.body.Rotation)) * Item.body.Mass * -50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
}
|
||||
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * Projectile.GetSpreadFromPool());
|
||||
projectile.Item.body.ApplyTorque(projectile.Item.body.Mass * degreeOfFailure * 20.0f * projectile.GetSpreadFromPool());
|
||||
}
|
||||
Item.RemoveContained(projectile.Item);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ namespace Barotrauma.Items.Components
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair multiple things at once, or will it only affect the first thing the ray from the barrel hits.")]
|
||||
public bool RepairMultiple { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Can the item repair multiple walls at once? Only relevant if RepairMultiple is true.")]
|
||||
public bool RepairMultipleWalls { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Can the item repair things through holes in walls.")]
|
||||
public bool RepairThroughHoles { get; set; }
|
||||
|
||||
@@ -383,6 +386,7 @@ namespace Barotrauma.Items.Components
|
||||
//stop the ray if it already hit a door/wall and is now about to hit some other type of entity
|
||||
if (lastHitType == typeof(Item) || lastHitType == typeof(Structure)) { break; }
|
||||
}
|
||||
if (!RepairMultipleWalls && (bodyType == typeof(Structure) || (body.UserData as Item)?.GetComponent<Door>() != null)) { break; }
|
||||
|
||||
Character hitCharacter = null;
|
||||
if (body.UserData is Limb limb)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -237,7 +237,7 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not enforced). Note that there's also a generic BotPriority for all item prefabs.")]
|
||||
[Serialize(0f, IsPropertySaveable.No, description: "How useful the item is in combat? Used by AI to decide which item it should use as a weapon. For the sake of clarity, use a value between 0 and 100 (not forced). Note that there's also a generic BotPriority for all item prefabs.")]
|
||||
public float CombatPriority { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -435,7 +435,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)
|
||||
@@ -682,9 +682,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public virtual void FlipY(bool relativeToSub) { }
|
||||
|
||||
public bool IsNotEmpty(Character user, bool checkContainedItems = true) =>
|
||||
HasRequiredContainedItems(user, addMessage: false) &&
|
||||
(!checkContainedItems || Item.OwnInventory == null || Item.OwnInventory.AllItems.Any(i => i.Condition > 0));
|
||||
/// <summary>
|
||||
/// Shorthand for !HasRequiredContainedItems()
|
||||
/// </summary>
|
||||
public bool IsEmpty(Character user) => !HasRequiredContainedItems(user, addMessage: false);
|
||||
|
||||
public bool HasRequiredContainedItems(Character user, bool addMessage, LocalizedString msg = null)
|
||||
{
|
||||
@@ -716,20 +717,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; }
|
||||
@@ -765,23 +796,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -511,25 +528,28 @@ namespace Barotrauma.Items.Components
|
||||
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
|
||||
StatusEffect effect = activeContainedItem.StatusEffect;
|
||||
|
||||
targets.Clear();
|
||||
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(ActionType.OnContaining, deltaTime, item, item.AllPropertyObjects);
|
||||
targets.AddRange(item.AllPropertyObjects);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
|
||||
{
|
||||
effect.Apply(ActionType.OnContaining, deltaTime, item, contained.AllPropertyObjects);
|
||||
targets.AddRange(contained.AllPropertyObjects);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
|
||||
{
|
||||
effect.Apply(ActionType.OnContaining, deltaTime, item, character);
|
||||
targets.Add(character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
||||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
||||
{
|
||||
targets.Clear();
|
||||
effect.AddNearbyTargets(item.WorldPosition, targets);
|
||||
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
|
||||
}
|
||||
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
|
||||
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
|
||||
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,18 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class Projectile : ItemComponent, IServerSerializable
|
||||
{
|
||||
const int SpreadCounterWrapAround = 256;
|
||||
|
||||
private static readonly ImmutableArray<float> spreadPool;
|
||||
static Projectile()
|
||||
{
|
||||
MTRandom random = new MTRandom(0);
|
||||
spreadPool = Enumerable.Range(0, SpreadCounterWrapAround).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
|
||||
spreadPool = Enumerable.Range(0, byte.MaxValue + 1).Select(f => (float)random.NextDouble() - 0.5f).ToImmutableArray();
|
||||
}
|
||||
|
||||
public static float GetSpreadFromPool()
|
||||
public static byte SpreadCounter { get; private set; }
|
||||
|
||||
public static void ResetSpreadCounter()
|
||||
{
|
||||
return spreadPool[SpreadCounter];
|
||||
SpreadCounter = 0;
|
||||
}
|
||||
|
||||
struct HitscanResult
|
||||
@@ -62,7 +62,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool removePending;
|
||||
|
||||
public static byte SpreadCounter { get; private set; }
|
||||
private byte spreadIndex;
|
||||
|
||||
//continuous collision detection is used while the projectile is moving faster than this
|
||||
const float ContinuousCollisionThreshold = 5.0f;
|
||||
@@ -299,7 +299,8 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
SpreadCounter = (byte)(item.ID % SpreadCounterWrapAround);
|
||||
spreadIndex = SpreadCounter;
|
||||
SpreadCounter++;
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
@@ -328,6 +329,12 @@ namespace Barotrauma.Items.Components
|
||||
originalCollisionTargets = item.body.CollidesWith;
|
||||
}
|
||||
|
||||
public float GetSpreadFromPool()
|
||||
{
|
||||
spreadIndex = (byte)MathUtils.PositiveModulo(spreadIndex, spreadPool.Length);
|
||||
return spreadPool[spreadIndex];
|
||||
}
|
||||
|
||||
private void Launch(Character user, Vector2 simPosition, float rotation, float damageMultiplier = 1f, float launchImpulseModifier = 0f)
|
||||
{
|
||||
if (Item.body == null) { return; }
|
||||
@@ -380,7 +387,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
#if SERVER
|
||||
launchRot = rotation;
|
||||
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(SpreadCounter - 1)));
|
||||
Item.CreateServerEvent(this, new EventData(launch: true, spreadCounter: (byte)(spreadIndex - 1)));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -396,7 +403,6 @@ namespace Barotrauma.Items.Components
|
||||
for (int i = 0; i < HitScanCount; i++)
|
||||
{
|
||||
float launchAngle;
|
||||
|
||||
if (StaticSpread)
|
||||
{
|
||||
launchAngle = initialRotation + MathHelper.ToRadians(i - ((float)(HitScanCount - 1) / 2)) * Spread;
|
||||
@@ -405,7 +411,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
launchAngle = initialRotation + MathHelper.ToRadians(Spread * GetSpreadFromPool());
|
||||
}
|
||||
SpreadCounter++;
|
||||
spreadIndex++;
|
||||
|
||||
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
|
||||
if (Hitscan)
|
||||
@@ -703,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)
|
||||
@@ -711,7 +717,7 @@ namespace Barotrauma.Items.Components
|
||||
DisableProjectileCollisions();
|
||||
Unstick();
|
||||
}
|
||||
base.Drop(dropper);
|
||||
base.Drop(dropper, setTransform);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
@@ -885,17 +891,8 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector2 normalizedVel;
|
||||
Vector2 dir;
|
||||
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
|
||||
{
|
||||
normalizedVel = Vector2.Zero;
|
||||
dir = contact.Manifold.LocalNormal;
|
||||
}
|
||||
else
|
||||
{
|
||||
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
|
||||
}
|
||||
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
|
||||
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
|
||||
|
||||
//do a raycast in the sub's coordinate space to see if it hit a structure
|
||||
var wallBody = Submarine.PickBody(
|
||||
@@ -904,7 +901,7 @@ namespace Barotrauma.Items.Components
|
||||
collisionCategory: Physics.CollisionWall);
|
||||
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
|
||||
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
|
||||
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
|
||||
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
|
||||
{
|
||||
target = wallBody.FixtureList.First();
|
||||
if (hits.Contains(target.Body))
|
||||
@@ -942,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,7 +29,7 @@ namespace Barotrauma.Items.Components
|
||||
private readonly Item item;
|
||||
|
||||
public readonly bool IsOutput;
|
||||
|
||||
|
||||
public readonly List<StatusEffect> Effects;
|
||||
|
||||
public readonly List<(ushort wireId, int? connectionIndex)> LoadedWires;
|
||||
@@ -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;
|
||||
@@ -292,6 +295,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public void SendSignal(Signal signal)
|
||||
{
|
||||
LastSentSignal = signal;
|
||||
enumeratingWires = true;
|
||||
foreach (var wire in wires)
|
||||
{
|
||||
@@ -302,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
|
||||
|
||||
foreach (ItemComponent ic in recipient.item.Components)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user