This commit is contained in:
Evil Factory
2022-06-15 13:26:49 -03:00
410 changed files with 11140 additions and 5815 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);
}
@@ -34,14 +34,18 @@ namespace Barotrauma
public float SoundRange
{
get { return soundRange; }
set
set
{
if (float.IsNaN(value))
{
DebugConsole.ThrowError("Attempted to set the SoundRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
soundRange = MathHelper.Clamp(value, MinSoundRange, MaxSoundRange);
if (soundRange > 0.0f && !Static && FadeOutTime > 0.0f)
{
NeedsUpdate = true;
}
}
}
@@ -55,7 +59,11 @@ namespace Barotrauma
DebugConsole.ThrowError("Attempted to set the SightRange of an AITarget to NaN.\n" + Environment.StackTrace.CleanupStackTrace());
return;
}
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
sightRange = MathHelper.Clamp(value, MinSightRange, MaxSightRange);
if (sightRange > 0 && !Static && FadeOutTime > 0.0f)
{
NeedsUpdate = true;
}
}
}
@@ -99,13 +107,33 @@ namespace Barotrauma
/// </summary>
public bool InDetectable
{
get => inDetectable || (SoundRange <= 0 && SightRange <= 0);
set => inDetectable = value;
get
{
return inDetectable || (SoundRange <= 0 && SightRange <= 0);
}
set
{
inDetectable = value;
if (inDetectable)
{
NeedsUpdate = true;
}
}
}
public float MinSoundRange, MinSightRange;
public float MaxSoundRange = 100000, MaxSightRange = 100000;
/// <summary>
/// Does the AI target do something that requires Update() to be called (e.g. static targets don't need to be updated)
/// </summary>
public bool NeedsUpdate
{
get;
private set;
} = true;
public TargetType Type { get; private set; }
public enum TargetType
@@ -190,14 +218,22 @@ namespace Barotrauma
if (!Static && FadeOutTime > 0)
{
// The aitarget goes silent/invisible if the components don't keep it active
if (!StaticSight && SightRange > 0)
if (!StaticSight && sightRange > 0)
{
DecreaseSightRange(deltaTime);
}
if (!StaticSound && SoundRange > 0)
if (!StaticSound && soundRange > 0)
{
DecreaseSoundRange(deltaTime);
}
if (sightRange <= 0 && soundRange <= 0)
{
NeedsUpdate = false;
}
}
else
{
NeedsUpdate = false;
}
}
@@ -275,7 +275,11 @@ namespace Barotrauma
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "chooserandom":
LoadSubElement(subElement.Elements().ToArray().GetRandom(random));
var subElements = subElement.Elements();
if (subElements.Any())
{
LoadSubElement(subElements.ToArray().GetRandom(random));
}
break;
default:
LoadSubElement(subElement);
@@ -1055,6 +1059,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 +1434,41 @@ 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)
{
Vector2 targetVelocity = Vector2.Zero;
Submarine targetSub = SelectedAiTarget.Entity.Submarine;
if (targetSub != null)
{
targetVelocity = targetSub.Velocity;
}
else if (targetCharacter != null)
{
targetVelocity = targetCharacter.AnimController.Collider.LinearVelocity;
}
else if (SelectedAiTarget.Entity is Item i && i.body != null)
{
targetVelocity = i.body.LinearVelocity;
}
float mySpeed = Character.AnimController.Collider.LinearVelocity.LengthSquared();
float targetSpeed = targetVelocity.LengthSquared();
if (mySpeed < 0.1f || mySpeed > targetSpeed)
{
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 +2000,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 +3517,7 @@ namespace Barotrauma
{
observeTimer = targetParams.Timer * Rand.Range(0.75f, 1.25f);
}
reachTimer = 0;
}
protected override void OnStateChanged(AIState from, AIState to)
@@ -3496,6 +3538,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.");
}
}
@@ -837,8 +866,8 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer();
if (rootContainer?.GetComponent<Fabricator>() != null || rootContainer?.GetComponent<Deconstructor>() != null) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
if (isRestrictionsDefined)
@@ -853,7 +882,12 @@ namespace Barotrauma
}
else
{
return isPreferencesDefined ? 0 : 1;
if (isPreferencesDefined)
{
// Use any valid locker as a fall back container.
return container.Item.HasTag("locker") ? 0.5f : 0;
}
return 1;
}
}
}
@@ -1149,7 +1183,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 +1390,10 @@ namespace Barotrauma
Character FindInstigator()
{
if (Character.IsInstigator)
{
return Character;
}
if (attacker.IsInstigator)
{
return attacker;
@@ -1545,7 +1583,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 +1927,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,20 +1935,32 @@ 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));
}
float dangerousItemsFactor = 1f;
foreach (Item item in Item.ItemList)
foreach (Item item in Item.DangerousItems)
{
if (item.CurrentHull != hull) { continue; }
if (item.Prefab != null && item.Prefab.IsDangerous)
{
if (item.CurrentHull == hull)
{
dangerousItemsFactor = 0;
break;
}
}
float safety = oxygenFactor * waterFactor * fireFactor * enemyFactor * dangerousItemsFactor;
@@ -245,7 +245,7 @@ namespace Barotrauma
{
get
{
if (IgnoreAtOutpost && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if (IgnoreAtOutpost && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
@@ -2,7 +2,6 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System;
namespace Barotrauma
{
@@ -48,6 +47,9 @@ namespace Barotrauma
protected override bool Filter(Item target)
{
System.Diagnostics.Debug.Assert(target.GetComponent<Pickable>() is { } pickable && !pickable.IsAttached, "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking pickable, non-attached items.");
System.Diagnostics.Debug.Assert(target.Prefab.PreferredContainers.Any(), "Invalid target in AIObjectiveCleanUpItems - the the objective should only be checking items that have preferred containers defined.");
// If the target was selected as a valid target, we'll have to accept it so that the objective can be completed.
// The validity changes when a character picks the item up.
if (!IsValidTarget(target, character, checkInventory: true)) { return Objectives.ContainsKey(target) && IsItemInsideValidSubmarine(target, character); }
@@ -57,7 +59,7 @@ namespace Barotrauma
return true;
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override IEnumerable<Item> GetList() => Item.CleanableItems;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveCleanupItem(item, character, objectiveManager, priorityModifier: PriorityModifier)
@@ -102,9 +104,6 @@ namespace Barotrauma
}
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; }
var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; }
if (pickable is Holdable h && h.Attachable && h.Attached) { return false; }
var wire = item.GetComponent<Wire>();
if (wire != null)
{
@@ -113,15 +112,11 @@ 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;
}
}
if (item.Prefab.PreferredContainers.None())
{
return false;
}
if (!checkInventory)
{
return true;
@@ -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()
@@ -48,16 +48,29 @@ namespace Barotrauma
}
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));
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float reduction = isPriority ? 1 : 2;
float max = AIObjectiveManager.LowestOrderPriority - reduction;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, max, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
{
// 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));
}
}
return Priority;
}
@@ -165,6 +178,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()
},
@@ -201,7 +215,7 @@ namespace Barotrauma
// This is an approximation, because we don't know the exact reach until the pose is taken.
// And even then the actual range depends on the direction we are aiming to.
// Found out that without any multiplier the value (209) is often too short.
return repairTool.Range + armLength * 1.3f;
return repairTool.Range + armLength * 2;
}
}
}
@@ -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; }
@@ -185,6 +185,11 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
}
}
else
{
@@ -290,12 +295,25 @@ namespace Barotrauma
{
PathSteering.SteeringSeek(character.GetRelativeSimPosition(currentTarget), weight: 1, nodeFilter: node => node.Waypoint.CurrentHull != null);
}
else
{
PathSteering.ResetPath();
PathSteering.Reset();
}
}
}
public void Wander(float deltaTime)
{
if (character.IsClimbing) { return; }
if (character.IsClimbing)
{
if (character.AnimController.GetHeightFromFloor() < 0.1f)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedConstruction = null;
}
return;
}
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
{
@@ -470,7 +488,7 @@ namespace Barotrauma
if (hull != null)
{
itemsToClean.Clear();
foreach (Item item in Item.ItemList)
foreach (Item item in Item.CleanableItems)
{
if (item.CurrentHull != hull) { continue; }
if (AIObjectiveCleanupItems.IsValidTarget(item, character, checkInventory: true, allowUnloading: false) && !ignoredItems.Contains(item))
@@ -39,12 +39,21 @@ namespace Barotrauma
{
TargetContainers.Add(targetContainer);
}
else
{
foreach (Item item in Item.ItemList)
{
if (!OrderPrefab.TargetItemsMatchItem(TargetContainerTags, item)) { continue; }
TargetContainers.Add(item);
}
}
TargetCondition = option == "turretammo" ? ItemCondition.Empty : ItemCondition.Full;
}
protected override bool Filter(Item target)
{
if (!IsValidTarget(target, character, TargetContainerTags, TargetCondition)) { return false; }
//don't pass TargetContainerTags to the method (no need to filter by tags anymore, it's already done when populating TargetContainers)
if (!IsValidTarget(target, character, null, TargetCondition)) { return false; }
if (target.CurrentHull == null || target.CurrentHull.FireSources.Count > 0) { return false; }
if (Character.CharacterList.Any(c => c.CurrentHull == target.CurrentHull && !HumanAIController.IsFriendly(c) && HumanAIController.IsActive(c))) { return false; }
return true;
@@ -52,8 +61,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, ImmutableArray<Identifier>? targetContainerTags = null, ItemCondition? targetCondition = null)
{
if (item == null) { return false; }
if (item.Removed) { return false; }
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 (container.Inventory == null) { return false; }
@@ -88,7 +96,7 @@ namespace Barotrauma
}
}
protected override IEnumerable<Item> GetList() => TargetContainers.Any() ? TargetContainers : Item.ItemList;
protected override IEnumerable<Item> GetList() => TargetContainers;
protected override AIObjective ObjectiveConstructor(Item target)
=> new AIObjectiveLoadItem(target, TargetContainerTags, TargetCondition, Option, character, objectiveManager, PriorityModifier);
@@ -151,7 +151,7 @@ namespace Barotrauma
}
var order = new Order(orderPrefab, autonomousObjective.Option, item ?? character.CurrentHull as Entity, orderPrefab.GetTargetItemComponent(item), orderGiver: character);
if (order == null) { continue; }
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
if ((order.IgnoreAtOutpost || autonomousObjective.IgnoreAtOutpost) && Level.IsLoadedFriendlyOutpost && character.TeamID != CharacterTeamType.FriendlyNPC)
{
if (Submarine.MainSub != null && Submarine.MainSub.DockedTo.None(s => s.TeamID != CharacterTeamType.FriendlyNPC && s.TeamID != character.TeamID))
{
@@ -13,7 +13,7 @@ namespace Barotrauma
public override bool KeepDivingGearOn => true;
public override bool AllowAutomaticItemUnequipping => true;
private IEnumerable<Pump> pumpList;
private List<Pump> pumpList;
public AIObjectivePumpWater(Character character, AIObjectiveManager objectiveManager, Identifier option, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier, option) { }
@@ -26,13 +26,9 @@ namespace Barotrauma
protected override bool Filter(Pump pump)
{
if (pump == null) { return false; }
if (pump?.Item == null || pump.Item.Removed) { return false; }
if (pump.Item.IgnoreByAI(character)) { return false; }
if (!pump.Item.IsInteractable(character)) { return false; }
if (pump.Item.HasTag("ballast")) { return false; }
if (pump.Item.Submarine == null) { return false; }
if (pump.Item.CurrentHull == null) { return false; }
if (pump.Item.Submarine.TeamID != character.TeamID) { return false; }
if (pump.IsAutoControlled) { return false; }
if (pump.Item.ConditionPercentage <= 0) { return false; }
if (pump.Item.CurrentHull.FireSources.Count > 0) { return false; }
@@ -50,7 +46,16 @@ namespace Barotrauma
if (pumpList == null)
{
if (character == null || character.Submarine == null) { return Array.Empty<Pump>(); }
pumpList = character.Submarine.GetItems(true).Select(i => i.GetComponent<Pump>()).Where(p => p != null);
pumpList = new List<Pump>();
foreach (Item item in character.Submarine.GetItems(true))
{
var pump = item.GetComponent<Pump>();
if (pump == null || pump.Item.Submarine == null || pump.Item.CurrentHull == null) { continue; }
if (pump.Item.Submarine.TeamID != character.TeamID) { continue; }
if (pump.Item.HasTag("ballast")) { continue; }
pumpList.Add(pump);
}
}
return pumpList;
}
@@ -238,7 +238,7 @@ namespace Barotrauma
};
if (repairTool != null)
{
objective.CloseEnough = repairTool.Range * 0.75f;
objective.CloseEnough = AIObjectiveFixLeak.CalculateReach(repairTool, character);
}
return objective;
},
@@ -136,7 +136,7 @@ namespace Barotrauma
return MathHelper.Lerp(0, 100, MathHelper.Clamp(damagePriority * successFactor, 0, 1));
}
protected override IEnumerable<Item> GetList() => Item.ItemList;
protected override IEnumerable<Item> GetList() => Item.RepairableItems;
protected override AIObjective ObjectiveConstructor(Item item)
=> new AIObjectiveRepairItem(character, item, objectiveManager, priorityModifier: PriorityModifier, isPriority: item == PrioritizedItem);
@@ -156,6 +156,9 @@ namespace Barotrauma
if (character.IsOnPlayerTeam && item.Submarine.Info.IsOutpost) { return false; }
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { return false; }
if (item.Repairables.None()) { return false; }
System.Diagnostics.Debug.Assert(item.Repairables.Any(), "Invalid target in AIObjectiveRepairItems - the objective should only be checking items that have a Repairable component (Item.RepairableItems)");
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);
}
}
@@ -1,8 +1,6 @@
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using FarseerPhysics;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -90,6 +88,10 @@ namespace Barotrauma
{
steering = Vector2.Normalize(steering) * Math.Abs(speed);
}
if (host is AIController aiController && aiController?.Character.CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier()) != null)
{
steering = -steering;
}
host.Steering = steering;
}
@@ -85,7 +85,7 @@ namespace Barotrauma
public readonly Identifier[] ForbiddenAmmunition;
public static WreckAIConfig GetRandom() => Prefabs.GetRandom(Rand.RandSync.ServerAndClient);
public static WreckAIConfig GetRandom() => Prefabs.OrderBy(p => p.UintIdentifier).GetRandom(Rand.RandSync.ServerAndClient);
protected override Identifier DetermineIdentifier(XElement element)
{
@@ -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;
}
@@ -22,8 +22,7 @@ namespace Barotrauma
{
if (_ragdollParams == null)
{
#warning TODO: this is kinda janky, this should probably be done better
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.VariantOf.IfEmpty(character.SpeciesName));
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
if (!character.VariantOf.IsEmpty)
{
_ragdollParams.ApplyVariantScale(character.Params.VariantFile);
@@ -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))
{
@@ -75,7 +75,7 @@ namespace Barotrauma
}
}
public bool HasMultipleLimbsOfSameType => limbs == null ? false : Limbs.Length > limbDictionary.Count;
public bool HasMultipleLimbsOfSameType => limbs != null && limbs.Length > limbDictionary.Count;
private bool frozen;
public bool Frozen
@@ -228,9 +228,13 @@ namespace Barotrauma
{
mainLimb = Limbs.FirstOrDefault(l => IsValid(l));
}
if (mainLimb == null)
{
DebugConsole.ThrowError("Couldn't find a valid main limb. The limb can't be hidden nor be set to ignore collisions!");
mainLimb = Limbs.FirstOrDefault();
}
}
bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden;
static bool IsValid(Limb limb) => limb != null && !limb.IsSevered && !limb.IgnoreCollisions && !limb.Hidden;
return mainLimb;
}
}
@@ -1858,36 +1862,30 @@ namespace Barotrauma
}
/// <summary>
/// Note that if there are multiple limbs of the same type, only the first of them is found in the dictionary.
/// Note that if there are multiple limbs of the same type, only the first (valid) limb is returned.
/// </summary>
public Limb GetLimb(LimbType limbType, bool excludeSevered = true)
{
Limb limb = null;
if (HasMultipleLimbsOfSameType)
if (limbDictionary.TryGetValue(limbType, out Limb limb))
{
for (int i = 0; i < 10; i++)
if (excludeSevered && limb.IsSevered)
{
limbDictionary.TryGetValue(limbType, out limb);
if (limb == null)
limb = null;
}
}
if (limb == null && HasMultipleLimbsOfSameType)
{
// Didn't find a (valid) limb of the matching type. If there's multiple limbs of the same type, check the other limbs.
foreach (var l in limbs)
{
if (l.type != limbType) { continue; }
if (!excludeSevered || !l.IsSevered)
{
// No limbs found
break;
}
if (!excludeSevered || !limb.IsSevered)
{
// Found a valid limb
limb = l;
break;
}
}
}
else
{
limbDictionary.TryGetValue(limbType, out limb);
}
if (excludeSevered && limb != null && limb.IsSevered)
{
limb = null;
}
return limb;
}
@@ -120,7 +120,29 @@ namespace Barotrauma
protected Key[] keys;
public HumanPrefab HumanPrefab;
private HumanPrefab humanPrefab;
public HumanPrefab HumanPrefab
{
get { return humanPrefab; }
set
{
if (humanPrefab == value) { return; }
humanPrefab = value;
if (humanPrefab != null)
{
HumanPrefabHealthMultiplier = humanPrefab.HealthMultiplier;
if (GameMain.NetworkMember != null)
{
HumanPrefabHealthMultiplier *= humanPrefab.HealthMultiplierInMultiplayer;
}
}
else
{
HumanPrefabHealthMultiplier = 1.0f;
}
}
}
private CharacterTeamType teamID;
public CharacterTeamType TeamID
@@ -156,12 +178,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.");
}
@@ -563,18 +585,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);
}
}
}
@@ -1181,7 +1220,7 @@ namespace Barotrauma
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
}
if (Params.Husk && speciesName != "husk")
if (Params.Husk && speciesName != "husk" && Prefab.VariantOf != "husk")
{
// Get the non husked name and find the ragdoll with it
var matchingAffliction = AfflictionPrefab.List
@@ -1381,7 +1420,7 @@ namespace Barotrauma
if (inputType == InputType.Up || inputType == InputType.Down ||
inputType == InputType.Left || inputType == InputType.Right)
{
var invertControls = CharacterHealth.GetAffliction("invertcontrols");
var invertControls = CharacterHealth.GetAfflictionOfType("invertcontrols".ToIdentifier());
if (invertControls != null)
{
switch (inputType)
@@ -1451,7 +1490,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);
@@ -1643,14 +1682,9 @@ namespace Barotrauma
}
/// <summary>
/// Can be used to modify a character's health for runtime session. Change with AddHealthMultiplier
/// Health multiplier of the human prefab this character is an instance of (if any)
/// </summary>
public float StaticHealthMultiplier { get; private set; } = 1;
public void AddStaticHealthMultiplier(float newMultiplier)
{
StaticHealthMultiplier *= newMultiplier;
}
public float HumanPrefabHealthMultiplier { get; private set; } = 1;
/// <summary>
/// Speed reduction from the current limb specific damage. Min 0, max 1.
@@ -3976,7 +4010,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)
@@ -4826,21 +4863,21 @@ namespace Barotrauma
}
}
private readonly List<AbilityFlags> abilityFlags = new List<AbilityFlags>();
private AbilityFlags abilityFlags;
public void AddAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Add(abilityFlag);
abilityFlags |= abilityFlag;
}
public void RemoveAbilityFlag(AbilityFlags abilityFlag)
{
abilityFlags.Remove(abilityFlag);
abilityFlags &= ~abilityFlag;
}
public bool HasAbilityFlag(AbilityFlags abilityFlag)
{
return abilityFlags.Contains(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
}
private readonly Dictionary<Identifier, float> abilityResistances = new Dictionary<Identifier, float>();
@@ -94,11 +94,67 @@ namespace Barotrauma
public Vector2 SheetIndex => Preset.SheetIndex;
public ContentXElement HairElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairIndex);
public ContentXElement HairWithHatElement => CharacterInfo.Hairs?.ElementAtOrDefault(HairWithHatIndex);
public ContentXElement BeardElement => CharacterInfo.Beards?.ElementAtOrDefault(BeardIndex);
public ContentXElement MoustacheElement => CharacterInfo.Moustaches?.ElementAtOrDefault(MoustacheIndex);
public ContentXElement FaceAttachment => CharacterInfo.FaceAttachments?.ElementAtOrDefault(FaceAttachmentIndex);
public ContentXElement HairElement
{
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (hairIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex);
}
}
public ContentXElement HairWithHatElement
{
get
{
if (CharacterInfo.Hairs == null) { return null; }
if (HairWithHatIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex);
}
}
public ContentXElement BeardElement
{
get
{
if (CharacterInfo.Beards == null) { return null; }
if (BeardIndex >= CharacterInfo.Beards.Count)
{
DebugConsole.AddWarning($"Beard index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {BeardIndex})");
}
return CharacterInfo.Beards.ElementAtOrDefault(BeardIndex);
}
}
public ContentXElement MoustacheElement
{
get
{
if (CharacterInfo.Moustaches == null) { return null; }
if (MoustacheIndex >= CharacterInfo.Moustaches.Count)
{
DebugConsole.AddWarning($"Moustache index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {MoustacheIndex})");
}
return CharacterInfo.Moustaches.ElementAtOrDefault(MoustacheIndex);
}
}
public ContentXElement FaceAttachment
{
get
{
if (CharacterInfo.FaceAttachments == null) { return null; }
if (FaceAttachmentIndex >= CharacterInfo.FaceAttachments.Count)
{
DebugConsole.AddWarning($"Face attachment index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {FaceAttachmentIndex})");
}
return CharacterInfo.FaceAttachments.ElementAtOrDefault(FaceAttachmentIndex);
}
}
public HeadInfo(CharacterInfo characterInfo, HeadPreset headPreset, int hairIndex = 0, int beardIndex = 0, int moustacheIndex = 0, int faceAttachmentIndex = 0)
{
@@ -130,6 +186,10 @@ namespace Barotrauma
head = value;
HeadSprite = null;
AttachmentSprites = null;
hairs = null;
beards = null;
moustaches = null;
faceAttachments = null;
}
}
}
@@ -252,12 +312,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));
}
@@ -297,7 +356,10 @@ namespace Barotrauma
}
}
public bool OmitJobInPortraitClothing;
/// <summary>
/// Can be used to disable displaying the job in any info panels
/// </summary>
public bool OmitJobInMenus;
private Sprite portrait;
public Sprite Portrait
@@ -375,7 +437,7 @@ namespace Barotrauma
{
if (attachmentSprites == null)
{
LoadAttachmentSprites(OmitJobInPortraitClothing);
LoadAttachmentSprites();
}
return attachmentSprites;
}
@@ -844,7 +906,14 @@ namespace Barotrauma
public void RecreateHead(ImmutableHashSet<Identifier> tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
{
HeadPreset headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.SetEquals(tags));
if (headPreset == null) { headPreset = Prefab.Heads.GetRandomUnsynced(); }
if (headPreset == null)
{
if (tags.Count == 1)
{
headPreset = Prefab.Heads.FirstOrDefault(h => h.TagSet.Contains(tags.First()));
}
headPreset ??= Prefab.Heads.GetRandomUnsynced();
}
head = new HeadInfo(this, headPreset, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ReloadHeadAttachments();
}
@@ -1026,7 +1095,7 @@ namespace Barotrauma
private static IEnumerable<float> GetWeights(IEnumerable<ContentXElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
partial void LoadAttachmentSprites(bool omitJob);
partial void LoadAttachmentSprites();
private int CalculateSalary()
{
@@ -1182,7 +1251,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(":");
@@ -35,6 +35,7 @@ namespace Barotrauma
if (newValue > _strength)
{
PendingAdditionStrength = Prefab.GrainBurst;
Duration = Prefab.Duration;
}
_strength = newValue;
}
@@ -60,6 +61,8 @@ namespace Barotrauma
public double AppliedAsSuccessfulTreatmentTime, AppliedAsFailedTreatmentTime;
public float Duration;
/// <summary>
/// Which character gave this affliction
/// </summary>
@@ -75,6 +78,8 @@ namespace Barotrauma
_strength = strength;
Identifier = prefab.Identifier;
Duration = prefab.Duration;
foreach (var periodicEffect in prefab.PeriodicEffects)
{
PeriodicEffectTimers[periodicEffect] = Rand.Range(periodicEffect.MinInterval, periodicEffect.MaxInterval);
@@ -315,8 +320,7 @@ namespace Barotrauma
public bool HasFlag(AbilityFlags flagType)
{
if (!(GetViableEffect() is AfflictionPrefab.Effect currentEffect)) { return false; }
return currentEffect.AfflictionAbilityFlags.Contains(flagType);
return currentEffect.AfflictionAbilityFlags.HasFlag(flagType);
}
private AfflictionPrefab.Effect GetViableEffect()
@@ -235,7 +235,7 @@ namespace Barotrauma
public Identifier[] BlockTransformation { get; private set; }
public readonly Dictionary<StatTypes, (float minValue, float maxValue)> AfflictionStatValues = new Dictionary<StatTypes, (float minValue, float maxValue)>();
public readonly HashSet<AbilityFlags> AfflictionAbilityFlags = new HashSet<AbilityFlags>();
public AbilityFlags AfflictionAbilityFlags;
//statuseffects applied on the character when the affliction is active
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
@@ -265,7 +265,7 @@ namespace Barotrauma
break;
case "abilityflag":
var flagType = CharacterAbilityGroup.ParseFlagType(subElement.GetAttributeString("flagtype", ""), parentDebugName);
AfflictionAbilityFlags.Add(flagType);
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.");
@@ -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"];
@@ -353,6 +354,11 @@ namespace Barotrauma
//how strong the affliction needs to be before bots attempt to treat it
public readonly float TreatmentThreshold = 5.0f;
/// <summary>
/// The affliction is automatically removed after this time. 0 = unlimited
/// </summary>
public readonly float Duration;
//how much karma changes when a player applies this affliction to someone (per strength of the affliction)
public float KarmaChangeOnApplied;
@@ -406,8 +412,10 @@ namespace Barotrauma
!IsBuff &&
AfflictionType != "geneticmaterialbuff" &&
AfflictionType != "geneticmaterialdebuff");
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier).ToLowerInvariant(), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost).ToLowerInvariant(), 0);
HealCostMultiplier = element.GetAttributeFloat(nameof(HealCostMultiplier), 1f);
BaseHealCost = element.GetAttributeInt(nameof(BaseHealCost), 0);
Duration = element.GetAttributeFloat(nameof(Duration), 0.0f);
if (element.GetAttribute("nameidentifier") != null)
{
@@ -1,6 +1,9 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
@@ -125,7 +128,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);
@@ -138,7 +152,7 @@ namespace Barotrauma
{
max += Character.Info.Job.Prefab.VitalityModifier;
}
max *= Character.StaticHealthMultiplier;
max *= Character.HumanPrefabHealthMultiplier;
max *= 1f + Character.GetStatValue(StatTypes.MaximumHealthMultiplier);
return max * Character.HealthMultiplier;
}
@@ -701,6 +715,7 @@ namespace Barotrauma
newStrength = Math.Min(existingAffliction.Prefab.MaxStrength, newStrength);
if (existingAffliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
existingAffliction.Strength = newStrength;
existingAffliction.Duration = existingAffliction.Prefab.Duration;
if (newAffliction.Source != null) { existingAffliction.Source = newAffliction.Source; }
CalculateVitality();
if (Vitality <= MinVitality)
@@ -737,6 +752,8 @@ namespace Barotrauma
AddLimbAffliction(limbHealth: null, newAffliction, allowStacking);
}
partial void UpdateSkinTint();
partial void UpdateLimbAfflictionOverlays();
public void Update(float deltaTime)
@@ -758,6 +775,15 @@ namespace Barotrauma
if (!irremovableAfflictions.Contains(affliction)) { afflictionsToRemove.Add(affliction); }
continue;
}
if (affliction.Prefab.Duration > 0.0f)
{
affliction.Duration -= deltaTime;
if (affliction.Duration <= 0.0f)
{
afflictionsToRemove.Add(affliction);
continue;
}
}
afflictionsToUpdate.Add(kvp);
}
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictionsToUpdate)
@@ -800,7 +826,7 @@ namespace Barotrauma
if (!Character.GodMode)
{
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
UpdateSkinTint();
CalculateVitality();
if (Vitality <= MinVitality)
@@ -810,23 +836,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;
@@ -917,6 +926,7 @@ namespace Barotrauma
if (Unkillable || Character.GodMode) { return; }
var (type, affliction) = GetCauseOfDeath();
UpdateLimbAfflictionOverlays();
UpdateSkinTint();
Character.Kill(type, affliction);
#if CLIENT
@@ -105,19 +105,13 @@ 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)
{
npc.AddStaticHealthMultiplier(HealthMultiplier);
if (GameMain.NetworkMember != null)
{
npc.AddStaticHealthMultiplier(HealthMultiplierInMultiplayer);
}
var humanAI = npc.AIController as HumanAIController;
if (humanAI != 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>())
{
@@ -79,7 +79,6 @@ namespace Barotrauma
/// </summary>
public static IReadOnlyDictionary<Identifier, float> ItemRepairPriorities => _itemRepairPriorities;
public static ContentXElement NoJobElement;
public static JobPrefab Get(string identifier)
{
if (Prefabs.ContainsKey(identifier))
@@ -213,7 +212,7 @@ namespace Barotrauma
public SkillPrefab PrimarySkill => Skills?.FirstOrDefault(s => s.IsPrimarySkill);
public ContentXElement Element { get; private set; }
public ContentXElement ClothingElement { get; private set; }
public int Variants { get; private set; }
public JobPrefab(ContentXElement element, JobsFile file) : base(file, element.GetAttributeIdentifier("identifier", ""))
@@ -288,11 +287,8 @@ namespace Barotrauma
Variants = variant;
Skills.Sort((x,y) => y.LevelRange.Start.CompareTo(x.LevelRange.Start));
// Disabled on purpose, TODO: remove all references?
//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
}
@@ -352,7 +352,7 @@ namespace Barotrauma
public Vector2 Position
{
get { return ConvertUnits.ToDisplayUnits(body.SimPosition); }
get { return ConvertUnits.ToDisplayUnits(body?.SimPosition ?? Vector2.Zero); }
}
public Vector2 SimPosition
@@ -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);
@@ -574,7 +574,7 @@ namespace Barotrauma
public float AggressionGreed { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "If the health drops below this threshold, the character flees. In percentages."), Editable(minValue: 0f, maxValue: 100f)]
public float FleeHealthThreshold { get; private set; }
public float FleeHealthThreshold { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the character attack when provoked? When enabled, overrides the predefined targeting state with Attack and increases the priority of it."), Editable()]
public bool AttackWhenProvoked { get; private set; }
@@ -121,7 +121,7 @@ namespace Barotrauma
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
public static T GetDefaultRagdollParams<T>(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
public static T GetDefaultRagdollParams<T>(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName);
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
@@ -138,61 +138,53 @@ namespace Barotrauma
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
{
return (T)ragdoll;
}
string selectedFile = null;
void tryFolderForSpecies(Identifier species, out string err)
Identifier ragdollSpecies = speciesName;
if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab))
{
err = null;
string folder = GetFolder(species);
if (!prefab.VariantOf.IsEmpty)
{
ragdollSpecies = prefab.VariantOf;
}
string error = null;
string folder = GetFolder(ragdollSpecies);
if (!Directory.Exists(folder))
{
err = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
return;
}
string[] files = Directory.GetFiles(folder);
if (files.None())
{
err = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(species);
error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
string[] files = Directory.GetFiles(folder);
if (files.None())
{
err = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
selectedFile = GetDefaultFile(species);
error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
}
}
if (error != null)
{
DebugConsole.ThrowError(error);
}
}
tryFolderForSpecies(speciesName, out var error);
Identifier parentSpeciesName = CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab)
? prefab.VariantOf
: Identifier.Empty;
if (!error.IsNullOrEmpty() && !parentSpeciesName.IsEmpty)
{
tryFolderForSpecies(parentSpeciesName, out error);
}
if (!error.IsNullOrEmpty())
{
DebugConsole.ThrowError(error);
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
@@ -200,7 +192,7 @@ namespace Barotrauma
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T r = new T();
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies))
{
if (!ragdolls.ContainsKey(r.Name))
{
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
abstract class AbilityConditionDataless : AbilityCondition
{
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -19,10 +16,9 @@ namespace Barotrauma.Abilities
protected override void ApplyEffect()
{
IEnumerable<Character> chosenCharacters = Character.GetFriendlyCrew(Character).Where(c => allowSelf || c != Character);
foreach (Character character in chosenCharacters)
foreach (Character character in Character.GetFriendlyCrew(Character))
{
if (!allowSelf && character == Character) { continue; }
if (maxDistance < float.MaxValue)
{
if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; }
@@ -1,7 +1,4 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToAttacker : CharacterAbilityApplyStatusEffects
{
@@ -1,6 +1,4 @@
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityApplyStatusEffectsToLastOrderedCharacter : CharacterAbilityApplyStatusEffects
{
@@ -1,6 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupEffect : CharacterAbilityGroup
{
@@ -30,7 +24,14 @@ namespace Barotrauma.Abilities
private bool IsApplicable(AbilityObject abilityObject)
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition(abilityObject));
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition(abilityObject))
{
return false;
}
}
return true;
}
}
}
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class CharacterAbilityGroupInterval : CharacterAbilityGroup
{
@@ -49,7 +43,14 @@ namespace Barotrauma.Abilities
private bool IsApplicable()
{
if (timesTriggered >= maxTriggerCount) { return false; }
return abilityConditions.All(c => c.MatchesCondition());
foreach (var abilityCondition in abilityConditions)
{
if (!abilityCondition.MatchesCondition())
{
return false;
}
}
return true;
}
}
}
@@ -1,8 +1,6 @@
using System;
using Barotrauma.Abilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
namespace Barotrauma
{
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "ballastflorabehavior";
protected override bool MatchesPlural(Identifier identifier) => identifier == "ballastflorabehaviors";
protected override PrefabCollection<BallastFloraPrefab> prefabs => BallastFloraPrefab.Prefabs;
protected override PrefabCollection<BallastFloraPrefab> Prefabs => BallastFloraPrefab.Prefabs;
protected override BallastFloraPrefab CreatePrefab(ContentXElement element)
{
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "cave";
protected override bool MatchesPlural(Identifier identifier) => identifier == "cavegenerationparameters";
protected override PrefabCollection<CaveGenerationParams> prefabs => CaveGenerationParams.CaveParams;
protected override PrefabCollection<CaveGenerationParams> Prefabs => CaveGenerationParams.CaveParams;
protected override CaveGenerationParams CreatePrefab(ContentXElement element)
{
return new CaveGenerationParams(element, this);
@@ -77,7 +77,7 @@ namespace Barotrauma
{
HashSet<string> texturePaths = new HashSet<string>
{
ragdollParams.Texture
ContentPath.FromRaw(CharacterPrefab.Prefabs[speciesName].ContentPackage, ragdollParams.Texture).Value
};
foreach (RagdollParams.LimbParams limb in ragdollParams.Limbs)
{
@@ -2,12 +2,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using Barotrauma.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -69,10 +67,10 @@ namespace Barotrauma
.ToImmutableHashSet();
}
public static Result<ContentFile, string> CreateFromXElement(ContentPackage contentPackage, XElement element)
public static Result<ContentFile, LoadError> CreateFromXElement(ContentPackage contentPackage, XElement element)
{
static Result<ContentFile, string> fail(string error, string? stackTrace = null)
=> Result<ContentFile, string>.Failure(error, stackTrace);
static Result<ContentFile, LoadError> fail(string error, Exception? exception = null)
=> Result<ContentFile, LoadError>.Failure(new LoadError(error, exception));
Identifier elemName = element.NameAsIdentifier();
var type = Types.FirstOrDefault(t => t.Names.Contains(elemName));
@@ -95,11 +93,11 @@ namespace Barotrauma
var file = type.CreateInstance(contentPackage, filePath);
return file is null
? throw new Exception($"Content type is not implemented correctly")
: Result<ContentFile, string>.Success(file);
: Result<ContentFile, LoadError>.Success(file);
}
catch (Exception e)
{
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e.StackTrace.CleanupStackTrace());
return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e);
}
}
@@ -125,5 +123,23 @@ namespace Barotrauma
}
public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer);
public readonly struct LoadError
{
public readonly string Message;
public readonly Exception? Exception;
public LoadError(string message, Exception? exception)
{
Message = message;
Exception = exception;
}
public override string ToString()
=> Message
+ (Exception is { StackTrace: var stackTrace }
? '\n' + stackTrace.CleanupStackTrace()
: string.Empty);
}
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "corpse";
protected override bool MatchesPlural(Identifier identifier) => identifier == "corpses";
protected override PrefabCollection<CorpsePrefab> prefabs => CorpsePrefab.Prefabs;
protected override PrefabCollection<CorpsePrefab> Prefabs => CorpsePrefab.Prefabs;
protected override CorpsePrefab CreatePrefab(ContentXElement element)
{
return new CorpsePrefab(element, this);
@@ -8,7 +8,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "EventManagerSettings";
protected override PrefabCollection<EventManagerSettings> prefabs => EventManagerSettings.Prefabs;
protected override PrefabCollection<EventManagerSettings> Prefabs => EventManagerSettings.Prefabs;
protected override EventManagerSettings CreatePrefab(ContentXElement element)
{
return new EventManagerSettings(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "faction";
protected override bool MatchesPlural(Identifier identifier) => identifier == "factions";
protected override PrefabCollection<FactionPrefab> prefabs => FactionPrefab.Prefabs;
protected override PrefabCollection<FactionPrefab> Prefabs => FactionPrefab.Prefabs;
protected override FactionPrefab CreatePrefab(ContentXElement element)
{
return new FactionPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected abstract bool MatchesSingular(Identifier identifier);
protected abstract bool MatchesPlural(Identifier identifier);
protected abstract PrefabCollection<T> prefabs { get; }
protected abstract PrefabCollection<T> Prefabs { get; }
protected abstract T CreatePrefab(ContentXElement element);
private void LoadFromXElement(ContentXElement parentElement, bool overriding)
@@ -29,14 +29,14 @@ namespace Barotrauma
}
else if (elemName == "clear")
{
prefabs.AddOverrideFile(this);
Prefabs.AddOverrideFile(this);
}
else if (MatchesSingular(elemName))
{
T prefab = CreatePrefab(parentElement);
try
{
prefabs.Add(prefab, overriding);
Prefabs.Add(prefab, overriding);
}
catch
{
@@ -53,7 +53,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"GenericPrefabFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -68,12 +68,12 @@ namespace Barotrauma
public override sealed void UnloadFile()
{
prefabs.RemoveByFile(this);
Prefabs.RemoveByFile(this);
}
public sealed override void Sort()
{
prefabs.SortAll();
Prefabs.SortAll();
}
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "itemassembly";
protected override bool MatchesPlural(Identifier identifier) => identifier == "itemassemblies";
protected override PrefabCollection<ItemAssemblyPrefab> prefabs => ItemAssemblyPrefab.Prefabs;
protected override PrefabCollection<ItemAssemblyPrefab> Prefabs => ItemAssemblyPrefab.Prefabs;
protected override ItemAssemblyPrefab CreatePrefab(ContentXElement element)
{
return new ItemAssemblyPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "items";
protected override PrefabCollection<ItemPrefab> prefabs => ItemPrefab.Prefabs;
protected override PrefabCollection<ItemPrefab> Prefabs => ItemPrefab.Prefabs;
protected override ItemPrefab CreatePrefab(ContentXElement element)
{
return new ItemPrefab(element, this);
@@ -22,11 +22,7 @@ namespace Barotrauma
{
foreach (var element in mainElement.Elements())
{
if (element.NameAsIdentifier() == "nojob")
{
JobPrefab.NoJobElement ??= element;
}
else if (element.NameAsIdentifier() == "ItemRepairPriorities")
if (element.NameAsIdentifier() == "ItemRepairPriorities")
{
foreach (var subElement in element.Elements())
{
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "levelobjects";
protected override PrefabCollection<LevelObjectPrefab> prefabs => LevelObjectPrefab.Prefabs;
protected override PrefabCollection<LevelObjectPrefab> Prefabs => LevelObjectPrefab.Prefabs;
protected override LevelObjectPrefab CreatePrefab(ContentXElement element)
{
return new LevelObjectPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "locationtypes";
protected override PrefabCollection<LocationType> prefabs => LocationType.Prefabs;
protected override PrefabCollection<LocationType> Prefabs => LocationType.Prefabs;
protected override LocationType CreatePrefab(ContentXElement element)
{
return new LocationType(element, this);
@@ -23,7 +23,7 @@ namespace Barotrauma
/*missionTypes.Any(t => identifier == t.Name)
|| identifier == "OutpostDestroyMission" || identifier == "OutpostRescueMission";*/
protected override bool MatchesPlural(Identifier identifier) => identifier == "missions";
protected override PrefabCollection<MissionPrefab> prefabs => MissionPrefab.Prefabs;
protected override PrefabCollection<MissionPrefab> Prefabs => MissionPrefab.Prefabs;
protected override MissionPrefab CreatePrefab(ContentXElement element)
{
return new MissionPrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "npcset";
protected override bool MatchesPlural(Identifier identifier) => identifier == "npcsets";
protected override PrefabCollection<NPCSet> prefabs => NPCSet.Sets;
protected override PrefabCollection<NPCSet> Prefabs => NPCSet.Sets;
protected override NPCSet CreatePrefab(ContentXElement element)
{
return new NPCSet(element, this);
@@ -42,7 +42,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"OrdersFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "OutpostConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "OutpostGenerationParameters";
protected override PrefabCollection<OutpostGenerationParams> prefabs => OutpostGenerationParams.OutpostParams;
protected override PrefabCollection<OutpostGenerationParams> Prefabs => OutpostGenerationParams.OutpostParams;
protected override OutpostGenerationParams CreatePrefab(ContentXElement element)
{
return new OutpostGenerationParams(element, this);
@@ -14,7 +14,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "particles";
protected override PrefabCollection<ParticlePrefab> prefabs => ParticlePrefab.Prefabs;
protected override PrefabCollection<ParticlePrefab> Prefabs => ParticlePrefab.Prefabs;
protected override ParticlePrefab CreatePrefab(ContentXElement element)
{
return new ParticlePrefab(element, this);
@@ -57,7 +57,7 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
DebugConsole.ThrowError($"RandomEventsFile: Invalid {GetType().Name} element: {parentElement.Name} in {Path}");
}
}
@@ -10,7 +10,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "RuinConfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "RuinGenerationParameters";
protected override PrefabCollection<RuinGenerationParams> prefabs => RuinGenerationParams.RuinParams;
protected override PrefabCollection<RuinGenerationParams> Prefabs => RuinGenerationParams.RuinParams;
protected override RuinGenerationParams CreatePrefab(ContentXElement element)
{
return new RuinGenerationParams(element, this);
@@ -11,7 +11,7 @@ namespace Barotrauma
{
public SoundsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override PrefabCollection<SoundPrefab> prefabs => SoundPrefab.Prefabs;
protected override PrefabCollection<SoundPrefab> Prefabs => SoundPrefab.Prefabs;
protected override SoundPrefab CreatePrefab(ContentXElement element)
{
@@ -0,0 +1,12 @@
namespace Barotrauma
{
sealed class StartItemsFile : GenericPrefabFile<StartItemSet>
{
public StartItemsFile(ContentPackage contentPackage, ContentPath path) : base(contentPackage, path) { }
protected override bool MatchesSingular(Identifier identifier) => identifier == "itemset";
protected override bool MatchesPlural(Identifier identifier) => identifier == "startitems";
protected override PrefabCollection<StartItemSet> Prefabs => StartItemSet.Sets;
protected override StartItemSet CreatePrefab(ContentXElement element) => new StartItemSet(element, this);
}
}
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => !MatchesPlural(identifier);
protected override bool MatchesPlural(Identifier identifier) => identifier == "prefabs" || identifier == "structures";
protected override PrefabCollection<StructurePrefab> prefabs => StructurePrefab.Prefabs;
protected override PrefabCollection<StructurePrefab> Prefabs => StructurePrefab.Prefabs;
protected override StructurePrefab CreatePrefab(ContentXElement element)
{
return new StructurePrefab(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "talenttree";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talenttrees";
protected override PrefabCollection<TalentTree> prefabs => TalentTree.JobTalentTrees;
protected override PrefabCollection<TalentTree> Prefabs => TalentTree.JobTalentTrees;
protected override TalentTree CreatePrefab(ContentXElement element)
{
return new TalentTree(element, this);
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "talent";
protected override bool MatchesPlural(Identifier identifier) => identifier == "talents";
protected override PrefabCollection<TalentPrefab> prefabs => TalentPrefab.TalentPrefabs;
protected override PrefabCollection<TalentPrefab> Prefabs => TalentPrefab.TalentPrefabs;
protected override TalentPrefab CreatePrefab(ContentXElement element)
{
return new TalentPrefab(element, this);
@@ -17,7 +17,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "TraitorMission";
protected override bool MatchesPlural(Identifier identifier) => identifier == "TraitorMissions";
protected override PrefabCollection<PrefabType> prefabs => PrefabType.Prefabs;
protected override PrefabCollection<PrefabType> Prefabs => PrefabType.Prefabs;
protected override PrefabType CreatePrefab(ContentXElement element)
{
return new PrefabType(element, this);
@@ -14,7 +14,7 @@ namespace Barotrauma
protected override bool MatchesPlural(Identifier identifier) =>
identifier == "upgrademodules";
protected override PrefabCollection<UpgradeContentPrefab> prefabs => UpgradeContentPrefab.PrefabsAndCategories;
protected override PrefabCollection<UpgradeContentPrefab> Prefabs => UpgradeContentPrefab.PrefabsAndCategories;
protected override UpgradeContentPrefab CreatePrefab(ContentXElement element)
{
Identifier elemName = element.NameAsIdentifier();
@@ -9,7 +9,7 @@ namespace Barotrauma
protected override bool MatchesSingular(Identifier identifier) => identifier == "wreckaiconfig";
protected override bool MatchesPlural(Identifier identifier) => identifier == "wreckaiconfigs";
protected override PrefabCollection<WreckAIConfig> prefabs => WreckAIConfig.Prefabs;
protected override PrefabCollection<WreckAIConfig> Prefabs => WreckAIConfig.Prefabs;
protected override WreckAIConfig CreatePrefab(ContentXElement element)
{
return new WreckAIConfig(element, this);
@@ -14,8 +14,7 @@ namespace Barotrauma
{
public abstract class ContentPackage
{
#warning TODO: make this independent of the current version
public static readonly Version MinimumHashCompatibleVersion = GameMain.Version;
public static readonly Version MinimumHashCompatibleVersion = new Version(0, 18, 3, 0);
public const string LocalModsDir = "LocalMods";
public static readonly string WorkshopModsDir = Barotrauma.IO.Path.Combine(
@@ -34,11 +33,11 @@ namespace Barotrauma
public readonly Version GameVersion;
public readonly string ModVersion;
public readonly Md5Hash Hash;
public Md5Hash Hash { get; private set; }
public readonly DateTime? InstallTime;
public readonly ImmutableArray<ContentFile> Files;
public readonly ImmutableArray<(string error, string? stackTrace)> Errors;
public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
public async Task<bool> IsUpToDate()
{
@@ -56,7 +55,7 @@ namespace Barotrauma
/// <summary>
/// Does the content package include some content that needs to match between all players in multiplayer.
/// </summary>
public readonly bool HasMultiplayerSyncedContent;
public bool HasMultiplayerSyncedContent { get; private set; }
protected ContentPackage(XDocument doc, string path)
{
@@ -85,13 +84,13 @@ namespace Barotrauma
.ToArray();
Files = fileResults
.OfType<Success<ContentFile, string>>()
.OfType<Success<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Value)
.ToImmutableArray();
Errors = fileResults
.OfType<Failure<ContentFile, string>>()
.Select(f => (f.Error, f.StackTrace))
.OfType<Failure<ContentFile, ContentFile.LoadError>>()
.Select(f => f.Error)
.ToImmutableArray();
HasMultiplayerSyncedContent = Files.Any(f => !f.NotSyncedInMultiplayer);
@@ -128,18 +127,13 @@ namespace Barotrauma
try
{
if (doc.Root.GetAttributeBool("corepackage", false))
{
return new CorePackage(doc, path);
}
else
{
return new RegularPackage(doc, path);
}
return doc.Root.GetAttributeBool("corepackage", false)
? (ContentPackage)new CorePackage(doc, path)
: new RegularPackage(doc, path);
}
catch (Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
e = e.GetInnermost();
DebugConsole.ThrowError($"{e.Message}: {e.StackTrace}");
return null;
}
@@ -279,12 +273,42 @@ namespace Barotrauma
Files.ForEach(f => f.UnloadFile());
}
public override int GetHashCode()
public void ReloadSubsAndItemAssemblies()
{
byte[] shortHash = Encoding.ASCII.GetBytes(Hash.StringRepresentation.Substring(0, 4));
return (shortHash[0] << 24) | (shortHash[1] << 16) | (shortHash[2] << 8) | shortHash[3];
XDocument doc = XMLExtensions.TryLoadXml(Path);
List<ContentFile> newFileList = new List<ContentFile>();
XElement rootElement = doc.Root ?? throw new NullReferenceException("XML document is invalid: root element is null.");
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))
.ToArray();
foreach (var result in fileResults)
{
switch (result)
{
case Success<ContentFile, ContentFile.LoadError> { Value: var file }:
if (file is BaseSubFile || file is ItemAssemblyFile)
{
newFileList.Add(file);
}
else
{
var existingFile = Files.FirstOrDefault(f => f.Path == file.Path);
newFileList.Add(existingFile ?? file);
}
break;
}
}
UnloadFilesOfType<BaseSubFile>();
UnloadFilesOfType<ItemAssemblyFile>();
Files = newFileList.ToImmutableArray();
Hash = CalculateHash();
LoadFilesOfType<BaseSubFile>();
LoadFilesOfType<ItemAssemblyFile>();
}
public static bool PathAllowedAsLocalModFile(string path)
{
#if DEBUG
@@ -306,21 +330,17 @@ namespace Barotrauma
public void LogErrors()
{
if (Errors.Any())
if (!Errors.Any())
{
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package\"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(e => errorToStr(e.error, e.stackTrace))));
static string errorToStr(string error, string? stackTrace)
{
string str = error;
if (stackTrace != null)
{
str += '\n' + stackTrace;
}
return str;
}
return;
}
DebugConsole.AddWarning(
$"The following errors occurred while loading the content package \"{Name}\". The package might not work correctly.\n" +
string.Join('\n', Errors.Select(errorToStr)));
static string errorToStr(ContentFile.LoadError error)
=> error.ToString();
}
}
}
@@ -430,9 +430,9 @@ namespace Barotrauma
public static void LoadVanillaFileList()
{
VanillaCorePackage = new CorePackage(XDocument.Load(VanillaFileList), VanillaFileList);
foreach ((string error, string? stackTrace) in VanillaCorePackage.Errors)
foreach (ContentFile.LoadError error in VanillaCorePackage.Errors)
{
DebugConsole.ThrowError(error + (stackTrace == null ? string.Empty : '\n' + stackTrace));
DebugConsole.ThrowError(error.ToString());
}
}
@@ -49,7 +49,10 @@ namespace Barotrauma
.Replace(string.Format(OtherModDirFmt, ContentPackage.SteamWorkshopId.ToString(CultureInfo.InvariantCulture)), modPath, StringComparison.OrdinalIgnoreCase);
}
}
var allPackages = ContentPackageManager.EnabledPackages.All;
var allPackages = ContentPackageManager.AllPackages;
#if CLIENT
if (GameMain.ModDownloadScreen?.DownloadedPackages != null) { allPackages = allPackages.Concat(GameMain.ModDownloadScreen.DownloadedPackages); }
#endif
foreach (Identifier otherModName in otherMods)
{
if (!UInt64.TryParse(otherModName.Value, out UInt64 workshopId)) { workshopId = 0; }
@@ -27,7 +27,7 @@ namespace Barotrauma
public string BaseUri => Element.BaseUri;
public XDocument Document => Element.Document ?? throw new NullReferenceException("XML element is invalid: document is null.");
public XDocument? Document => Element.Document;
public ContentXElement? FirstElement() => Elements().FirstOrDefault();
@@ -11,7 +11,7 @@ namespace Barotrauma
{
Message = $"\"{whoAsked?.Name ?? "[NULL]"}\" depends on a package " +
$"with name or ID \"{missingPackage ?? "[NULL]"}\" " +
$"that is not currently enabled.";
$"that is not currently installed.";
}
}
}
@@ -781,7 +781,7 @@ namespace Barotrauma
return;
}
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
newEvent.Init();
NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua);
return;
}
@@ -1123,7 +1123,7 @@ namespace Barotrauma
{
var gamesession = new GameSession(
SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)),
GameModePreset.DevSandbox);
GameModePreset.DevSandbox ?? GameModePreset.Sandbox);
string seed = ToolBox.RandomSeed(16);
gamesession.StartRound(seed);
@@ -1830,6 +1830,17 @@ namespace Barotrauma
}));
#endif
commands.Add(new Command("startitems|startitemset", "start item set identifier", (string[] args) =>
{
if (args.Length == 0)
{
ThrowError($"No start item set identifier defined!");
return;
}
AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier();
NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\"");
}, isCheat: false));
//"dummy commands" that only exist so that the server can give clients permissions to use them
//TODO: alphabetical order?
commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
@@ -2516,5 +2527,15 @@ namespace Barotrauma
ThrowError("Saving debug console log to " + filePath + " failed", e);
}
}
public static void DeactivateCheats()
{
#if CLIENT
GameMain.DebugDraw = false;
GameMain.LightManager.LightingEnabled = true;
#endif
Hull.EditWater = false;
Hull.EditFire = false;
}
}
}
@@ -33,13 +33,13 @@ namespace Barotrauma
public enum AbilityEffectType
{
Undefined,
None,
None,
OnAttack,
OnAttackResult,
OnAttacked,
OnAttackedResult,
OnGainSkillPoint,
OnAllyGainSkillPoint,
OnGainSkillPoint,
OnAllyGainSkillPoint,
OnRepairComplete,
OnItemFabricationSkillGain,
OnItemFabricatedAmount,
@@ -131,21 +131,22 @@ namespace Barotrauma
MaxAttachableCount,
}
[Flags]
public enum AbilityFlags
{
None,
MustWalk,
ImmuneToPressure,
IgnoredByEnemyAI,
MoveNormallyWhileDragging,
CanTinker,
CanTinkerFabricatorsAndDeconstructors,
TinkeringPowersDevices,
GainSkillPastMaximum,
RetainExperienceForNewCharacter,
AllowSecondOrderedTarget,
PowerfulCPR,
AlwaysStayConscious,
None = 0,
MustWalk = 0x1,
ImmuneToPressure = 0x2,
IgnoredByEnemyAI = 0x4,
MoveNormallyWhileDragging = 0x8,
CanTinker = 0x10,
CanTinkerFabricatorsAndDeconstructors = 0x20,
TinkeringPowersDevices = 0x40,
GainSkillPastMaximum = 0x80,
RetainExperienceForNewCharacter = 0x100,
AllowSecondOrderedTarget = 0x200,
PowerfulCPR = 0x400,
AlwaysStayConscious = 0x800,
}
[Flags]
@@ -155,4 +156,32 @@ namespace Barotrauma
Player = 0b10,
Both = Bot | Player
}
public enum StartingBalanceAmount
{
Low,
Medium,
High,
}
public enum GameDifficulty
{
Easy,
Medium,
Hard,
Hellish
}
public enum NumberType
{
Int,
Float
}
public enum ChatMode
{
None,
Local,
Radio
}
}
@@ -53,8 +53,9 @@ namespace Barotrauma
}
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
spawnPos = Level.Loaded.GetRandomItemPos(
(Rand.Value(Rand.RandSync.ServerAndClient) < 0.5f) ?
Level.PositionType.MainPath | Level.PositionType.SidePath :
@@ -111,7 +112,7 @@ namespace Barotrauma
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) return;
Finished();
Finish();
state = 2;
break;
}
@@ -5,13 +5,16 @@ using System.Collections.Generic;
namespace Barotrauma
{
class Event
{
{
public event Action Finished;
protected bool isFinished;
protected readonly EventPrefab prefab;
public EventPrefab Prefab => prefab;
public EventSet ParentSet { get; private set; }
public Func<Level.InterestingPosition, bool> SpawnPosFilter;
public bool IsFinished
@@ -42,23 +45,20 @@ namespace Barotrauma
yield break;
}
public virtual void Init(bool affectSubImmediately)
public virtual void Init(EventSet parentSet = null)
{
ParentSet = parentSet;
}
public virtual void Update(float deltaTime)
{
}
public virtual void Finished()
public virtual void Finish()
{
isFinished = true;
}
public virtual bool CanAffectSubImmediately(Level level)
{
return true;
}
Finished?.Invoke();
}
public virtual bool LevelMeetsRequirements()
{
@@ -313,10 +313,14 @@ namespace Barotrauma
bool isValid = e is Character character && !character.Removed && !character.IsDead && !character.IsIncapacitated &&
(e == Character.Controlled || character.IsRemotePlayer);
#if SERVER
UpdateIgnoredClients();
isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
if (!dialogOpened)
{
UpdateIgnoredClients();
isValid &= !ignoredClients.Keys.Any(c => c.Character == e);
}
#elif CLIENT
isValid &= (e != Character.Controlled || !GUI.InputBlockingMenuOpen);
bool block = GUI.InputBlockingMenuOpen && !dialogOpened;
isValid &= (e != Character.Controlled || !block);
#endif
return isValid;
}
@@ -80,7 +80,7 @@ namespace Barotrauma
}
if (campaign is MultiPlayerCampaign mpCampaign)
{
mpCampaign.LastUpdateID++;
mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
}
if (prefab != null)
{
@@ -64,8 +64,6 @@ namespace Barotrauma
campaign.GetWallet(client).Give(Amount);
}
}
((MultiPlayerCampaign)campaign).LastUpdateID++;
#else
campaign.Wallet.Give(Amount);
#endif
@@ -49,6 +49,12 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SpawnPointTag { get; set; }
[Serialize(CharacterTeamType.FriendlyNPC, IsPropertySaveable.Yes)]
public CharacterTeamType Team { get; protected set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should we spawn the entity even when no spawn points with matching tags were found?")]
public bool RequireSpawnPointTag { get; set; }
private readonly HashSet<Identifier> targetModuleTags = new HashSet<Identifier>();
[Serialize("", IsPropertySaveable.Yes, "What outpost module tags does the entity prefer to spawn in.")]
@@ -79,7 +85,7 @@ namespace Barotrauma
public SpawnAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
{
ignoreSpawnPointType = !element.Attributes().Any(a => a.Name.ToString().Equals("spawnpointtype", StringComparison.OrdinalIgnoreCase));
ignoreSpawnPointType = element.GetAttribute("spawnpointtype") == null;
}
public override bool IsFinished(ref string goTo)
@@ -110,22 +116,40 @@ namespace Barotrauma
if (humanPrefab != null)
{
ISpatialEntity spawnPos = GetSpawnPos();
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos?.WorldPosition ?? Vector2.Zero, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
if (spawnPos != null)
{
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = CharacterTeamType.FriendlyNPC;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), humanPrefab.GetCharacterInfo(), onSpawn: newCharacter =>
{
foreach (Item item in newCharacter.Inventory.AllItems)
if (newCharacter == null) { return; }
newCharacter.HumanPrefab = humanPrefab;
newCharacter.TeamID = Team;
newCharacter.EnableDespawn = false;
humanPrefab.GiveItems(newCharacter, newCharacter.Submarine);
if (LootingIsStealing)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
foreach (Item item in newCharacter.Inventory.AllItems)
{
item.SpawnedInCurrentOutpost = true;
item.AllowStealing = false;
}
}
}
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
humanPrefab.InitializeCharacter(newCharacter, spawnPos);
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
}
}
else if (!SpeciesName.IsEmpty)
{
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
@@ -134,20 +158,9 @@ namespace Barotrauma
});
}
}
else if (!SpeciesName.IsEmpty)
{
Entity.Spawner.AddCharacterToSpawnQueue(SpeciesName, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawn: newCharacter =>
{
if (!TargetTag.IsEmpty && newCharacter != null)
{
ParentEvent.AddTarget(TargetTag, newCharacter);
}
spawnedEntity = newCharacter;
});
}
else if (!ItemIdentifier.IsEmpty)
{
if (!(MapEntityPrefab.Find(null, identifier: ItemIdentifier) is ItemPrefab itemPrefab))
if (!(MapEntityPrefab.FindByIdentifier(ItemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Error in SpawnAction (item prefab \"" + ItemIdentifier + "\" not found)");
}
@@ -178,7 +191,11 @@ namespace Barotrauma
if (spawnInventory == null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(GetSpawnPos()?.WorldPosition ?? Vector2.Zero, 100.0f), onSpawned: onSpawned);
ISpatialEntity spawnPos = GetSpawnPos();
if (spawnPos != null)
{
Entity.Spawner.AddItemToSpawnQueue(itemPrefab, OffsetSpawnPos(spawnPos.WorldPosition, 100.0f), onSpawned: onSpawned);
}
}
else
{
@@ -244,10 +261,10 @@ namespace Barotrauma
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable());
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag);
}
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false)
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false)
{
List<WayPoint> potentialSpawnPoints = spawnLocation switch
{
@@ -274,18 +291,24 @@ namespace Barotrauma
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag)))
.Where(wp => wp.ConnectedDoor == null && !wp.isObstructed);
.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && !wp.isObstructed));
if (spawnPoints.Any())
if (requireTaggedSpawnPoint || spawnPoints.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
}
}
if (potentialSpawnPoints.Count == 0)
if (potentialSpawnPoints.None())
{
DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
{
DebugConsole.NewMessage($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation} (tag: {string.Join(",", spawnpointTags)}), skipping.", color: Color.White);
}
else
{
DebugConsole.ThrowError($"Could not find a spawn point for a SpawnAction (spawn location: {spawnLocation})");
}
return null;
}
@@ -307,7 +330,7 @@ namespace Barotrauma
validSpawnPoints = validSpawnPoints.Except(airlockSpawnPoints);
}
if (!validSpawnPoints.Any())
if (validSpawnPoints.None())
{
DebugConsole.ThrowError($"Could not find a spawn point of the correct type for a SpawnAction (spawn location: {spawnLocation}, type: {spawnPointType}, module flags: {((moduleFlags == null || !moduleFlags.Any()) ? "none" : string.Join(", ", moduleFlags))})");
return potentialSpawnPoints.GetRandomUnsynced();
@@ -320,7 +343,7 @@ namespace Barotrauma
}
//if not trying to spawn at a tagged spawnpoint, favor spawnpoints without tags
if (spawnpointTags == null || !spawnpointTags.Any())
if (spawnpointTags == null || spawnpointTags.None())
{
var spawnPoints = validSpawnPoints.Where(wp => !wp.Tags.Any());
if (spawnPoints.Any())
@@ -117,6 +117,8 @@ namespace Barotrauma
public bool Enabled = true;
private MTRandom rand;
public void StartRound(Level level)
{
this.level = level;
@@ -147,7 +149,7 @@ namespace Barotrauma
seed ^= ToolBox.IdentifierToInt(previousEvent.Identifier);
}
}
MTRandom rand = new MTRandom(seed);
rand = new MTRandom(seed);
EventSet initialEventSet = SelectRandomEvents(EventSet.Prefabs.ToList(), requireCampaignSet: GameMain.GameSession?.GameMode is CampaignMode, rand);
EventSet additiveSet = null;
@@ -159,12 +161,12 @@ namespace Barotrauma
if (initialEventSet != null)
{
pendingEventSets.Add(initialEventSet);
CreateEvents(initialEventSet, rand);
CreateEvents(initialEventSet);
}
if (additiveSet != null)
{
pendingEventSets.Add(additiveSet);
CreateEvents(additiveSet, rand);
CreateEvents(additiveSet);
}
if (level?.LevelData?.Type == LevelData.LevelType.Outpost)
@@ -183,7 +185,7 @@ namespace Barotrauma
if (unlockPathEventPrefab != null)
{
var newEvent = unlockPathEventPrefab.CreateInstance();
newEvent.Init(true);
newEvent.Init();
ActiveEvents.Add(newEvent);
}
else
@@ -258,9 +260,15 @@ namespace Barotrauma
throw new InvalidOperationException("Could not select EventManager settings (level not set).");
}
float extraDifficulty = 0;
if (GameMain.GameSession.Campaign?.Settings != null)
{
extraDifficulty = GameMain.GameSession.Campaign.Settings.ExtraEventManagerDifficulty;
}
float modifiedDifficulty = Math.Clamp(level.Difficulty + extraDifficulty, 0, 100);
var suitableSettings = EventManagerSettings.OrderedByDifficulty.Where(s =>
level.Difficulty >= s.MinLevelDifficulty &&
level.Difficulty <= s.MaxLevelDifficulty).ToArray();
modifiedDifficulty >= s.MinLevelDifficulty &&
modifiedDifficulty <= s.MaxLevelDifficulty).ToArray();
if (suitableSettings.Length == 0)
{
@@ -362,8 +370,9 @@ namespace Barotrauma
return retVal;
}
private void CreateEvents(EventSet eventSet, Random rand)
private void CreateEvents(EventSet eventSet)
{
selectedEvents.Remove(eventSet);
if (level == null) { return; }
if (level.LevelData.HasHuntingGrounds && eventSet.DisableInHuntingGrounds) { return; }
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
@@ -399,6 +408,14 @@ namespace Barotrauma
bool isPrefabSuitable(EventPrefab e)
=> e.BiomeIdentifier.IsEmpty ||
e.BiomeIdentifier == level.LevelData?.Biome?.Identifier;
foreach (var subEventPrefab in eventSet.EventPrefabs)
{
foreach (Identifier missingId in subEventPrefab.GetMissingIdentifiers())
{
DebugConsole.ThrowError($"Error in event set \"{eventSet.Identifier}\" ({eventSet.ContentFile?.ContentPackage?.Name ?? "null"}) - could not find an event prefab with the identifier \"{missingId}\".");
}
}
var suitablePrefabSubsets = eventSet.EventPrefabs.Where(
e => e.EventPrefabs.Any(isPrefabSuitable)).ToArray();
@@ -421,7 +438,7 @@ namespace Barotrauma
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
newEvent.Init(eventSet);
if (i < spawnPosFilter.Count) { newEvent.SpawnPosFilter = spawnPosFilter[i]; }
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
@@ -438,7 +455,7 @@ namespace Barotrauma
var newEventSet = SelectRandomEvents(eventSet.ChildSets, random: rand);
if (newEventSet != null)
{
CreateEvents(newEventSet, rand);
CreateEvents(newEventSet);
}
}
}
@@ -451,7 +468,7 @@ namespace Barotrauma
var eventPrefab = ToolBox.SelectWeightedRandom(eventPrefabs.Where(isPrefabSuitable), e => e.Commonness, rand);
var newEvent = eventPrefab.CreateInstance();
if (newEvent == null) { continue; }
newEvent.Init(true);
newEvent.Init(eventSet);
DebugConsole.NewMessage($"Initialized event {newEvent}", debugOnly: true);
if (!selectedEvents.ContainsKey(eventSet))
{
@@ -465,7 +482,7 @@ namespace Barotrauma
{
if (!IsValidForLevel(childEventSet, level)) { continue; }
if (location != null && !IsValidForLocation(childEventSet, location)) { continue; }
CreateEvents(childEventSet, rand);
CreateEvents(childEventSet);
}
}
}
@@ -666,6 +683,14 @@ namespace Barotrauma
{
eventCoolDown = settings.EventCooldown;
}
if (eventSet.ResetTime > 0)
{
ev.Finished += () =>
{
pendingEventSets.Add(eventSet);
CreateEvents(eventSet);
};
}
}
}
@@ -58,7 +58,7 @@ namespace Barotrauma
}
#endif
public static List<EventPrefab> GetAllEventPrefabs()
public static List<EventPrefab> GetAllEventPrefabs()
{
List<EventPrefab> eventPrefabs = EventPrefab.Prefabs.ToList();
foreach (var eventSet in Prefabs)
@@ -118,6 +118,8 @@ namespace Barotrauma
public readonly float DefaultCommonness;
public readonly ImmutableDictionary<Identifier, float> OverrideCommonness;
public readonly float ResetTime;
public readonly struct SubEventPrefab
{
public SubEventPrefab(Either<Identifier[], EventPrefab> prefabOrIdentifiers, float? commonness, float? probability)
@@ -140,11 +142,15 @@ namespace Barotrauma
{
foreach (var id in (Identifier[])PrefabOrIdentifier)
{
yield return EventPrefab.Prefabs[id];
if (EventPrefab.Prefabs.TryGet(id, out EventPrefab prefab))
{
yield return prefab;
}
}
}
}
}
public readonly float? SelfCommonness;
public float Commonness => SelfCommonness ?? EventPrefabs.MaxOrNull(p => p.Commonness) ?? 0.0f;
@@ -157,6 +163,20 @@ namespace Barotrauma
commonness = Commonness;
probability = Probability;
}
public IEnumerable<Identifier> GetMissingIdentifiers()
{
if (PrefabOrIdentifier.TryCast<Identifier[]>(out var ids))
{
foreach (var id in ids)
{
if (!EventPrefab.Prefabs.ContainsKey(id))
{
yield return id;
}
}
}
}
}
public readonly ImmutableArray<SubEventPrefab> EventPrefabs;
@@ -244,6 +264,7 @@ namespace Barotrauma
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0);
DefaultCommonness = 1.0f;
foreach (var subElement in element.Elements())
@@ -454,7 +475,6 @@ namespace Barotrauma
{
childSet.Dispose();
}
}
}
}
@@ -39,13 +39,9 @@ namespace Barotrauma
targetItemIdentifiers = prefab.ConfigElement.GetAttributeIdentifierArray("itemidentifiers", Array.Empty<Identifier>());
}
public override bool CanAffectSubImmediately(Level level)
{
return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount;
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.ServerAndClient);
for (int i = 0; i < itemAmount; i++)
@@ -60,7 +56,7 @@ namespace Barotrauma
if (isFinished) return;
if (targetItems.Count == 0 || timer >= duration)
{
Finished();
Finish();
return;
}
@@ -125,10 +125,6 @@ namespace Barotrauma
if (!AllTargetsEliminated()) { return; }
State = 1;
break;
case 1:
if (!Submarine.MainSub.AtEndExit && !Submarine.MainSub.AtStartExit) { return; }
State = 2;
break;
}
}
@@ -166,11 +162,16 @@ namespace Barotrauma
public override void End()
{
if (State == 2)
bool exitingLevel = GameMain.GameSession?.GameMode is CampaignMode campaign ?
campaign.GetAvailableTransition() != CampaignMode.TransitionType.None :
Submarine.MainSub is { } sub && (sub.AtEndExit || sub.AtStartExit);
if (State > 0 && exitingLevel)
{
GiveReward();
completed = true;
}
failed = !completed && State > 0;
}
}
@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -23,15 +22,16 @@ namespace Barotrauma
private int calculatedReward;
private int maxItemCount;
private Submarine sub;
private Submarine currentSub;
private SubmarineInfo nextRoundSubInfo;
private readonly List<CargoMission> previouslySelectedMissions = new List<CargoMission>();
public override LocalizedString Description
{
get
{
if (Submarine.MainSub != sub)
if ((GameMain.GameSession?.Campaign?.PendingSubmarineSwitch ?? Submarine.MainSub?.Info) != nextRoundSubInfo)
{
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
@@ -43,7 +43,8 @@ namespace Barotrauma
public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
this.sub = sub;
this.currentSub = sub;
this.nextRoundSubInfo = sub?.Info;
itemConfig = prefab.ConfigElement.GetChildElement("Items");
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f);
//this can get called between rounds when the client receives a campaign save
@@ -57,39 +58,13 @@ namespace Barotrauma
private void DetermineCargo()
{
if (this.sub == null || itemConfig == null)
if (this.currentSub == null || itemConfig == null)
{
calculatedReward = Prefab.Reward;
return;
}
itemsToSpawn.Clear();
List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers();
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
previouslySelectedMissions.Clear();
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
if (!(mission is CargoMission otherMission)) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
foreach (var (element, container) in otherMission.itemsToSpawn)
{
for (int i = 0; i < containers.Count; i++)
{
if (containers[i].container == container)
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
break;
}
}
}
}
}
maxItemCount = 0;
foreach (var subElement in itemConfig.Elements())
@@ -98,18 +73,85 @@ namespace Barotrauma
maxItemCount += maxCount;
}
for (int i = 0; i < containers.Count; i++)
var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch;
if (pendingSubInfo != null && pendingSubInfo != currentSub.Info)
{
foreach (var subElement in itemConfig.Elements())
//if we've got a submarine switch pending, calculate the amount of cargo based on it's cargo capacity
//TODO: this isn't guaranteed to be accurate, because we don't take existing items in the new sub's cargo containers
//or items that might get transferred in them into account
maxItemCount = Math.Min(maxItemCount, pendingSubInfo.CargoCapacity);
previouslySelectedMissions.Clear();
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
itemsToSpawn.Add((subElement, containers[i].container));
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
if (!(mission is CargoMission otherMission)) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
maxItemCount -= otherMission.itemsToSpawn.Count;
}
}
for (int i = 0; i < maxItemCount; i++)
{
foreach (var subElement in itemConfig.Elements())
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
while (itemsToSpawn.Count < maxItemCount)
{
itemsToSpawn.Add((subElement, null));
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
}
}
}
maxItemCount = Math.Max(0, maxItemCount);
nextRoundSubInfo = pendingSubInfo;
}
else
{
List<(ItemContainer container, int freeSlots)> containers = currentSub.GetCargoContainers();
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
previouslySelectedMissions.Clear();
if (GameMain.GameSession?.StartLocation?.SelectedMissions != null)
{
bool isPriorMission = true;
foreach (Mission mission in GameMain.GameSession.StartLocation.SelectedMissions)
{
if (!(mission is CargoMission otherMission)) { continue; }
if (mission == this) { isPriorMission = false; }
previouslySelectedMissions.Add(otherMission);
if (!isPriorMission) { continue; }
foreach (var (element, container) in otherMission.itemsToSpawn)
{
for (int i = 0; i < containers.Count; i++)
{
if (containers[i].container == container)
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
break;
}
}
}
}
}
for (int i = 0; i < containers.Count; i++)
{
foreach (var subElement in itemConfig.Elements())
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
itemsToSpawn.Add((subElement, containers[i].container));
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
}
}
}
}
@@ -135,7 +177,7 @@ namespace Barotrauma
}
if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; }
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(currentSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
@@ -167,18 +209,26 @@ namespace Barotrauma
}
}
}
if (sub != this.sub || missionsChanged)
var pendingSubInfo = GameMain.GameSession?.Campaign?.PendingSubmarineSwitch;
if (pendingSubInfo != null && nextRoundSubInfo != pendingSubInfo)
{
this.sub = sub;
this.nextRoundSubInfo = pendingSubInfo;
DetermineCargo();
}
else if (sub != this.currentSub || missionsChanged)
{
this.currentSub = sub;
this.nextRoundSubInfo = sub.Info;
DetermineCargo();
}
return calculatedReward;
}
private void InitItems()
{
this.sub = Submarine.MainSub;
this.currentSub = Submarine.MainSub;
DetermineCargo();
items.Clear();
@@ -323,7 +323,7 @@ namespace Barotrauma
{
var newEvent = eventPrefab.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
newEvent.Init();
}
}
@@ -382,7 +382,8 @@ namespace Barotrauma
#if SERVER
totalReward = DistributeRewardsToCrew(GameSession.GetSessionCrewCharacters(CharacterType.Player), totalReward);
#endif
if (totalReward > 0)
bool isSingleplayerOrServer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (isSingleplayerOrServer && totalReward > 0)
{
campaign.Bank.Give(totalReward);
}
@@ -146,8 +146,15 @@ namespace Barotrauma
tags = element.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
Name = TextManager.Get($"MissionName.{TextIdentifier}").Fallback(element.GetAttributeString("name", ""));
Description = TextManager.Get($"MissionDescription.{TextIdentifier}").Fallback(element.GetAttributeString("description", ""));
Name =
TextManager.Get($"MissionName.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("name", "")))
.Fallback(element.GetAttributeString("name", ""));
Description =
TextManager.Get($"MissionDescription.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("description", "")))
.Fallback(element.GetAttributeString("description", ""));
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
@@ -160,10 +167,15 @@ namespace Barotrauma
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
SuccessMessage = TextManager.Get($"MissionSuccess.{TextIdentifier}").Fallback(element.GetAttributeString("successmessage", "Mission completed successfully"));
FailureMessage = TextManager.Get($"MissionFailure.{TextIdentifier}").Fallback(
TextManager.Get("missionfailed")).Fallback(
GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : "");
SuccessMessage =
TextManager.Get($"MissionSuccess.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("successmessage", "")))
.Fallback(element.GetAttributeString("successmessage", "Mission completed successfully"));
FailureMessage =
TextManager.Get($"MissionFailure.{TextIdentifier}")
.Fallback(TextManager.Get(element.GetAttributeString("missionfailed", "")))
.Fallback(TextManager.Get("missionfailed"))
.Fallback(GameSettings.CurrentConfig.Language == TextManager.DefaultLanguage ? element.GetAttributeString("failuremessage", "") : "");
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
@@ -208,8 +220,14 @@ namespace Barotrauma
headers.Add(string.Empty);
messages.Add(string.Empty);
}
headers[messageIndex] = TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] = TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}").Fallback(subElement.GetAttributeString("text", ""));
headers[messageIndex] =
TextManager.Get($"MissionHeader{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("header", "")))
.Fallback(subElement.GetAttributeString("header", ""));
messages[messageIndex] =
TextManager.Get($"MissionMessage{messageIndex}.{TextIdentifier}")
.Fallback(TextManager.Get(subElement.GetAttributeString("text", "")))
.Fallback(subElement.GetAttributeString("text", ""));
messageIndex++;
break;
case "locationtype":
@@ -135,8 +135,10 @@ namespace Barotrauma
monster.Enabled = false;
if (monster.Params.AI != null && monster.Params.AI.EnforceAggressiveBehaviorForMissions)
{
monster.Params.AI.FleeHealthThreshold = 0;
foreach (var targetParam in monster.Params.AI.Targets)
{
if (targetParam.Tag.Equals("engine", StringComparison.OrdinalIgnoreCase)) { continue; }
switch (targetParam.State)
{
case AIState.Avoid:
@@ -270,7 +270,7 @@ namespace Barotrauma
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.Prefab.Identifier == "idcard")
if (item?.GetComponent<IdCard>() != null)
{
item.AddTag("id_pirate");
}
@@ -16,6 +16,8 @@ namespace Barotrauma
private readonly float scatter;
private readonly float offset;
private readonly float delayBetweenSpawns;
private float resetTime;
private float resetTimer;
private Vector2? spawnPos;
@@ -24,7 +26,7 @@ namespace Barotrauma
public readonly Level.PositionType SpawnPosType;
private readonly string spawnPointTag;
private bool spawnPending;
private bool spawnPending, spawnReady;
public readonly int MaxAmountPerLevel = int.MaxValue;
@@ -96,6 +98,7 @@ namespace Barotrauma
offset = prefab.ConfigElement.GetAttributeFloat("offset", 0);
scatter = Math.Clamp(prefab.ConfigElement.GetAttributeFloat("scatter", 500), 0, 3000);
delayBetweenSpawns = prefab.ConfigElement.GetAttributeFloat("delaybetweenspawns", 0.1f);
resetTime = prefab.ConfigElement.GetAttributeFloat("resettime", 0);
if (GameMain.NetworkMember != null)
{
@@ -131,14 +134,14 @@ namespace Barotrauma
}
}
public override bool CanAffectSubImmediately(Level level)
{
float maxRange = Sonar.DefaultSonarRange * 0.8f;
return GetAvailableSpawnPositions().Any(p => Vector2.DistanceSquared(p.Position.ToVector2(), GetReferenceSub().WorldPosition) < maxRange * maxRange);
}
public override void Init(bool affectSubImmediately)
public override void Init(EventSet parentSet)
{
base.Init(parentSet);
if (parentSet != null && resetTime == 0)
{
// Use the parent reset time only if there's no reset time defined for the event.
resetTime = parentSet.ResetTime;
}
if (GameSettings.CurrentConfig.VerboseLogging)
{
DebugConsole.NewMessage("Initialized MonsterEvent (" + SpeciesName + ")", Color.White);
@@ -199,7 +202,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
Submarine refSub = GetReferenceSub();
@@ -267,22 +270,17 @@ namespace Barotrauma
if (!isRuinOrWreck)
{
float minDistance = 20000;
var refSub = GetReferenceSub();
availablePositions.RemoveAll(p => Vector2.DistanceSquared(refSub.WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
if (Submarine.MainSubs.Length > 1)
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
for (int i = 1; i < Submarine.MainSubs.Length; i++)
{
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
if (Submarine.MainSubs[i] == null) { continue; }
availablePositions.RemoveAll(p => Vector2.DistanceSquared(Submarine.MainSubs[i].WorldPosition, p.Position.ToVector2()) < minDistance * minDistance);
}
}
if (availablePositions.None())
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
chosenPosition = availablePositions.GetRandomUnsynced();
@@ -306,7 +304,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
}
@@ -344,7 +342,7 @@ namespace Barotrauma
{
//no suitable position found, disable the event
spawnPos = null;
Finished();
Finish();
return;
}
}
@@ -352,20 +350,42 @@ namespace Barotrauma
}
}
private float GetMinDistanceToSub(Submarine submarine)
private float GetMinDistanceToSub(Submarine submarine)
{
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
float minDist = Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), Sonar.DefaultSonarRange * 0.9f);
if (SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
minDist *= 2;
}
return minDist;
}
public override void Update(float deltaTime)
{
if (disallowed)
{
Finished();
Finish();
return;
}
if (isFinished) { return; }
if (resetTimer > 0)
{
resetTimer -= deltaTime;
if (resetTimer <= 0)
{
if (ParentSet?.ResetTime > 0)
{
// If parent has reset time defined, the set is recreated. Otherwise we'll just reset this event.
Finish();
}
else
{
spawnReady = false;
spawnPos = null;
}
}
return;
}
if (spawnPos == null)
{
@@ -373,7 +393,11 @@ namespace Barotrauma
{
if (Character.CharacterList.Count(c => c.SpeciesName == SpeciesName) >= MaxAmountPerLevel)
{
disallowed = true;
// If the event is set to reset, let's just wait until the old corpse is removed (after being disabled).
if (resetTime == 0)
{
disallowed = true;
}
return;
}
}
@@ -384,9 +408,14 @@ namespace Barotrauma
spawnPending = true;
}
bool spawnReady = false;
if (spawnPending)
{
System.Diagnostics.Debug.Assert(spawnPos.HasValue);
if (spawnPos == null)
{
Finish();
return;
}
//wait until there are no submarines at the spawnpos
if (SpawnPosType.HasFlag(Level.PositionType.MainPath) || SpawnPosType.HasFlag(Level.PositionType.SidePath) || SpawnPosType.HasFlag(Level.PositionType.Abyss))
{
@@ -408,7 +437,7 @@ namespace Barotrauma
{
minDistance = 5000;
}
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck))
else if (SpawnPosType.HasFlag(Level.PositionType.Wreck) || SpawnPosType.HasFlag(Level.PositionType.BeaconStation))
{
minDistance = 3000;
}
@@ -554,28 +583,24 @@ namespace Barotrauma
}
}
if (!spawnReady) { return; }
Entity targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
#if CLIENT
if (Character.Controlled != null) { targetEntity = Character.Controlled; }
#endif
bool monstersDead = true;
foreach (Character monster in monsters)
if (spawnReady)
{
if (!monster.IsDead)
if (monsters.None())
{
monstersDead = false;
if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f)
Finish();
}
else if (monsters.All(m => m.IsDead))
{
if (resetTime > 0)
{
break;
resetTimer = resetTime;
}
else
{
Finish();
}
}
}
if (monstersDead) { Finished(); }
}
}
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -12,6 +11,7 @@ namespace Barotrauma
private readonly Dictionary<Identifier, List<Entity>> cachedTargets = new Dictionary<Identifier, List<Entity>>();
private int prevEntityCount;
private int prevPlayerCount, prevBotCount;
private Character prevControlled;
private readonly string[] requiredDestinationTypes;
public readonly bool RequireBeaconStation;
@@ -163,24 +163,25 @@ namespace Barotrauma
botCount++;
}
}
if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount)
if (Entity.EntityCount != prevEntityCount || botCount != prevBotCount || playerCount != prevPlayerCount || prevControlled != Character.Controlled)
{
cachedTargets.Clear();
prevEntityCount = Entity.EntityCount;
prevBotCount = botCount;
prevPlayerCount = playerCount;
prevControlled = Character.Controlled;
}
if (!Actions.Any())
{
Finished();
Finish();
return;
}
var currentAction = Actions[CurrentActionIndex];
if (!currentAction.CanBeFinished())
{
Finished();
Finish();
return;
}
@@ -207,7 +208,7 @@ namespace Barotrauma
if (CurrentActionIndex >= Actions.Count || CurrentActionIndex < 0)
{
Finished();
Finish();
}
}
else
@@ -232,9 +233,9 @@ namespace Barotrauma
return false;
}
public override void Finished()
public override void Finish()
{
base.Finished();
base.Finish();
GameAnalyticsManager.AddDesignEvent($"ScriptedEvent:{prefab.Identifier}:Finished:{CurrentActionIndex}");
}
}
@@ -339,11 +339,6 @@ namespace Barotrauma
loadContext = null;
assembly = null;
}
~Implementation()
{
OnQuit();
}
}
private static Implementation? loadedImplementation;
@@ -1,74 +1,141 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
#warning TODO: This class needs some changes:
// - We shouldn't be iterating over MapEntityPrefab.List. It has no guarantee of any sort of order and becomes entirely unpredictable once you start adding mods.
// - Note: iterating over ItemPrefab.Prefabs would also be incorrect. Sorting by UintIdentifier is necessary for determinism.
// - SpawnItems and SpawnItem are named incorrectly.
static class AutoItemPlacer
{
public static bool OutputDebugInfo = false;
/// <summary>
/// If we are spawning in an area where difficulty should not be a factor, assume difficulty is at the exact "middle"
/// </summary>
public const float DefaultDifficultyModifier = 0f;
public static void PlaceIfNeeded()
public static void SpawnItems(Identifier? startItemSet = null)
{
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
for (int i = 0; i < Submarine.MainSubs.Length; i++)
//player has more than one sub = we must have given the start items already
bool startItemsGiven = GameMain.GameSession?.OwnedSubmarines != null && GameMain.GameSession.OwnedSubmarines.Count > 1;
if (!startItemsGiven)
{
if (Submarine.MainSubs[i] == null || Submarine.MainSubs[i].Info.InitialSuppliesSpawned) { continue; }
List<Submarine> subs = new List<Submarine>() { Submarine.MainSubs[i] };
subs.AddRange(Submarine.MainSubs[i].DockedTo.Where(d => !d.Info.IsOutpost));
Place(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
for (int i = 0; i < Submarine.MainSubs.Length; i++)
{
var sub = Submarine.MainSubs[i];
if (sub == null || sub.Info.InitialSuppliesSpawned || !sub.Info.IsPlayer) { continue; }
//1st pass: items defined in the start item set, only spawned in the main sub (not drones/shuttles or other linked subs)
SpawnStartItems(sub, startItemSet);
//2nd pass: items defined using preferred containers, spawned in the main sub and all the linked subs (drones, shuttles etc)
var subs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID);
CreateAndPlace(subs);
subs.ForEach(s => s.Info.InitialSuppliesSpawned = true);
}
}
float difficultyModifier = GetLevelDifficultyModifier();
//spawn items in wrecks, beacon stations and pirate subs
foreach (var sub in Submarine.Loaded)
{
if (sub.Info.Type == SubmarineType.Player ||
sub.Info.Type == SubmarineType.Outpost ||
sub.Info.Type == SubmarineType.OutpostModule ||
sub.Info.Type == SubmarineType.EnemySubmarine)
sub.Info.Type == SubmarineType.OutpostModule)
{
continue;
}
Place(sub.ToEnumerable(), difficultyModifier: difficultyModifier);
if (sub.Info.InitialSuppliesSpawned) { continue; }
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
if (Level.Loaded?.StartOutpost != null && Level.Loaded.Type == LevelData.LevelType.Outpost)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(Level.Loaded.StartOutpost.Info.Name));
Place(Level.Loaded.StartOutpost.ToEnumerable());
var sub = Level.Loaded.StartOutpost;
if (!sub.Info.InitialSuppliesSpawned)
{
Rand.SetSyncedSeed(ToolBox.StringToInt(sub.Info.Name));
CreateAndPlace(sub.ToEnumerable());
sub.Info.InitialSuppliesSpawned = true;
}
}
}
private const float MaxDifficultyModifier = 0.2f;
/// <summary>
/// Spawn probability of loot is modified by difficulty, -20% less loot at 0% difficulty and +20% loot at 100% difficulty.
/// </summary>
private static float GetLevelDifficultyModifier()
{
return Math.Clamp(Level.Loaded?.Difficulty is float difficulty ? (difficulty / 100f) * (MaxDifficultyModifier * 2) - MaxDifficultyModifier : DefaultDifficultyModifier, -MaxDifficultyModifier, MaxDifficultyModifier);
}
public static void RegenerateLoot(Submarine sub, ItemContainer regeneratedContainer)
{
// Level difficulty currently doesn't affect regenerated loot for the sake of simplicity
Place(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
CreateAndPlace(sub.ToEnumerable(), regeneratedContainer: regeneratedContainer);
}
private static void Place(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null, float difficultyModifier = DefaultDifficultyModifier)
public static Identifier DefaultStartItemSet = new Identifier("normal");
/// <summary>
/// Spawns the items defined in the start item set in the specified sub.
/// </summary>
private static void SpawnStartItems(Submarine sub, Identifier? startItemSet)
{
Identifier setIdentifier = startItemSet ?? DefaultStartItemSet;
if (!StartItemSet.Sets.TryGet(setIdentifier, out StartItemSet itemSet))
{
DebugConsole.AddWarning($"Couldn't find a start item set matching the identifier \"{setIdentifier}\"!");
if (!StartItemSet.Sets.TryGet(DefaultStartItemSet, out StartItemSet defaultSet))
{
DebugConsole.ThrowError($"Couldn't find the default start item set \"{DefaultStartItemSet}\"!");
return;
}
itemSet = defaultSet;
}
WayPoint wp = WayPoint.GetRandom(SpawnType.Cargo, null, sub);
ISpatialEntity initialSpawnPos;
if (wp?.CurrentHull == null)
{
var spawnHull = Hull.HullList.Where(h => h.Submarine == sub && !h.IsWetRoom).GetRandomUnsynced();
if (spawnHull == null)
{
DebugConsole.AddWarning($"Failed to spawn start items in the sub. No cargo waypoint or dry hulls found to spawn the items in.");
return;
}
initialSpawnPos = spawnHull;
}
else
{
initialSpawnPos = wp;
}
var newItems = new List<Item>();
foreach (var startItem in itemSet.Items)
{
if (!ItemPrefab.Prefabs.TryGet(startItem.Item, out ItemPrefab itemPrefab))
{
DebugConsole.AddWarning($"Cannot find a start item with with the identifier \"{startItem.Item}\"");
continue;
}
for (int i = 0; i < startItem.Amount; i++)
{
var item = new Item(itemPrefab, initialSpawnPos.Position, sub, callOnItemLoaded: false);
// Is this necessary?
foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = sub.TeamID;
}
newItems.Add(item);
}
}
var cargoContainers = new List<ItemContainer>();
foreach (var item in newItems)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
var container = sub.FindContainerFor(item, onlyPrimary: true);
if (container == null)
{
var cargoContainer = CargoManager.GetOrCreateCargoContainerFor(item.Prefab, initialSpawnPos, ref cargoContainers);
container = cargoContainer?.Item;
}
container?.OwnInventory.TryPutItem(item, user: null);
}
}
private static void CreateAndPlace(IEnumerable<Submarine> subs, ItemContainer regeneratedContainer = null)
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
@@ -76,7 +143,7 @@ namespace Barotrauma
return;
}
List<Item> spawnedItems = new List<Item>(100);
List<Item> itemsToSpawn = new List<Item>(100);
int itemCountApprox = MapEntityPrefab.List.Count() / 3;
var containers = new List<ItemContainer>(70 + 30 * subs.Count());
@@ -100,11 +167,11 @@ namespace Barotrauma
containers.Shuffle(Rand.RandSync.ServerAndClient);
}
foreach (ItemPrefab ip in ItemPrefab.Prefabs)
var itemPrefabs = ItemPrefab.Prefabs.OrderBy(p => p.UintIdentifier);
foreach (ItemPrefab ip in itemPrefabs)
{
if (!ip.PreferredContainers.Any()) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) &&
ItemPrefab.Prefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
if (ip.PreferredContainers.None()) { continue; }
if (ip.ConfigElement.Elements().Any(e => string.Equals(e.Name.ToString(), typeof(ItemContainer).Name.ToString(), StringComparison.OrdinalIgnoreCase)) && itemPrefabs.Any(ip2 => CanSpawnIn(ip2, ip)))
{
prefabsItemsCanSpawnIn.Add(ip);
}
@@ -141,9 +208,9 @@ namespace Barotrauma
{
var subNames = subs.Select(s => s.Info.Name).ToList();
DebugConsole.NewMessage($"Automatically placed items in { string.Join(", ", subNames) }:");
foreach (string itemName in spawnedItems.Select(it => it.Name).Distinct())
foreach (string itemName in itemsToSpawn.Select(it => it.Name).Distinct())
{
DebugConsole.NewMessage(" - " + itemName + " x" + spawnedItems.Count(it => it.Name == itemName));
DebugConsole.NewMessage(" - " + itemName + " x" + itemsToSpawn.Count(it => it.Name == itemName));
}
}
@@ -153,24 +220,28 @@ namespace Barotrauma
{
foreach (Location.TakenItem takenItem in GameMain.GameSession.StartLocation.TakenItems)
{
var matchingItem = spawnedItems.Find(it => takenItem.Matches(it));
var matchingItem = itemsToSpawn.Find(it => takenItem.Matches(it));
if (matchingItem == null) { continue; }
var containedItems = spawnedItems.FindAll(it => it.ParentInventory?.Owner == matchingItem);
if (OutputDebugInfo)
{
DebugConsole.NewMessage($"Removing the stolen item: {matchingItem.Prefab.Identifier} ({matchingItem.ID})");
}
var containedItems = itemsToSpawn.FindAll(it => it.ParentInventory?.Owner == matchingItem);
matchingItem.Remove();
spawnedItems.Remove(matchingItem);
itemsToSpawn.Remove(matchingItem);
foreach (Item containedItem in containedItems)
{
containedItem.Remove();
spawnedItems.Remove(containedItem);
itemsToSpawn.Remove(containedItem);
}
}
}
foreach (Item spawnedItem in spawnedItems)
foreach (Item item in itemsToSpawn)
{
#if SERVER
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(spawnedItem));
Entity.Spawner.CreateNetworkEvent(new EntitySpawner.SpawnEntity(item));
#endif
foreach (ItemComponent ic in spawnedItem.Components)
foreach (ItemComponent ic in item.Components)
{
ic.OnItemLoaded();
}
@@ -186,9 +257,12 @@ namespace Barotrauma
return false;
}
bool success = false;
bool isCampaign = GameMain.GameSession?.GameMode is CampaignMode;
foreach (PreferredContainer preferredContainer in itemPrefab.PreferredContainers)
{
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0) { continue; }
if (preferredContainer.CampaignOnly && !isCampaign) { continue; }
if (preferredContainer.NotCampaign && isCampaign) { continue; }
if (preferredContainer.SpawnProbability <= 0.0f || preferredContainer.MaxAmount <= 0 && preferredContainer.Amount <= 0) { continue; }
validContainers = GetValidContainers(preferredContainer, containers, validContainers, primary: true);
if (validContainers.None())
{
@@ -196,10 +270,10 @@ namespace Barotrauma
}
foreach (var validContainer in validContainers)
{
var newItems = SpawnItem(itemPrefab, containers, validContainer, difficultyModifier);
var newItems = CreateItems(itemPrefab, containers, validContainer);
if (newItems.Any())
{
spawnedItems.AddRange(newItems);
itemsToSpawn.AddRange(newItems);
success = true;
}
}
@@ -238,16 +312,20 @@ namespace Barotrauma
(3, 0.0f),
};
private static List<Item> SpawnItem(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer, float difficultyModifier)
private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{
List<Item> spawnedItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability * (1f + difficultyModifier)) { return spawnedItems; }
List<Item> newItems = new List<Item>();
if (Rand.Value(Rand.RandSync.ServerAndClient) > validContainer.Value.SpawnProbability) { return newItems; }
// Don't add dangerously reactive materials in thalamus wrecks
if (validContainer.Key.Item.Submarine.WreckAI != null && itemPrefab.Tags.Contains("explodesinwater"))
{
return spawnedItems;
return newItems;
}
int amount = validContainer.Value.Amount;
if (amount == 0)
{
amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
}
int amount = Rand.Range(validContainer.Value.MinAmount, validContainer.Value.MaxAmount + 1, Rand.RandSync.ServerAndClient);
for (int i = 0; i < amount; i++)
{
if (validContainer.Key.Inventory.IsFull(takeStacksIntoAccount: true))
@@ -255,14 +333,12 @@ namespace Barotrauma
containers.Remove(validContainer.Key);
break;
}
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab);
int quality =
existingItem?.Quality ??
ToolBox.SelectWeightedRandom(
qualityCommonnesses.Select(q => q.quality).ToList(),
qualityCommonnesses.Select(q => q.commonness).ToList(),
Rand.RandSync.ServerAndClient);
qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient);
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{
@@ -277,11 +353,11 @@ namespace Barotrauma
{
wifiComponent.TeamID = validContainer.Key.Item.Submarine.TeamID;
}
spawnedItems.Add(item);
newItems.Add(item);
validContainer.Key.Inventory.TryPutItem(item, null, createNetworkEvent: false);
containers.AddRange(item.GetComponents<ItemContainer>());
}
return spawnedItems;
return newItems;
}
}
}

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