Build 0.18.0.0

This commit is contained in:
Markus Isberg
2022-05-13 00:55:52 +09:00
parent 15d18e6ff6
commit 7547a9b78a
218 changed files with 3881 additions and 2192 deletions
@@ -31,6 +31,17 @@ namespace Barotrauma
if (_previousAiTarget != null)
{
_lastAiTarget = _previousAiTarget;
if (_selectedAiTarget != null)
{
if (_selectedAiTarget.Entity is Item i && _previousAiTarget.Entity is Character c)
{
if (i.IsOwnedBy(c)) { return; }
}
else if (_previousAiTarget.Entity is Item it && _selectedAiTarget.Entity is Character ch)
{
if (it.IsOwnedBy(ch)) { return; }
}
}
}
OnTargetChanged(_previousAiTarget, _selectedAiTarget);
}
@@ -1055,6 +1055,9 @@ namespace Barotrauma
private Vector2 attackWorldPos;
private Vector2 attackSimPos;
private float reachTimer;
// How long the monster tries to reach out for the target when it's close to it before ignoring it.
private const float reachTimeOut = 10;
private void UpdateAttack(float deltaTime)
{
@@ -1427,6 +1430,22 @@ namespace Barotrauma
// Check that we can reach the target
distance = toTarget.Length();
canAttack = distance < AttackLimb.attack.Range;
if (canAttack)
{
reachTimer = 0;
}
else if (selectedTargetingParams.AttackPattern == AttackPattern.Straight && distance < AttackLimb.attack.Range * 5)
{
reachTimer += deltaTime;
if (reachTimer > reachTimeOut)
{
reachTimer = 0;
IgnoreTarget(SelectedAiTarget);
State = AIState.Idle;
ResetAITarget();
return;
}
}
// Crouch if the target is down (only humanoids), so that we can reach it.
if (Character.AnimController is HumanoidAnimController humanoidAnimController && distance < AttackLimb.attack.Range * 2)
@@ -1958,9 +1977,8 @@ namespace Barotrauma
}
if (!isFriendly && attackResult.Damage > 0.0f)
{
ignoredTargets.Remove(attacker.AiTarget);
bool canAttack = attacker.Submarine == Character.Submarine && canAttackCharacters || attacker.Submarine != null && canAttackWalls;
if (AIParams.AttackWhenProvoked && canAttack)
if (AIParams.AttackWhenProvoked && canAttack && !ignoredTargets.Contains(attacker.AiTarget))
{
if (attacker.IsHusk)
{
@@ -3476,6 +3494,7 @@ namespace Barotrauma
{
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
reachTimer = 0;
}
protected override void OnStateChanged(AIState from, AIState to)
@@ -3496,6 +3515,7 @@ namespace Barotrauma
SetStateResetTimer();
}
blockCheckTimer = 0;
reachTimer = 0;
}
private void SetStateResetTimer() => stateResetTimer = stateResetCooldown * Rand.Range(0.75f, 1.25f);
@@ -59,7 +59,11 @@ namespace Barotrauma
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800;
private readonly float enemySpotDistanceInside = 1000;
private float enemycheckTimer;
private float enemyCheckTimer;
private readonly float reportProblemsInterval = 1.0f;
private float reportProblemsTimer;
/// <summary>
/// How far other characters can hear reports done by this character (e.g. reports for fires, intruders). Defaults to infinity.
@@ -166,6 +170,7 @@ namespace Barotrauma
objectiveManager = new AIObjectiveManager(c);
reactTimer = GetReactionTime();
SortTimer = Rand.Range(0f, sortObjectiveInterval);
reportProblemsTimer = Rand.Range(0f, reportProblemsInterval);
}
public override void Update(float deltaTime)
@@ -309,10 +314,10 @@ namespace Barotrauma
{
// 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
enemycheckTimer -= deltaTime;
if (enemycheckTimer < 0)
enemyCheckTimer -= deltaTime;
if (enemyCheckTimer < 0)
{
enemycheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
enemyCheckTimer = enemyCheckInterval * Rand.Range(0.75f, 1.25f);
if (!objectiveManager.IsCurrentObjective<AIObjectiveCombat>())
{
float closestDistance = 0;
@@ -407,19 +412,29 @@ namespace Barotrauma
{
if (Character.IsOnPlayerTeam)
{
VisibleHulls.ForEach(h => PropagateHullSafety(Character, h));
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
}
}
else
{
// Outpost npcs don't inform each other about threats, like crew members do.
VisibleHulls.ForEach(h => RefreshHullSafety(h));
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
}
}
}
if (Character.SpeechImpediment < 100.0f)
{
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
reportProblemsTimer -= deltaTime;
if (reportProblemsTimer <= 0.0f)
{
ReportProblems();
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{
ReportProblems();
}
reportProblemsTimer = reportProblemsInterval;
}
UpdateSpeaking();
}
@@ -785,9 +800,10 @@ namespace Barotrauma
if (item == null || item.Removed) { return; }
if (!itemsToRelocate.Contains(item)) { return; }
var mainSub = Submarine.MainSub;
if (item.ParentInventory != null)
Entity owner = item.GetRootInventoryOwner();
if (owner != null)
{
if (item.ParentInventory.Owner is Character c)
if (owner is Character c)
{
if (c.TeamID == CharacterTeamType.Team1 || c.TeamID == CharacterTeamType.Team2)
{
@@ -795,24 +811,37 @@ namespace Barotrauma
return;
}
}
else if (item.ParentInventory.Owner.Submarine == mainSub)
else if (owner.Submarine == mainSub)
{
// Placed inside an inventory that's already in the main sub.
return;
}
}
// Laying on ground inside the main sub.
// Laying on the ground inside the main sub.
if (item.Submarine == mainSub)
{
return;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub);
if (wp != null)
if (owner != null && owner != item)
{
item.Submarine = mainSub;
item.SetTransform(wp.SimPosition, 0.0f);
item.Drop(null);
}
item.Submarine = mainSub;
Item newContainer = mainSub.FindContainerFor(item, onlyPrimary: false);
if (newContainer == null || !newContainer.OwnInventory.TryPutItem(item, user: null))
{
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, mainSub) ?? WayPoint.GetRandom(SpawnType.Path, null, mainSub);
if (wp != null)
{
item.SetTransform(wp.SimPosition, 0.0f, findNewHull: false, setPrevTransform: false);
}
else
{
DebugConsole.AddWarning($"Failed to relocate item {item.Prefab.Identifier} ({item.ID}), because no cargo spawn point could be found!");
}
}
itemsToRelocate.Remove(item);
DebugConsole.Log($"Relocated item {item.Prefab.Identifier} ({item.ID}) back to the main sub.");
}
}
@@ -1149,7 +1178,7 @@ namespace Barotrauma
bool isAttackerFightingEnemy = false;
float minorDamageThreshold = 1;
float majorDamageThreshold = 20;
if (attacker.TeamID == Character.TeamID)
if (attacker.TeamID == Character.TeamID && !attacker.IsInstigator)
{
minorDamageThreshold = 10;
majorDamageThreshold = 40;
@@ -1356,6 +1385,10 @@ namespace Barotrauma
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
if (attacker.IsInstigator)
{
return attacker;
@@ -1545,7 +1578,7 @@ namespace Barotrauma
(!requireEquipped || character.HasEquippedItem(i)) &&
(predicate == null || predicate(i)), recursive, matchingItems);
items = matchingItems;
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
return matchingItems.Any(i => i != null && (containedTag.IsEmpty || i.OwnInventory == null || i.ContainedItems.Any(it => it.HasTag(containedTag) && it.ConditionPercentage > conditionPercentage)));
}
public static void StructureDamaged(Structure structure, float damageAmount, Character character)
@@ -1889,7 +1922,7 @@ namespace Barotrauma
float fireFactor = 1;
if (!ignoreFire)
{
float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
static float calculateFire(Hull h) => h.FireSources.Count * 0.5f + h.FireSources.Sum(fs => fs.DamageRange) / h.Size.X;
// Even the smallest fire reduces the safety by 50%
float fire = visibleHulls == null ? calculateFire(hull) : visibleHulls.Sum(h => calculateFire(h));
fireFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(fire, 0, 1));
@@ -1897,10 +1930,22 @@ namespace Barotrauma
float enemyFactor = 1;
if (!ignoreEnemies)
{
bool isValidTarget(Character e) => IsActive(e) && !IsFriendly(character, e) && !e.IsArrested;
int enemyCount = visibleHulls == null ?
Character.CharacterList.Count(e => isValidTarget(e) && e.CurrentHull == hull) :
Character.CharacterList.Count(e => isValidTarget(e) && visibleHulls.Contains(e.CurrentHull));
int enemyCount = 0;
foreach (Character c in Character.CharacterList)
{
if (visibleHulls == null)
{
if (c.CurrentHull != hull) { continue; }
}
else
{
if (!visibleHulls.Contains(c.CurrentHull)) { continue; }
}
if (IsActive(c) && !IsFriendly(character, c) && !c.IsArrested)
{
enemyCount++;
}
}
// The hull safety decreases 90% per enemy up to 100% (TODO: test smaller percentages)
enemyFactor = MathHelper.Lerp(1, 0, MathHelper.Clamp(enemyCount * 0.9f, 0, 1));
}
@@ -1911,6 +1956,7 @@ namespace Barotrauma
if (item.Prefab != null && item.Prefab.IsDangerous)
{
dangerousItemsFactor = 0;
break;
}
}
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
@@ -113,7 +113,7 @@ namespace Barotrauma
else
{
var connectionPanel = item.GetComponent<ConnectionPanel>();
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Any(w => w != null)))
if (connectionPanel != null && connectionPanel.Connections.Any(c => c.Wires.Count > 0))
{
return false;
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using FarseerPhysics.Dynamics;
using static Barotrauma.AIObjectiveFindSafety;
namespace Barotrauma
{
@@ -775,7 +776,13 @@ namespace Barotrauma
}
else
{
retreatTarget = findSafety.FindBestHull(HumanAIController.VisibleHulls, allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
HullSearchStatus hullSearchStatus = findSafety.FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
if (hullSearchStatus != HullSearchStatus.Finished)
{
findSafety.UpdateSimpleEscape(deltaTime);
return;
}
retreatTarget = potentialSafeHull;
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -785,21 +792,21 @@ namespace Barotrauma
{
UsePathingOutside = false
},
onAbandon: () =>
onAbandon: () =>
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
if (Enemy != null && HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
// If in the same room with an enemy -> don't try to escape because we'd want to fight it
SteeringManager.Reset();
RemoveSubObjective(ref retreatObjective);
}
else
{
// else abandon and fall back to find safety mode
Abandon = true;
}
},
onCompleted: () => RemoveSubObjective(ref retreatObjective));
}
}
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -192,9 +193,17 @@ namespace Barotrauma
}
else
{
HullSearchStatus hullSearchStatus = FindBestHull(out Hull potentialSafeHull, allowChangingSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
if (hullSearchStatus != HullSearchStatus.Finished)
{
UpdateSimpleEscape(deltaTime);
return;
}
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = FindBestHull(allowChangingTheSubmarine: character.TeamID != CharacterTeamType.FriendlyNPC);
currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
@@ -250,58 +259,122 @@ namespace Barotrauma
}
}
if (subObjectives.Any(so => so.CanBeCompleted)) { return; }
if (currentHull != null)
UpdateSimpleEscape(deltaTime);
}
}
public void UpdateSimpleEscape(float deltaTime)
{
Vector2 escapeVel = Vector2.Zero;
if (character.CurrentHull != null)
{
foreach (Hull hull in HumanAIController.VisibleHulls)
{
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
// -> attempt to manually steer away from hazards
Vector2 escapeVel = Vector2.Zero;
foreach (Hull hull in HumanAIController.VisibleHulls)
foreach (FireSource fireSource in hull.FireSources)
{
foreach (FireSource fireSource in hull.FireSources)
{
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
if (escapeVel != Vector2.Zero)
{
float left = currentHull.Rect.X + 50;
float right = currentHull.Rect.Right - 50;
//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.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
return;
Vector2 dir = character.Position - fireSource.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
foreach (Character enemy in Character.CharacterList)
{
if (!HumanAIController.IsActive(enemy) || HumanAIController.IsFriendly(enemy) || enemy.IsArrested) { continue; }
if (HumanAIController.VisibleHulls.Contains(enemy.CurrentHull))
{
Vector2 dir = character.Position - enemy.Position;
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
escapeVel += new Vector2(Math.Sign(dir.X) * distMultiplier, !character.IsClimbing ? 0 : Math.Sign(dir.Y) * distMultiplier);
}
}
}
if (escapeVel != Vector2.Zero)
{
float left = character.CurrentHull.Rect.X + 50;
float right = character.CurrentHull.Rect.Right - 50;
//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.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
{
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
character.AIController.SteeringManager.Reset();
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
public Hull FindBestHull(IEnumerable<Hull> ignoredHulls = null, bool allowChangingTheSubmarine = true)
public enum HullSearchStatus
{
//sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
//path calculations, only to discard all of them when going through the hulls in the outpost)
float EstimateHullSuitability(Hull hull)
Running,
Finished
}
private readonly List<Hull> hulls = new List<Hull>();
private int hullSearchIndex = -1;
float bestHullValue = 0;
bool bestHullIsAirlock = false;
Hull potentialBestHull;
/// <summary>
/// Tries to find the best (safe, nearby) hull the character can find a path to.
/// Checks one hull at a time, and returns HullSearchStatus.Finished when all potential hulls have been checked.
/// </summary>
public HullSearchStatus FindBestHull(out Hull bestHull, IEnumerable<Hull> ignoredHulls = null, bool allowChangingSubmarine = true)
{
if (hullSearchIndex == -1)
{
bestHullValue = 0;
potentialBestHull = null;
bestHullIsAirlock = false;
hulls.Clear();
var connectedSubs = character.Submarine?.GetConnectedSubs();
foreach (Hull hull in Hull.HullList)
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
if (hull.Submarine.Info.IsRuin) { continue; }
if (!allowChangingSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
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
//path calculations, only to discard all of them when going through the hulls in the outpost)
float hullSuitability = EstimateHullSuitability(character, hull);
if (!hulls.Any())
{
hulls.Add(hull);
}
else
{
for (int i = 0; i < hulls.Count; i++)
{
if (hullSuitability > EstimateHullSuitability(character, hulls[i]))
{
hulls.Insert(i, hull);
break;
}
}
}
}
if (hulls.None())
{
bestHull = null;
return HullSearchStatus.Finished;
}
hullSearchIndex = 0;
}
static float EstimateHullSuitability(Character character, Hull hull)
{
float dist =
Math.Abs(hull.WorldPosition.X - character.WorldPosition.X) +
@@ -314,86 +387,91 @@ namespace Barotrauma
return suitability;
}
Hull bestHull = null;
float bestValue = 0;
bool bestIsAirlock = false;
foreach (Hull hull in Hull.HullList.OrderByDescending(h => EstimateHullSuitability(h)))
Hull potentialHull = hulls[hullSearchIndex];
float hullSafety = 0;
bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{
if (hull.Submarine == null) { continue; }
// Ruins are mazes filled with water. There's no safe hulls and we don't want to use the resources on it.
if (hull.Submarine.Info.IsRuin) { continue; }
if (!allowChangingTheSubmarine && hull.Submarine != character.Submarine) { continue; }
if (hull.Rect.Height < ConvertUnits.ToDisplayUnits(character.AnimController.ColliderHeightFromFloor) * 2) { continue; }
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
float hullSafety = 0;
bool hullIsAirlock = false;
bool isCharacterInside = character.CurrentHull != null && character.Submarine != null;
if (isCharacterInside)
{
if (!character.Submarine.IsConnectedTo(hull.Submarine)) { continue; }
hullSafety = HumanAIController.GetHullSafety(hull, hull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - hull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - hull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety < bestValue) { continue; }
hullSafety = HumanAIController.GetHullSafety(potentialHull, potentialHull.GetConnectedHulls(true, 1), character);
float yDist = Math.Abs(character.WorldPosition.Y - potentialHull.WorldPosition.Y);
yDist = yDist > 100 ? yDist * 3 : 0;
float dist = Math.Abs(character.WorldPosition.X - potentialHull.WorldPosition.X) + yDist;
float distanceFactor = MathHelper.Lerp(1, 0.9f, MathUtils.InverseLerp(0, 10000, dist));
hullSafety *= distanceFactor;
//skip the hull if the safety is already less than the best hull
//(no need to do the expensive pathfinding if we already know we're not going to choose this hull)
if (hullSafety > bestHullValue)
{
//avoid airlock modules if not allowed to change the sub
if (!allowChangingTheSubmarine && hull.OutpostModuleTags.Any(t => t == "airlock"))
if (allowChangingSubmarine || !potentialHull.OutpostModuleTags.Any(t => t == "airlock"))
{
continue;
// 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);
if (path.Unreachable)
{
hullSafety = 0;
HumanAIController.UnreachableHulls.Add(potentialHull);
}
else
{
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(potentialHull, true))
{
hullSafety /= 10;
}
}
}
// Don't allow to go outside if not already outside.
var path = PathSteering.PathFinder.FindPath(character.SimPosition, hull.SimPosition, character.Submarine, nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
else
{
HumanAIController.UnreachableHulls.Add(hull);
continue;
hullSafety = 0;
}
// Each unsafe node reduces the hull safety value.
// Ignore the current hull, because otherwise we couldn't find a path out.
int unsafeNodes = path.Nodes.Count(n => n.CurrentHull != character.CurrentHull && HumanAIController.UnsafeHulls.Contains(n.CurrentHull));
hullSafety /= 1 + unsafeNodes;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
if (!character.Submarine.IsEntityFoundOnThisSub(hull, true))
{
hullSafety /= 10;
}
}
else
{
// TODO: could also target gaps that get us inside?
if (hull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestIsAirlock && hull.LeadsOutside(character))
{
hullSafety = 100;
}
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, hull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (hull.Submarine.TeamID != character.TeamID && hull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestValue || (!isCharacterInside && hullIsAirlock && !bestIsAirlock))
{
bestHull = hull;
bestValue = hullSafety;
bestIsAirlock = hullIsAirlock;
}
}
return bestHull;
else
{
// TODO: could also target gaps that get us inside?
if (potentialHull.IsTaggedAirlock())
{
hullSafety = 100;
hullIsAirlock = true;
}
else if(!bestHullIsAirlock && potentialHull.LeadsOutside(character))
{
hullSafety = 100;
}
// Huge preference for closer targets
float distance = Vector2.DistanceSquared(character.WorldPosition, potentialHull.WorldPosition);
float distanceFactor = MathHelper.Lerp(1, 0.2f, MathUtils.InverseLerp(0, MathUtils.Pow(100000, 2), distance));
hullSafety *= distanceFactor;
// If the target is not inside a friendly submarine, considerably reduce the hull safety.
// Intentionally exclude wrecks from this check
if (potentialHull.Submarine.TeamID != character.TeamID && potentialHull.Submarine.TeamID != CharacterTeamType.FriendlyNPC)
{
hullSafety /= 10;
}
}
if (hullSafety > bestHullValue || (!isCharacterInside && hullIsAirlock && !bestHullIsAirlock))
{
potentialBestHull = potentialHull;
bestHullValue = hullSafety;
bestHullIsAirlock = hullIsAirlock;
}
bestHull = potentialBestHull;
hullSearchIndex++;
if (hullSearchIndex >= hulls.Count)
{
hullSearchIndex = -1;
return HullSearchStatus.Finished;
}
return HullSearchStatus.Running;
}
public override void Reset()
@@ -165,6 +165,7 @@ namespace Barotrauma
requiredCondition = () =>
Leak.Submarine == character.Submarine &&
Leak.linkedTo.Any(e => e is Hull h && character.CurrentHull == h),
endNodeFilter = n => n.Waypoint.CurrentHull != null && Leak.linkedTo.Any(e => e is Hull h && h == n.Waypoint.CurrentHull),
// The Go To objective can be abandoned if the leak is fixed (in which case we don't want to use the dialogue)
SpeakCannotReachCondition = () => !CheckObjectiveSpecific()
},
@@ -471,7 +471,8 @@ namespace Barotrauma
{
if (spawnItemIfNotFound)
{
if (!(MapEntityPrefab.List.FirstOrDefault(me => me is ItemPrefab ip && IdentifiersOrTags.Any(id => id == ip.Identifier || ip.Tags.Contains(id))) is ItemPrefab prefab))
ItemPrefab prefab = FindItemToSpawn();
if (prefab == null)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item with the following identifier(s) or tag(s): {string.Join(", ", IdentifiersOrTags)}, tried to spawn the item but no matching item prefabs were found.", Color.Yellow);
@@ -501,6 +502,33 @@ namespace Barotrauma
}
}
/// <summary>
/// Returns the "best" item to spawn when using <see cref="spawnItemIfNotFound"/> and there's multiple suitable items.
/// Best in this context is the one that's sold at the lowest price in stores (usually the most "basic" item)
/// </summary>
/// <returns></returns>
private ItemPrefab FindItemToSpawn()
{
ItemPrefab bestItem = null;
float lowestCost = float.MaxValue;
foreach (MapEntityPrefab prefab in MapEntityPrefab.List)
{
if (!(prefab is ItemPrefab itemPrefab)) { continue; }
if (IdentifiersOrTags.Any(id => id == prefab.Identifier || prefab.Tags.Contains(id)))
{
float cost = itemPrefab.DefaultPrice != null && itemPrefab.CanBeBought ?
itemPrefab.DefaultPrice.Price :
float.MaxValue;
if (cost < lowestCost || bestItem == null)
{
bestItem = itemPrefab;
lowestCost = cost;
}
}
}
return bestItem;
}
protected override bool CheckObjectiveSpecific()
{
if (IsCompleted) { return true; }
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using static Barotrauma.AIObjectiveFindSafety;
namespace Barotrauma
{
@@ -186,7 +187,9 @@ namespace Barotrauma
}
else
{
safeHull = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(HumanAIController.VisibleHulls);
HullSearchStatus hullSearchStatus = objectiveManager.GetObjective<AIObjectiveFindSafety>().FindBestHull(out Hull potentialSafeHull, HumanAIController.VisibleHulls);
if (hullSearchStatus != HullSearchStatus.Finished) { return; }
safeHull = potentialSafeHull;
findHullTimer = findHullInterval * Rand.Range(0.9f, 1.1f);
}
}
@@ -24,7 +24,7 @@ namespace Barotrauma
public bool IsAiming => wasAiming;
public bool IsAimingMelee => wasAimingMelee;
protected bool Aiming => aiming || aimingMelee;
protected bool Aiming => aiming || aimingMelee || LockFlippingUntil > Timing.TotalTime && character.IsKeyDown(InputType.Aim);
public float ArmLength => upperArmLength + forearmLength;
@@ -275,6 +275,8 @@ namespace Barotrauma
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
public bool IsAboveFloor => GetHeightFromFloor() > -0.1f;
public float LockFlippingUntil;
public void UpdateUseItem(bool allowMovement, Vector2 handWorldPos)
{
useItemTimer = 0.5f;
@@ -380,18 +382,10 @@ namespace Barotrauma
{
//if holding two items that should control the characters' pose, let the item in the right hand do it
bool anotherItemControlsPose = equippedInLefthand && rightHandItem != item && (rightHandItem?.GetComponent<Holdable>()?.ControlPose ?? false);
if (!anotherItemControlsPose)
if (!anotherItemControlsPose && TargetMovement == Vector2.Zero && inWater)
{
var head = GetLimb(LimbType.Head);
if (head != null)
{
head.body.SmoothRotate(itemAngle, force: 30 * head.Mass);
}
if (TargetMovement == Vector2.Zero && inWater)
{
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
torso.body.AngularVelocity -= torso.body.AngularVelocity * 0.1f;
torso.body.ApplyForce(torso.body.LinearVelocity * -0.5f);
}
aiming = true;
}
@@ -164,8 +164,6 @@ namespace Barotrauma
public float LegBendTorque => CurrentGroundedParams.LegBendTorque * RagdollParams.JointScale;
public Vector2 HandMoveOffset => CurrentGroundedParams.HandMoveOffset * RagdollParams.JointScale;
public float LockFlippingUntil;
public override Vector2 AimSourceSimPos
{
get
@@ -841,7 +839,7 @@ namespace Barotrauma
rotation += 360;
}
float targetSpeed = TargetMovement.Length();
if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !character.IsKeyDown(InputType.Aim))
if (targetSpeed > 0.1f && !character.IsRemotelyControlled && !Aiming)
{
if (Anim != Animation.UsingConstruction && !(character.SelectedConstruction?.GetComponent<Controller>()?.ControlCharacterPose ?? false))
{
@@ -153,12 +153,12 @@ namespace Barotrauma
protected ActiveTeamChange currentTeamChange;
const string OriginalTeamIdentifier = "original";
public static void ThrowIfAccessingWalletsInSingleplayer()
private void ThrowIfAccessingWalletsInSingleplayer()
{
#if CLIENT && DEBUG
if (Screen.Selected is TestScreen) { return; }
#endif
if (GameMain.NetworkMember is null || GameMain.IsSingleplayer)
if ((GameMain.NetworkMember is null || GameMain.IsSingleplayer) && IsPlayer)
{
throw new InvalidOperationException($"Tried to access crew wallets in singleplayer. Use {nameof(CampaignMode)}.{nameof(CampaignMode.Bank)} or {nameof(CampaignMode)}.{nameof(CampaignMode.GetWallet)} instead.");
}
@@ -560,18 +560,35 @@ namespace Barotrauma
#if CLIENT
CharacterHealth.SetHealthBarVisibility(value == null);
#elif SERVER
if (value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
{
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign)
#if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
{
mpCampaign.Bank.Give(balance);
switch (settings.LootedMoneyDestination)
{
case LootedMoneyDestination.Wallet when IsPlayer:
Wallet.Give(balance);
break;
default:
mpCampaign.Bank.Give(balance);
break;
}
}
grabbedWallet.Deduct(balance);
GameServer.Log($"{GameServer.CharacterLogName(this)} grabbed {value.Name}'s body and received {grabbedWallet.Balance} mk.", ServerLog.MessageType.Money);
}
#elif CLIENT
if (GameMain.GameSession.Campaign is SinglePlayerCampaign spCampaign)
{
spCampaign.Bank.Give(balance);
}
#endif
grabbedWallet.Deduct(balance);
}
}
}
@@ -1443,7 +1460,7 @@ namespace Barotrauma
foreach (Item item in Inventory.AllItems)
{
if (item?.Prefab.Identifier != "idcard") { continue; }
if (item?.GetComponent<IdCard>() == null) { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
@@ -3954,7 +3971,10 @@ namespace Barotrauma
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
{
// OnDamaged is called only for the limb that is hit.
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
foreach (Limb limb in AnimController.Limbs)
{
limb.ApplyStatusEffects(actionType, deltaTime);
}
}
//OnActive effects are handled by the afflictions themselves
if (actionType != ActionType.OnActive)
@@ -252,12 +252,11 @@ namespace Barotrauma
}
/// <summary>
/// Endocrine boosters can unlock talents outside the user's talent tree. This method is used to specifically get them
/// Returns unlocked talents that aren't part of the character's talent tree (which can be unlocked e.g. with an endocrine booster)
/// </summary>
public IEnumerable<Identifier> GetEndocrineTalents()
public IEnumerable<Identifier> GetUnlockedTalentsOutsideTree()
{
if (!TalentTree.JobTalentTrees.TryGet(Job.Prefab.Identifier, out TalentTree talentTree)) { return Enumerable.Empty<Identifier>(); }
return UnlockedTalents.Where(t => !talentTree.TalentIsInTree(t));
}
@@ -1182,7 +1181,7 @@ namespace Barotrauma
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
if (item.Prefab.Identifier != "idcard" && !item.Tags.Contains("despawncontainer")) { continue; }
if (!item.HasTag("identitycard") && !item.HasTag("despawncontainer")) { continue; }
foreach (var tag in item.Tags.Split(','))
{
var splitTag = tag.Split(":");
@@ -300,6 +300,7 @@ namespace Barotrauma
}
public static AfflictionPrefab InternalDamage => Prefabs["internaldamage"];
public static AfflictionPrefab BiteWounds => Prefabs["bitewounds"];
public static AfflictionPrefab ImpactDamage => Prefabs["blunttrauma"];
public static AfflictionPrefab Bleeding => Prefabs["bleeding"];
public static AfflictionPrefab Burn => Prefabs["burn"];
@@ -124,7 +124,18 @@ namespace Barotrauma
public float PressureKillDelay { get; private set; } = 5.0f;
public float Vitality { get; private set; }
private float vitality;
public float Vitality
{
get
{
return Character.IsDead ? minVitality : vitality;
}
private set
{
vitality = value;
}
}
public float HealthPercentage => MathUtils.Percentage(Vitality, MaxVitality);
@@ -725,6 +736,8 @@ namespace Barotrauma
AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
}
partial void UpdateSkinTint();
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
@@ -788,7 +801,7 @@ namespace Barotrauma
if (!Character.GodMode)
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
@@ -798,23 +811,6 @@ namespace Barotrauma
}
}
private void UpdateSkinTint()
{
FaceTint = DefaultFaceTint;
BodyTint = Color.TransparentBlack;
if (!(Character?.Params?.Health.ApplyAfflictionColors ?? false)) { return; }
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{
var affliction = kvp.Key;
Color faceTint = affliction.GetFaceTint();
if (faceTint.A > FaceTint.A) { FaceTint = faceTint; }
Color bodyTint = affliction.GetBodyTint();
if (bodyTint.A > BodyTint.A) { BodyTint = bodyTint; }
}
}
private void UpdateDamageReductions(float deltaTime)
{
float healthRegen = Character.Params.Health.ConstantHealthRegeneration;
@@ -905,6 +901,7 @@ namespace Barotrauma
if (Unkillable || Character.GodMode) { return; }
var (type, affliction) = GetCauseOfDeath();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
Character.Kill(type, affliction);
#if CLIENT
@@ -105,9 +105,9 @@ namespace Barotrauma
return spawnPointTags;
}
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced)
public JobPrefab GetJobPrefab(Rand.RandSync randSync = Rand.RandSync.Unsynced, Func<JobPrefab, bool> predicate = null)
{
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync);
return Job != null && Job != "any" ? JobPrefab.Get(Job) : JobPrefab.Random(randSync, predicate);
}
public void InitializeCharacter(Character npc, ISpatialEntity positionToStayIn = null)
@@ -209,11 +209,8 @@ namespace Barotrauma
}
}
if (item.Prefab.Identifier == "idcard")
{
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(spawnPoint, character);
}
IdCard idCardComponent = item.GetComponent<IdCard>();
idCardComponent?.Initialize(spawnPoint, character);
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
@@ -293,6 +293,6 @@ namespace Barotrauma
//ClothingElement = element.GetChildElement("PortraitClothing");
}
public static JobPrefab Random(Rand.RandSync sync) => Prefabs.GetRandom(p => !p.HiddenJob, sync);
public static JobPrefab Random(Rand.RandSync sync, Func<JobPrefab, bool> predicate = null) => Prefabs.GetRandom(p => !p.HiddenJob && (predicate == null || predicate(p)), sync);
}
}
@@ -331,7 +331,7 @@ namespace Barotrauma
#if CLIENT
if (isSevered)
{
damageOverlayStrength = 100.0f;
damageOverlayStrength = 1.0f;
}
#endif
}
@@ -597,18 +597,7 @@ namespace Barotrauma
dir = Direction.Right;
body = new PhysicsBody(limbParams);
type = limbParams.Type;
if (limbParams.IgnoreCollisions)
{
body.CollisionCategories = Category.None;
body.CollidesWith = Category.None;
IgnoreCollisions = true;
}
else
{
//limbs don't collide with each other
body.CollisionCategories = Physics.CollisionCharacter;
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
}
IgnoreCollisions = limbParams.IgnoreCollisions;
body.UserData = this;
pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale))
{
@@ -646,10 +635,9 @@ namespace Barotrauma
}
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
}
if (!character.VariantOf.IsEmpty)
if (character is { VariantOf: { IsEmpty: false } })
{
var attackElement = CharacterPrefab.Prefabs.TryGet(character.VariantOf, out var basePrefab)
? basePrefab.ConfigElement.GetChildElement("attack") : null;
var attackElement = character.Params.VariantFile.Root.GetChildElement("attack");
if (attackElement != null)
{
attack.DamageMultiplier = attackElement.GetAttributeFloat("damagemultiplier", 1f);