Release 1.10.5.0 - Autumn Update 2025

This commit is contained in:
Regalis11
2025-09-17 13:44:21 +03:00
parent d13836ce87
commit caa0326cf8
120 changed files with 2584 additions and 635 deletions
@@ -312,9 +312,10 @@ namespace Barotrauma
if (!slots.HasFlag(characterInventory.SlotTypes[i])) { continue; }
}
targetSlot = i;
//slot free, continue
var otherItem = targetInventory.GetItemAt(i);
//slot free, continue
if (otherItem == null) { continue; }
if (!otherItem.IsInteractable(Character)) { return false; }
//try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{
@@ -1092,6 +1092,8 @@ namespace Barotrauma
}
if (!isFleeing)
{
CheckForDraggedCorpses();
foreach (Character target in Character.CharacterList)
{
if (target.CurrentHull != hull) { continue; }
@@ -1209,6 +1211,34 @@ namespace Barotrauma
}
}
}
private void CheckForDraggedCorpses()
{
if (Character.IsOnPlayerTeam) { return; }
if (Character.Submarine is not { Info.IsOutpost: true }) { return; }
//find corpses in the same team
foreach (Character otherCharacter in Character.CharacterList)
{
if (otherCharacter.SelectedCharacter == null ||
!otherCharacter.SelectedCharacter.IsDead ||
otherCharacter.SelectedCharacter.TeamID != Character.TeamID ||
otherCharacter.IsInstigator)
{
continue;
}
if (!Character.CanSeeTarget(otherCharacter)) { continue; }
// Player is dragging a corpse from our team
string dialogTag = Character.IsSecurity ? "dialogdraggingcorpsereactionsecurity" : "dialogdraggingcorpsereaction";
Character.Speak(TextManager.Get(dialogTag).Value, messageType: null,
delay: Rand.Range(0.5f, 1.0f), identifier: "dialogdraggingcorpsereaction".ToIdentifier(), minDurationBetweenSimilar: 10.0f);
AddCombatObjective(Character.IsSecurity ? AIObjectiveCombat.CombatMode.Arrest : AIObjectiveCombat.CombatMode.Retreat, otherCharacter);
break; // Only react to one at a time
}
}
public override void OnHealed(Character healer, float healAmount)
{
@@ -1660,6 +1690,10 @@ namespace Barotrauma
public AIObjective SetForcedOrder(Order order)
{
var objective = ObjectiveManager.CreateObjective(order);
if (order != null && !order.IsDismissal)
{
System.Diagnostics.Debug.Assert(objective != null);
}
ObjectiveManager.SetForcedOrder(objective);
return objective;
}
@@ -71,7 +71,7 @@ namespace Barotrauma
private static List<Identifier> GetCurrentFlags(Character speaker)
{
var currentFlags = new List<Identifier>();
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); }
if (Submarine.MainSub != null && Submarine.MainSub.AtCosmeticDamageDepth) { currentFlags.Add("SubmarineDeep".ToIdentifier()); }
if (GameMain.GameSession != null && Level.Loaded != null)
{
@@ -84,7 +84,6 @@ namespace Barotrauma
if (GameMain.GameSession.RoundDuration < 120.0f &&
speaker?.CurrentHull != null &&
GameMain.GameSession.Map?.CurrentLocation?.Reputation?.Value >= 0.0f &&
(speaker.TeamID == CharacterTeamType.FriendlyNPC || speaker.TeamID == CharacterTeamType.None) &&
Character.CharacterList.Any(c => c.TeamID != speaker.TeamID && c.CurrentHull == speaker.CurrentHull))
{
currentFlags.Add("EnterOutpost".ToIdentifier());
@@ -101,6 +101,7 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true, bool requireValidContainer = true, bool ignoreItemsMarkedForDeconstruction = true)
{
if (item == null) { return false; }
if (item.GetComponents<Pickable>().None(c => c is not Door && c.CanBePicked)) { return false; }
if (item.DontCleanUp) { return false; }
if (item.Illegitimate == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null)
@@ -65,7 +65,7 @@ namespace Barotrauma
private readonly AIObjectiveFindSafety findSafety;
private readonly HashSet<ItemComponent> weapons = new HashSet<ItemComponent>();
private readonly HashSet<Item> ignoredWeapons = new HashSet<Item>();
private readonly HashSet<ItemComponent> ignoredWeapons = new HashSet<ItemComponent>();
private AIObjectiveContainItem seekAmmunitionObjective;
private AIObjectiveGoTo retreatObjective;
@@ -503,7 +503,8 @@ namespace Barotrauma
HashSet<ItemComponent> allWeapons = FindWeaponsFromInventory();
while (allWeapons.Any())
{
Weapon = GetWeapon(allWeapons, out _weaponComponent);
Weapon = GetWeapon(allWeapons, out ItemComponent newWeaponComponent);
_weaponComponent = newWeaponComponent;
if (Weapon == null)
{
// No weapons
@@ -512,7 +513,7 @@ namespace Barotrauma
if (!character.Inventory.Contains(Weapon) || WeaponComponent == null)
{
// Not in the inventory anymore or cannot find the weapon component
allWeapons.Remove(WeaponComponent);
allWeapons.RemoveWhere(weaponComponent => weaponComponent.Item == Weapon);
Weapon = null;
continue;
}
@@ -540,7 +541,7 @@ namespace Barotrauma
else
{
// No ammo and should not try to seek ammo.
allWeapons.Remove(WeaponComponent);
allWeapons.RemoveWhere(weaponComponent => weaponComponent.Item == Weapon);
Weapon = null;
}
}
@@ -980,7 +981,6 @@ namespace Barotrauma
weapons.Clear();
foreach (var item in character.Inventory.AllItems)
{
if (ignoredWeapons.Contains(item)) { continue; }
GetWeapons(item, weapons);
if (item.OwnInventory != null)
{
@@ -990,11 +990,12 @@ namespace Barotrauma
return weapons;
}
private static void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
private void GetWeapons(Item item, ICollection<ItemComponent> weaponList)
{
if (item == null) { return; }
foreach (var component in item.Components)
{
if (ignoredWeapons.Contains(component)) { continue; }
if (component.CombatPriority > 0)
{
weaponList.Add(component);
@@ -1332,19 +1333,21 @@ namespace Barotrauma
{
SteeringManager.Reset();
RemoveSubObjective(ref seekAmmunitionObjective);
ignoredWeapons.Add(Weapon);
ignoredWeapons.Add(WeaponComponent);
Weapon = null;
});
}
/// <summary>
/// Reloads the ammunition found in the inventory.
/// If seekAmmo is true, tries to get find the ammo elsewhere.
/// </summary>
/// <returns>True if the weapon was reloaded successfully.</returns>
private bool Reload(bool seekAmmo)
{
if (WeaponComponent == null) { return false; }
if (Weapon.OwnInventory == null) { return true; }
if (!Weapon.IsInteractable(character)) { return false; }
HumanAIController.UnequipEmptyItems(Weapon, allowDestroying: !character.IsOnPlayerTeam);
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.RequiredItems.ContainsKey(RelatedItem.RelationType.Contained))
@@ -443,6 +443,10 @@ namespace Barotrauma
newCurrentObjective.Abandoned += () => DismissSelf(order);
CurrentOrders.Add(order.WithObjective(newCurrentObjective));
}
else if (!order.IsDismissal)
{
DebugConsole.ThrowError($"Failed to create an objective for the order: {order.Name}");
}
if (!HasOrders())
{
// Recreate objectives, because some of them may be removed, if impossible to complete (e.g. due to path finding)
@@ -458,6 +462,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Creates an AI objective based on the order. Note that the method can return null in the case of e.g. Dismissal orders or orders that erroneously target something non-interactable.
/// </summary>
public AIObjective CreateObjective(Order order, float priorityModifier = 1)
{
if (order == null || order.IsDismissal) { return null; }
@@ -341,6 +341,8 @@ namespace Barotrauma
{
if (component?.GetType() is Type componentType)
{
// Items used via a controller (i.e. turrets) are not selectable but should still be valid targets.
if (!UseController && !component.CanBeSelected) { continue; }
if (componentType == ItemComponentType) { return component; }
if (CanTypeBeSubclass && componentType.IsSubclassOf(ItemComponentType)) { return component; }
}
@@ -5,6 +5,7 @@ using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using static Barotrauma.CharacterParams;
@@ -434,6 +435,21 @@ namespace Barotrauma
var petBehavior = (c.AIController as EnemyAIController)?.PetBehavior;
if (petBehavior == null) { continue; }
//never save hostile pets or pets left outside
if (c.TeamID == CharacterTeamType.None ||
c.TeamID == CharacterTeamType.Team2 ||
c.Submarine == null)
{
continue;
}
//pets must be in a player sub or owned by someone to be persistent
if (c.Submarine is not { Info.IsPlayer: true } &&
petBehavior.Owner is not { IsOnPlayerTeam: true })
{
continue;
}
XElement petElement = new XElement("pet",
new XAttribute("speciesname", c.SpeciesName),
new XAttribute("ownerhash", petBehavior.Owner?.Info?.GetIdentifier() ?? 0),
@@ -42,7 +42,7 @@ namespace Barotrauma
{
enemyAi.PetBehavior?.Update(deltaTime);
}
if (IsDead || Vitality <= 0.0f || Stun > 0.0f || IsIncapacitated)
if (IsDead || IsUnconscious || Stun > 0.0f || IsIncapacitated)
{
//don't enable simple physics on dead/incapacitated characters
//the ragdoll controls the movement of incapacitated characters instead of the collider,
@@ -8,8 +8,15 @@ using Barotrauma.Extensions;
namespace Barotrauma
{
abstract class AnimController : Ragdoll
abstract class AnimController : Ragdoll, ISerializableEntity
{
/// <summary>
/// Most of the properties in this class are read-only, but can be useful for conditionals
/// </summary>
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; }
public string Name => nameof(AnimController);
public Vector2 RightHandIKPos { get; protected set; }
public Vector2 LeftHandIKPos { get; protected set; }
@@ -200,7 +207,10 @@ namespace Barotrauma
public float WalkPos { get; protected set; }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams)
{
SerializableProperties = SerializableProperty.GetProperties(this);
}
public void UpdateAnimations(float deltaTime)
{
@@ -2411,7 +2411,9 @@ namespace Barotrauma
if (Inventory != null)
{
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false })
//this doesn't need to be run by the server, clients sync the contents of their inventory with the server instead of the inputs used to manipulate the inventory
#if CLIENT
if (IsKeyHit(InputType.DropItem) && Screen.Selected is { IsEditor: false } && CharacterHUD.ShouldDrawInventory(this))
{
foreach (Item item in HeldItems)
{
@@ -2429,6 +2431,7 @@ namespace Barotrauma
break;
}
}
#endif
bool CanUseItemsWhenSelected(Item item) => item == null || !item.Prefab.DisableItemUsageWhenSelected;
if (CanUseItemsWhenSelected(SelectedItem) && CanUseItemsWhenSelected(SelectedSecondaryItem))
@@ -2632,6 +2635,7 @@ namespace Barotrauma
public bool Unequip(Item item)
{
if (!HasEquippedItem(item)) { return false; }
if (!item.IsInteractable(this)) { return false; }
if (!TryPutItemInAnySlot(item))
{
if (!TryPutItemInBag(item))
@@ -2642,7 +2646,7 @@ namespace Barotrauma
return true;
}
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
public bool CanAccessInventory(Inventory inventory, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.AllowBotsAndPets)
{
if (!CanInteract || inventory.Locked) { return false; }
@@ -2686,7 +2690,7 @@ namespace Barotrauma
/// <summary>
/// Is the inventory accessible to the character? Doesn't check if the character can actually interact with it (distance checks etc).
/// </summary>
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.Limited)
public bool IsInventoryAccessibleTo(Character character, CharacterInventory.AccessLevel accessLevel = CharacterInventory.AccessLevel.AllowBotsAndPets)
{
if (Removed || Inventory == null) { return false; }
if (!Inventory.AccessibleWhenAlive && !IsDead)
@@ -2701,9 +2705,9 @@ namespace Barotrauma
if (IsKnockedDownOrRagdolled || LockHands) { return true; }
return accessLevel switch
{
CharacterInventory.AccessLevel.Restricted => false,
CharacterInventory.AccessLevel.Limited => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.Allowed => IsOnSameTeam() || IsFriendlyPet(),
CharacterInventory.AccessLevel.OnlyIfIncapacitated => false,
CharacterInventory.AccessLevel.AllowBotsAndPets => (IsBot && IsOnSameTeam()) || IsFriendlyPet(),
CharacterInventory.AccessLevel.AllowFriendly => IsOnSameTeam() || IsFriendlyPet(),
_ => throw new NotImplementedException()
};
@@ -5714,7 +5718,7 @@ namespace Barotrauma
public bool HasRecipeForItem(Identifier recipeIdentifier)
{
if (GameMain.GameSession != null && GameMain.GameSession.UnlockedRecipes.Contains(recipeIdentifier)) { return true; }
if (GameMain.GameSession != null && GameMain.GameSession.HasUnlockedRecipe(this, recipeIdentifier)) { return true; }
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
@@ -135,6 +135,9 @@ namespace Barotrauma
private Affliction stunAffliction;
public Affliction BloodlossAffliction { get => bloodlossAffliction; }
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return Character.IsDead || (Vitality <= 0.0f && !Character.HasAbilityFlag(AbilityFlags.AlwaysStayConscious)); }