Release 1.10.5.0 - Autumn Update 2025
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<contentpackage name="[DebugOnlyTest]PipeTestSub" modversion="1.0.0" corepackage="False" gameversion="1.8.4.2">
|
||||
<Submarine file="%ModDir%/Dugong_PipeTest.sub" />
|
||||
</contentpackage>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<contentpackage name="[DebugOnlyTest]testGapSub" modversion="1.0.3" corepackage="False" gameversion="1.9.5.0">
|
||||
<Submarine file="%ModDir%/testGapSub.sub" />
|
||||
</contentpackage>
|
||||
Binary file not shown.
@@ -6,7 +6,6 @@
|
||||
<Workshop name="Meaningful Upgrades" id="2183524355" />
|
||||
<Workshop name="Community Conversation Pack" id="2435017882" />
|
||||
<Workshop name="Stations from beyond" id="2585543390" />
|
||||
<Workshop name="FrithsMissionTweak" id="2788861460" />
|
||||
<Workshop name="New Wrecks For Barotrauma (With sellable wrecks)" id="2184257427" />
|
||||
<Workshop name="DynamicEuropa" id="2532991202" />
|
||||
<Workshop name="32x Stack" id="2683570256" />
|
||||
|
||||
@@ -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());
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+11
-8
@@ -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))
|
||||
|
||||
+7
@@ -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)); }
|
||||
|
||||
+2
@@ -106,6 +106,8 @@ namespace Barotrauma
|
||||
void AddTexturePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) { return; }
|
||||
//if the path contains a gender variable, we can't load it yet because we don't know which gender we need
|
||||
if (path.Contains("[GENDER]")) { return; }
|
||||
texturePaths.Add(ContentPath.FromRaw(characterPrefab.ContentPackage, ragdollParams.Texture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,13 +656,17 @@ namespace Barotrauma
|
||||
NewMessage("***************", Color.Cyan);
|
||||
}));
|
||||
|
||||
commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
|
||||
commands.Add(new Command("godmode", "godmode [character name] [remove afflictions (true/false)]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
|
||||
(string[] args) =>
|
||||
{
|
||||
bool? godmodeStateOnFirstCharacter = null;
|
||||
HandleCommandForCrewOrSingleCharacter(args, ToggleGodMode);
|
||||
void ToggleGodMode(Character targetCharacter)
|
||||
{
|
||||
if (args.Length > 1 && bool.TryParse(args[1], out bool removeafflictions))
|
||||
{
|
||||
if (removeafflictions) { targetCharacter.CharacterHealth.RemoveAllAfflictions(); }
|
||||
}
|
||||
targetCharacter.GodMode = godmodeStateOnFirstCharacter ?? !targetCharacter.GodMode;
|
||||
godmodeStateOnFirstCharacter = targetCharacter.GodMode;
|
||||
NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name,
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
[Serialize(0, IsPropertySaveable.Yes, description: "The state to set the mission to, or how much to add to the state of the mission.")]
|
||||
public int State { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If set to true, the mission is forced to fail without a chance of retrying it.")]
|
||||
public bool ForceFailure { get; set; }
|
||||
|
||||
private bool isFinished;
|
||||
|
||||
public MissionStateAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element)
|
||||
@@ -31,7 +34,7 @@
|
||||
DebugConsole.ThrowError($"Error in event \"{parentEvent.Prefab.Identifier}\": MissionIdentifier has not been configured.",
|
||||
contentPackage: element.ContentPackage);
|
||||
}
|
||||
if (Operation == OperationType.Add && State == 0)
|
||||
if (Operation == OperationType.Add && State == 0 && !ForceFailure)
|
||||
{
|
||||
DebugConsole.AddWarning($"Potential error in event \"{parentEvent.Prefab.Identifier}\": {nameof(MissionStateAction)} is set to add 0 to the mission state, which will do nothing.",
|
||||
contentPackage: element.ContentPackage);
|
||||
@@ -54,6 +57,11 @@
|
||||
foreach (Mission mission in GameMain.GameSession.Missions)
|
||||
{
|
||||
if (mission.Prefab.Identifier != MissionIdentifier) { continue; }
|
||||
if (ForceFailure)
|
||||
{
|
||||
mission.ForceFailure = true;
|
||||
}
|
||||
|
||||
switch (Operation)
|
||||
{
|
||||
case OperationType.Set:
|
||||
|
||||
@@ -410,16 +410,39 @@ namespace Barotrauma
|
||||
public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable<Identifier> moduleFlags = null, IEnumerable<Identifier> spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false, bool allowInPlayerView = true)
|
||||
{
|
||||
bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost;
|
||||
List<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
|
||||
potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
|
||||
|
||||
IEnumerable<WayPoint> potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull));
|
||||
potentialSpawnPoints = potentialSpawnPoints.Where(wp => wp.ConnectedDoor == null && wp.Ladders == null && wp.IsTraversable);
|
||||
|
||||
//find spawnpoints with the desired type, or any random spawnpoints if not specified
|
||||
IEnumerable<WayPoint> spawnPointsWithCorrectType;
|
||||
if (spawnPointType.HasValue)
|
||||
{
|
||||
spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp =>
|
||||
spawnPointType.Value.HasFlag(wp.SpawnType) &&
|
||||
//need to handle zero (SpawnType.Path) separately, because spawnPointType will always have the flag 0
|
||||
(wp.SpawnType != 0 || spawnPointType.Value == 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
spawnPointsWithCorrectType = potentialSpawnPoints.Where(wp => wp.SpawnType != SpawnType.Path);
|
||||
}
|
||||
if (spawnPointsWithCorrectType.Any())
|
||||
{
|
||||
potentialSpawnPoints = spawnPointsWithCorrectType;
|
||||
}
|
||||
|
||||
//with correct module flags, if there are any
|
||||
if (moduleFlags != null && moduleFlags.Any())
|
||||
{
|
||||
var spawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
|
||||
if (spawnPoints.Any())
|
||||
var spawnPointsWithCorrectFlags = potentialSpawnPoints.Where(wp => wp.CurrentHull is Hull h && h.OutpostModuleTags.Any(moduleFlags.Contains));
|
||||
if (spawnPointsWithCorrectFlags.Any())
|
||||
{
|
||||
potentialSpawnPoints = spawnPoints.ToList();
|
||||
potentialSpawnPoints = spawnPointsWithCorrectFlags.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
//with correct spawn point tags, if there are any
|
||||
if (spawnpointTags != null && spawnpointTags.Any())
|
||||
{
|
||||
var spawnPointsWithTag = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
|
||||
@@ -461,16 +484,8 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
IEnumerable<WayPoint> validSpawnPoints;
|
||||
if (spawnPointType.HasValue)
|
||||
{
|
||||
validSpawnPoints = potentialSpawnPoints.FindAll(wp => spawnPointType.Value.HasFlag(wp.SpawnType));
|
||||
}
|
||||
else
|
||||
{
|
||||
validSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.SpawnType != SpawnType.Path);
|
||||
if (!validSpawnPoints.Any()) { validSpawnPoints = potentialSpawnPoints; }
|
||||
}
|
||||
//spawnpoints that match the desired criteria found, choose the best one next
|
||||
IEnumerable<WayPoint> validSpawnPoints = potentialSpawnPoints;
|
||||
|
||||
//don't spawn in an airlock module if there are other options
|
||||
var airlockSpawnPoints = potentialSpawnPoints.Where(wp => wp.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -23,10 +24,12 @@ namespace Barotrauma
|
||||
private bool swarmSpawned;
|
||||
private readonly List<MonsterSet> monsterSets = new List<MonsterSet>();
|
||||
private readonly LocalizedString sonarLabel;
|
||||
private readonly ImmutableArray<Identifier> beaconTags;
|
||||
|
||||
public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
|
||||
{
|
||||
swarmSpawned = false;
|
||||
beaconTags = prefab.ConfigElement.GetAttributeIdentifierArray("beacontags", []).ToImmutableArray();
|
||||
|
||||
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
|
||||
{
|
||||
@@ -185,6 +188,29 @@ namespace Barotrauma
|
||||
{
|
||||
levelData.HasBeaconStation = true;
|
||||
levelData.IsBeaconActive = false;
|
||||
|
||||
if (beaconTags.Length > 0)
|
||||
{
|
||||
var selectedBeacon = GetRandomBeaconByTags(beaconTags, levelData);
|
||||
if (selectedBeacon != null)
|
||||
{
|
||||
levelData.ForceBeaconStation = selectedBeacon;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Beacon mission \"{Prefab.Identifier}\" could not find a suitable beacon station with beacontags \"{string.Join(", ", beaconTags)}\" for level difficulty {levelData.Difficulty:F1}.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SubmarineInfo GetRandomBeaconByTags(ImmutableArray<Identifier> tags, LevelData levelData)
|
||||
{
|
||||
return GetRandomSubmarineByTagsAndDifficulty(
|
||||
tags,
|
||||
levelData,
|
||||
s => s.IsBeacon,
|
||||
"beacon station");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ namespace Barotrauma
|
||||
|
||||
protected static bool IsClient => GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
|
||||
|
||||
private readonly CheckDataAction completeCheckDataAction;
|
||||
protected readonly CheckDataAction completeCheckDataAction;
|
||||
|
||||
public readonly ImmutableArray<LocalizedString> Headers;
|
||||
public readonly ImmutableArray<LocalizedString> Messages;
|
||||
@@ -110,9 +110,11 @@ namespace Barotrauma
|
||||
|
||||
public bool Failed
|
||||
{
|
||||
get { return failed; }
|
||||
get { return failed || ForceFailure; }
|
||||
}
|
||||
|
||||
public bool ForceFailure;
|
||||
|
||||
public virtual bool AllowRespawning
|
||||
{
|
||||
get { return true; }
|
||||
@@ -541,9 +543,10 @@ namespace Barotrauma
|
||||
{
|
||||
if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
completed =
|
||||
completed =
|
||||
!ForceFailure &&
|
||||
DetermineCompleted() &&
|
||||
(completeCheckDataAction == null ||completeCheckDataAction.GetSuccess());
|
||||
(completeCheckDataAction == null || completeCheckDataAction.GetSuccess());
|
||||
}
|
||||
if (completed)
|
||||
{
|
||||
@@ -569,6 +572,10 @@ namespace Barotrauma
|
||||
TimesAttempted++;
|
||||
|
||||
EndMissionSpecific(completed);
|
||||
if (ForceFailure)
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool DetermineCompleted();
|
||||
@@ -829,6 +836,51 @@ namespace Barotrauma
|
||||
cargoSpawnPos.Position.X + Rand.Range(-20.0f, 20.0f, Rand.RandSync.ServerAndClient),
|
||||
cargoRoom.Rect.Y - cargoRoom.Rect.Height + itemPrefab.Size.Y / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a random submarine by tags, filtered by difficulty. Used by missions that force specific submarines (wrecks, beacons, etc.)
|
||||
/// </summary>
|
||||
/// <param name="tags">Mission tags to match against</param>
|
||||
/// <param name="seed">Random seed for selection</param>
|
||||
/// <param name="submarineSelector">Function to filter submarines by type (e.g., s => s.IsWreck)</param>
|
||||
/// <param name="submarineTypeName">Name of submarine type for error messages (e.g., "wreck", "beacon station")</param>
|
||||
/// <returns>Selected submarine, or null if none found</returns>
|
||||
protected static SubmarineInfo GetRandomSubmarineByTagsAndDifficulty(
|
||||
IEnumerable<Identifier> tags,
|
||||
LevelData levelData,
|
||||
Func<SubmarineInfo, bool> submarineSelector,
|
||||
string submarineTypeName)
|
||||
{
|
||||
var rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
|
||||
float levelDifficulty = levelData.Difficulty;
|
||||
|
||||
var submarinesWithTags = SubmarineInfo.SavedSubmarines
|
||||
.Where(submarineSelector)
|
||||
.Where(s =>
|
||||
{
|
||||
return s.GetExtraSubmarineInfo is { } extraInfo && (tags.None() || tags.Any(t => extraInfo.MissionTags.Contains(t)));
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var matchingSubmarines = submarinesWithTags
|
||||
.Where(s =>
|
||||
{
|
||||
return s.GetExtraSubmarineInfo is { } extraInfo &&
|
||||
levelDifficulty >= extraInfo.MinLevelDifficulty &&
|
||||
levelDifficulty <= extraInfo.MaxLevelDifficulty;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (matchingSubmarines.Count == 0)
|
||||
{
|
||||
if (submarinesWithTags.Count > 0)
|
||||
{
|
||||
DebugConsole.ThrowError($"Found {submarinesWithTags.Count} {submarineTypeName}(s) with matching tags \"{string.Join(", ", tags)}\", but none are suitable for level difficulty {levelDifficulty:F1}.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return matchingSubmarines[rand.Next(matchingSubmarines.Count)];
|
||||
}
|
||||
}
|
||||
|
||||
class AbilityMissionMoneyGainMultiplier : AbilityObject, IAbilityValue, IAbilityMission
|
||||
|
||||
@@ -4,6 +4,7 @@ using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -240,6 +241,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private readonly float requiredDeliveryAmount;
|
||||
|
||||
private readonly ImmutableArray<Identifier> wreckTags;
|
||||
|
||||
private LocalizedString pickedUpMessage;
|
||||
|
||||
/// <summary>
|
||||
@@ -311,12 +314,13 @@ namespace Barotrauma
|
||||
: base(prefab, locations, sub)
|
||||
{
|
||||
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeFloat(nameof(requiredDeliveryAmount), 0.98f);
|
||||
|
||||
//LevelData may not be instantiated at this point, in that case use the name identifier of the location
|
||||
rng = new MTRandom(ToolBox.StringToInt(
|
||||
locations[0].LevelData?.Seed ?? locations[0].NameIdentifier.Value +
|
||||
locations[1].LevelData?.Seed ?? locations[1].NameIdentifier.Value));
|
||||
|
||||
wreckTags = prefab.ConfigElement.GetAttributeIdentifierArray("wrecktags", []).ToImmutableArray();
|
||||
|
||||
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
|
||||
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
|
||||
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
|
||||
@@ -756,5 +760,31 @@ namespace Barotrauma
|
||||
target.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public override void AdjustLevelData(LevelData levelData)
|
||||
{
|
||||
if (wreckTags.Length > 0)
|
||||
{
|
||||
var selectedWreck = GetRandomWreckByTags(wreckTags, levelData);
|
||||
if (selectedWreck != null)
|
||||
{
|
||||
levelData.ForceWreck = selectedWreck;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Salvage mission \"{Prefab.Identifier}\" could not find a suitable wreck with wrecktags \"{string.Join(", ", wreckTags)}\" for level difficulty {levelData.Difficulty:F1}.",
|
||||
contentPackage: Prefab.ContentPackage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static SubmarineInfo GetRandomWreckByTags(ImmutableArray<Identifier> tags, LevelData levelData)
|
||||
{
|
||||
return GetRandomSubmarineByTagsAndDifficulty(
|
||||
tags,
|
||||
levelData,
|
||||
s => s.IsWreck,
|
||||
"wreck");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,12 +304,17 @@ namespace Barotrauma
|
||||
// Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
|
||||
var buyValues = GetBuyValuesAtCurrentLocation(storeIdentifier, itemsToPurchase.Select(i => i.ItemPrefab));
|
||||
var itemsInStoreCrate = GetBuyCrateItems(storeIdentifier, create: true);
|
||||
foreach (PurchasedItem item in itemsToPurchase)
|
||||
//handle checking which items can be purchased and deducting money first
|
||||
foreach (PurchasedItem item in itemsToPurchase.ToList())
|
||||
{
|
||||
if (item.Quantity <= 0) { continue; }
|
||||
// Exchange money
|
||||
int itemValue = item.Quantity * buyValues[item.ItemPrefab];
|
||||
if (!campaign.TryPurchase(client, itemValue)) { continue; }
|
||||
if (!campaign.TryPurchase(client, itemValue))
|
||||
{
|
||||
itemsToPurchase.Remove(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to the purchased items
|
||||
var purchasedItem = itemsPurchasedFromStore.Find(pi => pi.ItemPrefab == item.ItemPrefab && pi.DeliverImmediately == item.DeliverImmediately);
|
||||
@@ -329,6 +334,7 @@ namespace Barotrauma
|
||||
}
|
||||
store.Balance += itemValue;
|
||||
}
|
||||
//actually spawn the items at this point
|
||||
if (GameMain.NetworkMember is not { IsClient: true })
|
||||
{
|
||||
Character targetCharacter;
|
||||
|
||||
@@ -1724,7 +1724,10 @@ namespace Barotrauma
|
||||
GameMain.GameSession.EventManager.Load(subElement);
|
||||
break;
|
||||
case "unlockedrecipe":
|
||||
GameMain.GameSession.UnlockRecipe(subElement.GetAttributeIdentifier("identifier", Identifier.Empty), showNotifications: false);
|
||||
GameMain.GameSession.UnlockRecipe(
|
||||
subElement.GetAttributeEnum("team", CharacterTeamType.Team1),
|
||||
subElement.GetAttributeIdentifier("identifier", Identifier.Empty),
|
||||
showNotifications: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,8 @@ namespace Barotrauma
|
||||
|
||||
public Submarine? Submarine { get; set; }
|
||||
|
||||
private readonly HashSet<Identifier> unlockedRecipes = new HashSet<Identifier>();
|
||||
public IEnumerable<Identifier> UnlockedRecipes => unlockedRecipes;
|
||||
private readonly HashSet<(CharacterTeamType team, Identifier identifier)> unlockedRecipes = new HashSet<(CharacterTeamType, Identifier)>();
|
||||
public IEnumerable<(CharacterTeamType, Identifier)> UnlockedRecipes => unlockedRecipes;
|
||||
|
||||
public CampaignDataPath DataPath { get; set; }
|
||||
|
||||
@@ -1499,25 +1499,32 @@ namespace Barotrauma
|
||||
#endif
|
||||
}
|
||||
|
||||
public void UnlockRecipe(Identifier identifier, bool showNotifications)
|
||||
public void UnlockRecipe(CharacterTeamType team, Identifier identifier, bool showNotifications)
|
||||
{
|
||||
if (unlockedRecipes.Add(identifier))
|
||||
if (unlockedRecipes.Add((team, identifier)))
|
||||
{
|
||||
#if CLIENT
|
||||
if (showNotifications)
|
||||
{
|
||||
foreach (var character in GetSessionCrewCharacters(CharacterType.Both))
|
||||
{
|
||||
if (character.TeamID != team) { continue; }
|
||||
LocalizedString recipeName = TextManager.Get($"entityname.{identifier}").Fallback(identifier.Value);
|
||||
character.AddMessage(TextManager.GetWithVariable("recipeunlockednotification", "[name]", recipeName).Value, GUIStyle.Yellow, playSound: true);
|
||||
}
|
||||
}
|
||||
#else
|
||||
GameMain.Server.UnlockRecipe(identifier);
|
||||
GameMain.Server.UnlockRecipe(team, identifier);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasUnlockedRecipe(Character character, Identifier itemIdentifier)
|
||||
{
|
||||
if (character == null) { return false; }
|
||||
return unlockedRecipes.Contains((character.TeamID, itemIdentifier));
|
||||
}
|
||||
|
||||
public static bool IsCompatibleWithEnabledContentPackages(IList<string> contentPackageNames, out LocalizedString errorMsg)
|
||||
{
|
||||
errorMsg = "";
|
||||
|
||||
@@ -17,15 +17,21 @@ namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// How much access other characters have to the inventory?
|
||||
/// <see cref="Restricted"/> = Only accessible when character is knocked down or handcuffed.
|
||||
/// <see cref="Limited"/> = Can also access inventories of bots on the same team and friendly pets.
|
||||
/// <see cref="Allowed"/> = Can also access other players in the same team (used for drag and drop give).
|
||||
/// </summary>
|
||||
public enum AccessLevel
|
||||
{
|
||||
Restricted,
|
||||
Limited,
|
||||
Allowed
|
||||
/// <summary>
|
||||
/// Only accessible when character is knocked down or handcuffed.
|
||||
/// </summary>
|
||||
OnlyIfIncapacitated,
|
||||
/// <summary>
|
||||
/// Can also access inventories of bots on the same team and friendly pets.
|
||||
/// </summary>
|
||||
AllowBotsAndPets,
|
||||
/// <summary>
|
||||
/// Can also access other players in the same team (used for drag and drop give).
|
||||
/// </summary>
|
||||
AllowFriendly
|
||||
}
|
||||
|
||||
private readonly Character character;
|
||||
@@ -342,8 +348,9 @@ namespace Barotrauma
|
||||
{
|
||||
foreach (Item existingItem in slots[slot].Items.ToList())
|
||||
{
|
||||
if (!existingItem.IsInteractable(character)) { continue; }
|
||||
existingItem.Drop(user);
|
||||
if (existingItem.ParentInventory != null) { existingItem.ParentInventory.RemoveItem(existingItem); }
|
||||
existingItem.ParentInventory?.RemoveItem(existingItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
@@ -197,7 +198,16 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
item.Drop(CurrentThrower, createNetworkEvent: GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer);
|
||||
item.WaterDragCoefficient = WaterDragCoefficient;
|
||||
item.body.ApplyLinearImpulse(throwVector * ThrowForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
|
||||
float throwForce = ThrowForce;
|
||||
//Reduce force when aiming down
|
||||
float downwardsDotProduct = Vector2.Dot(-Vector2.UnitY, throwVector); //1 when pointing directly down, 0 when sideways, -1 when up
|
||||
if (downwardsDotProduct > 0)
|
||||
{
|
||||
throwForce *= (1.0f - downwardsDotProduct * 0.7f);
|
||||
}
|
||||
|
||||
item.body.ApplyLinearImpulse(throwVector * throwForce * item.body.Mass * 3.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
||||
|
||||
//disable platform collisions until the item comes back to rest again
|
||||
item.body.CollidesWith = Physics.CollisionWall | Physics.CollisionLevel;
|
||||
|
||||
@@ -483,6 +483,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public virtual bool UpdateWhenInactive => false;
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, "If true, the component will retain its normal functionality when the item reaches 0 condition.")]
|
||||
public bool UpdateWhenBroken { get; set; }
|
||||
|
||||
//called when isActive is true and condition > 0.0f
|
||||
public virtual void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
|
||||
@@ -132,6 +132,12 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should a button that allows sorting the items alphabetically be shown in the container's UI panel?")]
|
||||
public bool ShowSortButton { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.No, description: "Should a button that merges items into stacks be shown in the container's UI panel?")]
|
||||
public bool ShowMergeButton { get; set; }
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
|
||||
public bool QuickUseMovesItemsInside { get; set; }
|
||||
|
||||
|
||||
@@ -406,6 +406,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (IsOutOfPower()) { return false; }
|
||||
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
|
||||
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
|
||||
{
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
||||
@@ -421,8 +422,7 @@ namespace Barotrauma.Items.Components
|
||||
item.SendSignal(new Signal(output, sender: user), "trigger_out");
|
||||
}
|
||||
|
||||
lastUsed = Timing.TotalTime;
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
|
||||
lastUsed = Timing.TotalTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -541,6 +541,7 @@ namespace Barotrauma.Items.Components
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnUse, picker);
|
||||
#endif
|
||||
ApplyStatusEffects(ActionType.OnUse, 1f, picker);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -84,7 +84,10 @@ namespace Barotrauma.Items.Components
|
||||
CurrFlow = 0.0f;
|
||||
}
|
||||
|
||||
private void GetVents()
|
||||
/// <summary>
|
||||
/// Finds all the linked vents and calculates how much oxygen should be distributed to each of them based on the hull volumes.
|
||||
/// </summary>
|
||||
public void GetVents()
|
||||
{
|
||||
totalHullVolume = 0.0f;
|
||||
ventList ??= new List<(Vent vent, float hullVolume)>();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
@@ -35,7 +36,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
get
|
||||
{
|
||||
if (item.ConditionPercentage > 10.0f || !IsActive) { return 0.0f; }
|
||||
if (item.ConditionPercentage > 10.0f || !IsActive || Disabled) { return 0.0f; }
|
||||
return (1.0f - item.ConditionPercentage / 10.0f) * 100.0f;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +62,23 @@ namespace Barotrauma.Items.Components
|
||||
set => maxFlow = value;
|
||||
}
|
||||
|
||||
private bool disabled;
|
||||
[Serialize(false, IsPropertySaveable.Yes, description: "If true, the pump is unable to pump water.", alwaysUseInstanceValues: true)]
|
||||
public bool Disabled
|
||||
{
|
||||
get => disabled;
|
||||
set
|
||||
{
|
||||
if (disabled == value) { return; }
|
||||
disabled = value;
|
||||
#if SERVER
|
||||
//send a network update soon
|
||||
//don't force to 0 though so this doesn't lead to spam if the property is toggled rapidly
|
||||
networkUpdateTimer = Math.Min(networkUpdateTimer, 0.5f);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
||||
public bool IsOn
|
||||
{
|
||||
@@ -68,15 +86,13 @@ namespace Barotrauma.Items.Components
|
||||
set { IsActive = value; }
|
||||
}
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool CanCauseLethalPressure { get; set; }
|
||||
|
||||
private float currFlow;
|
||||
public float CurrFlow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsActive) { return 0.0f; }
|
||||
return Math.Abs(currFlow);
|
||||
}
|
||||
}
|
||||
public float CurrFlow => IsActive ? Math.Abs(currFlow) : 0.0f;
|
||||
|
||||
public bool IsHullFull => item.CurrentHull != null && item.CurrentHull.WaterVolume >= item.CurrentHull.Volume * Hull.MaxCompress;
|
||||
|
||||
public override bool HasPower => IsActive && Voltage >= MinVoltage;
|
||||
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
|
||||
@@ -85,7 +101,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool UpdateWhenInactive => true;
|
||||
|
||||
public float CurrentStress => Math.Abs(flowPercentage / 100.0f);
|
||||
public float CurrentStress => IsActive ? Math.Abs(flowPercentage / 100.0f) : 0.0f;
|
||||
|
||||
public Pump(Item item, ContentXElement element)
|
||||
: base(item, element)
|
||||
@@ -95,48 +111,42 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void InitProjSpecific(ContentXElement element);
|
||||
|
||||
private readonly List<Hull> linkedHulls = [];
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
pumpSpeedLockTimer -= deltaTime;
|
||||
isActiveLockTimer -= deltaTime;
|
||||
|
||||
if (!IsActive)
|
||||
currFlow = 0f;
|
||||
|
||||
if (item.CurrentHull == null)
|
||||
{
|
||||
if (TargetLevel != null) { FlowPercentage = 0f; }
|
||||
return;
|
||||
}
|
||||
|
||||
currFlow = 0.0f;
|
||||
|
||||
if (TargetLevel != null)
|
||||
{
|
||||
float hullPercentage = 0.0f;
|
||||
if (item.CurrentHull != null)
|
||||
float hullWaterVolume = item.CurrentHull.WaterVolume;
|
||||
float totalHullVolume = item.CurrentHull.Volume;
|
||||
|
||||
linkedHulls.Clear();
|
||||
//hidden hulls still affect buoyancy, include them here
|
||||
item.CurrentHull.GetLinkedHulls(linkedHulls, includeHiddenHulls: true);
|
||||
foreach (var linkedHull in linkedHulls)
|
||||
{
|
||||
float hullWaterVolume = item.CurrentHull.WaterVolume;
|
||||
float totalHullVolume = item.CurrentHull.Volume;
|
||||
foreach (var linked in item.CurrentHull.linkedTo)
|
||||
{
|
||||
if ((linked is Hull linkedHull))
|
||||
{
|
||||
hullWaterVolume += linkedHull.WaterVolume;
|
||||
totalHullVolume += linkedHull.Volume;
|
||||
}
|
||||
}
|
||||
hullPercentage = hullWaterVolume / totalHullVolume * 100.0f;
|
||||
hullWaterVolume += linkedHull.WaterVolume;
|
||||
totalHullVolume += linkedHull.Volume;
|
||||
}
|
||||
float hullPercentage = hullWaterVolume / totalHullVolume * 100.0f;
|
||||
FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f;
|
||||
}
|
||||
|
||||
if (!HasPower)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UpdateNetworking(deltaTime);
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
|
||||
if (item.CurrentHull == null) { return; }
|
||||
if (!IsActive || Disabled) { return; }
|
||||
if (flowPercentage <= 0f && item.CurrentHull.WaterVolume <= 0f) { return; }
|
||||
|
||||
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
|
||||
|
||||
@@ -150,8 +160,22 @@ namespace Barotrauma.Items.Components
|
||||
//less effective when in a bad condition
|
||||
currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition);
|
||||
|
||||
item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate;
|
||||
if (item.CurrentHull.WaterVolume > item.CurrentHull.Volume) { item.CurrentHull.Pressure += 30.0f * deltaTime; }
|
||||
if (MathUtils.NearlyEqual(currFlow, 0f, epsilon: 0.01f))
|
||||
{
|
||||
currFlow = 0f; // Set to 0 for conditionals.
|
||||
return;
|
||||
}
|
||||
|
||||
item.CurrentHull.WaterVolume += currFlow * deltaTime * Timing.FixedUpdateRate;
|
||||
|
||||
if (flowPercentage > 0f && item.CurrentHull.WaterVolume > item.CurrentHull.Volume)
|
||||
{
|
||||
item.CurrentHull.Pressure += 30f * deltaTime;
|
||||
if (CanCauseLethalPressure) { item.CurrentHull.LethalPressure += Hull.PressureBuildUpSpeed * deltaTime; }
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
||||
UpdateProjSpecific(deltaTime);
|
||||
}
|
||||
|
||||
public void InfectBallast(Identifier identifier, bool allowMultiplePerShip = false)
|
||||
@@ -188,7 +212,7 @@ namespace Barotrauma.Items.Components
|
||||
public override float GetCurrentPowerConsumption(Connection connection = null)
|
||||
{
|
||||
//There shouldn't be other power connections to this
|
||||
if (connection != this.powerIn || !IsActive)
|
||||
if (connection != this.powerIn || !IsActive || Disabled)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
@@ -202,6 +226,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
partial void UpdateNetworking(float deltaTime);
|
||||
|
||||
public override void ReceiveSignal(Signal signal, Connection connection)
|
||||
{
|
||||
if (Hijacked) { return; }
|
||||
@@ -276,5 +302,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
base.RemoveComponentSpecific();
|
||||
linkedHulls.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,11 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
#if CLIENT
|
||||
CreateGUI();
|
||||
if (Screen.Selected is not { IsEditor: true })
|
||||
{
|
||||
//set text via the property to refresh the UI
|
||||
Name = name;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,24 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class TriggerComponent : ItemComponent
|
||||
{
|
||||
[Editable, Serialize(0.0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
|
||||
[Editable, Serialize(0f, IsPropertySaveable.Yes, description: "The maximum amount of force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
|
||||
public float Force { get; set; }
|
||||
|
||||
[Editable, Serialize("0,0", IsPropertySaveable.Yes, description: "The maximum amount of directional force applied to the triggering entitites.", alwaysUseInstanceValues: true)]
|
||||
public Vector2 DirectionalForce { get; set; }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, $"If true, {nameof(DirectionalForce)} is relative to the angle between the target and the item, Similar to {nameof(Force)}.\nIf false, it always pushes in the same direction, with respect to the item's rotation.", alwaysUseInstanceValues: true)]
|
||||
public bool RelativeDirectionalForce { get; set; }
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no vertical force will be applied.", alwaysUseInstanceValues: true)]
|
||||
public bool VerticalForce { get; set; }
|
||||
|
||||
[Editable, Serialize(true, IsPropertySaveable.Yes, "If false, no horizontal force will be applied.", alwaysUseInstanceValues: true)]
|
||||
public bool HorizontalForce { get; set; }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force gets higher the closer the triggerer is to the center of the trigger.", alwaysUseInstanceValues: true)]
|
||||
public bool DistanceBasedForce { get; set; }
|
||||
|
||||
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
|
||||
public bool ForceFluctuation { get; set; }
|
||||
|
||||
@@ -141,12 +155,29 @@ namespace Barotrauma.Items.Components
|
||||
get => base.IsActive;
|
||||
set
|
||||
{
|
||||
bool wasActive = base.IsActive;
|
||||
|
||||
base.IsActive = value;
|
||||
if (!IsActive)
|
||||
{
|
||||
TriggerActive = false;
|
||||
triggerers.Clear();
|
||||
}
|
||||
else if (!wasActive && PhysicsBody?.FarseerBody != null)
|
||||
{
|
||||
//when the trigger becomes active, we need to check which entities are inside it
|
||||
ContactEdge ce = PhysicsBody.FarseerBody.ContactList;
|
||||
while (ce != null && ce.Contact != null)
|
||||
{
|
||||
if (ce.Contact.Enabled)
|
||||
{
|
||||
var thisFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureA : ce.Contact.FixtureB;
|
||||
var otherFixture = ce.Contact.FixtureA.Body == PhysicsBody.FarseerBody ? ce.Contact.FixtureB : ce.Contact.FixtureA;
|
||||
OnCollision(thisFixture, otherFixture, ce.Contact);
|
||||
}
|
||||
ce = ce.Next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +405,9 @@ namespace Barotrauma.Items.Components
|
||||
float amount = MathUtils.InverseLerp(-1.0f, 1.0f, v);
|
||||
CurrentForceFluctuation = MathHelper.Lerp(1.0f - ForceFluctuationStrength, 1.0f, amount);
|
||||
ForceFluctuationTimer = 0.0f;
|
||||
GameMain.NetworkMember?.CreateEntityEvent(this);
|
||||
#if SERVER
|
||||
item.CreateServerEvent(this);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +431,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(Force) < 0.01f)
|
||||
if (Force < 0.01f && DirectionalForce.LengthSquared() < 0.0001f)
|
||||
{
|
||||
// Just ignore very minimal forces
|
||||
continue;
|
||||
@@ -436,7 +469,25 @@ namespace Barotrauma.Items.Components
|
||||
if (diff.LengthSquared() < 0.0001f) { return; }
|
||||
float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
|
||||
if (distanceFactor <= 0.0f) { return; }
|
||||
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
|
||||
Vector2 radialForce = Force * Vector2.Normalize(diff);
|
||||
Vector2 directionalForce;
|
||||
if (RelativeDirectionalForce)
|
||||
{
|
||||
directionalForce = DirectionalForce * new Vector2(Math.Sign(diff.X), Math.Sign(diff.Y));
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 flippedForce = DirectionalForce;
|
||||
if (item.FlippedX) { flippedForce.X = -flippedForce.X; }
|
||||
if (item.FlippedY) { flippedForce.Y = -flippedForce.Y; }
|
||||
directionalForce = MathUtils.RotatePoint(flippedForce, -item.RotationRad);
|
||||
}
|
||||
|
||||
Vector2 force = (radialForce + directionalForce) * CurrentForceFluctuation * distanceFactor * multiplier;
|
||||
|
||||
if (!HorizontalForce) { force.Y = 0.0f; }
|
||||
if (!VerticalForce) { force.Y = 0.0f; }
|
||||
|
||||
if (force.LengthSquared() < 0.01f) { return; }
|
||||
if (body.Mass < 1)
|
||||
{
|
||||
|
||||
@@ -461,20 +461,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public float ImpactTolerance
|
||||
{
|
||||
get { return Prefab.ImpactTolerance; }
|
||||
}
|
||||
|
||||
public float InteractDistance
|
||||
{
|
||||
get { return Prefab.InteractDistance; }
|
||||
}
|
||||
public float ImpactTolerance => Prefab.ImpactTolerance;
|
||||
|
||||
public float InteractPriority
|
||||
{
|
||||
get { return Prefab.InteractPriority; }
|
||||
}
|
||||
public float ImpactDamage => Prefab.ImpactDamage;
|
||||
public float ImpactDamageProbability => Prefab.ImpactDamageProbability;
|
||||
|
||||
public float InteractDistance => Prefab.InteractDistance;
|
||||
|
||||
public float InteractPriority => Prefab.InteractPriority;
|
||||
|
||||
|
||||
public override Vector2 Position
|
||||
{
|
||||
@@ -1767,7 +1762,7 @@ namespace Barotrauma
|
||||
ic.Move(amount, ignoreContacts);
|
||||
}
|
||||
|
||||
if (body != null && (Submarine == null || !Submarine.Loading)) { FindHull(); }
|
||||
if (body != null && (Submarine == null || !Submarine.Loading) || Screen.Selected is { IsEditor: true }) { FindHull(); }
|
||||
}
|
||||
|
||||
public Rectangle TransformTrigger(Rectangle trigger, bool world = false)
|
||||
@@ -2387,7 +2382,7 @@ namespace Barotrauma
|
||||
{
|
||||
while (impactQueue.TryDequeue(out float impact))
|
||||
{
|
||||
HandleCollision(impact);
|
||||
ReceiveImpact(impact);
|
||||
}
|
||||
}
|
||||
if (isDroppedStackOwner && body != null)
|
||||
@@ -2461,7 +2456,7 @@ namespace Barotrauma
|
||||
|
||||
if (ic.IsActive || ic.UpdateWhenInactive)
|
||||
{
|
||||
if (condition <= 0.0f)
|
||||
if (!ic.UpdateWhenBroken && condition <= 0.0f)
|
||||
{
|
||||
ic.UpdateBroken(deltaTime, cam);
|
||||
}
|
||||
@@ -2713,25 +2708,35 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleCollision(float impact)
|
||||
public void ReceiveImpact(float impactStrength, bool recursive = true)
|
||||
{
|
||||
OnCollisionProjSpecific(impact);
|
||||
OnCollisionProjSpecific(impactStrength);
|
||||
if (GameMain.NetworkMember is { IsClient: true }) { return; }
|
||||
|
||||
if (ImpactTolerance > 0.0f && Math.Abs(impact) > ImpactTolerance && hasStatusEffectsOfType[(int)ActionType.OnImpact])
|
||||
if (ImpactTolerance > 0.0f && Math.Abs(impactStrength) > ImpactTolerance && Rand.Range(0.0f, 1.0f) < ImpactDamageProbability)
|
||||
{
|
||||
foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact])
|
||||
if (ImpactDamage != 0.0f)
|
||||
{
|
||||
ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f);
|
||||
Condition -= impactStrength * ImpactDamage;
|
||||
}
|
||||
|
||||
if (hasStatusEffectsOfType[(int)ActionType.OnImpact])
|
||||
{
|
||||
foreach (StatusEffect effect in statusEffectLists[ActionType.OnImpact])
|
||||
{
|
||||
ApplyStatusEffect(effect, ActionType.OnImpact, deltaTime: 1.0f);
|
||||
}
|
||||
#if SERVER
|
||||
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
|
||||
GameMain.Server?.CreateEntityEvent(this, new ApplyStatusEffectEventData(ActionType.OnImpact));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (!recursive) { return; }
|
||||
|
||||
foreach (Item contained in ContainedItems)
|
||||
{
|
||||
if (contained.body != null) { contained.HandleCollision(impact); }
|
||||
if (contained.body != null) { contained.ReceiveImpact(impactStrength, recursive: true); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3698,18 +3703,15 @@ namespace Barotrauma
|
||||
SerializableProperty property = extraData.SerializableProperty;
|
||||
ISerializableEntity entity = extraData.Entity;
|
||||
|
||||
msg.WriteVariableUInt32((uint)allProperties.Count);
|
||||
|
||||
if (property != null)
|
||||
{
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
|
||||
if (propertyIndex < 0)
|
||||
if (allProperties.None(p => p.property == property && p.obj == entity))
|
||||
{
|
||||
throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
|
||||
}
|
||||
msg.WriteVariableUInt32((uint)propertyIndex);
|
||||
msg.WriteIdentifier(property.Name.ToIdentifier());
|
||||
}
|
||||
|
||||
object value = property.GetValue(entity);
|
||||
@@ -3814,21 +3816,11 @@ namespace Barotrauma
|
||||
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
|
||||
if (allProperties.Count == 0) { return; }
|
||||
|
||||
int propertyCount = (int)msg.ReadVariableUInt32();
|
||||
if (propertyCount != allProperties.Count)
|
||||
Identifier propertyIdentifier = msg.ReadIdentifier();
|
||||
int propertyIndex = allProperties.IndexOf(p => p.property.Name == propertyIdentifier);
|
||||
if (propertyIndex < 0)
|
||||
{
|
||||
throw new Exception($"Error in {nameof(ReadPropertyChange)}. The number of properties on the item \"{Prefab.Identifier}\" does not match between the server and the client. Server: {propertyCount}, client: {allProperties.Count}.");
|
||||
}
|
||||
|
||||
int propertyIndex = 0;
|
||||
if (allProperties.Count > 1)
|
||||
{
|
||||
propertyIndex = (int)msg.ReadVariableUInt32();
|
||||
}
|
||||
|
||||
if (propertyIndex >= allProperties.Count || propertyIndex < 0)
|
||||
{
|
||||
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Property index out of bounds (item: {Prefab.Identifier}, index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
|
||||
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Could not find the property \"{propertyIdentifier}\" in item \"{Prefab.Identifier}\" (property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
|
||||
}
|
||||
|
||||
bool allowEditing = true;
|
||||
|
||||
@@ -821,6 +821,15 @@ namespace Barotrauma
|
||||
set { impactTolerance = Math.Max(value, 0.0f); }
|
||||
}
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No, description: "The amount of damage the item takes from impacts. Acts as a multiplier on the strength of the impact. Note that ImpactTolerance must be set for impacts to register.")]
|
||||
public float ImpactDamage { get; set; }
|
||||
|
||||
[Serialize(1.0f, IsPropertySaveable.No, description: "Probability for impacts to register. Defaults to 1. Note that ImpactTolerance must also be set for impacts to register.")]
|
||||
public float ImpactDamageProbability { get; set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, "If true, submarine impacts will trigger OnImpact effects. Only applies to items with a null or non-dynamic physics body - items with dynamic bodies always react to impacts.")]
|
||||
public bool ReceiveSubmarineImpacts { get; set; }
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.No)]
|
||||
public float OnDamagedThreshold { get; set; }
|
||||
|
||||
|
||||
@@ -102,12 +102,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Index of the slot the target must be in when targeting a Contained item
|
||||
/// Index of the slot the target must be in when targeting a Contained item or a character inventory.
|
||||
/// </summary>
|
||||
public int TargetSlot = -1;
|
||||
|
||||
/// <summary>
|
||||
/// The slot type the target must be in when targeting an item contained inside a character's inventory
|
||||
/// The slot type the target must be in when targeting an item contained inside a character's inventory.
|
||||
/// </summary>
|
||||
public InvSlotType CharacterInventorySlotType;
|
||||
|
||||
@@ -329,7 +329,6 @@ namespace Barotrauma
|
||||
IgnoreInEditor = element.GetAttributeBool("ignoreineditor", false);
|
||||
MatchOnEmpty = element.GetAttributeBool("matchonempty", false);
|
||||
TargetSlot = element.GetAttributeInt("targetslot", -1);
|
||||
|
||||
}
|
||||
|
||||
public bool CheckRequirements(Character character, Item parentItem)
|
||||
@@ -344,22 +343,21 @@ namespace Barotrauma
|
||||
return CheckItem(parentItem.Container, this);
|
||||
case RelationType.Equipped:
|
||||
if (character == null) { return false; }
|
||||
var heldItems = character.HeldItems;
|
||||
if (RequireOrMatchOnEmpty && heldItems.None()) { return true; }
|
||||
foreach (Item equippedItem in heldItems)
|
||||
foreach (var item in character.Inventory.AllItemsMod)
|
||||
{
|
||||
if (equippedItem == null) { continue; }
|
||||
if (CheckItem(equippedItem, this))
|
||||
if (character.HasEquippedItem(item) && CheckItem(item, this))
|
||||
{
|
||||
if (RequireEmpty && equippedItem.Condition > 0) { return false; }
|
||||
if (RequireEmpty && item.Condition > 0) { return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
//got this far -> no matching item was equipped
|
||||
//return true if we require or want to match "empty" (no matching item), otherwise false
|
||||
return RequireOrMatchOnEmpty;
|
||||
case RelationType.Picked:
|
||||
if (character == null) { return false; }
|
||||
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
|
||||
var allItems = character.Inventory.AllItems;
|
||||
var allItems = TargetSlot == -1 ? character.Inventory.AllItems : character.Inventory.GetItemsAt(TargetSlot);
|
||||
if (RequireOrMatchOnEmpty && allItems.None()) { return true; }
|
||||
foreach (Item pickedItem in allItems)
|
||||
{
|
||||
|
||||
@@ -396,7 +396,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) && Attack.Afflictions.None())
|
||||
if (Attack.Afflictions.None() &&
|
||||
MathUtils.NearlyEqual(force, 0.0f) && MathUtils.NearlyEqual(Attack.Stun, 0.0f) &&
|
||||
MathUtils.NearlyEqual(Attack.ItemDamage, 0.0f) &&
|
||||
MathUtils.NearlyEqual(Attack.StructureDamage, 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,18 +370,31 @@ namespace Barotrauma
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
Hull hull1 = linkedTo.Count < 1 ? null : linkedTo[0] as Hull;
|
||||
Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1];
|
||||
|
||||
int updateInterval = 4;
|
||||
float flowMagnitude = flowForce.LengthSquared();
|
||||
if (flowMagnitude < 1.0f)
|
||||
//if one hull is at lethal pressure (connected to outside), and the other not yet,
|
||||
//we need frequent updates to quickly move water into the other hull
|
||||
if (hull1 != null && hull2 != null &&
|
||||
hull1.LethalPressure > 0.0f != hull2.LethalPressure > 0.0f)
|
||||
{
|
||||
//very sparse updates if there's practically no water moving
|
||||
updateInterval = 8;
|
||||
}
|
||||
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
|
||||
{
|
||||
//frequent updates if water is moving between hulls
|
||||
updateInterval = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
float flowMagnitude = flowForce.LengthSquared();
|
||||
if (flowMagnitude < 1.0f)
|
||||
{
|
||||
//very sparse updates if there's practically no water moving
|
||||
updateInterval = 8;
|
||||
}
|
||||
else if (linkedTo.Count == 2 && flowMagnitude > 10.0f)
|
||||
{
|
||||
//frequent updates if water is moving between hulls
|
||||
updateInterval = 1;
|
||||
}
|
||||
}
|
||||
|
||||
updateCount++;
|
||||
if (updateCount < updateInterval) { return; }
|
||||
@@ -409,8 +422,6 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
Hull hull1 = (Hull)linkedTo[0];
|
||||
Hull hull2 = linkedTo.Count < 2 ? null : (Hull)linkedTo[1];
|
||||
if (hull1 == hull2) { return; }
|
||||
|
||||
UpdateOxygen(hull1, hull2, deltaTime);
|
||||
@@ -469,6 +480,8 @@ namespace Barotrauma
|
||||
higherSurface = Math.Max(hull1.Surface, hull2.Surface + subOffset.Y);
|
||||
float delta = 0.0f;
|
||||
|
||||
Hull flowSourceHull = null;
|
||||
|
||||
//water level is above the lower boundary of the gap
|
||||
if (Math.Max(hull1.Surface + hull1.WaveY[hull1.WaveY.Length - 1], hull2.Surface + subOffset.Y + hull2.WaveY[0]) > rect.Y - Size)
|
||||
{
|
||||
@@ -479,10 +492,9 @@ namespace Barotrauma
|
||||
{
|
||||
if (!(hull2.WaterVolume > 0.0f)) { return; }
|
||||
lowerSurface = hull1.Surface - hull1.WaveY[hull1.WaveY.Length - 1];
|
||||
//delta = Math.Min((room2.water.pressure - room1.water.pressure) * sizeModifier, Math.Min(room2.water.Volume, room2.Volume));
|
||||
//delta = Math.Min(delta, room1.Volume - room1.water.Volume + Water.MaxCompress);
|
||||
|
||||
flowTargetHull = hull1;
|
||||
flowSourceHull = hull2;
|
||||
|
||||
//make sure not to move more than what the room contains
|
||||
delta = Math.Min(((hull2.Pressure + subOffset.Y) - hull1.Pressure) * 300.0f * sizeModifier * deltaTime, Math.Min(hull2.WaterVolume, hull2.Volume));
|
||||
@@ -504,6 +516,7 @@ namespace Barotrauma
|
||||
lowerSurface = hull2.Surface - hull2.WaveY[hull2.WaveY.Length - 1];
|
||||
|
||||
flowTargetHull = hull2;
|
||||
flowSourceHull = hull1;
|
||||
|
||||
//make sure not to move more than what the room contains
|
||||
delta = Math.Min((hull1.Pressure - (hull2.Pressure + subOffset.Y)) * 300.0f * sizeModifier * deltaTime, Math.Min(hull1.WaterVolume, hull1.Volume));
|
||||
@@ -547,7 +560,6 @@ namespace Barotrauma
|
||||
if (hull2.Pressure + subOffset.Y > hull1.Pressure && hull2.WaterVolume > 0.0f)
|
||||
{
|
||||
float delta = Math.Min(hull2.WaterVolume - hull2.Volume + (hull2.Volume * Hull.MaxCompress), deltaTime * 8000.0f * sizeModifier);
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
if (hull1.WaterVolume + delta > hull1.Volume * Hull.MaxCompress)
|
||||
{
|
||||
@@ -623,19 +635,30 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateRoomToOut(float deltaTime, Hull hull1)
|
||||
/// <summary>
|
||||
/// How much water can flow through the gap to the hull if the gap is connected outside.
|
||||
/// </summary>
|
||||
private float GetWaterFlowFromOutside(Hull hull, float deltaTime, bool ignoreCurrentWater = false)
|
||||
{
|
||||
//a variable affecting the water flow through the gap
|
||||
//the larger the gap is, the faster the water flows
|
||||
float sizeModifier = Size * open * open * (1.0f - overlappingGapFlowRateReduction);
|
||||
|
||||
float delta = 500.0f * sizeModifier * deltaTime;
|
||||
if (!ignoreCurrentWater)
|
||||
{
|
||||
delta = Math.Min(delta, hull.Volume * Hull.MaxCompress - hull.WaterVolume);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
void UpdateRoomToOut(float deltaTime, Hull hull1)
|
||||
{
|
||||
float delta = GetWaterFlowFromOutside(hull1, deltaTime);
|
||||
|
||||
//make sure not to place more water to the target room than it can hold
|
||||
delta = Math.Min(delta, hull1.Volume * Hull.MaxCompress - hull1.WaterVolume);
|
||||
hull1.WaterVolume += delta;
|
||||
|
||||
if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 30.0f * deltaTime; }
|
||||
if (hull1.WaterVolume > hull1.Volume) { hull1.Pressure += 100.0f * deltaTime; }
|
||||
|
||||
flowTargetHull = hull1;
|
||||
|
||||
@@ -698,6 +721,65 @@ namespace Barotrauma
|
||||
hull1.LethalPressure += ((Submarine != null && Submarine.AtDamageDepth) ? 100.0f : Hull.PressureBuildUpSpeed) * PressureDistributionSpeed * deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (hull1.LethalPressure > 0)
|
||||
{
|
||||
SimulateWaterFlowFromOutsideToConnectedHulls(hull1, maxFlow: GetWaterFlowFromOutside(hull1, deltaTime, ignoreCurrentWater: true), deltaTime: deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private Hull GetOtherLinkedHull(Hull hull1)
|
||||
{
|
||||
if (linkedTo.Count != 2 || hull1 == null) { return null; }
|
||||
return (linkedTo[0] == hull1 ? linkedTo[1] : linkedTo[0]) as Hull;
|
||||
}
|
||||
|
||||
private static readonly HashSet<Hull> checkedHulls = new HashSet<Hull>();
|
||||
|
||||
/// <summary>
|
||||
/// Simulates water flow from the source to all the hulls it's connected to across the sub, as if the water was coming directly from outside.
|
||||
/// Used to prevent gaps from slowing down flooding when hulls are directly connected outside and highly pressurized.
|
||||
/// </summary>
|
||||
void SimulateWaterFlowFromOutsideToConnectedHulls(Hull hull, float maxFlow, float deltaTime)
|
||||
{
|
||||
checkedHulls.Clear();
|
||||
checkedHulls.Add(hull);
|
||||
foreach (var connectedGap in hull.ConnectedGaps)
|
||||
{
|
||||
if (connectedGap == this || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; }
|
||||
var otherHull = connectedGap.GetOtherLinkedHull(hull);
|
||||
if (otherHull == null) { continue; }
|
||||
SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, hull, maxFlow, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
static void SimulateWaterFlowFromOutsideToConnectedHullsRecursive(Hull targetHull, Gap gap, HashSet<Hull> checkedHulls, Hull originHull, float maxFlow, float deltaTime)
|
||||
{
|
||||
const float decay = 0.95f;
|
||||
|
||||
maxFlow = Math.Min(maxFlow, gap.GetWaterFlowFromOutside(targetHull, deltaTime, ignoreCurrentWater: true)) * decay;
|
||||
if (maxFlow <= 0.001f) { return; }
|
||||
|
||||
checkedHulls.Add(targetHull);
|
||||
|
||||
//don't multiply by deltatime here, we already did that in GetWaterFlowFromOutside
|
||||
targetHull.WaterVolume += maxFlow;
|
||||
//lerp lethal pressure up very fast
|
||||
if (targetHull.WaterVolume > targetHull.Volume)
|
||||
{
|
||||
targetHull.LethalPressure = Math.Max(targetHull.LethalPressure, MathHelper.Lerp(targetHull.LethalPressure, originHull.LethalPressure, 0.1f));
|
||||
}
|
||||
|
||||
//stop pushing water to the following hulls once we get to a hull that's not at high pressure yet
|
||||
if (targetHull.LethalPressure <= 0 || targetHull.WaterVolume < targetHull.Volume) { return; }
|
||||
|
||||
foreach (var connectedGap in targetHull.ConnectedGaps)
|
||||
{
|
||||
if (connectedGap == gap || !connectedGap.IsRoomToRoom || connectedGap.open <= 0.0f) { continue; }
|
||||
var otherHull = connectedGap.GetOtherLinkedHull(targetHull);
|
||||
if (otherHull == null || checkedHulls.Contains(otherHull)) { continue; }
|
||||
SimulateWaterFlowFromOutsideToConnectedHullsRecursive(otherHull, connectedGap, checkedHulls, originHull, maxFlow, deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RefreshOutsideCollider()
|
||||
@@ -884,6 +966,8 @@ namespace Barotrauma
|
||||
base.Remove();
|
||||
GapList.Remove(this);
|
||||
|
||||
checkedHulls.Clear();
|
||||
|
||||
foreach (Hull hull in Hull.HullList)
|
||||
{
|
||||
hull.ConnectedGaps.Remove(this);
|
||||
|
||||
@@ -785,7 +785,7 @@ namespace Barotrauma
|
||||
#region Shared network write
|
||||
private void SharedStatusWrite(IWriteMessage msg)
|
||||
{
|
||||
msg.WriteRangedSingle(MathHelper.Clamp(waterVolume / Volume, 0.0f, 1.5f), 0.0f, 1.5f, 8);
|
||||
msg.WriteSingle(waterVolume);
|
||||
|
||||
System.Diagnostics.Debug.Assert(FireSources.Count <= MaxFireSources, $"Too many fire sources ({FireSources.Count}) in hull {ID} (max {MaxFireSources}).");
|
||||
msg.WriteRangedInteger(Math.Min(FireSources.Count, MaxFireSources), 0, MaxFireSources);
|
||||
@@ -833,7 +833,7 @@ namespace Barotrauma
|
||||
|
||||
private void SharedStatusRead(IReadMessage msg, out float newWaterVolume, out NetworkFireSource[] newFireSources)
|
||||
{
|
||||
newWaterVolume = msg.ReadRangedSingle(0.0f, 1.5f, 8) * Volume;
|
||||
newWaterVolume = msg.ReadSingle();
|
||||
|
||||
int fireSourceCount = msg.ReadRangedInteger(0, MaxFireSources);
|
||||
newFireSources = new NetworkFireSource[fireSourceCount];
|
||||
@@ -1269,6 +1269,23 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively find all the hulls linked to the specified hull.
|
||||
/// </summary>
|
||||
public void GetLinkedHulls(List<Hull> linkedHulls, bool includeHiddenHulls = false)
|
||||
{
|
||||
foreach (var linkedEntity in linkedTo)
|
||||
{
|
||||
if (linkedEntity is Hull linkedHull)
|
||||
{
|
||||
if (linkedHulls.Contains(linkedHull)) { continue; }
|
||||
if (!includeHiddenHulls && linkedHull.IsHidden) { continue; }
|
||||
linkedHulls.Add(linkedHull);
|
||||
linkedHull.GetLinkedHulls(linkedHulls, includeHiddenHulls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DetectItemVisibility(Character c=null)
|
||||
{
|
||||
if (c==null)
|
||||
|
||||
@@ -4291,6 +4291,11 @@ namespace Barotrauma
|
||||
{
|
||||
placeableWrecks.RemoveAt(i);
|
||||
}
|
||||
// Exclude wrecks that have mission tags, those can't show up randomly
|
||||
else if (wreckInfo.MissionTags.Count != 0)
|
||||
{
|
||||
placeableWrecks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
if (placeableWrecks.None())
|
||||
{
|
||||
@@ -4742,6 +4747,11 @@ namespace Barotrauma
|
||||
{
|
||||
beaconStationFiles.RemoveAt(i);
|
||||
}
|
||||
// Exclude beacons that have mission tags, those can't show up randomly
|
||||
else if (beaconInfo.MissionTags.Count != 0)
|
||||
{
|
||||
beaconStationFiles.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (beaconStationFiles.None())
|
||||
@@ -4926,17 +4936,17 @@ namespace Barotrauma
|
||||
{
|
||||
int corpseCount = Rand.Range(Loaded.GenerationParams.MinCorpseCount, Loaded.GenerationParams.MaxCorpseCount + 1);
|
||||
var allSpawnPoints = WayPoint.WayPointList.FindAll(wp => wp.Submarine == wreck && wp.CurrentHull != null);
|
||||
var pathPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Path);
|
||||
var humanSpawnPoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Human);
|
||||
var corpsePoints = allSpawnPoints.FindAll(wp => wp.SpawnType == SpawnType.Corpse);
|
||||
if (!corpsePoints.Any() && !pathPoints.Any()) { continue; }
|
||||
pathPoints.Shuffle(Rand.RandSync.ServerAndClient);
|
||||
if (corpsePoints.None() && humanSpawnPoints.None()) { continue; }
|
||||
humanSpawnPoints.Shuffle(Rand.RandSync.ServerAndClient);
|
||||
// Sort by job so that we first spawn those with a predefined job (might have special id cards)
|
||||
corpsePoints = corpsePoints.OrderBy(p => p.AssignedJob == null).ThenBy(p => Rand.Value()).ToList();
|
||||
var usedJobs = new HashSet<JobPrefab>();
|
||||
int spawnCounter = 0;
|
||||
for (int j = 0; j < corpseCount; j++)
|
||||
{
|
||||
WayPoint sp = corpsePoints.FirstOrDefault() ?? pathPoints.FirstOrDefault();
|
||||
WayPoint sp = corpsePoints.FirstOrDefault() ?? humanSpawnPoints.FirstOrDefault();
|
||||
JobPrefab job = sp?.AssignedJob;
|
||||
CorpsePrefab selectedPrefab;
|
||||
if (job == null)
|
||||
@@ -4949,8 +4959,8 @@ namespace Barotrauma
|
||||
if (selectedPrefab == null)
|
||||
{
|
||||
corpsePoints.Remove(sp);
|
||||
pathPoints.Remove(sp);
|
||||
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null);
|
||||
humanSpawnPoints.Remove(sp);
|
||||
sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? humanSpawnPoints.FirstOrDefault(sp => sp.AssignedJob == null);
|
||||
// Deduce the job from the selected prefab
|
||||
selectedPrefab = GetCorpsePrefab(usedJobs);
|
||||
if (selectedPrefab != null)
|
||||
@@ -4972,7 +4982,7 @@ namespace Barotrauma
|
||||
{
|
||||
worldPos = sp.WorldPosition;
|
||||
corpsePoints.Remove(sp);
|
||||
pathPoints.Remove(sp);
|
||||
humanSpawnPoints.Remove(sp);
|
||||
}
|
||||
|
||||
job ??= selectedPrefab.GetJobPrefab(predicate: p => !usedJobs.Contains(p));
|
||||
|
||||
@@ -473,7 +473,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry) || m.ForceFailure);
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
@@ -1091,6 +1091,12 @@ namespace Barotrauma
|
||||
mission.TimesAttempted = loadedMission.TimesAttempted;
|
||||
availableMissions.Add(mission);
|
||||
if (loadedMission.SelectedMission) { selectedMissions.Add(mission); }
|
||||
|
||||
var levelData = destination == this ? LevelData : Connections.FirstOrDefault(c => c.OtherLocation(this) == destination)?.LevelData;
|
||||
if (levelData != null)
|
||||
{
|
||||
mission.AdjustLevelData(levelData);
|
||||
}
|
||||
}
|
||||
loadedMissions = null;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry));
|
||||
availableMissions.RemoveAll(m => m.Completed || (m.Failed && !m.Prefab.AllowRetry) || m.ForceFailure);
|
||||
return availableMissions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,15 +235,17 @@ namespace Barotrauma
|
||||
|
||||
//backwards compatibility (or support for loading maps created with mods that modify the end biome setup):
|
||||
//if there's too few end locations, create more
|
||||
int missingOutpostCount = endLocations.First().Biome.EndBiomeLocationCount - endLocations.Count;
|
||||
|
||||
Location firstEndLocation = EndLocations[0];
|
||||
Biome endBiome = firstEndLocation.Biome;
|
||||
int missingOutpostCount = endBiome.EndBiomeLocationCount - endLocations.Count;
|
||||
|
||||
for (int i = 0; i < missingOutpostCount; i++)
|
||||
{
|
||||
Vector2 mapPos = new Vector2(
|
||||
MathHelper.Lerp(firstEndLocation.MapPosition.X, Width, MathHelper.Lerp(0.2f, 0.8f, i / (float)missingOutpostCount)),
|
||||
Height * MathHelper.Lerp(0.2f, 1.0f, (float)rand.NextDouble()));
|
||||
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, firstEndLocation.Biome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
|
||||
var newEndLocation = new Location(mapPos, generationParams.DifficultyZones, endBiome.Identifier, rand, forceLocationType: firstEndLocation.Type, existingLocations: Locations);
|
||||
newEndLocation.Biome = endBiome;
|
||||
newEndLocation.LevelData = new LevelData(newEndLocation, this, difficulty: 100.0f);
|
||||
Locations.Add(newEndLocation);
|
||||
endLocations.Add(newEndLocation);
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace Barotrauma
|
||||
|
||||
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
|
||||
|
||||
public HashSet<Identifier> MissionTags { get; } = [];
|
||||
|
||||
[Serialize(0.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float MinLevelDifficulty { get; set; }
|
||||
|
||||
@@ -21,6 +23,10 @@ namespace Barotrauma
|
||||
{
|
||||
Name = $"{nameof(ExtraSubmarineInfo)} ({submarineInfo.Name})";
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), []))
|
||||
{
|
||||
MissionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public ExtraSubmarineInfo(SubmarineInfo submarineInfo)
|
||||
@@ -41,11 +47,18 @@ namespace Barotrauma
|
||||
kvp.Value.TrySetValue(this, kvp.Value.GetValue(original));
|
||||
}
|
||||
}
|
||||
foreach (var missionTag in original.MissionTags)
|
||||
{
|
||||
MissionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Save(XElement element)
|
||||
{
|
||||
SerializableProperty.SerializeProperties(this, element);
|
||||
// MissionTags is not automatically serialized because HashSet<Identifier> is not a supported type
|
||||
// We need to manually serialize it as a comma-separated string
|
||||
element.SetAttributeValue(nameof(MissionTags), string.Join(',', MissionTags));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,18 +148,10 @@ namespace Barotrauma
|
||||
[Serialize(50.0f, IsPropertySaveable.Yes), Editable]
|
||||
public float PreferredDifficulty { get; set; }
|
||||
|
||||
private readonly HashSet<Identifier> missionTags = new HashSet<Identifier>();
|
||||
|
||||
public HashSet<Identifier> MissionTags => missionTags;
|
||||
|
||||
public EnemySubmarineInfo(SubmarineInfo submarineInfo, XElement element) : base(submarineInfo, element)
|
||||
{
|
||||
Name = $"{nameof(EnemySubmarineInfo)} ({submarineInfo.Name})";
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
foreach (var missionTag in element.GetAttributeIdentifierArray(nameof(MissionTags), Array.Empty<Identifier>()))
|
||||
{
|
||||
missionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public EnemySubmarineInfo(SubmarineInfo submarineInfo) : base(submarineInfo)
|
||||
@@ -156,16 +161,8 @@ namespace Barotrauma
|
||||
|
||||
public EnemySubmarineInfo(EnemySubmarineInfo original) : base(original)
|
||||
{
|
||||
foreach (var missionTag in original.missionTags)
|
||||
{
|
||||
missionTags.Add(missionTag);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Save(XElement element)
|
||||
{
|
||||
base.Save(element);
|
||||
element.Add(new XAttribute(nameof(MissionTags), string.Join(',', missionTags)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,11 +159,11 @@ namespace Barotrauma
|
||||
if (GameMain.NetworkMember?.ServerSettings is { } serverSettings &&
|
||||
serverSettings.SelectedOutpostName != "Random")
|
||||
{
|
||||
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
|
||||
//...but only if the outpost is suitable for the mission (or if the mission has no specific requirements for the outpost)
|
||||
if (outpostInfosSuitableForMission.None() ||
|
||||
outpostInfosSuitableForMission.Any(outpostInfo => outpostInfo.OutpostTags.Contains(serverSettings.SelectedOutpostName)))
|
||||
if (outpostInfosSuitableForMission.Contains(matchingOutpost) ||
|
||||
outpostInfosSuitableForMission.None())
|
||||
{
|
||||
var matchingOutpost = outpostInfos.FirstOrDefault(o => o.Name == serverSettings.SelectedOutpostName);
|
||||
if (matchingOutpost != null)
|
||||
{
|
||||
return matchingOutpost;
|
||||
|
||||
@@ -37,14 +37,14 @@ namespace Barotrauma
|
||||
public bool RequiresUnlock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used when neither <see cref="MinAvailableAmount"/> or <see cref="MaxAvailableAmount"/> are defined.
|
||||
/// Default minimum amount when no MinAvailableAmount is defined.
|
||||
/// </summary>
|
||||
private const int DefaultAmount = 5;
|
||||
private const int DefaultMinAmount = 1;
|
||||
|
||||
/// <summary>
|
||||
/// How much more the maximum stock is relative to the minimum stock if not defined. Stores will gradually stock up towards the maximum.
|
||||
/// Default maximum amount when no MaxAvailableAmount is defined.
|
||||
/// </summary>
|
||||
private const float DefaultMaxAvailabilityRelativeToMin = 1.2f;
|
||||
private const int DefaultMaxAmount = 5;
|
||||
|
||||
/// <summary>
|
||||
/// If set, the item is only available in outposts with this faction.
|
||||
@@ -66,11 +66,12 @@ namespace Barotrauma
|
||||
public PriceInfo(XElement element)
|
||||
{
|
||||
Price = element.GetAttributeInt("buyprice", 0);
|
||||
MinLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
|
||||
MinLevelDifficulty = GetMinLevelDifficulty(element, 0);
|
||||
BuyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
|
||||
CanBeBought = true;
|
||||
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultAmount), CargoManager.MaxQuantity);
|
||||
MaxAvailableAmount = MathHelper.Clamp(GetMaxAmount(element, defaultValue: (int)(MinAvailableAmount * DefaultMaxAvailabilityRelativeToMin)), MinAvailableAmount, CargoManager.MaxQuantity);
|
||||
MinAvailableAmount = Math.Min(GetMinAmount(element, defaultValue: DefaultMinAmount), CargoManager.MaxQuantity);
|
||||
int maxAmount = GetMaxAmount(element, defaultValue: DefaultMaxAmount);
|
||||
MaxAvailableAmount = MathHelper.Clamp(maxAmount, MinAvailableAmount, CargoManager.MaxQuantity);
|
||||
RequiresUnlock = element.GetAttributeBool("requiresunlock", false);
|
||||
RequiredFaction = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
|
||||
System.Diagnostics.Debug.Assert(MaxAvailableAmount >= MinAvailableAmount);
|
||||
@@ -112,27 +113,27 @@ namespace Barotrauma
|
||||
var priceInfos = new List<PriceInfo>();
|
||||
defaultPrice = null;
|
||||
int basePrice = element.GetAttributeInt("baseprice", 0);
|
||||
int minAmount = GetMinAmount(element, defaultValue: DefaultAmount);
|
||||
int maxAmount = GetMaxAmount(element, defaultValue: (int)(DefaultAmount * DefaultMaxAvailabilityRelativeToMin));
|
||||
int minLevelDifficulty = element.GetAttributeInt("minleveldifficulty", 0);
|
||||
int minAmount = GetMinAmount(element, defaultValue: DefaultMinAmount);
|
||||
int maxAmount = GetMaxAmount(element, defaultValue: DefaultMaxAmount);
|
||||
int minLevelDifficulty = GetMinLevelDifficulty(element, 0);
|
||||
bool canBeSpecial = element.GetAttributeBool("canbespecial", true);
|
||||
float buyingPriceMultiplier = element.GetAttributeFloat("buyingpricemultiplier", 1f);
|
||||
bool displayNonEmpty = element.GetAttributeBool("displaynonempty", false);
|
||||
bool soldByDefault = element.GetAttributeBool("sold", element.GetAttributeBool("soldbydefault", true));
|
||||
bool soldByDefault = GetSold(element, element.GetAttributeBool("soldbydefault", true));
|
||||
bool requiresUnlock = element.GetAttributeBool("requiresunlock", false);
|
||||
Identifier requiredFactionByDefault = element.GetAttributeIdentifier(nameof(RequiredFaction), Identifier.Empty);
|
||||
foreach (XElement childElement in element.GetChildElements("price"))
|
||||
{
|
||||
float priceMultiplier = childElement.GetAttributeFloat("multiplier", 1.0f);
|
||||
bool sold = childElement.GetAttributeBool("sold", soldByDefault);
|
||||
int storeMinLevelDifficulty = childElement.GetAttributeInt("minleveldifficulty", minLevelDifficulty);
|
||||
bool sold = GetSold(childElement, soldByDefault);
|
||||
int storeMinLevelDifficulty = GetMinLevelDifficulty(childElement, minLevelDifficulty);
|
||||
float storeBuyingMultiplier = childElement.GetAttributeFloat("buyingpricemultiplier", buyingPriceMultiplier);
|
||||
string backwardsCompatibleIdentifier = childElement.GetAttributeString("locationtype", "");
|
||||
if (!string.IsNullOrEmpty(backwardsCompatibleIdentifier))
|
||||
{
|
||||
backwardsCompatibleIdentifier = $"merchant{backwardsCompatibleIdentifier}";
|
||||
}
|
||||
string storeIdentifier = childElement.GetAttributeString("storeidentifier", backwardsCompatibleIdentifier);
|
||||
string storeIdentifier = GetStoreIdentifier(childElement, backwardsCompatibleIdentifier);
|
||||
// TODO: Add some error messages if we have defined the min or max amount while the item is not sold
|
||||
var priceInfo = new PriceInfo(price: (int)(priceMultiplier * basePrice),
|
||||
canBeBought: sold,
|
||||
@@ -167,12 +168,40 @@ namespace Barotrauma
|
||||
return priceInfos;
|
||||
}
|
||||
|
||||
private static int GetMinAmount(XElement element, int defaultValue) => element != null ?
|
||||
element.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) :
|
||||
defaultValue;
|
||||
private static int GetMinAmount(XElement element, int defaultValue) =>
|
||||
element?.GetAttributeInt("minamount", element.GetAttributeInt("minavailable", defaultValue)) ?? defaultValue;
|
||||
|
||||
private static int GetMaxAmount(XElement element, int defaultValue) => element != null ?
|
||||
element.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) :
|
||||
defaultValue;
|
||||
private static int GetMaxAmount(XElement element, int defaultValue) =>
|
||||
element?.GetAttributeInt("maxamount", element.GetAttributeInt("maxavailable", defaultValue)) ?? defaultValue;
|
||||
|
||||
public static bool HasMinAmountDefined(XElement element) => element != null &&
|
||||
(element.GetAttribute("minamount") != null || element.GetAttribute("minavailable") != null);
|
||||
|
||||
public static bool HasMaxAmountDefined(XElement element) => element != null &&
|
||||
(element.GetAttribute("maxamount") != null || element.GetAttribute("maxavailable") != null);
|
||||
|
||||
public static bool HasSoldDefined(XElement element) => element != null &&
|
||||
element.GetAttribute("sold") != null;
|
||||
|
||||
public static string GetMinAmountString(XElement element)
|
||||
{
|
||||
if (element == null) { return null; }
|
||||
return element.GetAttributeString("minamount", null) ?? element.GetAttributeString("minavailable", null);
|
||||
}
|
||||
|
||||
public static string GetMaxAmountString(XElement element)
|
||||
{
|
||||
if (element == null) { return null; }
|
||||
return element.GetAttributeString("maxamount", null) ?? element.GetAttributeString("maxavailable", null);
|
||||
}
|
||||
|
||||
public static bool GetSold(XElement element, bool defaultValue = true) =>
|
||||
element?.GetAttributeBool("sold", defaultValue) ?? defaultValue;
|
||||
|
||||
public static int GetMinLevelDifficulty(XElement element, int defaultValue = 0) =>
|
||||
element?.GetAttributeInt("minleveldifficulty", defaultValue) ?? defaultValue;
|
||||
|
||||
public static string GetStoreIdentifier(XElement element, string defaultValue = "unknown") =>
|
||||
element?.GetAttributeString("storeidentifier", defaultValue) ?? defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +343,9 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Is the sub at the depth where it starts to take damage to appear due to the pressure?
|
||||
/// </summary>
|
||||
public bool AtDamageDepth
|
||||
{
|
||||
get
|
||||
@@ -352,6 +355,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the sub at the depth where cosmetic effects (e.g. camera shake) start to appear due to the pressure?
|
||||
/// </summary>
|
||||
public bool AtCosmeticDamageDepth
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Level.Loaded == null || subBody == null) { return false; }
|
||||
return RealWorldDepth > Level.Loaded.RealWorldCrushDepth + SubmarineBody.CosmeticDamageEffectThreshold && RealWorldDepth > RealWorldCrushDepth + SubmarineBody.CosmeticDamageEffectThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRespawnShuttle =>
|
||||
GameMain.NetworkMember?.RespawnManager is { } respawnManager && respawnManager.RespawnShuttles.Contains(this);
|
||||
|
||||
|
||||
@@ -579,13 +579,16 @@ namespace Barotrauma
|
||||
Body.SetTransform(ConvertUnits.ToSimUnits(position), 0.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Camera shake and sounds start playing 500 meters before crush depth
|
||||
/// </summary>
|
||||
public const float CosmeticDamageEffectThreshold = -500.0f;
|
||||
|
||||
private void UpdateDepthDamage(float deltaTime)
|
||||
{
|
||||
if (GameMain.GameSession?.GameMode is TestGameMode) { return; }
|
||||
if (Level.Loaded == null) { return; }
|
||||
|
||||
//camera shake and sounds start playing 500 meters before crush depth
|
||||
const float CosmeticEffectThreshold = -500.0f;
|
||||
//breaches won't get any more severe 500 meters below crush depth
|
||||
const float MaxEffectThreshold = 500.0f;
|
||||
const float MinWallDamageProbability = 0.1f;
|
||||
@@ -598,7 +601,7 @@ namespace Barotrauma
|
||||
//(gives you a bit of time to react and return if you start the round in a level that's too deep)
|
||||
const float MinRoundDuration = 60.0f;
|
||||
|
||||
if (Submarine.RealWorldDepth < Level.Loaded.RealWorldCrushDepth + CosmeticEffectThreshold || Submarine.RealWorldDepth < Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)
|
||||
if (!Submarine.AtCosmeticDamageDepth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -606,9 +609,9 @@ namespace Barotrauma
|
||||
damageSoundTimer -= deltaTime;
|
||||
if (damageSoundTimer <= 0.0f)
|
||||
{
|
||||
const float PressureSoundRange = -CosmeticEffectThreshold;
|
||||
const float PressureSoundRange = -CosmeticDamageEffectThreshold;
|
||||
//Ratio between 0 (where the 'approaching crush depth' indication starts) and 1 (at crush depth or past it)
|
||||
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticEffectThreshold)) / PressureSoundRange, 0f, 1f);
|
||||
float closenessToCrushDepthRatio = Math.Clamp((Submarine.RealWorldDepth - (Submarine.RealWorldCrushDepth + CosmeticDamageEffectThreshold)) / PressureSoundRange, 0f, 1f);
|
||||
#if CLIENT
|
||||
SoundPlayer.PlayDamageSound("pressure", MathHelper.Lerp(0f, 100f, closenessToCrushDepthRatio), submarine.WorldPosition + Rand.Vector(Rand.Range(0.0f, Math.Min(submarine.Borders.Width, submarine.Borders.Height))), 20000.0f, gain: 1f + closenessToCrushDepthRatio * 2);
|
||||
#endif
|
||||
@@ -1066,8 +1069,15 @@ namespace Barotrauma
|
||||
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine != submarine || item.CurrentHull == null || item.body == null || !item.body.Enabled) { continue; }
|
||||
if (item.body.Mass > impulseMagnitude) { continue; }
|
||||
if (item.Submarine != submarine) { continue; }
|
||||
|
||||
if (item.body is not { BodyType: BodyType.Dynamic })
|
||||
{
|
||||
if (!item.Prefab.ReceiveSubmarineImpacts) { continue; }
|
||||
item.ReceiveImpact(impact, recursive: false);
|
||||
}
|
||||
|
||||
if (!item.body.Enabled || item.CurrentHull == null || item.body.Mass > impulseMagnitude) { continue; }
|
||||
|
||||
item.body.ApplyLinearImpulse(impulse, 10.0f);
|
||||
item.PositionUpdateInterval = 0.0f;
|
||||
|
||||
@@ -820,16 +820,22 @@ namespace Barotrauma
|
||||
{
|
||||
int prevPos = inc.BitPosition;
|
||||
|
||||
const int MaxBytesToLog = 500;
|
||||
|
||||
StringBuilder hexData = new();
|
||||
inc.BitPosition = 0;
|
||||
while (inc.BitPosition < inc.LengthBits)
|
||||
while (inc.BitPosition < inc.LengthBits &&
|
||||
inc.BytePosition < MaxBytesToLog)
|
||||
{
|
||||
byte b = inc.ReadByte();
|
||||
hexData.Append($"{b:X2} ");
|
||||
}
|
||||
// trim the last space if there is one
|
||||
if (hexData.Length > 0) { hexData.Length--; }
|
||||
|
||||
if (inc.BytePosition >= MaxBytesToLog)
|
||||
{
|
||||
hexData.Append($" (data truncated, {inc.LengthBytes} bytes in the full message)");
|
||||
}
|
||||
inc.BitPosition = prevPos;
|
||||
|
||||
//only log the error once per sender, so this can't be abused by spamming the server with malformed data to fill up the console with errors
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Barotrauma.Networking
|
||||
return msg;
|
||||
}
|
||||
#endif
|
||||
public static void WriteNetSerializableStruct(this IWriteMessage msg, INetSerializableStruct serializableStruct)
|
||||
public static void WriteNetSerializableStruct<T>(this IWriteMessage msg, T serializableStruct) where T : INetSerializableStruct
|
||||
{
|
||||
serializableStruct.Write(msg);
|
||||
}
|
||||
|
||||
+17
-6
@@ -94,6 +94,12 @@ namespace Barotrauma.Networking
|
||||
public Option<int> RetriesLeft;
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
internal readonly record struct DoSProtectionPacket(string EndpointStr, bool ShouldBan) : INetSerializableStruct
|
||||
{
|
||||
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr);
|
||||
}
|
||||
|
||||
[NetworkSerialize]
|
||||
internal readonly struct PeerDisconnectPacket : INetSerializableStruct
|
||||
{
|
||||
@@ -109,19 +115,24 @@ namespace Barotrauma.Networking
|
||||
AdditionalInformation = additionalInformation;
|
||||
}
|
||||
|
||||
public LocalizedString ChatMessage(Client c)
|
||||
public LocalizedString ChatMessage(string? name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
name = TextManager.Get("ServerMessage.UnknownClient").Value;
|
||||
}
|
||||
|
||||
LocalizedString message = DisconnectReason switch
|
||||
{
|
||||
DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer",
|
||||
"[client]", c.Name),
|
||||
DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", c.Name),
|
||||
DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", c.Name),
|
||||
"[client]", name),
|
||||
DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", name),
|
||||
DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", name),
|
||||
_ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason",
|
||||
("[client]", c.Name),
|
||||
("[client]", name),
|
||||
("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}")))
|
||||
};
|
||||
if (!string.IsNullOrEmpty(AdditionalInformation) &&
|
||||
if (!string.IsNullOrEmpty(AdditionalInformation) &&
|
||||
DisconnectReason is DisconnectReason.Banned or DisconnectReason.Kicked)
|
||||
{
|
||||
message += " "+ TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation);
|
||||
|
||||
@@ -89,8 +89,6 @@ namespace Barotrauma.Networking
|
||||
private readonly Queue<LogMessage> lines;
|
||||
private readonly Queue<LogMessage> unsavedLines;
|
||||
|
||||
private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
|
||||
|
||||
public int LinesPerFile
|
||||
{
|
||||
get { return linesPerFile; }
|
||||
|
||||
@@ -79,14 +79,13 @@ namespace Barotrauma
|
||||
bool cleared = false;
|
||||
foreach (var subElement in element.Elements())
|
||||
{
|
||||
if (elementNamesToRemove.Contains(subElement.NameAsIdentifier()))
|
||||
{
|
||||
if (!elementsToRemove.Contains(subElement)) { elementsToRemove.Add(subElement); }
|
||||
matchingElementFound = true;
|
||||
continue;
|
||||
}
|
||||
if (replacementSubElement.Name.ToString().Equals("clearall", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (elementNamesToRemove.Contains(subElement.NameAsIdentifier()))
|
||||
{
|
||||
if (!elementsToRemove.Contains(subElement)) { elementsToRemove.Add(subElement); }
|
||||
matchingElementFound = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (replacementSubElement.Name.ToString().Equals("clear", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
+21
-1
@@ -876,7 +876,18 @@ namespace Barotrauma
|
||||
Dictionary<Identifier, SerializableProperty> dictionary = new Dictionary<Identifier, SerializableProperty>();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var serializableProperty = new SerializableProperty(property);
|
||||
//if the getter is private, we must get it from the declaring type to access it and check if it exists
|
||||
SerializableProperty serializableProperty = null;
|
||||
try
|
||||
{
|
||||
serializableProperty = new SerializableProperty(property);
|
||||
}
|
||||
catch (AmbiguousMatchException)
|
||||
{
|
||||
//can happen e.g. with AnimController.CurrentGroundedParams, which is of an abstract type -
|
||||
//let's just ignore these types of properties (you can't really do anything with SerializableProperties that are reference types anyway)
|
||||
continue;
|
||||
}
|
||||
dictionary.Add(serializableProperty.Name.ToIdentifier(), serializableProperty);
|
||||
}
|
||||
|
||||
@@ -1066,6 +1077,15 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (attributeName == "unlockrecipe" || attributeName == "unlockrecipes")
|
||||
{
|
||||
var recipes = subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipes",
|
||||
def: subElement.GetAttributeIdentifierImmutableHashSet("unlockrecipe", ImmutableHashSet<Identifier>.Empty));
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
GameMain.GameSession?.UnlockRecipe(CharacterTeamType.Team1, recipe, showNotifications: false);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.SerializableProperties.TryGetValue(attributeName, out SerializableProperty property))
|
||||
{
|
||||
|
||||
@@ -696,7 +696,8 @@ namespace Barotrauma
|
||||
root.Add(CampaignSettings.CurrentSettings.Save());
|
||||
#endif
|
||||
|
||||
configDoc.SaveSafe(PlayerConfigPath);
|
||||
//allow retrying a few times because the file may be in use if the player is running multiple instances of the game on the same machine
|
||||
configDoc.SaveSafe(PlayerConfigPath, maxRetries: 4);
|
||||
|
||||
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
|
||||
{
|
||||
|
||||
@@ -425,6 +425,11 @@ namespace Barotrauma
|
||||
{
|
||||
return PropertyMatchesRequirement(targetChar, characterProperty);
|
||||
}
|
||||
else if (targetChar?.AnimController?.SerializableProperties is { } animControllerProperties
|
||||
&& animControllerProperties.TryGetValue(AttributeName, out var animControllerProperty))
|
||||
{
|
||||
return PropertyMatchesRequirement(targetChar.AnimController, animControllerProperty);
|
||||
}
|
||||
return ComparisonOperatorIsNotEquals;
|
||||
case ConditionType.SkillRequirement:
|
||||
if (targetChar != null)
|
||||
|
||||
@@ -420,7 +420,7 @@ namespace Barotrauma
|
||||
[Serialize(1, IsPropertySaveable.No, description: "How many characters to spawn.")]
|
||||
public int Count { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description:
|
||||
[Serialize(false, IsPropertySaveable.No, description:
|
||||
"Should the buffs of the character executing the effect be transferred to the spawned character?"+
|
||||
" Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
|
||||
public bool TransferBuffs { get; private set; }
|
||||
@@ -445,11 +445,11 @@ namespace Barotrauma
|
||||
[Serialize("", IsPropertySaveable.No, description: "An affliction to apply on the spawned character.")]
|
||||
public Identifier AfflictionOnSpawn { get; private set; }
|
||||
|
||||
[Serialize(1, IsPropertySaveable.No, description:
|
||||
[Serialize(1, IsPropertySaveable.No, description:
|
||||
$"The strength of the affliction applied on the spawned character. Only relevant if {nameof(AfflictionOnSpawn)} is defined.")]
|
||||
public int AfflictionStrength { get; private set; }
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description:
|
||||
[Serialize(false, IsPropertySaveable.No, description:
|
||||
"Should the player controlling the character that executes the effect gain control of the spawned character?" +
|
||||
" Useful for effects that \"transform\" a character to something else by deleting the character and spawning a new one on its place.")]
|
||||
public bool TransferControl { get; private set; }
|
||||
@@ -470,7 +470,7 @@ namespace Barotrauma
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No)]
|
||||
public bool InheritEventTags { get; private set; }
|
||||
|
||||
|
||||
[Serialize(false, IsPropertySaveable.No, description: "Should the character team be inherited from the entity that owns the status effect?")]
|
||||
public bool InheritTeam { get; private set; }
|
||||
|
||||
@@ -722,9 +722,9 @@ namespace Barotrauma
|
||||
private readonly List<(Identifier eventIdentifier, Identifier tag)> eventTargetTags;
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to make the effect unlock a fabrication recipe globally for the entire crew.
|
||||
/// Can be used to make the effect unlock a fabrication recipe (or multiple recipes separated by a comma) globally for the entire crew.
|
||||
/// </summary>
|
||||
public readonly Identifier UnlockRecipe;
|
||||
public readonly ImmutableHashSet<Identifier> UnlockRecipes;
|
||||
|
||||
private Character user;
|
||||
|
||||
@@ -740,6 +740,8 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public readonly float SeverLimbsProbability;
|
||||
|
||||
private readonly Vector2 randomCondition;
|
||||
|
||||
public PhysicsBody sourceBody;
|
||||
|
||||
/// <summary>
|
||||
@@ -802,11 +804,11 @@ namespace Barotrauma
|
||||
private readonly List<Identifier> talentTriggers;
|
||||
private readonly List<int> giveExperiences;
|
||||
private readonly List<GiveSkill> giveSkills;
|
||||
|
||||
|
||||
private HashSet<(Character targetCharacter, AnimLoadInfo anim)> failedAnimations;
|
||||
public readonly record struct AnimLoadInfo(AnimationType Type, Either<string, ContentPath> File, float Priority, ImmutableArray<Identifier> ExpectedSpeciesNames);
|
||||
private readonly List<AnimLoadInfo> animationsToTrigger;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// How long the effect runs (in seconds). Note that if <see cref="Stackable"/> is true,
|
||||
/// there can be multiple instances of the effect running at a time.
|
||||
@@ -903,9 +905,10 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
SeverLimbsProbability = MathHelper.Clamp(element.GetAttributeFloat(0.0f, "severlimbs", "severlimbsprobability"), 0.0f, 1.0f);
|
||||
randomCondition = element.GetAttributeVector2("randomcondition", Vector2.Zero);
|
||||
|
||||
string[] targetTypesStr =
|
||||
element.GetAttributeStringArray("target", null) ??
|
||||
string[] targetTypesStr =
|
||||
element.GetAttributeStringArray("target", null) ??
|
||||
element.GetAttributeStringArray("targettype", Array.Empty<string>());
|
||||
foreach (string s in targetTypesStr)
|
||||
{
|
||||
@@ -939,7 +942,10 @@ namespace Barotrauma
|
||||
playSoundOnRequiredItemFailure = element.GetAttributeBool("playsoundonrequireditemfailure", false);
|
||||
#endif
|
||||
|
||||
UnlockRecipe = element.GetAttributeIdentifier(nameof(UnlockRecipe), Identifier.Empty);
|
||||
UnlockRecipes =
|
||||
element.GetAttributeIdentifierImmutableHashSet(nameof(UnlockRecipes),
|
||||
//backwards compatibility
|
||||
def: element.GetAttributeIdentifierImmutableHashSet("UnlockRecipe", ImmutableHashSet<Identifier>.Empty));
|
||||
|
||||
List<XAttribute> propertyAttributes = new List<XAttribute>();
|
||||
propertyConditionals = new List<PropertyConditional>();
|
||||
@@ -1014,7 +1020,7 @@ namespace Barotrauma
|
||||
DebugConsole.AddWarning(
|
||||
$"StatusEffect tags defined using the attribute 'tags' in StatusEffect ({parentDebugName}). "+
|
||||
"Please use the attribute 'statuseffecttags' or 'settags' instead to make it more explicit whether the 'tags' attribute means the status effect's tags, or tags the effect is supposed to set. " +
|
||||
"The game now assumes it means the status effect's tags.",
|
||||
"The game now assumes it means the status effect's tags.",
|
||||
contentPackage: element.ContentPackage);
|
||||
#endif
|
||||
}
|
||||
@@ -1038,7 +1044,7 @@ namespace Barotrauma
|
||||
//if the status effect has a duration, assume tags mean this status effect's tags and leave item tags untouched.
|
||||
propertyAttributes.RemoveAll(a => a.Name.ToString().Equals("tags", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
|
||||
List<(Identifier propertyName, object value)> propertyEffects = new List<(Identifier propertyName, object value)>();
|
||||
foreach (XAttribute attribute in propertyAttributes)
|
||||
{
|
||||
@@ -1074,7 +1080,7 @@ namespace Barotrauma
|
||||
dropItem = true;
|
||||
break;
|
||||
case "removecharacter":
|
||||
removeCharacter = true;
|
||||
removeCharacter = true;
|
||||
containerForItemsOnCharacterRemoval = subElement.GetAttributeIdentifier("moveitemstocontainer", Identifier.Empty);
|
||||
break;
|
||||
case "breaklimb":
|
||||
@@ -1132,7 +1138,7 @@ namespace Barotrauma
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Affliction afflictionInstance = afflictionPrefab.Instantiate(subElement.GetAttributeFloat(1.0f, "amount", nameof(afflictionInstance.Strength)));
|
||||
// Deserializing the object normally might cause some unexpected side effects. At least it clamps the strength of the affliction, which we don't want here.
|
||||
// Could probably be solved by using the NonClampedStrength or by bypassing the clamping, but ran out of time and played it safe here.
|
||||
@@ -1166,10 +1172,10 @@ namespace Barotrauma
|
||||
break;
|
||||
case "spawnitem":
|
||||
var newSpawnItem = new ItemSpawnInfo(subElement, parentDebugName);
|
||||
if (newSpawnItem.ItemPrefab != null)
|
||||
if (newSpawnItem.ItemPrefab != null)
|
||||
{
|
||||
spawnItems ??= new List<ItemSpawnInfo>();
|
||||
spawnItems.Add(newSpawnItem);
|
||||
spawnItems.Add(newSpawnItem);
|
||||
}
|
||||
break;
|
||||
case "triggerevent":
|
||||
@@ -1247,7 +1253,7 @@ namespace Barotrauma
|
||||
Identifier[] expectedSpeciesNames = subElement.GetAttributeIdentifierArray("expectedspecies", Array.Empty<Identifier>());
|
||||
animationsToTrigger ??= new List<AnimLoadInfo>();
|
||||
animationsToTrigger.Add(new AnimLoadInfo(animType, file, priority, expectedSpeciesNames.ToImmutableArray()));
|
||||
|
||||
|
||||
break;
|
||||
case "forcesay":
|
||||
forceSayIdentifier = subElement.GetAttributeIdentifier("message", Identifier.Empty);
|
||||
@@ -1294,7 +1300,7 @@ namespace Barotrauma
|
||||
return conditionValue < 0.0f || (setValue && conditionValue <= 0.0f);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return randomCondition.X < 0f || randomCondition.Y < 0f;
|
||||
}
|
||||
|
||||
public bool IncreasesItemCondition()
|
||||
@@ -1306,7 +1312,7 @@ namespace Barotrauma
|
||||
return conditionValue > 0.0f || (setValue && conditionValue > 0.0f);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return randomCondition.X > 0f || randomCondition.Y > 0f;
|
||||
}
|
||||
|
||||
private bool ChangesItemCondition(Identifier propertyName, object value, out float conditionValue)
|
||||
@@ -1383,7 +1389,7 @@ namespace Barotrauma
|
||||
if (HasTargetType(TargetType.NearbyItems))
|
||||
{
|
||||
//optimization for powered components that can be easily fetched from Powered.PoweredList
|
||||
if (TargetIdentifiers != null &&
|
||||
if (TargetIdentifiers != null &&
|
||||
TargetIdentifiers.Count == 1 &&
|
||||
(TargetIdentifiers.Contains("powered") || TargetIdentifiers.Contains("junctionbox") || TargetIdentifiers.Contains("relaycomponent")))
|
||||
{
|
||||
@@ -1491,8 +1497,8 @@ namespace Barotrauma
|
||||
{
|
||||
owner = ownerItem.ParentInventory?.Owner;
|
||||
}
|
||||
if (owner is Item container)
|
||||
{
|
||||
if (owner is Item container)
|
||||
{
|
||||
if (pc.Type == PropertyConditional.ConditionType.HasTag)
|
||||
{
|
||||
//if we're checking for tags, just check the Item object, not the ItemComponents
|
||||
@@ -1500,8 +1506,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
|
||||
}
|
||||
if (shouldShortCircuit(AnyTargetMatches(container.AllPropertyObjects, pc.TargetItemComponent, pc), out valueToReturn)) { return valueToReturn; }
|
||||
}
|
||||
}
|
||||
if (owner is Character character && shouldShortCircuit(pc.Matches(character), out valueToReturn)) { return valueToReturn; }
|
||||
}
|
||||
@@ -1673,7 +1679,7 @@ namespace Barotrauma
|
||||
PlaySound(entity, GetHull(entity), GetPosition(entity, targets, worldPosition));
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Duration > 0.0f && !Stackable)
|
||||
@@ -1743,7 +1749,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
position += Offset;
|
||||
position += Rand.Vector(Rand.Range(0.0f, RandomOffset));
|
||||
@@ -1784,7 +1790,7 @@ namespace Barotrauma
|
||||
}
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
if (targets[i] is not Item item) { continue; }
|
||||
if (targets[i] is not Item item) { continue; }
|
||||
for (int j = 0; j < useItemCount; j++)
|
||||
{
|
||||
if (item.Removed) { continue; }
|
||||
@@ -1807,8 +1813,8 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
if (targets[i] is Item item)
|
||||
{
|
||||
if (targets[i] is Item item)
|
||||
{
|
||||
foreach (var itemContainer in item.GetComponents<ItemContainer>())
|
||||
{
|
||||
foreach (var containedItem in itemContainer.Inventory.AllItemsMod)
|
||||
@@ -1885,6 +1891,11 @@ namespace Barotrauma
|
||||
if (target is Entity targetEntity)
|
||||
{
|
||||
if (targetEntity.Removed) { continue; }
|
||||
if (targetEntity is Item targetItem && randomCondition != Vector2.Zero)
|
||||
{
|
||||
float newCondition = Rand.Range(randomCondition.X, randomCondition.Y);
|
||||
targetItem.Condition = GetModifiedValue(targetItem.Condition, newCondition, deltaTime);
|
||||
}
|
||||
}
|
||||
else if (target is Limb limb)
|
||||
{
|
||||
@@ -1893,10 +1904,7 @@ namespace Barotrauma
|
||||
}
|
||||
foreach (var (propertyName, value) in PropertyEffects)
|
||||
{
|
||||
if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!target.SerializableProperties.TryGetValue(propertyName, out SerializableProperty property)) { continue; }
|
||||
ApplyToProperty(target, property, value, deltaTime);
|
||||
}
|
||||
}
|
||||
@@ -2034,10 +2042,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TryTriggerAnimation(target, entity);
|
||||
|
||||
if (!forceSayIdentifier.IsEmpty)
|
||||
if (!forceSayIdentifier.IsEmpty)
|
||||
{
|
||||
LocalizedString messageToSay = TextManager.Get(forceSayIdentifier).Fallback(forceSayIdentifier.Value);
|
||||
|
||||
@@ -2121,7 +2129,7 @@ namespace Barotrauma
|
||||
foreach (GiveTalentInfo giveTalentInfo in giveTalentInfos)
|
||||
{
|
||||
if (giveTalentInfo.GiveRandom)
|
||||
{
|
||||
{
|
||||
// for the sake of technical simplicity, for now do not allow talents to be given if the character could unlock them in their talent tree as well
|
||||
IEnumerable<Identifier> viableTalents = giveTalentInfo.TalentIdentifiers.Where(id => !targetCharacter.Info.UnlockedTalents.Contains(id) && !characterTalentTree.AllTalentIdentifiers.Contains(id));
|
||||
if (viableTalents.None()) { continue; }
|
||||
@@ -2142,8 +2150,8 @@ namespace Barotrauma
|
||||
{
|
||||
foreach ((Identifier eventId, Identifier tag) in eventTargetTags)
|
||||
{
|
||||
if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev)
|
||||
{
|
||||
if (GameMain.GameSession.EventManager.ActiveEvents.FirstOrDefault(e => e.Prefab.Identifier == eventId) is ScriptedEvent ev)
|
||||
{
|
||||
targets.Where(t => t is Entity).ForEach(t => ev.AddTarget(tag, (Entity)t));
|
||||
}
|
||||
}
|
||||
@@ -2157,9 +2165,13 @@ namespace Barotrauma
|
||||
fire.Size = new Vector2(FireSize, fire.Size.Y);
|
||||
}
|
||||
|
||||
if (isNotClient && !UnlockRecipe.IsEmpty && GameMain.GameSession is { } gameSession)
|
||||
if (isNotClient && UnlockRecipes.Any() && GameMain.GameSession is { } gameSession)
|
||||
{
|
||||
gameSession.UnlockRecipe(UnlockRecipe, showNotifications: true);
|
||||
foreach (var unlockRecipe in UnlockRecipes)
|
||||
{
|
||||
Character targetCharacter = user ?? targets.Select(GetCharacterFromTarget).NotNull().FirstOrDefault();
|
||||
gameSession.UnlockRecipe(targetCharacter?.TeamID ?? CharacterTeamType.Team1, unlockRecipe, showNotifications: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNotClient && triggeredEvents != null && GameMain.GameSession?.EventManager is { } eventManager)
|
||||
@@ -2168,7 +2180,7 @@ namespace Barotrauma
|
||||
{
|
||||
Event ev = eventPrefab.CreateInstance(eventManager.RandomSeed);
|
||||
if (ev == null) { continue; }
|
||||
eventManager.QueuedEvents.Enqueue(ev);
|
||||
eventManager.QueuedEvents.Enqueue(ev);
|
||||
if (ev is ScriptedEvent scriptedEvent)
|
||||
{
|
||||
if (!triggeredEventTargetTag.IsEmpty)
|
||||
@@ -2198,7 +2210,7 @@ namespace Barotrauma
|
||||
foreach (CharacterSpawnInfo characterSpawnInfo in spawnCharacters)
|
||||
{
|
||||
var characters = new List<Character>();
|
||||
|
||||
|
||||
CharacterTeamType? inheritedTeam = null;
|
||||
if (characterSpawnInfo.InheritTeam)
|
||||
{
|
||||
@@ -2211,16 +2223,16 @@ namespace Barotrauma
|
||||
_ => null
|
||||
// Default to Team1, when we can't deduce the team (for example when spawning outside the sub AND character inventory).
|
||||
} ?? (isPvP ? CharacterTeamType.None : CharacterTeamType.Team1);
|
||||
|
||||
|
||||
CharacterTeamType? GetTeamFromSubmarine(MapEntity e)
|
||||
{
|
||||
if (e.Submarine == null) { return null; }
|
||||
// Don't allow team FriendlyNPC in outposts, because if you buy a spawner item (such as husk container) from the store and choose to get it immediately, it will be spawned in the outpost.
|
||||
return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ?
|
||||
return !isPvP && e.Submarine.Info.IsOutpost && e.Submarine.TeamID == CharacterTeamType.FriendlyNPC ?
|
||||
CharacterTeamType.Team1 : e.Submarine.TeamID;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < characterSpawnInfo.Count; i++)
|
||||
{
|
||||
Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset,
|
||||
@@ -2311,11 +2323,11 @@ namespace Barotrauma
|
||||
Character.Controlled = newCharacter;
|
||||
}
|
||||
#elif SERVER
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.Character != target) { continue; }
|
||||
GameMain.Server.SetClientCharacter(c, newCharacter);
|
||||
}
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.Character != target) { continue; }
|
||||
GameMain.Server.SetClientCharacter(c, newCharacter);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); }
|
||||
@@ -2477,7 +2489,7 @@ namespace Barotrauma
|
||||
position = entity.WorldPosition;
|
||||
if (entity is Item it)
|
||||
{
|
||||
sourceBody ??=
|
||||
sourceBody ??=
|
||||
(entity as Item)?.body ??
|
||||
(entity as Character)?.AnimController.Collider;
|
||||
}
|
||||
@@ -2532,7 +2544,7 @@ namespace Barotrauma
|
||||
else if (parentItem != null)
|
||||
{
|
||||
rotation = PhysicsBody.TransformRotation(
|
||||
-parentItem.RotationRad + chosenItemSpawnInfo.RotationRad,
|
||||
-parentItem.RotationRad + chosenItemSpawnInfo.RotationRad,
|
||||
dir: parentItem.FlippedX ? -1.0f : 1.0f);
|
||||
}
|
||||
break;
|
||||
@@ -2758,29 +2770,17 @@ namespace Barotrauma
|
||||
|
||||
private void ApplyToProperty(ISerializableEntity target, SerializableProperty property, object value, float deltaTime)
|
||||
{
|
||||
if (disableDeltaTime || setValue) { deltaTime = 1.0f; }
|
||||
if (value is int || value is float)
|
||||
if (value is int or float)
|
||||
{
|
||||
float propertyValueF = property.GetFloatValue(target);
|
||||
float newValue = GetModifiedValue(property.GetFloatValue(target), Convert.ToSingle(value), deltaTime);
|
||||
if (property.PropertyType == typeof(float))
|
||||
{
|
||||
float floatValue = value is float single ? single : (int)value;
|
||||
floatValue *= deltaTime;
|
||||
if (!setValue)
|
||||
{
|
||||
floatValue += propertyValueF;
|
||||
}
|
||||
property.TrySetValue(target, floatValue);
|
||||
property.TrySetValue(target, newValue);
|
||||
return;
|
||||
}
|
||||
else if (property.PropertyType == typeof(int))
|
||||
{
|
||||
int intValue = (int)(value is float single ? single * deltaTime : (int)value * deltaTime);
|
||||
if (!setValue)
|
||||
{
|
||||
intValue += (int)propertyValueF;
|
||||
}
|
||||
property.TrySetValue(target, intValue);
|
||||
property.TrySetValue(target, (int)newValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2792,6 +2792,13 @@ namespace Barotrauma
|
||||
property.TrySetValue(target, value);
|
||||
}
|
||||
|
||||
private float GetModifiedValue(float currValue, float newValue, float deltaTime)
|
||||
{
|
||||
if (setValue) { return newValue; }
|
||||
if (disableDeltaTime) { deltaTime = 1f; }
|
||||
return currValue + newValue * deltaTime;
|
||||
}
|
||||
|
||||
public static void UpdateAll(float deltaTime)
|
||||
{
|
||||
UpdateAllProjSpecific(deltaTime);
|
||||
@@ -2848,7 +2855,7 @@ namespace Barotrauma
|
||||
element.Parent.RegisterTreatmentResults(element.Parent.user, element.Entity as Item, limb, affliction, result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach ((Identifier affliction, float amount) in element.Parent.ReduceAffliction)
|
||||
{
|
||||
Limb targetLimb = null;
|
||||
@@ -2894,10 +2901,10 @@ namespace Barotrauma
|
||||
element.Parent.TryTriggerAnimation(target, element.Entity);
|
||||
}
|
||||
|
||||
element.Parent.ApplyProjSpecific(deltaTime,
|
||||
element.Entity,
|
||||
element.Targets,
|
||||
element.Parent.GetHull(element.Entity),
|
||||
element.Parent.ApplyProjSpecific(deltaTime,
|
||||
element.Entity,
|
||||
element.Targets,
|
||||
element.Parent.GetHull(element.Entity),
|
||||
element.Parent.GetPosition(element.Entity, element.Targets),
|
||||
playSound: element.Timer >= element.Duration);
|
||||
|
||||
@@ -2970,7 +2977,7 @@ namespace Barotrauma
|
||||
if (limb == null) { return; }
|
||||
foreach (Affliction limbAffliction in limb.character.CharacterHealth.GetAllAfflictions())
|
||||
{
|
||||
if (result.Afflictions != null &&
|
||||
if (result.Afflictions != null &&
|
||||
/* "affliction" is the affliction directly defined in the status effect (e.g. "5 internal damage (per second / per frame / however the effect is defined to run)"),
|
||||
* "result" is how much we actually applied of that affliction right now (taking into account the elapsed time, resistances and such) */
|
||||
result.Afflictions.FirstOrDefault(a => a.Prefab == limbAffliction.Prefab) is Affliction resultAffliction &&
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -30,6 +29,8 @@ namespace Barotrauma
|
||||
public LocalizedString NestedStr { get; private set; }
|
||||
public readonly LocalizedString SanitizedString;
|
||||
|
||||
public bool Loaded => loaded;
|
||||
|
||||
#if CLIENT
|
||||
private readonly GUIFont? font;
|
||||
private readonly GUIComponentStyle? componentStyle;
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
#if CLIENT
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Steam;
|
||||
@@ -99,7 +101,8 @@ namespace Barotrauma.IO
|
||||
this System.Xml.Linq.XDocument doc,
|
||||
string path,
|
||||
System.Xml.Linq.SaveOptions saveOptions = System.Xml.Linq.SaveOptions.None,
|
||||
bool throwExceptions = false)
|
||||
bool throwExceptions = false,
|
||||
int maxRetries = 0)
|
||||
{
|
||||
if (!Validation.CanWrite(path, false))
|
||||
{
|
||||
@@ -114,7 +117,21 @@ namespace Barotrauma.IO
|
||||
}
|
||||
return;
|
||||
}
|
||||
doc.Save(path, saveOptions);
|
||||
|
||||
for (int i = 0; i <= maxRetries; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
doc.Save(path, saveOptions);
|
||||
break;
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (i >= maxRetries) { throw; }
|
||||
DebugConsole.NewMessage("Failed save XML document {" + e.Message + "}, retrying in 250 ms...");
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveSafe(this System.Xml.Linq.XElement element, string path, bool throwExceptions = false)
|
||||
|
||||
@@ -116,10 +116,23 @@ namespace Barotrauma
|
||||
#if SERVER
|
||||
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); }
|
||||
#else
|
||||
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
|
||||
get
|
||||
|
||||
{
|
||||
string tempFolder = Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp");
|
||||
#if DEBUG
|
||||
if (GameClient.MultiClientTestMode && GameMain.Client != null)
|
||||
{
|
||||
//append the name of the client to the download folder to avoid multiple clients
|
||||
//from trying to download a file into the same path at the same time
|
||||
tempFolder += "_" + GameMain.Client.Name;
|
||||
}
|
||||
#endif
|
||||
return tempFolder;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
public static void EnsureSaveFolderExists()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,3 +1,69 @@
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
v1.10.5.0
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
Balance:
|
||||
- Railgun rebalance: The railgun has been deemed on the weaker side by the community. It has been given a satisfaction pass, where now the high-power shell behaves more as expected, capable of penetrating multiple limbs or submarine walls, dealing more damage.
|
||||
- Broad-spectrum Antibiotics now affect all limbs with infected wound with one application, but cure less infection per limb. Base prices for Plastiseal, Antibiotic Glue and Broad-Spectrum Antibiotics have been adjusted.
|
||||
- Antibiotic Glue is now less effective on healing burns, bleeding and infections and inflicts organ damage even with successful application (and even more with failure).
|
||||
- Added a pass to make grenades more easy to aim (and less likely to always hit the wall in the back due to excessive sliding/rolling)
|
||||
- Reduced and equalized throwforce of most throwables (grenades, explosives) to 3.5 (some were 3.5 already, some 4.0).
|
||||
- Add angular dampening on grenades to reduce excessive sliding/rolling.
|
||||
- Frag grenade now uses a rectangular body instead of round/capsule, to avoid rolling too far (more in line with other grenades).
|
||||
- Reduced throw force when aiming throwables downwards.
|
||||
- Lower the rate at which skyholder artifacts and portable pumps drain water (both were way too effective for managing leaks).
|
||||
- Made all variants of fractal guardians vulnerable to EMPs.
|
||||
- Security NPCs on submarine encounters are now better armed, with weapons fitting the faction.
|
||||
|
||||
Blueprints:
|
||||
- Added new "blueprint" type of items, which unlock crafting recipes.
|
||||
- Three new alien material blueprints: physicorium, dementonite (gravity) and incendium, which unlock relevant (ammo) recipes.
|
||||
- Alien blueprints need to be researched at a Research station.
|
||||
- Merchants of all factions now sell item blueprints fitting to the faction's identity, locked behind reputation.
|
||||
|
||||
Miscellaneous changes:
|
||||
- Made vent sounds a bit more quiet.
|
||||
- Made crate shelf and makeshift shelf container UIs vertical to match the look of the sprites.
|
||||
- Made some items be attachable only to the floor.
|
||||
- Planters cannot overlap, and attach only to the floor, rather than attaching to the wall.
|
||||
- Makeshift shelves are attached to the floor.
|
||||
- Outpost NPCs react to players dragging other outpost NPC's corpses. Normal NPCs flee, security arrests you.
|
||||
|
||||
Fixes:
|
||||
- Fixed gaps significantly slowing down the flow of water, especially when water is flowing up through the gaps.
|
||||
- Fixed PvP outpost selection not working reliably (when you selected some specific outpost, it was possible for some other outpost to get selected regardless).
|
||||
- Fixed bot AI not running if a bot below 0 vitality is forced to stay unconscious with e.g. adrenaline or talents.
|
||||
- Fixed incorrect offset on the light that renders on a boarding pod that's in a loader.
|
||||
- Fixed defense bot only escaping if it attemps to fire it's weapon and fails due to being out of ammo, but not when it doesn't have any ammo available.
|
||||
- Fixed outpost NPCs sometimes spawning on non-human spawnpoints, e.g. the monster spawnpoints in research modules.
|
||||
- Fixed NPC conversations about the submarine being very deep starting to appear too late (at the point when the sub is already at crush depth, instead of the point where camera shake and audio effects start appearing).
|
||||
- Fixed yet another issue with modded hairs: hairs got misaligned in the bottom-right character portrait if they were set to inherit the origin of the head sprite, and the head sprite's origin was not at the center.
|
||||
- Fixed all pets in the level getting added to the player crew at the end of a round, e.g. even hostile pets or pets inside beacon stations.
|
||||
- Fixed id cards only being sold in normal outposts and cities (not mining, research or military outposts).
|
||||
- Fixed loading screen tips often cycling too fast to read during the initial loading screen.
|
||||
- Fixed "Find Jacov Subra" mission completing if you enter the outpost he's hiding in, even if you don't find him.
|
||||
- Fixed vents' oxygen output warning being incorrectly shown when vents have been moved between hulls in the sub editor.
|
||||
- The "drop item" hotkey is disabled when the inventory is not visible (e.g. when operating a turret).
|
||||
- Fixed inability to issue orders that don't target any character in particular (e.g. ignore or deconstruct orders) when no-one can hear the order.
|
||||
- Server log fixes:
|
||||
- Fixed new lines added to the log ignoring the filtering.
|
||||
- Fixed log jumping up when new lines are added while you've scrolled up to read older messages.
|
||||
|
||||
Modding:
|
||||
- When saving a submarine or an outpost in the editor, the game suggests saving it in a subfolder that contains subs of the same type instead of always saving it in the root folder of the mod.
|
||||
- Fixed "Equipped" item requirement only checking held items, not worn items as the documentation says.
|
||||
- Option to define tags for wrecks and beacon stations in the sub editor, and to make a wreck or beacon mission choose a random wreck or beacon station with the specified tags using the attributes "BeaconTags" and "WreckTags".
|
||||
- Fixed new elements defined in an item variant failing to be added to the variant if there's ClearAll elements present.
|
||||
- Fixed submarine upgrade UI displaying an incorrect icon on items that don't have an inventory icon but use the item's normal sprite instead.
|
||||
- The "sort stacks" and "merge stacks" buttons can be hidden from containers using the attributes "ShowSortButton" and "ShowMergeButton".
|
||||
- Changed how store item availabilities work by default: if no min/max amount is defined, there can be 0-5 items available instead of there always being 5.
|
||||
- Some new decorative outpost props: empty versions of the existing storage shelves which contained some decorative items.
|
||||
- Fixed game freezing if a bot uses a weapon that has both a RangedWeapon and a RepairTool component.
|
||||
- AnimController properties can now be accessed using conditionals.
|
||||
- Fixed bots being able to unequip and reload non-interactable items if they have those in their inventory (e.g. if some custom event forces them to spawn with non-interactable items).
|
||||
- Fixed corpses spawning in wrecks that have neither human or corpse spawnpoints (making it impossible to create wrecks with no corpses).
|
||||
- Fixed unnecessary console errors about failing to load a texture when trying to preload some monster/enemy with [GENDER] tags in the filename.
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
v1.9.8.0
|
||||
-------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -76,6 +142,16 @@ Fixes:
|
||||
- Fixed inability to focus on unconscious characters by clicking on the crew list.
|
||||
- Fixed parts of the Jove sculptures found in ruins being misaligned in mirrored levels (= when travelling through the level backwards).
|
||||
- Fixed the Multi-tool not functioning as a screwdriver for event checks.
|
||||
|
||||
Changes and additions:
|
||||
- Upgraded to .NET 8. This should not cause any noticeable changes, aside from perhaps very minor performance improvements. Allows code mods to use C# 12 features.
|
||||
- Added option to filter by name and to change the sorting of the save files listed in the "load game" menu.
|
||||
- Made circuit boxes' external connections (the once that hook up to other items) use the same labels that have been set inside the circuit box.
|
||||
- Made the freecam console command a toggle: entering it again gives you back control of the character.
|
||||
- Added "loslightingfreecam" console command (convenient for testing, executes those 3 commands).
|
||||
|
||||
Fixes:
|
||||
- Fixed bots being reluctant to go into rooms with low oxygen, even if they're trying to get diving gear from that room.
|
||||
- Fixed "fake fires" you see when under psychosis sometimes turning into real fires in single player.
|
||||
- Fixed PvP mode weapon crates showing up in the sub editor.
|
||||
- Fixed undoing the removal of an item from a container sometimes causing a crash in the sub editor.
|
||||
|
||||
Reference in New Issue
Block a user