Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -4396,11 +4396,11 @@ namespace Barotrauma
else if (canAttackDoors && HasValidPath())
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door is { CanBeTraversed: false } && !door.HasAccess(Character))
if (door is { CanBeTraversed: false } && !door.HasAccess(Character) && door.Item.AiTarget is { } doorAiTarget)
{
if (SelectedAiTarget != door.Item.AiTarget || State != AIState.Attack)
if (SelectedAiTarget != doorAiTarget || State != AIState.Attack)
{
SelectTarget(door.Item.AiTarget, CurrentTargetMemory.Priority);
SelectTarget(doorAiTarget, CurrentTargetMemory.Priority);
State = AIState.Attack;
return false;
}
@@ -787,6 +787,13 @@ namespace Barotrauma
foreach (Item item in Character.HeldItems)
{
if (item == null || !item.IsInteractable(Character)) { continue; }
if (!item.UnequipAutomatically) { continue; }
//NPC set to operate the item they're holding, don't put it away
if (ObjectiveManager.CurrentObjective is AIObjectiveOperateItem operateItem &&
(operateItem.OperateTarget == item || operateItem.Component?.Item == item))
{
continue;
}
if (Character.TryPutItemInAnySlot(item)) { continue; }
if (Character.TryPutItemInBag(item)) { continue; }
if (item.HasTag(Tags.Weapon))
@@ -69,6 +69,7 @@ namespace Barotrauma
}
var getItemObjective = new AIObjectiveGetItem(character, gearTag, objectiveManager, equip: true)
{
IsFindDivingGearSubObjective = true,
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false,
AllowDangerousPressure = true,
@@ -49,6 +49,13 @@ namespace Barotrauma
public const float DefaultReach = 100;
public const float MaxReach = 150;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; }
@@ -378,6 +385,7 @@ namespace Barotrauma
{
return new AIObjectiveGoTo(moveToTarget, character, objectiveManager, repeat: false, getDivingGearIfNeeded: AllowToFindDivingGear, closeEnough: DefaultReach)
{
IsFindDivingGearSubObjective = IsFindDivingGearSubObjective,
// If the root container changes, the item is no longer where it was (taken by someone -> need to find another item)
AbortCondition = obj => targetItem == null || (targetItem.GetRootInventoryOwner() is Entity owner && owner != moveToTarget && owner != character),
SpeakIfFails = false,
@@ -14,6 +14,13 @@ namespace Barotrauma
public override bool KeepDivingGearOn => GetTargetHull() == null;
/// <summary>
/// Is the goal of this objective to get diving gear (i.e. has it been created by <see cref="AIObjectiveFindDivingGear"/>)?
/// If so, the objective won't attempt to create another objective if the path requires diving gear
/// (wouldn't make sense to start looking for diving gear so the bot can get to a room they're trying to get diving gear from!)
/// </summary>
public bool IsFindDivingGearSubObjective;
private AIObjectiveFindDivingGear findDivingGear;
private readonly bool repeat;
//how long until the path to the target is declared unreachable
@@ -379,85 +386,89 @@ namespace Barotrauma
}
}
if (Abandon) { return; }
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
if (!IsFindDivingGearSubObjective)
{
if (HumanAIController.HasDivingSuit(followTarget))
bool needsDivingSuit = (!isInside || hasOutdoorNodes) && !character.IsImmuneToPressure;
bool tryToGetDivingGear = needsDivingSuit || HumanAIController.NeedsDivingGear(targetHull, out needsDivingSuit);
bool tryToGetDivingSuit = needsDivingSuit;
Character followTarget = Target as Character;
if (Mimic && !character.IsImmuneToPressure)
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
if (HumanAIController.HasDivingSuit(followTarget))
{
tryToGetDivingGear = true;
tryToGetDivingSuit = true;
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
{
tryToGetDivingGear = true;
}
}
else if (HumanAIController.HasDivingMask(followTarget) && character.CharacterHealth.OxygenLowResistance < 1)
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
tryToGetDivingGear = true;
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
}
bool needsEquipment = false;
float minOxygen = AIObjectiveFindDivingGear.GetMinOxygen(character);
if (tryToGetDivingSuit)
{
needsEquipment = !HumanAIController.HasDivingSuit(character, minOxygen, requireSuitablePressureProtection: !objectiveManager.FailedToFindDivingGearForDepth);
}
else if (tryToGetDivingGear)
{
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
if (!getDivingGearIfNeeded)
{
if (needsEquipment)
else if (tryToGetDivingGear)
{
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
needsEquipment = !HumanAIController.HasDivingGear(character, minOxygen);
}
}
else
{
if (character.LockHands)
if (!getDivingGearIfNeeded)
{
cantFindDivingGear = true;
if (needsEquipment)
{
// Don't try to reach the target without proper equipment.
Abandon = true;
return;
}
}
if (cantFindDivingGear && needsDivingSuit)
else
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
cantFindDivingGear = true;
if (needsDivingSuit)
if (character.LockHands)
{
cantFindDivingGear = true;
}
if (cantFindDivingGear && needsDivingSuit)
{
// Don't try to reach the target without a suit because it's lethal.
Abandon = true;
return;
}
if (needsEquipment && !cantFindDivingGear)
{
SteeringManager.Reset();
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit (or mask)
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
cantFindDivingGear = true;
if (needsDivingSuit)
{
// Shouldn't try to reach the target without a suit, because it's lethal.
Abandon = true;
}
else
{
// Try again without requiring the diving suit (or mask)
RemoveSubObjective(ref findDivingGear);
TryAddSubObjective(ref findDivingGear, () => new AIObjectiveFindDivingGear(character, needsDivingSuit: !tryToGetDivingSuit, objectiveManager),
onAbandon: () =>
{
Abandon = character.CurrentHull != null && (objectiveManager.CurrentOrder != this || Target.Submarine == null);
RemoveSubObjective(ref findDivingGear);
},
onCompleted: () =>
{
RemoveSubObjective(ref findDivingGear);
});
}
},
onCompleted: () => RemoveSubObjective(ref findDivingGear));
return;
}
}
}
}
if (IsDoneFollowing())
{
OnCompleted();
@@ -347,7 +347,10 @@ namespace Barotrauma
useItemTimer = 0.05f;
StartUsingItem();
if (!allowMovement)
//make the character move towards the item they're using...
if (!allowMovement &&
//...except if they've selected an item that controls the character's direction (e.g. a periscope)
character.SelectedSecondaryItem?.GetComponent<Controller>() is not { Direction: Direction.Left or Direction.Right })
{
TargetMovement = Vector2.Zero;
TargetDir = handWorldPos.X > character.WorldPosition.X ? Direction.Right : Direction.Left;
@@ -4,7 +4,11 @@ namespace Barotrauma
{
enum CauseOfDeathType
{
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected,
/// <summary>
/// Special cause of death type returned by <see cref="Character.CauseOfDeathType"/> when the character is not dead.
/// </summary>
None
}
class CauseOfDeath
@@ -589,6 +589,9 @@ namespace Barotrauma
public Identifier VariantOf => Prefab.VariantOf;
/// <summary>
/// Non-localized name of the character (for characters with info, their name, for monsters, their species). E.g. "Mudraptor_veteran", "John Smith".
/// </summary>
public string Name
{
get
@@ -597,6 +600,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Localized display name of the character (e.g. "Mudraptor Veteran", "John Smith") - this should generally be used in any the player sees.
/// </summary>
public string DisplayName
{
get
@@ -1207,6 +1213,11 @@ namespace Barotrauma
private set;
}
/// <summary>
/// Can be used by mods to check the cause of death of the character using conditionals (e.g. if some <see cref="OnDeath"/> effects should or should not be triggered by certain causes of death).
/// </summary>
public CauseOfDeathType CauseOfDeathType => CauseOfDeath?.Type ?? CauseOfDeathType.None;
//can other characters select (= grab) this character
public bool CanBeSelected
{
@@ -2406,7 +2417,7 @@ namespace Barotrauma
{
if (!CanInteractWith(item)) { continue; }
if (SelectedItem?.OwnInventory != null && SelectedItem.OwnInventory.CanBePut(item))
if (SelectedItem?.OwnInventory != null && !SelectedItem.OwnInventory.Locked && SelectedItem.OwnInventory.CanBePut(item))
{
SelectedItem.OwnInventory.TryPutItem(item, this);
}
@@ -5703,6 +5714,7 @@ namespace Barotrauma
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
if (GameMain.GameSession != null && GameMain.GameSession.UnlockedRecipes.Contains(recipeIdentifier)) { return true; }
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
@@ -408,6 +408,11 @@ namespace Barotrauma
public HashSet<Identifier> UnlockedTalents { get; private set; } = new HashSet<Identifier>();
/// <summary>
/// Which of the character's extra talents (talents unlocked from outside their own talent tree) are reset when the talents are resetted using e.g. Mindwipe?
/// </summary>
public HashSet<Identifier> ResettableExtraTalents { get; private set; } = new HashSet<Identifier>();
private int talentResetCount;
/// <summary>
@@ -792,14 +797,6 @@ namespace Barotrauma
Salary = CalculateSalary();
}
OriginalName = !string.IsNullOrEmpty(originalName) ? originalName : Name;
TalentRefundPoints = CharacterConfigElement.GetAttributeInt("refundpoints", 0);
int loadedLastRewardDistribution = CharacterConfigElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
}
private void SetPersonalityTrait()
@@ -1016,10 +1013,22 @@ namespace Barotrauma
}
UnlockedTalents.Add(talentIdentifier);
if (talentElement.GetAttributeBool("resettable", defaultValue: false))
{
ResettableExtraTalents.Add(talentIdentifier);
}
}
}
}
TalentRefundPoints = infoElement.GetAttributeInt("refundpoints", 0);
int loadedLastRewardDistribution = infoElement.GetAttributeInt("lastrewarddistribution", -1);
if (loadedLastRewardDistribution >= 0)
{
LastRewardDistribution = Option.Some(loadedLastRewardDistribution);
}
LoadHeadAttachments();
}
@@ -1535,7 +1544,13 @@ namespace Barotrauma
if (TalentRefundPoints <= 0) { return; }
//e.g. talents from endocrine booster or extra talents some special NPC has
//stored in a list so we can re-unlock them on the character
var talentsFromOutsideTree = GetUnlockedTalentsOutsideTree().ToList();
//remove resettable talents, so they DON'T get re-unlocked
foreach (var resettableExtraTalent in ResettableExtraTalents)
{
talentsFromOutsideTree.Remove(resettableExtraTalent);
}
UnlockedTalents.Clear();
SavedStatValues.Clear();
@@ -1570,6 +1585,9 @@ namespace Barotrauma
public void Rename(string newName)
{
if (string.IsNullOrEmpty(newName)) { return; }
newName = Networking.Client.SanitizeName(newName);
// Replace the name tag of any existing id cards or duffel bags
foreach (var item in Item.ItemList)
{
@@ -1673,7 +1691,10 @@ namespace Barotrauma
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
talentElement.Add(
new XElement("Talent",
new XAttribute("identifier", talentIdentifier),
new XAttribute("resettable", ResettableExtraTalents.Contains(talentIdentifier))));
}
charElement.Add(savedStatElement);
@@ -882,6 +882,16 @@ namespace Barotrauma
/// Its opacity is controlled by the active effect's MinAfflictionOverlayAlphaMultiplier and MaxAfflictionOverlayAlphaMultiplier
/// </summary>
public readonly Sprite AfflictionOverlay;
/// <summary>
/// The speed of the affliction overlay animation.
/// Only applicable with AfflictionOverlayAnimated, and the overlay has to be a spritesheet so there's something to animate.
/// </summary>
public float AfflictionOverlayAnimSpeed
{
get;
set;
}
public ImmutableDictionary<Identifier, float> TreatmentSuitabilities
{
@@ -1005,6 +1015,10 @@ namespace Barotrauma
case "afflictionoverlay":
AfflictionOverlay = new Sprite(subElement);
break;
case "afflictionoverlayanimated":
AfflictionOverlay = new SpriteSheet(subElement);
AfflictionOverlayAnimSpeed = subElement.GetAttributeFloat("animspeed", 1.0f);
break;
case "statvalue":
DebugConsole.ThrowError($"Error in affliction \"{Identifier}\" - stat values should be configured inside the affliction's effects.",
contentPackage: element.ContentPackage);
@@ -302,7 +302,7 @@ namespace Barotrauma
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
item.UnequipAutomatically = false;
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
@@ -191,6 +191,7 @@ namespace Barotrauma
new List<InvSlotType>(item.GetComponent<Wearable>()?.AllowedSlots ?? item.GetComponent<Pickable>().AllowedSlots) :
new List<InvSlotType>(item.AllowedSlots);
allowedSlots.Remove(InvSlotType.Any);
item.UnequipAutomatically = false;
character.Inventory.TryPutItem(item, null, allowedSlots);
}
else
@@ -308,6 +308,9 @@ namespace Barotrauma
public Vector2 DebugTargetPos;
public Vector2 DebugRefPos;
/// <summary>
/// Is the limb the waist, a part of a leg or a tail?
/// </summary>
public bool IsLowerBody
{
get
@@ -330,22 +333,33 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the limb a leg or a part of a leg (upper or lower leg or foot)
/// </summary>
public bool IsLeg
{
get
{
switch (type)
return type switch
{
case LimbType.LeftFoot:
case LimbType.LeftLeg:
case LimbType.LeftThigh:
case LimbType.RightFoot:
case LimbType.RightLeg:
case LimbType.RightThigh:
return true;
default:
return false;
}
LimbType.LeftFoot or LimbType.LeftLeg or LimbType.LeftThigh or LimbType.RightFoot or LimbType.RightLeg or LimbType.RightThigh => true,
_ => false,
};
}
}
/// <summary>
/// Is the limb an arm or a part of an arm (upper or lower arm or hand)
/// </summary>
public bool IsArm
{
get
{
return type switch
{
LimbType.LeftArm or LimbType.LeftForearm or LimbType.LeftHand or LimbType.RightArm or LimbType.RightForearm or LimbType.RightHand => true,
_ => false,
};
}
}
@@ -156,6 +156,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The multiplier of the minimum distance required between this character and the player/submarine before the music starts playing. The default distance is twice the length of the submarine, or a minimum of 50 meters."), Editable]
public float MusicRangeMultiplier { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the entire crew get an achievement (assuming there is one) if someone from the crew kills the character?")]
public bool UnlockKillAchievementForWholeCrew { get; set; }
public readonly CharacterFile File;
public bool IsPet => AI?.IsPet ?? false;
@@ -69,9 +69,9 @@ namespace Barotrauma.Abilities
switch (targetType)
{
case TargetType.Enemy:
return !HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: false);
return !HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.Ally:
return HumanAIController.IsFriendly(character, targetCharacter, onlySameTeam: true);
return HumanAIController.IsFriendly(character, targetCharacter);
case TargetType.NotSelf:
return targetCharacter != character;
case TargetType.Alive:
@@ -84,5 +84,6 @@ namespace Barotrauma.Abilities
return true;
}
}
}
}
@@ -65,7 +65,7 @@ namespace Barotrauma.Abilities
}
if (!nearbyCharactersAppliesToAllies)
{
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character, onlySameTeam: true));
targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character));
}
if (!nearbyCharactersAppliesToEnemies)
{
@@ -3,7 +3,7 @@
class CharacterAbilityPsychoClown : CharacterAbility
{
private readonly StatTypes statType;
private readonly float maxValue;
private readonly float minValue, maxValue;
private readonly string afflictionIdentifier;
private float lastValue = 0f;
public override bool AllowClientSimulation => true;
@@ -11,8 +11,9 @@
public CharacterAbilityPsychoClown(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{
statType = CharacterAbilityGroup.ParseStatType(abilityElement.GetAttributeString("stattype", ""), CharacterTalent.DebugIdentifier);
maxValue = abilityElement.GetAttributeFloat("maxvalue", 0f);
afflictionIdentifier = abilityElement.GetAttributeString("afflictionidentifier", "");
maxValue = abilityElement.GetAttributeFloat(nameof(maxValue), 0f);
minValue = abilityElement.GetAttributeFloat(nameof(minValue), 0f);
afflictionIdentifier = abilityElement.GetAttributeString(nameof(afflictionIdentifier), "");
}
protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate)
@@ -31,7 +32,7 @@
afflictionStrength = affliction.Strength / affliction.Prefab.MaxStrength;
}
lastValue = afflictionStrength * maxValue;
lastValue = minValue + afflictionStrength * (maxValue - minValue);
Character.ChangeStat(statType, lastValue);
}
else
@@ -67,6 +67,7 @@ namespace Barotrauma.Abilities
{
if (Character.HasTalent(identifier)) { continue; }
Character.GiveTalent(identifier);
Character.Info.ResettableExtraTalents.Add(identifier);
}
}