Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-12-14 11:56:39 -03:00
376 changed files with 7775 additions and 2879 deletions
@@ -91,6 +91,11 @@ namespace Barotrauma
private IEnumerable<Hull> visibleHulls;
private float hullVisibilityTimer;
const float hullVisibilityInterval = 0.5f;
/// <summary>
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// </summary>
public IEnumerable<Hull> VisibleHulls
{
get
@@ -353,7 +358,7 @@ namespace Barotrauma
public static void UnequipContainedItems(Character character, Item parentItem, Func<Item, bool> predicate, bool avoidDroppingInSea = true, int? unequipMax = null)
{
var inventory = parentItem.OwnInventory;
if (inventory == null) { return; }
if (inventory == null || !inventory.Container.DrawInventory) { return; }
int removed = 0;
if (predicate == null || inventory.AllItems.Any(predicate))
{
@@ -262,7 +262,8 @@ namespace Barotrauma
if (aiElements.Count == 0)
{
DebugConsole.ThrowError("Error in file \"" + c.Params.File + "\" - no AI element found.");
DebugConsole.ThrowError("Error in file \"" + c.Params.File.Path + "\" - no AI element found.",
contentPackage: c.Prefab?.ContentPackage);
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, false, false);
return;
@@ -311,7 +312,7 @@ namespace Barotrauma
}
ReevaluateAttacks();
outsideSteering = new SteeringManager(this);
insideSteering = new IndoorsSteeringManager(this, Character.Params.AI.CanOpenDoors, canAttackDoors);
insideSteering = new IndoorsSteeringManager(this, AIParams.CanOpenDoors, canAttackDoors);
steeringManager = outsideSteering;
State = AIState.Idle;
requiredHoleCount = (int)Math.Ceiling(ConvertUnits.ToDisplayUnits(colliderWidth) / Structure.WallSectionSize);
@@ -321,6 +322,10 @@ namespace Barotrauma
}
private CharacterParams.AIParams _aiParams;
/// <summary>
/// Shorthand for <see cref="Character.Params.AI"/> with null checking.
/// </summary>
/// <returns><see cref="Character.Params.AI"/> or an empty params. Does not return nulls.</returns>
public CharacterParams.AIParams AIParams
{
get
@@ -330,7 +335,8 @@ namespace Barotrauma
_aiParams = Character.Params.AI;
if (_aiParams == null)
{
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.");
DebugConsole.ThrowError($"No AI Params defined for {Character.SpeciesName}. AI disabled.",
contentPackage: Character.Prefab.ContentPackage);
Enabled = false;
_aiParams = new CharacterParams.AIParams(null, Character.Params);
}
@@ -563,7 +569,7 @@ namespace Barotrauma
}
}
if (Character.Params.UsePathFinding && Character.Params.AI.UsePathFindingToGetInside && AIParams.CanOpenDoors)
if (Character.Params.UsePathFinding && AIParams.UsePathFindingToGetInside && AIParams.CanOpenDoors)
{
// Meant for monsters outside the player sub that target something inside the sub and can use the doors to access the sub (Husk).
bool IsCloseEnoughToTargetSub(float threshold) => SelectedAiTarget?.Entity?.Submarine is Submarine sub && sub != null && Vector2.DistanceSquared(Character.WorldPosition, sub.WorldPosition) < MathUtils.Pow(Math.Max(sub.Borders.Size.X, sub.Borders.Size.Y) / 2 + threshold, 2);
@@ -2503,7 +2509,8 @@ namespace Barotrauma
Limb mouthLimb = Character.AnimController.GetLimb(LimbType.Head);
if (mouthLimb == null)
{
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)");
DebugConsole.ThrowError("Character \"" + Character.SpeciesName + "\" failed to eat a target (No head limb defined)",
contentPackage: Character.Prefab.ContentPackage);
State = AIState.Idle;
return;
}
@@ -2540,7 +2547,11 @@ namespace Barotrauma
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.LastEatenTime = (float)Timing.TotalTimeUnpaused;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
item.AddDamage(Character,
item.WorldPosition,
new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed),
impulseDirection: Vector2.Zero,
deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
if (item.Condition <= 0.0f)
{
@@ -3090,7 +3101,10 @@ namespace Barotrauma
break;
}
valueModifier *= targetMemory.Priority / (float)Math.Sqrt(dist);
valueModifier *=
targetMemory.Priority /
//sqrt = the further the target is, the less the distance matters
MathF.Sqrt(dist);
if (valueModifier > targetValue)
{
@@ -685,8 +685,8 @@ namespace Barotrauma
}
if (removeDivingSuit)
{
var divingSuit = Character.Inventory.FindItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors))
var divingSuit = Character.Inventory.FindEquippedItemByTag(Tags.HeavyDivingGear);
if (divingSuit != null && !divingSuit.HasTag(Tags.DivingGearWearableIndoors) && divingSuit.IsInteractable(Character))
{
if (shouldActOnSuffocation || Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
@@ -727,54 +727,51 @@ namespace Barotrauma
}
}
if (takeMaskOff)
{
if (Character.HasEquippedItem(Tags.LightDivingGear))
{
var mask = Character.Inventory.FindEquippedItemByTag(Tags.LightDivingGear);
if (mask != null)
{
var mask = Character.Inventory.FindItemByTag(Tags.LightDivingGear);
if (mask != null)
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
{
if (!mask.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(mask, Character, new List<InvSlotType>() { InvSlotType.Any }))
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
{
if (Character.Submarine?.TeamID != Character.TeamID || ObjectiveManager.GetCurrentPriority() >= AIObjectiveManager.RunPriority)
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
else if (findItemState == FindItemState.None || findItemState == FindItemState.DivingMask)
{
findItemState = FindItemState.DivingMask;
if (FindSuitableContainer(mask, out Item targetContainer))
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
{
findItemState = FindItemState.None;
itemIndex = 0;
if (targetContainer != null)
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
var decontainObjective = new AIObjectiveDecontainItem(Character, mask, ObjectiveManager, targetContainer: targetContainer.GetComponent<ItemContainer>());
decontainObjective.Abandoned += () =>
{
ReequipUnequipped();
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
IgnoredItems.Add(targetContainer);
};
decontainObjective.Completed += () => ReequipUnequipped();
ObjectiveManager.CurrentObjective.AddSubObjective(decontainObjective, addFirst: true);
return;
}
else
{
mask.Drop(Character);
HandleRelocation(mask);
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
else
{
ReequipUnequipped();
}
}
}
}
}
@@ -784,13 +781,11 @@ namespace Barotrauma
if (findItemState == FindItemState.None || findItemState == FindItemState.OtherItem)
{
for (int i = 0; i < 2; i++)
foreach (Item item in Character.HeldItems)
{
var hand = i == 0 ? InvSlotType.RightHand : InvSlotType.LeftHand;
Item item = Character.Inventory.GetItemInLimbSlot(hand);
if (item == null) { continue; }
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Any }) && Character.Submarine?.TeamID == Character.TeamID )
if (!item.AllowedSlots.Contains(InvSlotType.Any) || !Character.Inventory.TryPutItem(item, Character, CharacterInventory.AnySlot) && Character.Submarine?.TeamID == Character.TeamID)
{
if (item.AllowedSlots.Contains(InvSlotType.Bag) && Character.Inventory.TryPutItem(item, Character, new List<InvSlotType>() { InvSlotType.Bag })) { continue; }
findItemState = FindItemState.OtherItem;
@@ -1389,7 +1384,10 @@ namespace Barotrauma
// Don't react to friendly enemy AI attacking other characters. E.g. husks attacking someone when whe are a cultist.
continue;
}
bool isWitnessing = otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) || otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull);
bool isWitnessing =
otherHumanAI.VisibleHulls.Contains(Character.CurrentHull) ||
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
//if the other character did not witness the attack, and the character is not within report range (or capable of reporting)
@@ -1754,11 +1752,11 @@ namespace Barotrauma
if (otherCharacter == character || otherCharacter.TeamID == character.TeamID || otherCharacter.IsDead ||
otherCharacter.Info?.Job == null ||
otherCharacter.AIController is not HumanAIController otherHumanAI ||
!otherHumanAI.VisibleHulls.Contains(character.CurrentHull))
Vector2.DistanceSquared(otherCharacter.WorldPosition, character.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
if (!otherCharacter.CanSeeTarget(character)) { continue; }
if (!otherCharacter.CanSeeTarget(character, seeThroughWindows: true)) { continue; }
if (!otherHumanAI.structureDamageAccumulator.ContainsKey(character)) { otherHumanAI.structureDamageAccumulator.Add(character, 0.0f); }
float prevAccumulatedDamage = otherHumanAI.structureDamageAccumulator[character];
@@ -1796,7 +1794,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
{
@@ -1840,13 +1838,13 @@ namespace Barotrauma
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter == thief || otherCharacter.TeamID == thief.TeamID || otherCharacter.IsIncapacitated || otherCharacter.Stun > 0.0f ||
otherCharacter.Info?.Job == null || !(otherCharacter.AIController is HumanAIController otherHumanAI) ||
!otherHumanAI.VisibleHulls.Contains(thief.CurrentHull))
otherCharacter.Info?.Job == null || otherCharacter.AIController is not HumanAIController otherHumanAI ||
Vector2.DistanceSquared(otherCharacter.WorldPosition, thief.WorldPosition) > 1000.0f * 1000.0f)
{
continue;
}
//if (!otherCharacter.IsFacing(thief.WorldPosition)) { continue; }
if (!otherCharacter.CanSeeTarget(thief)) { continue; }
if (!otherCharacter.CanSeeTarget(thief, seeThroughWindows: true)) { continue; }
// Don't react if the player is taking an extinguisher and there's any fires on the sub, or diving gear when the sub is flooding
// -> allow them to use the emergency items
if (thief.Submarine != null)
@@ -1857,16 +1855,11 @@ namespace Barotrauma
}
if (!someoneSpoke)
{
if (!item.StolenDuringRound &&
Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
if (!item.StolenDuringRound)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
ApplyStealingReputationLoss(item);
item.StolenDuringRound = true;
}
item.StolenDuringRound = true;
otherCharacter.Speak(TextManager.Get("dialogstealwarning").Value, null, Rand.Range(0.5f, 1.0f), "thief".ToIdentifier(), 10.0f);
someoneSpoke = true;
#if CLIENT
@@ -1877,7 +1870,7 @@ namespace Barotrauma
if (!TriggerSecurity(otherHumanAI))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderByDescending(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(thief.WorldPosition, c.WorldPosition)))
{
if (TriggerSecurity(security.AIController as HumanAIController))
{
@@ -1898,6 +1891,10 @@ namespace Barotrauma
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
if (humanAI.ObjectiveManager.GetObjective<AIObjectiveFindThieves>() is { } findThieves)
{
findThieves.InspectEveryone();
}
humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, thief, delay: GetReactionTime(),
abortCondition: obj => thief.Inventory.FindItem(it => it != null && it.StolenDuringRound, true) == null,
onAbort: () =>
@@ -1915,6 +1912,18 @@ namespace Barotrauma
}
}
public static void ApplyStealingReputationLoss(Item item)
{
if (Level.Loaded?.Type == LevelData.LevelType.Outpost &&
GameMain.GameSession?.Campaign?.Map?.CurrentLocation != null)
{
var reputationLoss = MathHelper.Clamp(
(item.Prefab.GetMinPrice() ?? 0) * Reputation.ReputationLossPerStolenItemPrice,
Reputation.MinReputationLossPerStolenItem, Reputation.MaxReputationLossPerStolenItem);
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation?.AddReputation(-reputationLoss);
}
}
// 0.225 - 0.375
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
@@ -197,17 +197,6 @@ namespace Barotrauma
}
}
/// <summary>
/// This method allows multiple subobjectives of same type. Use with caution.
/// </summary>
public void AddSubObjectiveInQueue(AIObjective objective)
{
if (!subObjectives.Contains(objective))
{
subObjectives.Add(objective);
}
}
public void RemoveSubObjective<T>(ref T objective) where T : AIObjective
{
if (objective != null)
@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveCheckStolenItems : AIObjective
{
public override Identifier Identifier { get; set; } = "check stolen items".ToIdentifier();
public override bool AllowOutsideSubmarine => false;
public override bool AllowInAnySub => false;
public float FindStolenItemsProbability = 1.0f;
enum State
{
GotoTarget,
Inspect,
Warn,
Done
}
private float inspectDelay;
private float warnDelay;
private State currentState;
public readonly Character TargetCharacter;
private AIObjectiveGoTo? goToObjective;
private readonly List<Item> stolenItems = new List<Item>();
public AIObjectiveCheckStolenItems(Character character, Character targetCharacter, AIObjectiveManager objectiveManager, float priorityModifier = 1) :
base(character, objectiveManager, priorityModifier)
{
TargetCharacter = targetCharacter;
inspectDelay = 5.0f;
warnDelay = 5.0f;
}
public override bool IsLoop
{
get => false;
set => throw new Exception("Trying to set the value for IsLoop from: " + Environment.StackTrace.CleanupStackTrace());
}
protected override bool CheckObjectiveSpecific() => false;
protected override float GetPriority()
{
if (!Abandon && !IsCompleted && objectiveManager.IsOrder(this))
{
Priority = objectiveManager.GetOrderPriority(this);
}
else
{
Priority = AIObjectiveManager.LowestOrderPriority - 1;
}
return Priority;
}
public void ForceComplete()
{
IsCompleted = true;
}
protected override void Act(float deltaTime)
{
switch (currentState)
{
case State.GotoTarget:
TryAddSubObjective(ref goToObjective,
constructor: () =>
{
return new AIObjectiveGoTo(TargetCharacter, character, objectiveManager, repeat: false)
{
SpeakIfFails = false
};
},
onCompleted: () =>
{
RemoveSubObjective(ref goToObjective);
currentState = State.Inspect;
stolenItems.Clear();
TargetCharacter.Inventory.FindAllItems(it => it.SpawnedInCurrentOutpost && !it.AllowStealing, recursive: true, stolenItems);
character.Speak(TextManager.Get("dialogcheckstolenitems").Value);
},
onAbandon: () =>
{
Abandon = true;
});
break;
case State.Inspect:
Inspect(deltaTime);
break;
case State.Warn:
Warn(deltaTime);
break;
}
}
private void Inspect(float deltaTime)
{
if (inspectDelay > 0.0f)
{
character.SelectCharacter(TargetCharacter);
inspectDelay -= deltaTime;
return;
}
if (stolenItems.Any() &&
Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) < FindStolenItemsProbability)
{
character.Speak(TextManager.Get("dialogcheckstolenitems.warn").Value);
currentState = State.Warn;
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.nostolenitems").Value);
currentState = State.Done;
IsCompleted = true;
}
character.DeselectCharacter();
}
private void Warn(float deltaTime)
{
if (warnDelay > 0.0f)
{
warnDelay -= deltaTime;
return;
}
var stolenItemsOnCharacter = stolenItems.Where(it => it.GetRootInventoryOwner() == TargetCharacter);
if (stolenItemsOnCharacter.Any())
{
character.Speak(TextManager.Get("dialogcheckstolenitems.arrest").Value);
HumanAIController.AddCombatObjective(AIObjectiveCombat.CombatMode.Arrest, TargetCharacter);
foreach (var stolenItem in stolenItemsOnCharacter)
{
HumanAIController.ApplyStealingReputationLoss(stolenItem);
}
}
else
{
character.Speak(TextManager.Get("dialogcheckstolenitems.comply").Value);
}
foreach (var item in stolenItems)
{
HumanAIController.ObjectiveManager.AddObjective(new AIObjectiveGetItem(character, item, objectiveManager, equip: false)
{
BasePriority = 10
});
}
currentState = State.Done;
IsCompleted = true;
}
}
}
@@ -1070,7 +1070,8 @@ namespace Barotrauma
{
// Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag(Tags.MobileRadio);
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
Item ammunition = character.Inventory.FindItem(i =>
i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i) && i.IsInteractable(character), recursive: true);
if (ammunition != null)
{
var container = Weapon.GetComponent<ItemContainer>();
@@ -1089,6 +1090,9 @@ namespace Barotrauma
}
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
// Inventory not drawn = it's not interactable
// If the weapon is empty and the inventory is inaccessible, it can't be reloaded
if (!Weapon.OwnInventory.Container.DrawInventory) { return false; }
SeekAmmunition(ammunitionIdentifiers);
}
return false;
@@ -14,7 +14,7 @@ namespace Barotrauma
private int escapeProgress;
private bool isBeingWatched;
private bool shouldSwitchTeams;
private readonly bool shouldSwitchTeams;
const string EscapeTeamChangeIdentifier = "escape";
@@ -88,10 +88,12 @@ namespace Barotrauma
escapeProgress += Rand.Range(2, 5);
if (escapeProgress > 15)
{
Item handcuffs = character.Inventory.FindItemByTag(Tags.HandLockerItem);
if (handcuffs != null)
foreach (var it in character.HeldItems)
{
handcuffs.Drop(character);
if (it.HasTag(Tags.HandLockerItem) && it.IsInteractable(character))
{
it.Drop(character);
}
}
}
escapeTimer = EscapeIntervalTimer * Rand.Range(0.75f, 1.25f);
@@ -0,0 +1,152 @@
#nullable enable
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
class AIObjectiveFindThieves : AIObjectiveLoop<Character>
{
public override Identifier Identifier { get; set; } = "find thieves".ToIdentifier();
protected override float IgnoreListClearInterval => 30;
public override bool IgnoreUnsafeHulls => true;
protected override float TargetUpdateTimeMultiplier => 1.0f;
const float DefaultInspectDistance = 200.0f;
/// <summary>
/// How close the NPC must be to the target to the inspect them? You can use high values to make the NPC
/// systematically go through targets no matter where they are, and low values to check targets they happen to come across.
/// </summary>
public float InspectDistance = DefaultInspectDistance;
private float? overrideInspectProbability;
/// <summary>
/// Chance of inspecting a valid target. The NPC won't try to inspect that target again for <see cref="inspectionInterval"/>
/// regardless if the target is inspected or not.
/// </summary>
public float InspectProbability
{
get
{
if (overrideInspectProbability.HasValue)
{
return overrideInspectProbability.Value;
}
if (GameMain.GameSession?.Campaign is { } campaign)
{
if (campaign.Map?.CurrentLocation?.Reputation is { } reputation)
{
return MathHelper.Lerp(
campaign.Settings.MaxStolenItemInspectionProbability,
campaign.Settings.MinStolenItemInspectionProbability,
reputation.NormalizedValue);
}
}
return 0.2f;
}
}
/// <summary>
/// When did the character last inspect whether some other character has stolen items on them?
/// </summary>
private static readonly Dictionary<Character, double> lastInspectionTimes = new Dictionary<Character, double>();
private readonly float inspectionInterval = 120.0f;
public AIObjectiveFindThieves(Character character, AIObjectiveManager objectiveManager, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) { }
protected override bool Filter(Character target)
{
if (!IsValidTarget(target, character)) { return false; }
if (Vector2.DistanceSquared(target.WorldPosition, character.WorldPosition) > InspectDistance * InspectDistance) { return false; }
if (lastInspectionTimes.TryGetValue(target, out double lastInspectionTime))
{
if (Timing.TotalTime < lastInspectionTime + inspectionInterval)
{
return false;
}
}
return true;
}
protected override IEnumerable<Character> GetList() => Character.CharacterList;
protected override float TargetEvaluation()
{
return subObjectives.Any() ? 50 : 0;
}
public void InspectEveryone()
{
lastInspectionTimes.Clear();
overrideInspectProbability = 1.0f;
InspectDistance = DefaultInspectDistance * 2;
}
protected override AIObjective ObjectiveConstructor(Character target)
{
var checkStolenItemsObjective = new AIObjectiveCheckStolenItems(character, target, objectiveManager);
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.Unsynced) >= InspectProbability)
{
checkStolenItemsObjective.ForceComplete();
lastInspectionTimes[target] = Timing.TotalTime;
}
return checkStolenItemsObjective;
}
private float checkVisibleStolenItemsTimer;
private const float CheckVisibleStolenItemsInterval = 5.0f;
public override void Update(float deltaTime)
{
base.Update(deltaTime);
if (checkVisibleStolenItemsTimer > 0.0f)
{
checkVisibleStolenItemsTimer -= deltaTime;
return;
}
foreach (var target in Character.CharacterList)
{
if (!IsValidTarget(target, character)) { continue; }
//if we spot someone wearing or holding stolen items, immediately check them (with 100% chance of spotting the stolen items)
if (target.Inventory.AllItems.Any(it => it.SpawnedInCurrentOutpost && !it.AllowStealing && target.HasEquippedItem(it)) &&
character.CanSeeTarget(target, seeThroughWindows: true))
{
AIObjectiveCheckStolenItems? existingObjective =
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
if (existingObjective == null)
{
objectiveManager.AddObjective(new AIObjectiveCheckStolenItems(character, target, objectiveManager));
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
checkVisibleStolenItemsTimer = CheckVisibleStolenItemsInterval;
}
private bool IsValidTarget(Character target, Character character)
{
if (target == null || target.Removed) { return false; }
if (target.IsIncapacitated) { return false; }
if (target == character) { return false; }
if (target.Submarine == null) { return false; }
if (character.Submarine == null) { return false; }
if (target.CurrentHull == null) { return false; }
if (target.Submarine != character.Submarine) { return false; }
//only player's crew can steal, ignore other teams
if (!target.IsOnPlayerTeam) { return false; }
if (target.IsArrested) { return false; }
return true;
}
protected override void OnObjectiveCompleted(AIObjective objective, Character target)
{
lastInspectionTimes[target] = Timing.TotalTime;
}
}
}
@@ -120,7 +120,7 @@ namespace Barotrauma
// The intention behind this is to reduce unnecessary path finding calls in cases where the bot can't find a path.
timerMargin += 0.5f;
timerMargin = Math.Min(timerMargin, newTargetIntervalMin);
newTargetTimer = Math.Min(newTargetTimer, timerMargin);
newTargetTimer = Math.Max(newTargetTimer, timerMargin);
}
private void SetTargetTimerHigh()
@@ -178,7 +178,7 @@ namespace Barotrauma
if (!objectiveManager.IsOrder(this))
{
// Battery or pump states cannot currently be reported (not implemented) and therefore we must ignore them -> the bots always know if they require attention.
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater;
bool ignore = this is AIObjectiveChargeBatteries || this is AIObjectivePumpWater || this is AIObjectiveFindThieves;
if (!ignore && !ReportedTargets.Contains(target)) { continue; }
}
if (!Filter(target)) { continue; }
@@ -151,6 +151,7 @@ namespace Barotrauma
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -558,6 +559,9 @@ namespace Barotrauma
case "escapehandcuffs":
newObjective = new AIObjectiveEscapeHandcuffs(character, this, priorityModifier: priorityModifier);
break;
case "findthieves":
newObjective = new AIObjectiveFindThieves(character, this, priorityModifier: priorityModifier);
break;
case "prepareforexpedition":
newObjective = new AIObjectivePrepare(character, this, order.GetTargetItems(order.Option), order.RequireItems)
{
@@ -441,7 +441,8 @@ namespace Barotrauma
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}");
DebugConsole.LogError($"Error creating a new Order instance: unexpected target type \"{targetType}\".\n{e.StackTrace.CleanupStackTrace()}",
contentPackage: ContentPackage);
return null;
}
}
@@ -663,6 +664,13 @@ namespace Barotrauma
WallSectionIndex = wallSectionIndex ?? other.WallSectionIndex;
UseController = useController ?? other.UseController;
#if DEBUG
if (UseController && ConnectedController == null)
{
DebugConsole.ThrowError($"AI: Created an Order {Identifier} that's set to use a Controller, but a Controller was not specified.\n{Environment.StackTrace.CleanupStackTrace()}");
}
#endif
}
public Order WithOption(Identifier option)
@@ -712,7 +720,12 @@ namespace Barotrauma
public Order WithItemComponent(Item item, ItemComponent component = null)
{
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item));
Controller controller = null;
if (UseController)
{
controller = item?.FindController(tags: ControllerTags);
}
return new Order(this, targetEntity: item, targetItemComponent: component ?? GetTargetItemComponent(item), connectedController: controller);
}
public Order WithWallSection(Structure wall, int? sectionIndex)
@@ -552,7 +552,8 @@ namespace Barotrauma
#if DEBUG
if (handlePos[i].LengthSquared() > ArmLength)
{
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)");
DebugConsole.AddWarning($"Aim position for the item {item.Name} may be incorrect (further than the length of the character's arm)",
item.Prefab.ContentPackage);
}
#endif
HandIK(
@@ -10,6 +10,17 @@ namespace Barotrauma
{
class HumanoidAnimController : AnimController
{
private const float SteepestWalkableSlopeAngleDegrees = 50f;
private const float SlowlyWalkableSlopeAngleDegrees = 30f;
private static readonly float SteepestWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SteepestWalkableSlopeAngleDegrees));
private static readonly float SlowlyWalkableSlopeNormalX =
MathF.Sin(MathHelper.ToRadians(SlowlyWalkableSlopeAngleDegrees));
private const float MaxSpeedOnStairs = 1.7f;
private const float SteepSlopePushMagnitude = MaxSpeedOnStairs;
public override RagdollParams RagdollParams
{
get { return HumanRagdollParams; }
@@ -150,7 +161,7 @@ namespace Barotrauma
private readonly float movementLerp;
private float cprAnimTimer,cprPump;
private float cprAnimTimer, cprPumpTimer;
private float fallingProneAnimTimer;
const float FallingProneAnimDuration = 1.0f;
@@ -243,14 +254,17 @@ namespace Barotrauma
if (MainLimb == null) { return; }
levitatingCollider = !IsHanging;
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
(character.SelectedSecondaryItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
character.SelectedSecondaryItem?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
if (onGround && character.CanMove)
{
Crouching = false;
if ((character.SelectedItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
(character.SelectedSecondaryItem?.GetComponent<Controller>()?.ControlCharacterPose ?? false) ||
character.SelectedSecondaryItem?.GetComponent<Ladder>() != null ||
(ForceSelectAnimationType != AnimationType.Crouch && ForceSelectAnimationType != AnimationType.NotDefined))
{
Crouching = false;
}
ColliderIndex = Crouching && !swimming ? 1 : 0;
}
ColliderIndex = Crouching && !swimming ? 1 : 0;
//stun (= disable the animations) if the ragdoll receives a large enough impact
if (strongestImpact > 0.0f)
@@ -276,7 +290,7 @@ namespace Barotrauma
if (!character.CanMove)
{
if (fallingProneAnimTimer < FallingProneAnimDuration)
if (fallingProneAnimTimer < FallingProneAnimDuration && onGround)
{
fallingProneAnimTimer += deltaTime;
UpdateFallingProne(1.0f);
@@ -285,7 +299,12 @@ namespace Barotrauma
Collider.FarseerBody.FixedRotation = false;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
Collider.Enabled = false;
if (Collider.Enabled)
{
//deactivating the collider -> make the main limb inherit the collider's velocity because it'll control the movement now
MainLimb.body.LinearVelocity = Collider.LinearVelocity;
Collider.Enabled = false;
}
Collider.LinearVelocity = MainLimb.LinearVelocity;
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
//reset pull joints to prevent the character from "hanging" mid-air if pull joints had been active when the character was still moving
@@ -386,6 +405,12 @@ namespace Barotrauma
DragCharacter(character.SelectedCharacter, deltaTime);
}
if (Anim != Animation.CPR)
{
cprAnimTimer = 0.0f;
cprPumpTimer = 0.0f;
}
switch (Anim)
{
case Animation.Climbing:
@@ -487,10 +512,14 @@ namespace Barotrauma
Limb leftLeg = GetLimb(LimbType.LeftLeg);
Limb rightLeg = GetLimb(LimbType.RightLeg);
bool onSlopeThatMakesSlow = Math.Abs(floorNormal.X) > SlowlyWalkableSlopeNormalX;
bool slowedDownBySlope = onSlopeThatMakesSlow && Math.Sign(floorNormal.X) == -Math.Sign(TargetMovement.X);
bool onSlopeTooSteepToClimb = Math.Abs(floorNormal.X) > SteepestWalkableSlopeNormalX;
float walkCycleMultiplier = 1.0f;
if (Stairs != null)
if (Stairs != null || slowedDownBySlope)
{
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -1.7f, 1.7f), TargetMovement.Y);
TargetMovement = new Vector2(MathHelper.Clamp(TargetMovement.X, -MaxSpeedOnStairs, MaxSpeedOnStairs), TargetMovement.Y);
walkCycleMultiplier *= 1.5f;
}
@@ -572,6 +601,15 @@ namespace Barotrauma
bool movingHorizontally = !MathUtils.NearlyEqual(TargetMovement.X, 0.0f);
if (Stairs == null && onSlopeTooSteepToClimb)
{
if (Math.Sign(targetMovement.X) != Math.Sign(floorNormal.X))
{
targetMovement.X = Math.Sign(floorNormal.X) * SteepSlopePushMagnitude;
movement = targetMovement;
}
}
if (Stairs != null || onSlope)
{
torso.PullJointWorldAnchorB = new Vector2(
@@ -648,14 +686,6 @@ namespace Barotrauma
if (!onGround)
{
Vector2 move = torso.PullJointWorldAnchorB - torso.SimPosition;
foreach (Limb limb in Limbs)
{
if (limb.IsSevered) { continue; }
MoveLimb(limb, limb.SimPosition + move, 15.0f, true);
}
return;
}
@@ -758,7 +788,7 @@ namespace Barotrauma
{
footPos = new Vector2(colliderPos.X + stepSize.X * i * 0.2f, colliderPos.Y - 0.1f);
}
if (Stairs == null)
if (Stairs == null && !onSlopeThatMakesSlow)
{
footPos.Y = Math.Max(Math.Min(FloorY, footPos.Y + 0.5f), footPos.Y);
}
@@ -1318,14 +1348,14 @@ namespace Barotrauma
}
}
void UpdateFallingProne(float strength)
void UpdateFallingProne(float strength, bool moveHands = true, bool moveTorso = true, bool moveLegs = true)
{
if (strength <= 0.0f) { return; }
Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso);
if (head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
if (moveHands && head != null && head.LinearVelocity.LengthSquared() > 1.0f && !head.IsSevered)
{
//if the head is moving, try to protect it with the hands
Limb leftHand = GetLimb(LimbType.LeftHand);
@@ -1347,7 +1377,7 @@ namespace Barotrauma
//make the torso tip over
//otherwise it tends to just drop straight down, pinning the characters legs in a weird pose
if (!InWater)
if (moveTorso && !InWater)
{
//prefer tipping over in the same direction the torso is rotating
//or moving
@@ -1358,27 +1388,30 @@ namespace Barotrauma
}
//attempt to make legs stay in a straight line with the torso to prevent the character from doing a split
for (int i = 0; i < 2; i++)
if (moveLegs)
{
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float diff = torso.Rotation - thigh.Rotation;
if (MathUtils.IsValid(diff))
for (int i = 0; i < 2; i++)
{
float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
}
var thigh = i == 0 ? GetLimb(LimbType.LeftThigh) : GetLimb(LimbType.RightThigh);
if (thigh == null) { continue; }
if (thigh.IsSevered) { continue; }
float thighDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, thigh.Rotation));
float diff = torso.Rotation - thigh.Rotation;
if (MathUtils.IsValid(diff))
{
float thighTorque = thighDiff * thigh.Mass * Math.Sign(diff) * 5.0f;
thigh.body.ApplyTorque(thighTorque * strength);
}
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
diff = torso.Rotation - leg.Rotation;
if (MathUtils.IsValid(diff))
{
float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
var leg = i == 0 ? GetLimb(LimbType.LeftLeg) : GetLimb(LimbType.RightLeg);
if (leg == null || leg.IsSevered) { continue; }
float legDiff = Math.Abs(MathUtils.GetShortestAngle(torso.Rotation, leg.Rotation));
diff = torso.Rotation - leg.Rotation;
if (MathUtils.IsValid(diff))
{
float legTorque = legDiff * leg.Mass * Math.Sign(diff) * 5.0f;
leg.body.ApplyTorque(legTorque * strength);
}
}
}
}
@@ -1398,7 +1431,8 @@ namespace Barotrauma
Crouching = true;
Vector2 diff = target.SimPosition - character.SimPosition;
Vector2 offset = Vector2.UnitX * -Dir * 0.75f;
Vector2 diff = (target.SimPosition + offset) - character.SimPosition;
Limb targetHead = target.AnimController.GetLimb(LimbType.Head);
Limb targetTorso = target.AnimController.GetLimb(LimbType.Torso);
if (targetTorso == null)
@@ -1412,7 +1446,23 @@ namespace Barotrauma
Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition;
targetMovement = new Vector2(diff.X, 0.0f);
const float CloseEnough = 0.1f;
if (Math.Abs(targetMovement.X) < CloseEnough)
{
targetMovement.X = 0.0f;
}
TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left;
//if the target's in some weird pose, we may not be able to flip it so it's facing up,
//so let's only try it once so we don't end up constantly flipping it
if (cprAnimTimer <= 0.0f && target.AnimController.Direction == TargetDir)
{
target.AnimController.Flip();
}
(target.AnimController as HumanoidAnimController)?.UpdateFallingProne(strength: 1.0f, moveHands: false, moveTorso: false);
head.Disabled = true;
torso.Disabled = true;
UpdateStanding();
@@ -1443,79 +1493,69 @@ namespace Barotrauma
}
}
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
if (cprAnimTimer > 15.0f && targetHead != null && head != null)
//Serverside code
if (GameMain.NetworkMember is not { IsClient: true })
{
float yPos = (float)Math.Sin(cprAnimTimer) * 0.2f;
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.3f + yPos);
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
if (targetHead != null && head != null)
{
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f);
head.PullJointEnabled = true;
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.2f));
torso.PullJointEnabled = true;
//Serverside code
if (GameMain.NetworkMember is not { IsClient: true })
{
if (target.Oxygen < -10.0f)
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
}
}
else
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f));
torso.PullJointEnabled = true;
if (cprPumpTimer >= 1)
{
if (targetHead != null && head != null)
torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
cprPumpTimer = 0;
if (skill < CPRSettings.Active.DamageSkillThreshold)
{
head.PullJointWorldAnchorB = new Vector2(targetHead.SimPosition.X, targetHead.SimPosition.Y + 0.8f);
head.PullJointEnabled = true;
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) },
stun: 0.0f,
playSound: true,
attackImpulse: Vector2.Zero,
attacker: null);
}
torso.PullJointWorldAnchorB = new Vector2(torso.SimPosition.X, colliderPos.Y + (TorsoPosition.Value - 0.1f));
torso.PullJointEnabled = true;
if (cprPump >= 1)
//need to CPR for at least a couple of seconds before the target can be revived
//(reviving the target when the CPR has barely started looks strange)
if (cprAnimTimer > 2.0f && GameMain.NetworkMember is not { IsClient: true })
{
torso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
targetTorso.body.ApplyLinearImpulse(new Vector2(0, -20f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
cprPump = 0;
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
reviveChance *= 1f + cprBoost;
if (skill < CPRSettings.Active.DamageSkillThreshold)
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
target.LastDamageSource = null;
target.DamageLimb(
targetTorso.WorldPosition, targetTorso,
new[] { CPRSettings.Active.InsufficientSkillAffliction.Instantiate((CPRSettings.Active.DamageSkillThreshold - skill) * CPRSettings.Active.DamageSkillMultiplier, source: character) },
0.0f, true, 0.0f, attacker: null);
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
GameMain.LuaCs.Hook.Call("human.CPRSuccess", this);
}
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
else
{
float reviveChance = skill * CPRSettings.Active.ReviveChancePerSkill;
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{
//increase oxygen and clamp it above zero
// -> the character should be revived if there are no major afflictions in addition to lack of oxygen
target.Oxygen = Math.Max(target.Oxygen + 10.0f, 10.0f);
GameMain.LuaCs.Hook.Call("human.CPRSuccess", this);
}
else
{
GameMain.LuaCs.Hook.Call("human.CPRFailed", this);
}
GameMain.LuaCs.Hook.Call("human.CPRFailed", this);
}
}
cprPump += deltaTime;
}
cprAnimTimer = (cprAnimTimer + deltaTime) % 17;
cprPumpTimer += deltaTime;
cprAnimTimer += deltaTime;
//got the character back into a non-critical state, increase medical skill
//BUT only if it has been more than 10 seconds since the character revived someone
@@ -1525,7 +1565,7 @@ namespace Barotrauma
target.CharacterHealth.CalculateVitality();
if (wasCritical && target.Vitality > 0.0f && Timing.TotalTime > lastReviveTime + 10.0f)
{
character.Info?.IncreaseSkillLevel("medical".ToIdentifier(), SkillSettings.Current.SkillIncreasePerCprRevive);
character.Info?.ApplySkillGain(Tags.MedicalSkill, SkillSettings.Current.SkillIncreasePerCprRevive);
SteamAchievementManager.OnCharacterRevived(target, character);
lastReviveTime = (float)Timing.TotalTime;
#if SERVER
@@ -1730,7 +1770,23 @@ namespace Barotrauma
targetAnchor += target.Submarine.SimPosition;
}
}
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
if (Vector2.DistanceSquared(pullLimb.PullJointWorldAnchorA, pullLimbAnchor) > 50.0f * 50.0f)
{
//there's a similar error check in the PullJointWorldAnchorB setter, but we seem to be getting quite a lot of
//errors specifically from this method, so let's use a more consistent error message here to prevent clogging GA with
//different error messages that all include a different coordinate
string errorMsg =
$"Attempted to move the anchor B of a limb's pull joint extremely far from the limb in {nameof(DragCharacter)}. " +
$"Character in sub: {character.Submarine != null}, target in sub: {target.Submarine != null}.";
GameAnalyticsManager.AddErrorEventOnce("DragCharacter:PullJointTooFar", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#endif
}
else
{
pullLimb.PullJointWorldAnchorB = pullLimbAnchor;
}
}
else
{
@@ -438,7 +438,18 @@ namespace Barotrauma
foreach (var huskAppendage in mainElement.GetChildElements("huskappendage"))
{
if (!inEditor && huskAppendage.GetAttributeBool("onlyfromafflictions", false)) { continue; }
AfflictionHusk.AttachHuskAppendage(character, huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty), huskAppendage, ragdoll: this);
Identifier afflictionIdentifier = huskAppendage.GetAttributeIdentifier("affliction", Identifier.Empty);
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab affliction) ||
affliction is not AfflictionPrefabHusk matchingAffliction)
{
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!",
contentPackage: huskAppendage.ContentPackage);
}
else
{
AfflictionHusk.AttachHuskAppendage(character, matchingAffliction, huskAppendage, ragdoll: this);
}
}
}
}
@@ -562,6 +573,10 @@ namespace Barotrauma
public void AddJoint(JointParams jointParams)
{
if (!checkLimbIndex(jointParams.Limb2, "Limb1") || !checkLimbIndex(jointParams.Limb2, "Limb2"))
{
return;
}
LimbJoint joint = new LimbJoint(Limbs[jointParams.Limb1], Limbs[jointParams.Limb2], jointParams, this);
GameMain.World.Add(joint.Joint);
for (int i = 0; i < LimbJoints.Length; i++)
@@ -572,6 +587,21 @@ namespace Barotrauma
}
Array.Resize(ref LimbJoints, LimbJoints.Length + 1);
LimbJoints[LimbJoints.Length - 1] = joint;
bool checkLimbIndex(int index, string debugName)
{
if (index < 0 || index >= limbs.Length)
{
string errorMsg = $"Failed to add a joint to character {character.Name}. {debugName} out of bounds (index: {index}, limbs: {limbs.Length}.";
DebugConsole.ThrowError(errorMsg, contentPackage: jointParams.Element?.ContentPackage);
if (jointParams.Element?.ContentPackage == GameMain.VanillaContent)
{
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.AddJoint:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
return false;
}
return true;
}
}
protected void AddLimb(LimbParams limbParams)
@@ -658,6 +688,13 @@ namespace Barotrauma
}
}
private enum LimbStairCollisionResponse
{
DontClimbStairs,
ClimbWithoutLimbCollision,
ClimbWithLimbCollision
}
public bool OnLimbCollision(Fixture f1, Fixture f2, Contact contact)
{
if (f2.Body.UserData is Submarine && character.Submarine == (Submarine)f2.Body.UserData) { return false; }
@@ -700,37 +737,53 @@ namespace Barotrauma
}
else if (structure.StairDirection != Direction.None)
{
Stairs = null;
//don't collider with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return false; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return false; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f) { return false; }
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out Vector2 normal, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return false; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return false; }
//---------------
//set stairs to that of the one dragging us
if (character.SelectedBy != null)
{
Stairs = character.SelectedBy.AnimController.Stairs;
}
else
Stairs = structure;
{
var collisionResponse = handleLimbStairCollision();
if (collisionResponse == LimbStairCollisionResponse.ClimbWithLimbCollision)
{
Stairs = structure;
}
else
{
if (collisionResponse == LimbStairCollisionResponse.DontClimbStairs) { Stairs = null; }
if (Stairs == null)
return false;
return false;
}
}
LimbStairCollisionResponse handleLimbStairCollision()
{
//don't collide with stairs if
//1. bottom of the collider is at the bottom of the stairs and the character isn't trying to move upwards
float stairBottomPos = ConvertUnits.ToSimUnits(structure.Rect.Y - structure.Rect.Height + 10);
if (colliderBottom.Y < stairBottomPos && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
//2. bottom of the collider is at the top of the stairs and the character isn't trying to move downwards
if (targetMovement.Y >= 0.0f && colliderBottom.Y >= ConvertUnits.ToSimUnits(structure.Rect.Y - Submarine.GridSize.Y * 5)) { return LimbStairCollisionResponse.DontClimbStairs; }
//3. collided with the stairs from below
if (contact.Manifold.LocalNormal.Y < 0.0f)
{
return Stairs != structure
? LimbStairCollisionResponse.DontClimbStairs
: LimbStairCollisionResponse.ClimbWithoutLimbCollision;
}
//4. contact points is above the bottom half of the collider
contact.GetWorldManifold(out _, out FarseerPhysics.Common.FixedArray2<Vector2> points);
if (points[0].Y > Collider.SimPosition.Y) { return LimbStairCollisionResponse.DontClimbStairs; }
//5. in water
if (inWater && targetMovement.Y < 0.5f) { return LimbStairCollisionResponse.DontClimbStairs; }
return LimbStairCollisionResponse.ClimbWithLimbCollision;
}
}
lock (impactQueue)
@@ -1332,17 +1385,28 @@ namespace Barotrauma
if (onGround && Collider.LinearVelocity.Y > -ImpactTolerance)
{
float targetY = standOnFloorY + ((float)Math.Abs(Math.Cos(Collider.Rotation)) * Collider.Height * 0.5f) + Collider.Radius + ColliderHeightFromFloor;
if (Math.Abs(Collider.SimPosition.Y - targetY) > 0.01f)
const float LevitationSpeedMultiplier = 5f;
// If the character is walking down a slope, target a position that moves along it
float slopePull = 0f;
if (floorNormal.Y is > 0f and < 1f
&& Math.Sign(movement.X) == Math.Sign(floorNormal.X))
{
if (Stairs != null)
slopePull = Math.Abs(movement.X * floorNormal.X / floorNormal.Y) / LevitationSpeedMultiplier;
}
if (Math.Abs(Collider.SimPosition.Y - targetY - slopePull) > 0.01f)
{
float yVelocity = (targetY - Collider.SimPosition.Y) * LevitationSpeedMultiplier;
if (Stairs != null && targetY < Collider.SimPosition.Y)
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X,
(targetY < Collider.SimPosition.Y ? Math.Sign(targetY - Collider.SimPosition.Y) : (targetY - Collider.SimPosition.Y)) * 5.0f);
}
else
{
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, (targetY - Collider.SimPosition.Y) * 5.0f);
yVelocity = Math.Sign(yVelocity);
}
yVelocity -= slopePull * LevitationSpeedMultiplier;
Collider.LinearVelocity = new Vector2(Collider.LinearVelocity.X, yVelocity);
}
}
else
@@ -1590,10 +1654,10 @@ namespace Barotrauma
// Force check floor y at least once a second so that we'll drop through gaps that we are standing upon.
private const float FloorYStaleTime = 1;
private float floorYCheckTimer;
private void RefreshFloorY(float deltaTime, Limb refLimb = null, bool ignoreStairs = false)
private void RefreshFloorY(float deltaTime, bool ignoreStairs = false)
{
floorYCheckTimer -= deltaTime;
PhysicsBody refBody = refLimb == null ? Collider : refLimb.body;
PhysicsBody refBody = Collider;
if (floorYCheckTimer < 0 ||
lastFloorCheckIgnoreStairs != ignoreStairs ||
lastFloorCheckIgnorePlatforms != IgnorePlatforms ||
@@ -1618,7 +1682,7 @@ namespace Barotrauma
if (HeadPosition.HasValue && MathUtils.IsValid(HeadPosition.Value)) { height = Math.Max(height, HeadPosition.Value); }
if (TorsoPosition.HasValue && MathUtils.IsValid(TorsoPosition.Value)) { height = Math.Max(height, TorsoPosition.Value); }
Vector2 rayEnd = rayStart - new Vector2(0.0f, height);
Vector2 rayEnd = rayStart - new Vector2(0.0f, height * 2f);
Vector2 colliderBottomDisplay = ConvertUnits.ToDisplayUnits(GetColliderBottom());
Fixture standOnFloorFixture = null;
@@ -1697,7 +1761,7 @@ namespace Barotrauma
}
}
if (closestFraction == 1) //raycast didn't hit anything
if (closestFraction >= 1) //raycast didn't hit anything
{
floorNormal = Vector2.UnitY;
if (CurrentHull == null)
@@ -1,11 +1,12 @@
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
namespace Barotrauma
{
{
public enum HitDetection
{
Distance,
@@ -391,7 +392,8 @@ namespace Barotrauma
element.GetAttribute("burndamage") != null ||
element.GetAttribute("bleedingdamage") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).",
contentPackage: element.ContentPackage);
}
//if level wall damage is not defined, default to the structure damage
@@ -414,12 +416,14 @@ namespace Barotrauma
AfflictionPrefab afflictionPrefab;
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.",
contentPackage: element.ContentPackage);
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Name.Equals(afflictionName, System.StringComparison.OrdinalIgnoreCase));
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.",
contentPackage: element.ContentPackage);
continue;
}
}
@@ -428,7 +432,8 @@ namespace Barotrauma
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out afflictionPrefab))
{
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.",
contentPackage: element.ContentPackage);
continue;
}
}
@@ -441,7 +446,7 @@ namespace Barotrauma
}
partial void InitProjSpecific(ContentXElement element);
public void ReloadAfflictions(XElement element, string parentDebugName)
public void ReloadAfflictions(ContentXElement element, string parentDebugName)
{
Afflictions.Clear();
foreach (var subElement in element.GetChildElements("affliction"))
@@ -450,13 +455,14 @@ namespace Barotrauma
Identifier afflictionIdentifier = subElement.GetAttributeIdentifier("identifier", "");
if (!AfflictionPrefab.Prefabs.TryGet(afflictionIdentifier, out AfflictionPrefab afflictionPrefab))
{
DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".");
DebugConsole.ThrowError($"Error in an Attack defined in \"{parentDebugName}\" - could not find an affliction with the identifier \"{afflictionIdentifier}\".",
contentPackage: element.ContentPackage);
continue;
}
affliction = afflictionPrefab.Instantiate(0.0f);
affliction.Deserialize(subElement);
//backwards compatibility
if (subElement.Attribute("amount") != null && subElement.Attribute("strength") == null)
if (subElement.GetAttribute("amount") != null && subElement.GetAttribute("strength") == null)
{
affliction.Strength = subElement.GetAttributeFloat("amount", 0.0f);
}
@@ -465,7 +471,7 @@ namespace Barotrauma
}
}
public void Serialize(XElement element)
public void Serialize(ContentXElement element)
{
SerializableProperty.SerializeProperties(this, element, true);
foreach (var affliction in Afflictions)
@@ -477,7 +483,7 @@ namespace Barotrauma
}
}
public void Deserialize(XElement element, string parentDebugName)
public void Deserialize(ContentXElement element, string parentDebugName)
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
ReloadAfflictions(element, parentDebugName);
@@ -497,8 +503,9 @@ namespace Barotrauma
SetUser(attacker);
DamageParticles(deltaTime, worldPosition);
var attackResult = target?.AddDamage(attacker, worldPosition, this, deltaTime, playSound) ?? new AttackResult();
Vector2 impulseDirection = GetImpulseDirection(target as ISpatialEntity, worldPosition, SourceItem);
var attackResult = target?.AddDamage(attacker, worldPosition, this, impulseDirection, deltaTime, playSound) ?? new AttackResult();
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
var additionalEffectType = ActionType.OnUse;
if (targetCharacter != null && targetCharacter.IsDead)
@@ -606,7 +613,7 @@ namespace Barotrauma
float penetration = Penetration;
RangedWeapon weapon =
SourceItem?.GetComponent<RangedWeapon>() ??
SourceItem?.GetComponent<RangedWeapon>() ??
SourceItem?.GetComponent<Projectile>()?.Launcher?.GetComponent<RangedWeapon>();
float? penetrationValue = weapon?.Penetration;
if (penetrationValue.HasValue)
@@ -614,7 +621,8 @@ namespace Barotrauma
penetration += penetrationValue.Value;
}
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb, penetration);
Vector2 impulseDirection = GetImpulseDirection(targetLimb, worldPosition, SourceItem);
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, impulseDirection, playSound, targetLimb, penetration);
var conditionalEffectType = attackResult.Damage > 0.0f ? ActionType.OnSuccess : ActionType.OnFailure;
foreach (StatusEffect effect in statusEffects)
@@ -666,6 +674,34 @@ namespace Barotrauma
return attackResult;
}
private Vector2 GetImpulseDirection(ISpatialEntity target, Vector2 sourceWorldPosition, Item sourceItem)
{
Vector2 impulseDirection = Vector2.Zero;
if (target != null)
{
impulseDirection = target.WorldPosition - sourceWorldPosition;
}
if (sourceItem?.body != null && sourceItem.body.Enabled && sourceItem.body.LinearVelocity.LengthSquared() > 0.0f)
{
impulseDirection = sourceItem.body.LinearVelocity;
}
else
{
var projectileComponent = sourceItem?.GetComponent<Projectile>();
if (projectileComponent != null)
{
impulseDirection = new Vector2(MathF.Cos(SourceItem.Rotation), MathF.Sin(SourceItem.Rotation));
}
}
if (impulseDirection.LengthSquared() > 0.0001f)
{
impulseDirection = Vector2.Normalize(impulseDirection);
}
return impulseDirection;
}
public float AttackTimer { get; private set; }
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
@@ -70,10 +70,18 @@ namespace Barotrauma
}
foreach (var item in HeldItems)
{
if (item.body != null)
if (item.body == null) { continue; }
if (!enabled)
{
item.body.Enabled = enabled;
item.body.Enabled = false;
}
else if (item.GetComponent<Holdable>() is { IsActive: true })
{
//held items includes all items in hand slots
//we only want to enable the physics body if it's an actual holdable item, not e.g. a wearable item like handcuffs
item.body.Enabled = true;
}
}
AnimController.Collider.Enabled = value;
}
@@ -939,10 +947,16 @@ namespace Barotrauma
{
var prevSelectedItem = _selectedItem;
_selectedItem = value;
if (value is not null)
{
CheckTalents(AbilityEffectType.OnItemSelected, new AbilityItemSelected(value));
}
#if CLIENT
HintManager.OnSetSelectedItem(this, prevSelectedItem, _selectedItem);
if (Controlled == this)
{
_selectedItem?.GetComponent<Fabricator>()?.RefreshSelectedItem();
if (_selectedItem == null)
{
GameMain.GameSession?.CrewManager?.ResetCrewList();
@@ -1101,6 +1115,15 @@ namespace Barotrauma
set { CharacterHealth.Unkillable = value; }
}
/// <summary>
/// Is the health interface available on this character? Can be used by status effects
/// </summary>
public bool UseHealthWindow
{
get { return CharacterHealth.UseHealthWindow; }
set { CharacterHealth.UseHealthWindow = value; }
}
public CampaignMode.InteractionType CampaignInteractionType;
public Identifier MerchantIdentifier;
@@ -1284,7 +1307,8 @@ namespace Barotrauma
{
if (!VariantOf.IsEmpty)
{
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!");
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!",
contentPackage: Prefab.ContentPackage);
}
if (characterInfo == null)
{
@@ -1408,7 +1432,8 @@ namespace Barotrauma
if (matchingAffliction == null || nonHuskedSpeciesName.IsEmpty)
{
DebugConsole.ThrowError($"Cannot find a husk infection that matches {speciesName}! Please make sure that the speciesname is added as 'targets' in the husk affliction prefab definition!\n"
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"");
+ "Note that all the infected speciesnames and files must stick the following pattern: [nonhuskedspeciesname][huskedspeciesname]. E.g. Humanhusk, Crawlerhusk, or Humancustomhusk, or Crawlerzombie. Not \"Customhumanhusk!\" or \"Zombiecrawler\"",
contentPackage: Prefab.ContentPackage);
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler".ToIdentifier();
speciesName = nonHuskedSpeciesName;
@@ -1690,31 +1715,21 @@ namespace Barotrauma
GameMain.LuaCs.Hook.Call("character.giveJobItems", this, spawnPoint);
}
public void GiveIdCardTags(WayPoint spawnPoint, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
public void GiveIdCardTags(WayPoint spawnPoint, bool createNetworkEvent = false)
{
GiveIdCardTags(spawnPoint.ToEnumerable(), requireSpawnPointTagsNotGiven, createNetworkEvent);
}
public void GiveIdCardTags(IEnumerable<WayPoint> spawnPoints, bool requireSpawnPointTagsNotGiven = true, bool createNetworkEvent = false)
{
if (info?.Job == null || spawnPoints == null) { return; }
if (info?.Job == null || spawnPoint == null) { return; }
foreach (Item item in Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() is not IdCard idCard) { continue; }
if (requireSpawnPointTagsNotGiven)
var idCard = item?.GetComponent<IdCard>();
if (idCard == null) { continue; }
//if the card belongs to someone else, don't add any tags.
//otherwise you can gain access to places you shouldn't by temporarily giving the card to someone (e.g. a captain bot) at the end of the round
if (idCard.OwnerName != info.Name) { continue; }
foreach (string s in spawnPoint.IdCardTags)
{
if (idCard.SpawnPointTagsGiven) { continue; }
item.AddTag(s);
}
foreach (var spawnPoint in spawnPoints)
{
foreach (string s in spawnPoint.IdCardTags)
{
item.AddTag(s);
}
}
idCard.SpawnPointTagsGiven = true;
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
@@ -2299,40 +2314,40 @@ namespace Barotrauma
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
}
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool checkFacing = false)
public bool CanSeeTarget(ISpatialEntity target, ISpatialEntity seeingEntity = null, bool seeThroughWindows = false, bool checkFacing = false)
{
seeingEntity ??= AnimController.SimplePhysicsEnabled ? this : GetSeeingLimb();
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
public static bool IsTargetVisible(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
if (seeingEntity is Character seeingCharacter)
{
return seeingCharacter.CanSeeTarget(target, checkFacing: checkFacing);
return seeingCharacter.CanSeeTarget(target, seeThroughWindows: seeThroughWindows, checkFacing: checkFacing);
}
if (target is Character targetCharacter)
{
return IsCharacterVisible(targetCharacter, seeingEntity, checkFacing);
return IsCharacterVisible(targetCharacter, seeingEntity, seeThroughWindows, checkFacing);
}
else
{
return CheckVisibility(target, seeingEntity, checkFacing);
return CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing);
}
}
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool IsCharacterVisible(Character target, ISpatialEntity seeingEntity, bool seeThroughWindows = false, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null || target.Removed) { return false; }
if (CheckVisibility(target, seeingEntity, checkFacing)) { return true; }
if (CheckVisibility(target, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (!target.AnimController.SimplePhysicsEnabled)
{
//find the limbs that are furthest from the target's position (from the viewer's point of view)
@@ -2361,13 +2376,13 @@ namespace Barotrauma
continue;
}
}
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, checkFacing)) { return true; }
if (leftExtremity != null && CheckVisibility(leftExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
if (rightExtremity != null && CheckVisibility(rightExtremity, seeingEntity, seeThroughWindows, checkFacing)) { return true; }
}
return false;
}
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool checkFacing = false)
private static bool CheckVisibility(ISpatialEntity target, ISpatialEntity seeingEntity, bool seeThroughWindows = true, bool checkFacing = false)
{
System.Diagnostics.Debug.Assert(target != null);
if (target == null) { return false; }
@@ -2378,38 +2393,41 @@ namespace Barotrauma
{
if (Math.Sign(diff.X) != seeingCharacter.AnimController.Dir) { return false; }
}
Body closestBody;
//both inside the same sub (or both outside)
//OR the we're inside, the other character outside
if (target.Submarine == seeingEntity.Submarine || target.Submarine == null)
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
return Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//we're outside, the other character inside
else if (seeingEntity.Submarine == null)
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
return Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
//both inside different subs
else
{
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
if (!IsBlocking(closestBody))
{
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
}
return
Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff, blocksVisibilityPredicate: IsBlocking) == null &&
Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff, blocksVisibilityPredicate: IsBlocking) == null;
}
return !IsBlocking(closestBody);
bool IsBlocking(Body body)
bool IsBlocking(Fixture f)
{
var body = f.Body;
if (body == null) { return false; }
if (body.UserData is Structure wall && wall.CastShadow)
if (body.UserData is Structure wall)
{
if (!wall.CastShadow && seeThroughWindows) { return false; }
return wall != target;
}
else if (body.UserData is Item item)
{
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
{
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
}
return item != target;
}
return true;
@@ -2504,9 +2522,21 @@ namespace Barotrauma
if (inventory.Owner is Item item)
{
if (!CanInteractWith(item) && !item.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
ItemContainer container = item.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!CanInteractWith(item))
{
//could be simplified with LINQ, but that'd require capturing variables which we shouldn't do in a method that's called as frequently as this
foreach (var linkedEntity in item.linkedTo)
{
if (linkedEntity is Item linkedItem && linkedItem.DisplaySideBySideWhenLinked && CanInteractWith(linkedItem)) { return true; }
}
return false;
}
ItemContainer container = (inventory as ItemInventory)?.Container;
if (container != null)
{
if (!container.HasRequiredItems(this, addMessage: false)) { return false; }
if (!container.DrawInventory) { return false; }
}
}
return true;
}
@@ -2771,9 +2801,17 @@ namespace Barotrauma
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
{
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
if (body != null && body.UserData as Item != item && (body.UserData as ItemComponent)?.Item != item && Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
if (body != null)
{
var otherItem = body.UserData as Item ?? (body.UserData as ItemComponent)?.Item;
if (otherItem != item &&
(body.UserData as ItemComponent)?.Item != item &&
/*allow interacting through open doors (e.g. duct blocks' colliders stay active despite being open)*/
otherItem?.GetComponent<Door>() is not { IsOpen: true } &&
Submarine.LastPickedFixture?.UserData as Item != item)
{
return false;
}
}
}
@@ -2800,7 +2838,12 @@ namespace Barotrauma
public void DeselectCharacter()
{
if (SelectedCharacter == null) { return; }
SelectedCharacter.AnimController?.ResetPullJoints();
if (!SelectedCharacter.AllowInput)
{
//we cannot reset the pull joints if the target is conscious (moving on its own),
//that'd interfere with its animations
SelectedCharacter.AnimController?.ResetPullJoints();
}
SelectedCharacter = null;
}
@@ -3316,10 +3359,7 @@ namespace Barotrauma
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.2f; }
}
if (IsRagdolled)
{
SetInput(InputType.Ragdoll, false, true);
}
SetInput(InputType.Ragdoll, false, IsRagdolled);
}
if (!wasRagdolled && IsRagdolled)
{
@@ -3577,6 +3617,8 @@ namespace Barotrauma
private void Despawn(bool createNetworkEvents = true)
{
if (!EnableDespawn) { return; }
Identifier despawnContainerId =
IsHuman ?
"despawncontainer".ToIdentifier() :
@@ -3658,10 +3700,12 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 20);
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SightRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SightRange = newRange;
}
}
private void UpdateSoundRange(float deltaTime)
@@ -3676,6 +3720,7 @@ namespace Barotrauma
float massFactor = (float)Math.Sqrt(Mass / 10);
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
newRange *= 1.0f + GetStatValue(StatTypes.SoundRangeMultiplier);
if (!float.IsNaN(newRange))
{
aiTarget.SoundRange = newRange;
@@ -3995,15 +4040,15 @@ namespace Barotrauma
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
}
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
{
return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null);
return ApplyAttack(attacker, worldPosition, attack, deltaTime, impulseDirection, playSound);
}
/// <summary>
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
/// </summary>
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, Vector2 impulseDirection, bool playSound = false, Limb targetLimb = null, float penetration = 0f)
{
if (Removed)
{
@@ -4015,7 +4060,16 @@ namespace Barotrauma
Limb limbHit = targetLimb;
float attackImpulse = attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier * deltaTime;
float impulseMagnitude = (attack.TargetImpulse + attack.TargetForce * attack.ImpactMultiplier) * deltaTime;
Vector2 attackImpulse = Vector2.Zero;
if (Math.Abs(impulseMagnitude) > 0.0f)
{
impulseDirection = impulseDirection.LengthSquared() > 0.0001f ?
Vector2.Normalize(impulseDirection) :
Vector2.UnitX;
attackImpulse = impulseDirection * impulseMagnitude;
}
AbilityAttackData attackData = new AbilityAttackData(attack, this, attacker);
IEnumerable<Affliction> attackAfflictions;
@@ -4144,12 +4198,12 @@ namespace Barotrauma
}
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null, float damageMultiplier = 1f)
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2? attackImpulse = null, Character attacker = null, float damageMultiplier = 1f)
{
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker, damageMultiplier: damageMultiplier);
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse ?? Vector2.Zero, out _, attacker, damageMultiplier: damageMultiplier);
}
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
{
hitLimb = null;
@@ -4182,7 +4236,7 @@ namespace Barotrauma
CreatureMetrics.RecordKill(target.SpeciesName);
}
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker = null, float damageMultiplier = 1, bool allowStacking = true, float penetration = 0f, bool shouldImplode = false)
{
if (Removed) { return new AttackResult(); }
@@ -4215,18 +4269,17 @@ namespace Barotrauma
}
Vector2 dir = hitLimb.WorldPosition - worldPosition;
if (Math.Abs(attackImpulse) > 0.0f)
if (attackImpulse.LengthSquared() > 0.0f)
{
Vector2 diff = dir;
if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); }
Vector2 impulse = Vector2.Normalize(diff) * attackImpulse;
Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff);
hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
hitLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
var mainLimb = hitLimb.character.AnimController.MainLimb;
if (hitLimb != mainLimb)
{
// Always add force to mainlimb
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
mainLimb.body.ApplyLinearImpulse(attackImpulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
}
}
bool wasDead = IsDead;
@@ -4325,19 +4378,16 @@ namespace Barotrauma
}
if (medicalDamage > 0)
{
IncreaseSkillLevel("medical".ToIdentifier(), medicalDamage);
IncreaseSkillLevel(Tags.MedicalSkill, medicalDamage);
}
if (weaponDamage > 0)
{
IncreaseSkillLevel("weapons".ToIdentifier(), weaponDamage);
IncreaseSkillLevel(Tags.WeaponsSkill, weaponDamage);
}
void IncreaseSkillLevel(Identifier skill, float damage)
{
float attackerSkillLevel = attacker.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
attacker.Info?.IncreaseSkillLevel(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, minSkillDivider));
attacker.Info?.ApplySkillGain(skill, damage * SkillSettings.Current.SkillIncreasePerHostileDamage, false, 1f);
}
}
@@ -4351,12 +4401,10 @@ namespace Barotrauma
{
medicalGain += affliction.Strength * affliction.Prefab.MedicalSkillGain;
}
if (medicalGain <= 0) { return; }
Identifier skill = new Identifier("medical");
float attackerSkillLevel = healer.GetSkillLevel(skill);
// The formula is too generous on low skill levels, hence the minimum divider.
float minSkillDivider = 15f;
healer.Info?.IncreaseSkillLevel(skill, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, minSkillDivider));
if (medicalGain > 0)
{
healer.Info?.ApplySkillGain(Tags.MedicalItem, medicalGain * SkillSettings.Current.SkillIncreasePerFriendlyHealed);
}
}
/// <summary>
@@ -4991,8 +5039,10 @@ namespace Barotrauma
private readonly List<Hull> visibleHulls = new List<Hull>();
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
/// <summary>
/// Returns hulls that are visible to the player, including the current hull.
/// Returns hulls that are visible to the character, including the current hull.
/// Note that this is not an accurate visibility check, it only checks for open gaps between the adjacent and linked hulls.
/// Can be heavy if used every frame.
/// </summary>
public List<Hull> GetVisibleHulls()
@@ -5006,7 +5056,9 @@ namespace Barotrauma
float maxDistance = 1000f;
foreach (var hull in adjacentHulls)
{
if (hull.ConnectedGaps.Any(g => g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) &&
if (hull.ConnectedGaps.Any(g =>
g.Open > 0.9f &&
g.linkedTo.Contains(CurrentHull) &&
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
{
if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2))
@@ -5047,7 +5099,7 @@ namespace Barotrauma
public bool IsEngineer => HasJob("engineer");
public bool IsMechanic => HasJob("mechanic");
public bool IsMedic => HasJob("medicaldoctor");
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer");
public bool IsSecurity => HasJob("securityofficer") || HasJob("vipsecurityofficer") || HasJob("outpostsecurityofficer");
public bool IsAssistant => HasJob("assistant");
public bool IsWatchman => HasJob("watchman");
public bool IsVip => HasJob("prisoner");
@@ -5566,4 +5618,12 @@ namespace Barotrauma
public Character Character { get; set; }
}
class AbilityItemSelected : AbilityObject, IAbilityItem
{
public AbilityItemSelected(Item item)
{
Item = item;
}
public Item Item { get; set; }
}
}
@@ -767,7 +767,7 @@ namespace Barotrauma
}
// Used for loading the data
public CharacterInfo(XElement infoElement, Identifier npcIdentifier = default)
public CharacterInfo(ContentXElement infoElement, Identifier npcIdentifier = default)
{
ID = idCounter;
idCounter++;
@@ -1214,6 +1214,21 @@ namespace Barotrauma
return (int)(salary * Job.Prefab.PriceMultiplier);
}
/// <summary>
/// Increases the characters skill at a rate proportional to their current skill.
/// If you want to increase the skill level by a specific amount instead, use <see cref="IncreaseSkillLevel"/>
/// </summary>
public void ApplySkillGain(Identifier skillIdentifier, float baseGain, bool gainedFromAbility = false, float maxGain = 2f)
{
float skillLevel = Job.GetSkillLevel(skillIdentifier);
// The formula is too generous on low skill levels, hence the minimum divider.
float skillDivider = MathF.Pow(Math.Max(skillLevel, 15f), SkillSettings.Current.SkillIncreaseExponent);
IncreaseSkillLevel(skillIdentifier, Math.Min(baseGain / skillDivider, maxGain), gainedFromAbility);
}
/// <summary>
/// Increase the skill by a specific amount. Talents may affect the actual, final skill increase.
/// </summary>
public void IncreaseSkillLevel(Identifier skillIdentifier, float increase, bool gainedFromAbility = false)
{
if (Job == null || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) || Character == null) { return; }
@@ -1222,9 +1237,7 @@ namespace Barotrauma
{
increase *= SkillSettings.Current.AssistantSkillIncreaseMultiplier;
}
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
@@ -1311,12 +1324,12 @@ namespace Barotrauma
OnExperienceChanged(prevAmount, ExperiencePoints);
}
const int BaseExperienceRequired = -50;
const int BaseExperienceRequired = 450;
const int AddedExperienceRequiredPerLevel = 500;
public int GetTotalTalentPoints()
{
return GetCurrentLevel() + AdditionalTalentPoints - 1;
return GetCurrentLevel() + AdditionalTalentPoints;
}
public int GetAvailableTalentPoints()
@@ -1342,16 +1355,19 @@ namespace Barotrauma
return experienceRequired + ExperienceRequiredPerLevel(level);
}
/// <summary>
/// How much more experience does the character need to reach the specified level?
/// </summary>
public int GetExperienceRequiredForLevel(int level)
{
int currentLevel = GetCurrentLevel(out int experienceRequired);
int currentLevel = GetCurrentLevel();
if (currentLevel >= level) { return 0; }
int required = experienceRequired;
for (int i = currentLevel + 1; i <= level; i++)
int required = 0;
for (int i = 0; i < level; i++)
{
required += ExperienceRequiredPerLevel(i);
}
return required;
return required - ExperiencePoints;
}
public int GetCurrentLevel()
@@ -1361,7 +1377,7 @@ namespace Barotrauma
private int GetCurrentLevel(out int experienceRequired)
{
int level = 1;
int level = 0;
experienceRequired = 0;
while (experienceRequired + ExperienceRequiredPerLevel(level) <= ExperiencePoints)
{
@@ -1899,13 +1915,30 @@ namespace Barotrauma
}
}
/// <summary>
/// Get the combined stat value of the identifier "all" and the specified identifier.
/// </summary>
/// <remarks>
/// The "all" identifier works like the "any" identifier in outpost modules where it doesn't literally mean everything but
/// is an unique identifier that indicates that it should target everything. For example if we wanted to make a talent
/// that increases the fabrication quality of every single item we could use something like:
/// <CharacterAbilityGivePermanentStat stattype="IncreaseFabricationQuality" statidentifier="all" />
/// (Granted IncreaseFabricationQuality doesn't support the "all" identifier so if we need this in vanilla it needs to be implemented in code)
/// </remarks>
public float GetSavedStatValueWithAll(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValue(statType, Tags.StatIdentifierTargetAll) +
GetSavedStatValue(statType, statIdentifier);
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier)
=> GetSavedStatValueWithBotsInMp(statType, statIdentifier, GameSession.GetSessionCrewCharacters(CharacterType.Bot));
public float GetSavedStatValueWithBotsInMp(StatTypes statType, Identifier statIdentifier, IReadOnlyCollection<Character> bots)
{
float statValue = GetSavedStatValue(statType, statIdentifier);
if (GameMain.NetworkMember is null) { return statValue; }
foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
foreach (Character bot in bots)
{
int botStatValue = (int)bot.Info.GetSavedStatValue(statType, statIdentifier);
statValue = Math.Max(statValue, botStatValue);
@@ -95,7 +95,8 @@ namespace Barotrauma
name = ParseName(mainElement, file);
if (name == Identifier.Empty)
{
DebugConsole.ThrowError($"No species name defined for: {file.Path}");
DebugConsole.ThrowError($"No species name defined for: {file.Path}",
contentPackage: file.ContentPackage);
return false;
}
return true;
@@ -75,7 +75,8 @@ namespace Barotrauma
HuskPrefab = prefab as AfflictionPrefabHusk;
if (HuskPrefab == null)
{
DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!");
DebugConsole.ThrowError("Error in husk affliction definition: the prefab is of wrong type!",
contentPackage: prefab.ContentPackage);
}
}
@@ -197,7 +198,7 @@ namespace Barotrauma
huskInfection.Add(AfflictionPrefab.InternalDamage.Instantiate(random * 10 * deltaTime / limbCount));
character.LastDamageSource = null;
float force = applyForce ? random * 0.5f * limb.Mass : 0;
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, force);
character.DamageLimb(limb.WorldPosition, limb, huskInfection, 0, false, Rand.Vector(force));
}
}
@@ -205,7 +206,7 @@ namespace Barotrauma
{
if (huskAppendage == null && character.Params.UseHuskAppendage)
{
huskAppendage = AttachHuskAppendage(character, Prefab.Identifier);
huskAppendage = AttachHuskAppendage(character, Prefab as AfflictionPrefabHusk);
}
if (Prefab is AfflictionPrefabHusk { NeedsAir: false })
@@ -285,13 +286,14 @@ namespace Barotrauma
if (prefab == null)
{
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.");
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - husk config file not found.",
contentPackage: Prefab.ContentPackage);
yield return CoroutineStatus.Success;
}
XElement parentElement = new XElement("CharacterInfo");
XElement infoElement = character.Info?.Save(parentElement);
CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(infoElement);
CharacterInfo huskCharacterInfo = infoElement == null ? null : new CharacterInfo(new ContentXElement(Prefab.ContentPackage, infoElement));
if (huskCharacterInfo != null)
{
@@ -372,31 +374,28 @@ namespace Barotrauma
yield return CoroutineStatus.Success;
}
public static List<Limb> AttachHuskAppendage(Character character, Identifier afflictionIdentifier, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
public static List<Limb> AttachHuskAppendage(Character character, AfflictionPrefabHusk matchingAffliction, ContentXElement appendageDefinition = null, Ragdoll ragdoll = null)
{
var appendage = new List<Limb>();
if (!(AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier) is AfflictionPrefabHusk matchingAffliction))
{
DebugConsole.ThrowError($"Could not find an affliction of type 'huskinfection' that matches the affliction '{afflictionIdentifier}'!");
return appendage;
}
Identifier nonhuskedSpeciesName = GetNonHuskedSpeciesName(character.SpeciesName, matchingAffliction);
Identifier huskedSpeciesName = GetHuskedSpeciesName(nonhuskedSpeciesName, matchingAffliction);
CharacterPrefab huskPrefab = CharacterPrefab.FindBySpeciesName(huskedSpeciesName);
if (huskPrefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!");
DebugConsole.ThrowError($"Failed to find the config file for the husk infected species with the species name '{huskedSpeciesName}'!",
contentPackage: matchingAffliction.ContentPackage);
return appendage;
}
var mainElement = huskPrefab.ConfigElement;
var element = appendageDefinition;
if (element == null)
{
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == afflictionIdentifier);
element = mainElement.GetChildElements("huskappendage").FirstOrDefault(e => e.GetAttributeIdentifier("affliction", Identifier.Empty) == matchingAffliction.Identifier);
}
if (element == null)
{
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{afflictionIdentifier}'!");
DebugConsole.ThrowError($"Error in '{huskPrefab.FilePath}': Failed to find a huskappendage that matches the affliction with an identifier '{matchingAffliction.Identifier}'!",
contentPackage: matchingAffliction.ContentPackage);
return appendage;
}
ContentPath pathToAppendage = element.GetAttributeContentPath("path") ?? ContentPath.Empty;
@@ -170,11 +170,13 @@ namespace Barotrauma
if (DormantThreshold > ActiveThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})");
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(DormantThreshold)} is greater than {nameof(ActiveThreshold)} ({DormantThreshold} > {ActiveThreshold})",
contentPackage: element.ContentPackage);
}
if (ActiveThreshold > TransitionThreshold)
{
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})");
DebugConsole.ThrowError($"Error in \"{Identifier}\": {nameof(ActiveThreshold)} is greater than {nameof(TransitionThreshold)} ({ActiveThreshold} > {TransitionThreshold})",
contentPackage: element.ContentPackage);
}
TransformThresholdOnDeath = element.GetAttributeFloat("transformthresholdondeath", ActiveThreshold);
@@ -440,13 +442,15 @@ namespace Barotrauma
AbilityFlags flagType = subElement.GetAttributeEnum("flagtype", AbilityFlags.None);
if (flagType is AbilityFlags.None)
{
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".");
DebugConsole.ThrowError($"Error in affliction \"{parentDebugName}\" - invalid ability flag type \"{subElement.GetAttributeString("flagtype", "")}\".",
contentPackage: element.ContentPackage);
continue;
}
AfflictionAbilityFlags |= flagType;
break;
case "affliction":
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.");
DebugConsole.AddWarning($"Error in affliction \"{parentDebugName}\" - additional afflictions caused by the affliction should be configured inside status effects.",
contentPackage: element.ContentPackage);
break;
}
}
@@ -537,14 +541,16 @@ namespace Barotrauma
}
else if (TextTag.IsEmpty)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.");
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - no text defined for one of the descriptions.",
contentPackage: element.ContentPackage);
}
MinStrength = element.GetAttributeFloat(nameof(MinStrength), 0.0f);
MaxStrength = element.GetAttributeFloat(nameof(MaxStrength), 100.0f);
if (MinStrength >= MaxStrength)
{
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.");
DebugConsole.ThrowError($"Error in affliction \"{affliction.Identifier}\" - max strength is not larger than min.",
contentPackage: element.ContentPackage);
}
Target = element.GetAttributeEnum(nameof(Target), TargetType.Any);
}
@@ -953,7 +959,8 @@ namespace Barotrauma
AfflictionOverlay = new Sprite(subElement);
break;
case "statvalue":
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.");
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.",
contentPackage: element.ContentPackage);
break;
case "effect":
case "periodiceffect":
@@ -962,7 +969,8 @@ namespace Barotrauma
descriptions.Add(new Description(subElement, this));
break;
default:
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})");
DebugConsole.AddWarning($"Unrecognized element in affliction \"{Identifier}\" ({subElement.Name})",
contentPackage: element.ContentPackage);
break;
}
}
@@ -1018,6 +1026,10 @@ namespace Barotrauma
}
}
/// <summary>
/// Removes all the effects of the prefab (including the sounds and other assets defined in them).
/// Note that you need to call LoadAllEffectsAndTreatmentSuitabilities before trying to use the affliction again!
/// </summary>
public static void ClearAllEffects()
{
Prefabs.ForEach(p => p.ClearEffects());
@@ -1046,7 +1058,8 @@ namespace Barotrauma
var b = effects[j];
if (a.MinStrength < b.MaxStrength && b.MinStrength < a.MaxStrength)
{
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.");
DebugConsole.AddWarning($"Affliction \"{Identifier}\" contains effects with overlapping strength ranges. Only one effect can be active at a time, meaning one of the effects won't work.",
ContentPackage);
}
}
}
@@ -54,7 +54,8 @@ namespace Barotrauma
case "vitalitymultiplier":
if (subElement.GetAttribute("name") != null)
{
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.");
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.",
contentPackage: element.ContentPackage);
continue;
}
var vitalityMultipliers = subElement.GetAttributeIdentifierArray("identifier", null) ?? subElement.GetAttributeIdentifierArray("identifiers", null);
@@ -66,7 +67,8 @@ namespace Barotrauma
VitalityMultipliers.Add(vitalityMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.Identifier == vitalityMultiplier))
{
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?");
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions with the identifier \"{vitalityMultiplier}\". Did you mean to define the afflictions by type instead?",
contentPackage: element.ContentPackage);
}
}
}
@@ -79,13 +81,15 @@ namespace Barotrauma
VitalityTypeMultipliers.Add(vitalityTypeMultiplier, multiplier);
if (AfflictionPrefab.Prefabs.None(p => p.AfflictionType == vitalityTypeMultiplier))
{
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?");
DebugConsole.AddWarning($"Potentially incorrectly defined vitality multiplier in \"{characterHealth.Character.Name}\". Could not find any afflictions of the type \"{vitalityTypeMultiplier}\". Did you mean to define the afflictions by identifier instead?",
contentPackage: element.ContentPackage);
}
}
}
if (vitalityMultipliers == null && VitalityTypeMultipliers == null)
{
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!");
DebugConsole.ThrowError($"Error in character health config {characterHealth.Character.Name}: affliction identifier(s) or type(s) not defined in the \"VitalityMultiplier\" elements!",
contentPackage: element.ContentPackage);
}
break;
}
@@ -1325,6 +1329,8 @@ namespace Barotrauma
public void Remove()
{
RemoveProjSpecific();
afflictionsToRemove.Clear();
afflictionsToUpdate.Clear();
}
partial void RemoveProjSpecific();
@@ -79,12 +79,13 @@ namespace Barotrauma
public ref readonly ImmutableArray<Identifier> ParsedAfflictionTypes => ref parsedAfflictionTypes;
public DamageModifier(XElement element, string parentDebugName, bool checkErrors = true)
public DamageModifier(ContentXElement element, string parentDebugName, bool checkErrors = true)
{
Deserialize(element);
if (element.Attribute("afflictionnames") != null)
if (element.GetAttribute("afflictionnames") != null)
{
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.",
contentPackage: element.ContentPackage);
}
if (checkErrors)
{
@@ -108,12 +109,12 @@ namespace Barotrauma
}
}
static void createWarningOrError(string msg)
void createWarningOrError(string msg)
{
#if DEBUG
DebugConsole.ThrowError(msg);
DebugConsole.ThrowError(msg, contentPackage: element.ContentPackage);
#else
DebugConsole.AddWarning(msg);
DebugConsole.AddWarning(msg, contentPackage: element.ContentPackage);
#endif
}
}
@@ -117,8 +117,8 @@ namespace Barotrauma
public XElement Element { get; protected set; }
public readonly List<(XElement element, float commonness)> ItemSets = new List<(XElement element, float commonness)>();
public readonly List<(XElement element, float commonness)> CustomCharacterInfos = new List<(XElement element, float commonness)>();
public readonly List<(ContentXElement element, float commonness)> ItemSets = new List<(ContentXElement element, float commonness)>();
public readonly List<(ContentXElement element, float commonness)> CustomCharacterInfos = new List<(ContentXElement element, float commonness)>();
public readonly Identifier NpcSetIdentifier;
@@ -196,7 +196,7 @@ namespace Barotrauma
var spawnItems = ToolBox.SelectWeightedRandom(ItemSets, it => it.commonness, randSync).element;
if (spawnItems != null)
{
foreach (XElement itemElement in spawnItems.GetChildElements("item"))
foreach (ContentXElement itemElement in spawnItems.GetChildElements("item"))
{
int amount = itemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
@@ -239,14 +239,15 @@ namespace Barotrauma
return characterInfo;
}
public static void InitializeItem(Character character, XElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
public static void InitializeItem(Character character, ContentXElement itemElement, Submarine submarine, HumanPrefab humanPrefab, WayPoint spawnPoint = null, Item parentItem = null, bool createNetworkEvents = true)
{
ItemPrefab itemPrefab;
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
itemPrefab = MapEntityPrefab.FindByIdentifier(itemIdentifier.ToIdentifier()) as ItemPrefab;
if (itemPrefab == null)
{
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
DebugConsole.ThrowError("Tried to spawn \"" + humanPrefab?.Identifier + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.",
contentPackage: itemElement?.ContentPackage);
return;
}
Item item = new Item(itemPrefab, character.Position, null);
@@ -301,7 +302,7 @@ namespace Barotrauma
wifiComponent.TeamID = character.TeamID;
}
parentItem?.Combine(item, user: null);
foreach (XElement childItemElement in itemElement.Elements())
foreach (ContentXElement childItemElement in itemElement.Elements())
{
int amount = childItemElement.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
@@ -47,13 +47,14 @@ namespace Barotrauma
}
}
public Job(XElement element)
public Job(ContentXElement element)
{
Identifier identifier = element.GetAttributeIdentifier("identifier", "");
JobPrefab p;
if (!JobPrefab.Prefabs.ContainsKey(identifier))
{
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.");
DebugConsole.ThrowError($"Could not find the job {identifier}. Giving the character a random job.",
contentPackage: element.ContentPackage);
p = JobPrefab.Random(Rand.RandSync.Unsynced);
}
else
@@ -43,7 +43,8 @@ namespace Barotrauma
Priority = element.GetAttributeFloat("priority", -1f);
if (Priority < 0)
{
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.");
DebugConsole.AddWarning($"The 'priority' attribute is missing from the the item repair priorities definition in {element} of {file.Path}.",
ContentPackage);
}
}
@@ -266,7 +266,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}");
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}",
contentPackage: characterPrefab.ContentPackage);
}
return a;
}
@@ -63,7 +63,8 @@ namespace Barotrauma
{
if (!character.AnimController.CanWalk)
{
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!");
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!",
contentPackage: character.Prefab.ContentPackage);
return false;
}
return true;
@@ -136,6 +136,12 @@ namespace Barotrauma
public readonly List<ParticleParams> DamageEmitters = new List<ParticleParams>();
public readonly List<InventoryParams> Inventories = new List<InventoryParams>();
public HealthParams Health { get; private set; }
/// <summary>
/// Parameters for EnemyAIController. Not used by HumanAIController.
/// </summary>
/// <returns>
/// AIParams or null. Use <see cref="EnemyAIController.AIParams"/>, if you don't expect nulls.
/// </returns>
public AIParams AI { get; private set; }
public CharacterParams(CharacterFile file)
@@ -559,7 +565,8 @@ namespace Barotrauma
DebugConsole.AddWarning($"Character \"{character.SpeciesName}\" has a negative crush depth. "+
"Previously the crush depths were defined as display units (e.g. -30000 would correspond to 300 meters below the level), "+
"but now they're in meters (e.g. 3000 would correspond to a depth of 3000 meters displayed on the nav terminal). "+
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.");
$"Changing the crush depth from {CrushDepth} to {newCrushDepth}.",
element.ContentPackage);
CrushDepth = newCrushDepth;
}
}
@@ -602,7 +609,8 @@ namespace Barotrauma
public void AddItem(string identifier = null)
{
identifier = identifier ?? "";
if (Element == null) { return; }
identifier ??= "";
var element = CreateElement("item", new XAttribute("identifier", identifier));
Element.Add(element);
var item = new InventoryItem(element, Character);
@@ -711,7 +719,8 @@ namespace Barotrauma
if (HasTag(tag))
{
target = null;
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!",
targetElement.ContentPackage);
return false;
}
else
@@ -730,6 +739,11 @@ namespace Barotrauma
public bool TryAddNewTarget(Identifier tag, AIState state, float priority, out TargetParams targetParams)
{
if (Element == null)
{
targetParams = null;
return false;
}
var element = TargetParams.CreateNewElement(Character, tag, state, priority);
if (TryAddTarget(element, out targetParams))
{
@@ -84,12 +84,14 @@ namespace Barotrauma
doc = XMLExtensions.TryLoadXml(Path);
if (doc == null)
{
DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.");
DebugConsole.ThrowError("[EditableParams] The document is null! Failed to load the parameters.",
contentPackage: file.ContentPackage);
return false;
}
if (MainElement == null)
{
DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.");
DebugConsole.ThrowError("[EditableParams] The main element is null! Failed to load the parameters.",
contentPackage: file.ContentPackage);
return false;
}
IsLoaded = Deserialize(MainElement);
@@ -106,7 +106,8 @@ namespace Barotrauma
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}' (content package {contentPackage?.Name ?? "null"})");
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'",
contentPackage: contentPackage);
return string.Empty;
}
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
@@ -183,7 +184,8 @@ namespace Barotrauma
}
if (error != null)
{
DebugConsole.ThrowError(error);
DebugConsole.ThrowError(error,
contentPackage: prefab?.ContentPackage);
}
}
if (selectedFile == null)
@@ -444,7 +446,8 @@ namespace Barotrauma
{
if (source.MainElement == null)
{
DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!");
DebugConsole.ThrowError("[RagdollParams] The source XML Element of the given RagdollParams is null!",
contentPackage: source.MainElement?.ContentPackage);
return;
}
Deserialize(source.MainElement, alsoChildren: false);
@@ -453,7 +456,8 @@ namespace Barotrauma
// TODO: cannot currently undo joint/limb deletion.
if (sourceSubParams.Count != subParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
DebugConsole.ThrowError("[RagdollParams] The count of the sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.",
contentPackage: source.MainElement?.ContentPackage);
return;
}
for (int i = 0; i < subParams.Count; i++)
@@ -461,7 +465,8 @@ namespace Barotrauma
var subSubParams = subParams[i].SubParams;
if (subSubParams.Count != sourceSubParams[i].SubParams.Count)
{
DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.");
DebugConsole.ThrowError("[RagdollParams] The count of the sub sub params differs! Failed to revert to the previous snapshot! Please reset the ragdoll to undo the changes.",
contentPackage: source.MainElement?.ContentPackage);
return;
}
subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false);
@@ -890,14 +895,14 @@ namespace Barotrauma
#if CLIENT
public DecorativeSprite DecorativeSprite { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DecorativeSprite.SerializableProperties = SerializableProperty.DeserializeProperties(DecorativeSprite, element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
SerializableProperty.SerializeProperties(DecorativeSprite, element ?? Element);
@@ -985,7 +990,8 @@ namespace Barotrauma
deformation = new PositionalDeformationParams(deformationElement);
break;
default:
DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'");
DebugConsole.ThrowError($"SpriteDeformationParams not implemented: '{typeName}'",
contentPackage: element.ContentPackage);
break;
}
if (deformation != null)
@@ -1000,14 +1006,14 @@ namespace Barotrauma
#if CLIENT
public Dictionary<SpriteDeformationParams, XElement> Deformations { get; private set; }
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Deformations.ForEach(d => d.Key.SerializableProperties = SerializableProperty.DeserializeProperties(d.Key, d.Value));
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Deformations.ForEach(d => SerializableProperty.SerializeProperties(d.Key, d.Value));
@@ -1098,14 +1104,14 @@ namespace Barotrauma
}
#if CLIENT
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
LightSource.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
LightSource.Serialize(element ?? Element);
@@ -1130,14 +1136,14 @@ namespace Barotrauma
Attack = new Attack(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
Attack.Deserialize(element ?? Element, parentDebugName: Ragdoll?.SpeciesName.ToString() ?? "null");
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
Attack.Serialize(element ?? Element);
@@ -1182,14 +1188,14 @@ namespace Barotrauma
DamageModifier = new DamageModifier(element, ragdoll.SpeciesName.Value);
}
public override bool Deserialize(XElement element = null, bool recursive = true)
public override bool Deserialize(ContentXElement element = null, bool recursive = true)
{
base.Deserialize(element, recursive);
DamageModifier.Deserialize(element ?? Element);
return SerializableProperties != null;
}
public override bool Serialize(XElement element = null, bool recursive = true)
public override bool Serialize(ContentXElement element = null, bool recursive = true)
{
base.Serialize(element, recursive);
DamageModifier.Serialize(element ?? Element);
@@ -1218,7 +1224,7 @@ namespace Barotrauma
public virtual string Name { get; set; }
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public ContentXElement Element { get; set; }
public XElement OriginalElement { get; protected set; }
public ContentXElement OriginalElement { get; protected set; }
public List<SubParam> SubParams { get; set; } = new List<SubParam>();
public RagdollParams Ragdoll { get; private set; }
@@ -1230,14 +1236,14 @@ namespace Barotrauma
public SubParam(ContentXElement element, RagdollParams ragdoll)
{
Element = element;
OriginalElement = new XElement(element);
OriginalElement = new ContentXElement(element.ContentPackage, element);
Ragdoll = ragdoll;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
}
public virtual bool Deserialize(XElement element = null, bool recursive = true)
public virtual bool Deserialize(ContentXElement element = null, bool recursive = true)
{
element = element ?? Element;
element ??= Element;
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
if (recursive)
{
@@ -1246,9 +1252,9 @@ namespace Barotrauma
return SerializableProperties != null;
}
public virtual bool Serialize(XElement element = null, bool recursive = true)
public virtual bool Serialize(ContentXElement element = null, bool recursive = true)
{
element = element ?? Element;
element ??= Element;
SerializableProperty.SerializeProperties(this, element, true);
if (recursive)
{
@@ -100,6 +100,13 @@ namespace Barotrauma
set;
}
[Serialize(1.5f, IsPropertySaveable.Yes)]
public float SkillIncreaseExponent
{
get;
set;
}
public SkillSettings(XElement element, SkillSettingsFile file) : base(file, "SkillSettings".ToIdentifier())
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
@@ -13,7 +13,7 @@ namespace Barotrauma.Abilities
public AbilityCondition(CharacterTalent characterTalent, ContentXElement conditionElement)
{
this.characterTalent = characterTalent;
this.characterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent));
character = characterTalent.Character;
invert = conditionElement.GetAttributeBool("invert", false);
}
@@ -40,7 +40,8 @@ namespace Barotrauma.Abilities
{
if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent.Prefab.ContentPackage);
}
targetTypes.Add(targetType);
}
@@ -1,7 +1,4 @@
using Barotrauma.Items.Components;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Linq;
namespace Barotrauma.Abilities
{
@@ -34,7 +34,8 @@ namespace Barotrauma.Abilities
string weaponTypeStr = conditionElement.GetAttributeString("weapontype", "Any");
if (!Enum.TryParse(weaponTypeStr, ignoreCase: true, out weapontype))
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent.DebugIdentifier}\": \"{weaponTypeStr}\" is not a valid weapon type.",
contentPackage: conditionElement.ContentPackage);
}
}
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -11,13 +10,19 @@ namespace Barotrauma.Abilities
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
/// <summary>
/// If enabled, the conditional is checked on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to true, except in the case of <see cref="AbilityConditionHasPermanentStat"/>, which by default targets the character who has the talent.
/// </summary>
private readonly bool targetAbilityTarget = false;
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements())
foreach (ContentXElement subElement in conditionElement.Elements())
{
if (subElement.NameAsIdentifier() == "conditional")
{
@@ -25,29 +30,47 @@ namespace Barotrauma.Abilities
}
}
if (!targetTypes.Any() && !conditionals.Any())
//don't log this error if this is a subclass of AbilityConditionCharacter
//(in that case not having any conditionals here is ok)
if (!targetTypes.Any() && !conditionals.Any() && GetType() == typeof(AbilityConditionCharacter))
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.",
contentPackage: conditionElement.ContentPackage);
}
targetAbilityTarget = conditionElement.GetAttributeBool(nameof(targetAbilityTarget), this is not AbilityConditionHasPermanentStat);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
public sealed override bool MatchesCondition()
{
if (abilityObject is IAbilityCharacter abilityCharacter)
//by default data-reliant conditions don't accept null, but in this case it's ok,
//because we can assume it's the character who has the talent
return MatchesCondition(abilityObject: null);
}
public sealed override bool MatchesCondition(AbilityObject abilityObject)
{
return invert ? !MatchesConditionSpecific(abilityObject) : MatchesConditionSpecific(abilityObject);
}
protected sealed override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? character :
character;
if (targetCharacter is null) { return false; }
if (!IsViableTarget(targetTypes, targetCharacter)) { return false; }
foreach (var conditional in conditionals)
{
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{
if (!conditional.Matches(character)) { return false; }
}
return true;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityCharacter));
return false;
if (!conditional.Matches(targetCharacter)) { return false; }
}
return MatchesCharacter(targetCharacter);
}
protected virtual bool MatchesCharacter(Character character)
{
return true;
}
}
}
@@ -1,6 +1,6 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionCharacter
{
private readonly Identifier identifier;
@@ -9,11 +9,9 @@ namespace Barotrauma.Abilities
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
return character != null &&!character.MarkedAsLooted.Contains(identifier);
}
}
}
@@ -2,15 +2,13 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionCharacter
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
protected override bool MatchesCharacter(Character character)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
return character is { IsUnconscious: true };
}
}
}
@@ -17,13 +17,15 @@ namespace Barotrauma.Abilities
protected void LogAbilityConditionError(AbilityObject abilityObject, Type expectedData)
{
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}");
DebugConsole.ThrowError($"Used data-reliant ability condition when data is incompatible! Expected {expectedData}, but received {abilityObject} in talent {characterTalent.DebugIdentifier}",
contentPackage: characterTalent.Prefab.ContentPackage);
}
protected abstract bool MatchesConditionSpecific(AbilityObject abilityObject);
public override bool MatchesCondition()
{
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.");
DebugConsole.ThrowError($"Used data-reliant ability condition in a state-based ability in talent {characterTalent.DebugIdentifier}! This is not allowed.",
contentPackage: characterTalent.Prefab.ContentPackage);
return false;
}
public override bool MatchesCondition(AbilityObject abilityObject)
@@ -19,7 +19,8 @@ namespace Barotrauma.Abilities
if (identifiers.None() && tags.None() && category == MapEntityCategory.None)
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.");
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers, tags or category defined.",
contentPackage: conditionElement.ContentPackage);
}
}
@@ -0,0 +1,19 @@
using Barotrauma.Items.Components;
namespace Barotrauma.Abilities
{
class AbilityConditionItemIsStatic : AbilityConditionData
{
public AbilityConditionItemIsStatic(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityItem { Item: var item })
{
return item.GetComponent<Holdable>() is null && item.GetComponent<Wearable>() is null;
}
return false;
}
}
}
@@ -22,7 +22,8 @@ namespace Barotrauma.Abilities
{
if (!isAffiliated)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.",
contentPackage: conditionElement.ContentPackage);
}
continue;
}
@@ -1,6 +1,8 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
class AbilityConditionHasPermanentStat : AbilityConditionCharacter
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
@@ -12,7 +14,8 @@
statIdentifier = conditionElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"No stat identifier defined for {this} in talent {characterTalent.DebugIdentifier}!",
contentPackage: conditionElement.ContentPackage);
}
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
@@ -20,8 +23,13 @@
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
if (character?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(AbilityConditionHasPermanentStat.MatchesCharacter)}: character {character} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return false;
}
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
@@ -13,7 +13,8 @@ namespace Barotrauma.Abilities
tag = conditionElement.GetAttributeIdentifier("tag", Identifier.Empty);
if (tag.IsEmpty)
{
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.");
DebugConsole.AddWarning($"Error in talent \"{characterTalent.Prefab.OriginalName}\" - tag not defined in AbilityConditionHasStatusTag.",
characterTalent.Prefab.ContentPackage);
}
}
@@ -2,21 +2,18 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless
internal sealed class AbilityConditionLowestLevel : AbilityConditionCharacter
{
public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
protected override bool MatchesCharacter(Character character)
{
int ownLevel = character.Info.GetCurrentLevel();
foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both))
foreach (Character otherCharacter in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (crew == character) { continue; }
if (crew.Info.GetCurrentLevel() < ownLevel) { return false; }
if (otherCharacter == character) { continue; }
if (otherCharacter.Info.GetCurrentLevel() < ownLevel) { return false; }
}
return true;
}
}
@@ -26,7 +26,7 @@ namespace Barotrauma.Abilities
public CharacterAbility(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement)
{
CharacterAbilityGroup = characterAbilityGroup;
CharacterAbilityGroup = characterAbilityGroup ?? throw new ArgumentNullException(nameof(characterAbilityGroup));
CharacterTalent = characterAbilityGroup.CharacterTalent;
Character = CharacterTalent.Character;
RequiresAlive = abilityElement.GetAttributeBool("requiresalive", true);
@@ -59,7 +59,8 @@ namespace Barotrauma.Abilities
protected virtual void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: Ability {this} does not have an implementation for VerifyState! This ability does not work in interval ability groups.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
}
public void ApplyAbilityEffect(AbilityObject abilityObject)
@@ -76,17 +77,20 @@ namespace Barotrauma.Abilities
protected virtual void ApplyEffect()
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not have a definition for ApplyEffect in talent {CharacterTalent.DebugIdentifier}",
CharacterTalent.Prefab.ContentPackage);
}
protected virtual void ApplyEffect(AbilityObject abilityObject)
{
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.AddWarning($"Ability {this} used improperly! This ability does not take a parameter for ApplyEffect in talent {CharacterTalent.DebugIdentifier}",
CharacterTalent.Prefab.ContentPackage);
}
protected void LogAbilityObjectMismatch()
{
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}");
DebugConsole.ThrowError($"Incompatible ability! Ability {this} is incompatitible with this type of ability effect type in talent {CharacterTalent.DebugIdentifier}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
}
// XML
@@ -99,13 +103,18 @@ namespace Barotrauma.Abilities
abilityType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (abilityType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")");
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")",
contentPackage: abilityElement.ContentPackage);
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e);
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the CharacterAbility \"" + type + "\" (" + characterAbilityGroup.CharacterTalent.DebugIdentifier + ")", e,
contentPackage: abilityElement.ContentPackage);
}
return null;
}
@@ -118,7 +127,8 @@ namespace Barotrauma.Abilities
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException);
DebugConsole.ThrowError("Error while creating an instance of a CharacterAbility of the type " + abilityType + ".", e.InnerException,
contentPackage: abilityElement.ContentPackage);
return null;
}
@@ -30,7 +30,8 @@ namespace Barotrauma.Abilities
}
else
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - \"{limbTypeStr}\" is not a valid limb type.",
contentPackage: abilityElement.ContentPackage);
}
}
}
@@ -21,7 +21,8 @@ namespace Barotrauma.Abilities
JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList);
if (apprenticeJob is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -14,7 +14,8 @@
targetAllies = abilityElement.GetAttributeBool("targetallies", false);
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -16,7 +16,8 @@
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -27,7 +28,8 @@
var afflictionPrefab = AfflictionPrefab.Prefabs.Find(a => a.Identifier == afflictionId);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".");
DebugConsole.ThrowError($"Error in CharacterAbilityGiveAffliction - could not find an affliction with the identifier \"{afflictionId}\".",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
float strength = this.strength;
@@ -14,29 +14,35 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
if (amount == 0 && level == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - no exp amount or level defined in {nameof(CharacterAbilityGiveExperience)}.",
contentPackage: abilityElement.ContentPackage);
}
if (amount > 0 && level > 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - {nameof(CharacterAbilityGiveExperience)} defines both an exp amount and a level.",
contentPackage: abilityElement.ContentPackage);
}
}
private void ApplyEffectSpecific(Character targetCharacter)
{
if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
if (level > 0)
{
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level));
targetCharacter.Info?.GiveExperience(targetCharacter.Info.GetExperienceRequiredForLevel(level) + amount);
}
else if (amount != 0)
{
targetCharacter.Info?.GiveExperience(amount);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
if (abilityObject is AbilityCharacterKill { Killer: { } killer })
{
ApplyEffectSpecific(killer);
}
else if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter)
{
ApplyEffectSpecific(targetCharacter);
}
@@ -7,12 +7,14 @@ namespace Barotrauma.Abilities
private readonly ItemTalentStats stat;
private readonly float value;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None);
value = abilityElement.GetAttributeFloat("value", 0f);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -27,7 +29,7 @@ namespace Barotrauma.Abilities
{
if (abilityObject is not IAbilityItem ability) { return; }
ability.Item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
ability.Item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -10,6 +10,7 @@ namespace Barotrauma.Abilities
private readonly float value;
private readonly ImmutableHashSet<Identifier> tags;
private readonly bool stackable;
private readonly bool save;
public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
@@ -17,6 +18,7 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeFloat("value", 0f);
tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet<Identifier>.Empty);
stackable = abilityElement.GetAttributeBool("stackable", true);
save = abilityElement.GetAttributeBool("save", false);
}
public override void InitializeAbility(bool addingFirstTime)
@@ -44,7 +46,7 @@ namespace Barotrauma.Abilities
if (item.Submarine?.TeamID != Character.TeamID) { continue; }
if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier))
{
item.StatManager.ApplyStat(stat, stackable, value, CharacterTalent);
item.StatManager.ApplyStat(stat, stackable, save, value, CharacterTalent);
}
}
}
@@ -14,7 +14,8 @@
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -1,4 +1,6 @@
namespace Barotrauma.Abilities
using System;
namespace Barotrauma.Abilities
{
public enum PermanentStatPlaceholder
{
@@ -19,7 +21,12 @@
private readonly bool setValue;
private readonly PermanentStatPlaceholder placeholder;
//private readonly float maximumValue;
/// <summary>
/// If enabled, the effect is applied on the target of the ability (e.g. the character that was killed if the effect type is OnKillCharacter).
/// Defaults to false (= targets the character who has the talent).
/// </summary>
private readonly bool targetAbilityTarget = false;
public override bool AllowClientSimulation => true;
public override bool AppliesEffectOnIntervalUpdate => true;
@@ -28,7 +35,8 @@
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.");
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
@@ -39,27 +47,28 @@
giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None);
setValue = abilityElement.GetAttributeBool("setvalue", false);
placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
targetAbilityTarget = abilityElement.GetAttributeBool(nameof(targetAbilityTarget), false);
}
public override void InitializeAbility(bool addingFirstTime)
{
if (giveOnAddingFirstTime && addingFirstTime)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject);
}
protected override void ApplyEffect()
{
ApplyEffectSpecific();
ApplyEffectSpecific(abilityObject: null);
}
private void ApplyEffectSpecific()
private void ApplyEffectSpecific(AbilityObject abilityObject)
{
Identifier identifier = HandlePlaceholders(placeholder, statIdentifier);
if (targetAllies)
@@ -71,7 +80,21 @@
}
else
{
Character?.Info?.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
Character targetCharacter =
targetAbilityTarget ?
(abilityObject as IAbilityCharacter)?.Character ?? Character :
Character;
if (targetCharacter == null)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character was null.\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
if (targetCharacter?.Info == null)
{
DebugConsole.AddWarning($"Error in {nameof(CharacterAbilityGivePermanentStat.ApplyEffectSpecific)}: character {targetCharacter} has no CharacterInfo. Are you trying to use the condition on a non-player character?\n{Environment.StackTrace.CleanupStackTrace()}");
return;
}
targetCharacter.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue);
}
}
@@ -82,7 +105,7 @@
switch (placeholder)
{
case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location:
return original.Replace("[placeholder]", location.Name);
return original.Replace("[placeholder]", location.NameIdentifier.Value);
case PermanentStatPlaceholder.LocationIndex:
return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString());
}
@@ -13,11 +13,13 @@ namespace Barotrauma.Abilities
amount = abilityElement.GetAttributeFloat("amount", 0f);
if (factionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -12,11 +12,13 @@
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.");
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -9,7 +9,8 @@
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@ namespace Barotrauma.Abilities
amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -16,11 +16,13 @@ namespace Barotrauma.Abilities
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill identifier not defined in CharacterAbilityIncreaseSkill.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(skillIncrease, 0))
{
DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.");
DebugConsole.AddWarning($"Possible error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - skill increase set to 0.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -8,7 +8,8 @@ namespace Barotrauma.Abilities
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -15,11 +15,13 @@
if (resistanceId.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.",
contentPackage: abilityElement.ContentPackage);
}
if (MathUtils.NearlyEqual(multiplier, 1.0f))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.");
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f))
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -11,7 +11,8 @@
amount = abilityElement.GetAttributeInt("amount", 1);
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.");
DebugConsole.ThrowError($"Error in talent \"{characterAbilityGroup.CharacterTalent.DebugIdentifier}\" - itemIdentifier not defined.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -19,14 +20,16 @@
{
if (itemIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.");
DebugConsole.ThrowError("Cannot put item in inventory - itemIdentifier not defined.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
ItemPrefab itemPrefab = ItemPrefab.Find(null, itemIdentifier);
if (itemPrefab == null)
{
DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.");
DebugConsole.ThrowError("Cannot put item in inventory - item prefab " + itemIdentifier + " not found.",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
for (int i = 0; i < amount; i++)
@@ -14,7 +14,8 @@ namespace Barotrauma.Abilities
if (afflictionId.IsEmpty)
{
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.");
DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -12,7 +12,8 @@ namespace Barotrauma.Abilities
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.",
contentPackage: abilityElement.ContentPackage);
}
}
protected override void ApplyEffect(AbilityObject abilityObject)
@@ -13,7 +13,8 @@ namespace Barotrauma.Abilities
value = abilityElement.GetAttributeInt("value", 0);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.");
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.",
contentPackage: abilityElement.ContentPackage);
}
}
@@ -24,7 +24,8 @@ namespace Barotrauma.Abilities
JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet());
if (apprentice is null)
{
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}");
DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -31,7 +31,7 @@ namespace Barotrauma.Abilities
public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup)
{
AbilityEffectType = abilityEffectType;
CharacterTalent = characterTalent;
CharacterTalent = characterTalent ?? throw new ArgumentNullException(nameof(characterTalent));
Character = CharacterTalent.Character;
maxTriggerCount = abilityElementGroup.GetAttributeInt("maxtriggercount", int.MaxValue);
foreach (var subElement in abilityElementGroup.Elements())
@@ -44,9 +44,13 @@ namespace Barotrauma.Abilities
case "fallbackabilities":
LoadFallbackAbilities(subElement);
break;
case "condition":
case "conditions":
LoadConditions(subElement);
break;
default:
DebugConsole.ThrowError($"Error in talent {characterTalent.Prefab.Identifier}: unrecognized xml element \"{subElement.Name}\".");
break;
}
}
@@ -55,7 +59,8 @@ namespace Barotrauma.Abilities
case AbilityEffectType.OnDieToCharacter:
if (characterAbilities.Any(a => a.RequiresAlive))
{
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.");
DebugConsole.AddWarning($"Potential error in talent {characterTalent}: an ability group has the type {AbilityEffectType.OnDieToCharacter}, but includes abilities that require the character to be alive, meaning they will never execute.",
characterTalent.Prefab.ContentPackage);
}
break;
}
@@ -90,7 +95,8 @@ namespace Barotrauma.Abilities
if (newCondition == null)
{
DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"AbilityCondition was not found in talent {CharacterTalent.DebugIdentifier}!",
contentPackage: conditionElement.ContentPackage);
return;
}
@@ -107,7 +113,8 @@ namespace Barotrauma.Abilities
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -118,7 +125,8 @@ namespace Barotrauma.Abilities
{
if (characterAbility == null)
{
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!",
contentPackage: CharacterTalent.Prefab.ContentPackage);
return;
}
@@ -135,13 +143,21 @@ namespace Barotrauma.Abilities
conditionType = Type.GetType("Barotrauma.Abilities." + type + "", false, true);
if (conditionType == null)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")");
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent.Prefab.ContentPackage);
}
return null;
}
}
catch (Exception e)
{
if (errorMessages) DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e);
if (errorMessages)
{
DebugConsole.ThrowError("Could not find the component \"" + type + "\" (" + characterTalent.DebugIdentifier + ")", e,
contentPackage: characterTalent.Prefab.ContentPackage);
}
return null;
}
@@ -154,13 +170,15 @@ namespace Barotrauma.Abilities
}
catch (TargetInvocationException e)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException);
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ".", e.InnerException,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
if (newCondition == null)
{
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null");
DebugConsole.ThrowError("Error while creating an instance of an ability condition of the type " + conditionType + ", instance was null",
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -189,7 +207,8 @@ namespace Barotrauma.Abilities
if (newAbility == null)
{
DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!");
DebugConsole.ThrowError($"Unable to create an ability for {characterTalent.DebugIdentifier}!",
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -200,7 +219,8 @@ namespace Barotrauma.Abilities
{
if (statusEffectElements == null)
{
DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier);
DebugConsole.ThrowError("StatusEffect list was not found in talent " + characterTalent.DebugIdentifier,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -233,7 +253,8 @@ namespace Barotrauma.Abilities
{
if (afflictionElements == null)
{
DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier);
DebugConsole.ThrowError("Affliction list was not found in talent " + characterTalent.DebugIdentifier,
contentPackage: characterTalent.Prefab.ContentPackage);
return null;
}
@@ -248,7 +269,8 @@ namespace Barotrauma.Abilities
AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(ap => ap.Identifier == afflictionIdentifier);
if (afflictionPrefab == null)
{
DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.");
DebugConsole.ThrowError("Error in CharacterTalent (" + characterTalent.DebugIdentifier + ") - Affliction prefab with the identifier \"" + afflictionIdentifier + "\" not found.",
contentPackage: characterTalent.Prefab.ContentPackage);
continue;
}
@@ -23,9 +23,8 @@ namespace Barotrauma
public CharacterTalent(TalentPrefab talentPrefab, Character character)
{
Character = character;
Prefab = talentPrefab;
Character = character ?? throw new ArgumentNullException(nameof(character));
Prefab = talentPrefab ?? throw new ArgumentNullException(nameof(talentPrefab));
var element = talentPrefab.ConfigElement;
DebugIdentifier = talentPrefab.OriginalName;
@@ -46,7 +45,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}");
DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}",
contentPackage: element.ContentPackage);
}
break;
case "addedstoreitem":
@@ -56,7 +56,8 @@ namespace Barotrauma
}
else
{
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}");
DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}",
contentPackage: element.ContentPackage);
}
break;
}
@@ -146,11 +147,13 @@ namespace Barotrauma
{
if (!Enum.TryParse(abilityEffectTypeString, true, out AbilityEffectType abilityEffectType))
{
DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Invalid ability effect type \"" + abilityEffectTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent?.Prefab?.ContentPackage);
}
if (abilityEffectType == AbilityEffectType.Undefined)
{
DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
DebugConsole.ThrowError("Ability effect type not defined in CharacterTalent (" + characterTalent.DebugIdentifier + ")",
contentPackage: characterTalent?.Prefab?.ContentPackage);
}
return abilityEffectType;
@@ -84,7 +84,8 @@ namespace Barotrauma
}
catch (Exception e)
{
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e);
DebugConsole.ThrowError($"Error while loading talent migration for talent \"{Identifier}\".", e,
element?.ContentPackage);
}
}
break;
@@ -37,7 +37,8 @@ namespace Barotrauma
if (Identifier.IsEmpty)
{
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!");
DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!",
contentPackage: element.ContentPackage);
return;
}
@@ -304,7 +305,8 @@ namespace Barotrauma
if (RequiredTalents > MaxChosenTalents)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.",
contentPackage: talentOptionsElement.ContentPackage);
}
HashSet<Identifier> identifiers = new HashSet<Identifier>();
@@ -333,11 +335,13 @@ namespace Barotrauma
if (RequiredTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.",
contentPackage: talentOptionsElement.ContentPackage);
}
if (MaxChosenTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.");
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.",
contentPackage: talentOptionsElement.ContentPackage);
}
}
}