Unstable 1.2.4.0
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,7 @@ 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);
|
||||
@@ -312,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);
|
||||
@@ -322,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
|
||||
@@ -565,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);
|
||||
@@ -3097,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];
|
||||
@@ -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)
|
||||
|
||||
+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);
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ namespace Barotrauma
|
||||
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))
|
||||
character.CanSeeTarget(target, seeThroughWindows: true))
|
||||
{
|
||||
AIObjectiveCheckStolenItems? existingObjective =
|
||||
objectiveManager.GetActiveObjectives<AIObjectiveCheckStolenItems>().FirstOrDefault(o => o.TargetCharacter == target);
|
||||
|
||||
+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()
|
||||
|
||||
@@ -664,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)
|
||||
@@ -713,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)
|
||||
|
||||
+45
-5
@@ -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; }
|
||||
@@ -501,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;
|
||||
}
|
||||
|
||||
@@ -586,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(
|
||||
@@ -764,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);
|
||||
}
|
||||
@@ -1536,7 +1560,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
|
||||
@@ -1741,7 +1765,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
|
||||
{
|
||||
|
||||
@@ -572,6 +572,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++)
|
||||
@@ -582,6 +586,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)
|
||||
@@ -668,6 +687,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; }
|
||||
@@ -710,37 +736,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)
|
||||
@@ -1335,17 +1377,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
|
||||
@@ -1593,10 +1646,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 ||
|
||||
@@ -1621,7 +1674,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;
|
||||
@@ -1700,7 +1753,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)
|
||||
|
||||
@@ -67,10 +67,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;
|
||||
}
|
||||
@@ -936,10 +944,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();
|
||||
@@ -1694,31 +1708,21 @@ namespace Barotrauma
|
||||
info.Job?.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));
|
||||
@@ -2303,40 +2307,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)
|
||||
@@ -2365,13 +2369,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; }
|
||||
@@ -2382,39 +2386,37 @@ 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)
|
||||
if (item.GetComponent<Door>() is { HasWindow: true } door && seeThroughWindows)
|
||||
{
|
||||
if (door.IsPositionOnWindow(ConvertUnits.ToDisplayUnits(Submarine.LastPickedPosition))) { return false; }
|
||||
}
|
||||
@@ -2513,9 +2515,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;
|
||||
}
|
||||
@@ -3584,6 +3598,8 @@ namespace Barotrauma
|
||||
|
||||
private void Despawn(bool createNetworkEvents = true)
|
||||
{
|
||||
if (!EnableDespawn) { return; }
|
||||
|
||||
Identifier despawnContainerId =
|
||||
IsHuman ?
|
||||
"despawncontainer".ToIdentifier() :
|
||||
@@ -3665,10 +3681,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)
|
||||
@@ -3683,6 +3701,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;
|
||||
@@ -4340,19 +4359,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4366,12 +4382,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>
|
||||
@@ -5005,8 +5019,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()
|
||||
@@ -5021,7 +5037,7 @@ namespace Barotrauma
|
||||
foreach (var hull in adjacentHulls)
|
||||
{
|
||||
if (hull.ConnectedGaps.Any(g =>
|
||||
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
|
||||
g.Open > 0.9f &&
|
||||
g.linkedTo.Contains(CurrentHull) &&
|
||||
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
|
||||
{
|
||||
@@ -5041,7 +5057,7 @@ namespace Barotrauma
|
||||
else
|
||||
{
|
||||
if (h.ConnectedGaps.Any(g =>
|
||||
(g.Open > 0.9f || g.ConnectedDoor is { HasWindow: true }) &&
|
||||
g.Open > 0.9f &&
|
||||
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) &&
|
||||
CanSeeTarget(g)))
|
||||
{
|
||||
@@ -5582,4 +5598,12 @@ namespace Barotrauma
|
||||
public Character Character { get; set; }
|
||||
}
|
||||
|
||||
class AbilityItemSelected : AbilityObject, IAbilityItem
|
||||
{
|
||||
public AbilityItemSelected(Item item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
public Item Item { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -1902,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);
|
||||
|
||||
+4
@@ -1026,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());
|
||||
|
||||
@@ -1313,6 +1313,8 @@ namespace Barotrauma
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
afflictionsToRemove.Clear();
|
||||
afflictionsToUpdate.Clear();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
|
||||
@@ -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)
|
||||
@@ -603,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);
|
||||
@@ -732,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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
+38
-16
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Abilities
|
||||
{
|
||||
@@ -11,6 +10,12 @@ 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(
|
||||
@@ -25,30 +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.",
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -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;
|
||||
@@ -21,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;
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -38,7 +38,11 @@ internal sealed class CharacterAbilityGiveExperience : CharacterAbility
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-8
@@ -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;
|
||||
|
||||
@@ -40,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)
|
||||
@@ -72,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user