Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop
This commit is contained in:
@@ -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)
|
||||
|
||||
+160
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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;
|
||||
|
||||
+6
-4
@@ -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);
|
||||
|
||||
+152
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
+4
@@ -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(
|
||||
|
||||
+159
-103
@@ -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;
|
||||
|
||||
+13
-14
@@ -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;
|
||||
|
||||
+22
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+28
-22
@@ -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);
|
||||
|
||||
+3
-2
@@ -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
-4
@@ -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
|
||||
{
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+41
-18
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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)
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
+12
-4
@@ -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;
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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
-8
@@ -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);
|
||||
}
|
||||
|
||||
+3
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-9
@@ -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());
|
||||
}
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -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++)
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+35
-13
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user