Release 1.9.7.0 - Summer Update 2025

This commit is contained in:
Regalis11
2025-06-17 16:38:11 +03:00
parent 22227f13e5
commit ea5a2bc693
297 changed files with 7344 additions and 2421 deletions
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
@@ -30,6 +30,9 @@ namespace Barotrauma
[Serialize(120.0f, IsPropertySaveable.Yes, description: "How long it takes for the NPC to \"cool down\" (stop attacking).")]
public float CoolDown { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC revert back to a normal state when the event resets?")]
public bool AbandonOnReset { get; set; }
private bool isFinished = false;
@@ -81,7 +84,7 @@ namespace Barotrauma
public override void Reset()
{
if (affectedNpcs != null)
if (affectedNpcs != null && AbandonOnReset)
{
foreach (var npc in affectedNpcs)
{
@@ -1,4 +1,4 @@
namespace Barotrauma
namespace Barotrauma
{
/// <summary>
/// Makes a specific character invulnerable to damage and unable to die.
@@ -36,13 +36,21 @@ namespace Barotrauma
{
if (target != null && target is Character character)
{
if (UpdateAfflictions)
if (Enabled)
{
character.CharacterHealth.Unkillable = Enabled;
if (UpdateAfflictions)
{
character.CharacterHealth.Unkillable = true;
}
else
{
character.GodMode = true;
}
}
else
{
character.GodMode = Enabled;
character.CharacterHealth.Unkillable = false;
character.GodMode = false;
}
}
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,7 +14,10 @@ namespace Barotrauma
[Serialize(true, IsPropertySaveable.Yes, description: "Should the NPC start or stop waiting?")]
public bool Wait { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "The event actions reset when a GoTo action makes the event jump to a different point. Should the NPC stop waiting when the event resets?")]
public bool AbandonOnReset { get; set; }
[Serialize(AIObjectiveManager.MaxObjectivePriority, IsPropertySaveable.Yes, description: "AI priority for the action. Uses 100 by default, which is the absolute maximum for any objectives, " +
"meaning nothing can be prioritized over it, including the emergency objectives, such as find safety and combat." +
"Setting the priority to 70 would function like a regular order, but with the highest priority." +
@@ -76,7 +79,7 @@ namespace Barotrauma
public override void Reset()
{
if (affectedNpcs != null)
if (affectedNpcs != null && AbandonOnReset)
{
foreach (var npc in affectedNpcs)
{
@@ -367,7 +367,7 @@ namespace Barotrauma
SpawnType? spawnPointType = null;
if (!ignoreSpawnPointType) { spawnPointType = SpawnPointType; }
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.IsEmpty ? null : SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag, allowInPlayerView: AllowInPlayerView);
}
private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine)
@@ -422,12 +422,32 @@ namespace Barotrauma
}
if (spawnpointTags != null && spawnpointTags.Any())
{
var spawnPoints = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPoints.Any())
var spawnPointsWithTag = potentialSpawnPoints.Where(wp => spawnpointTags.Any(tag => wp.Tags.Contains(tag) && wp.ConnectedDoor == null && wp.IsTraversable));
if (requireTaggedSpawnPoint || spawnPointsWithTag.Any())
{
potentialSpawnPoints = spawnPoints.ToList();
potentialSpawnPoints = spawnPointsWithTag.ToList();
}
else
{
//no spawnpoints with the tag we want -> choose something with no tags
TryGetSpawnPointsWithNoTag();
}
}
else
{
//if no tags are specified, prefer a spawnpoint with no tags, i.e. prefer a "generic" spawnpoint instead of some special one like a jail spawnpoint
TryGetSpawnPointsWithNoTag();
}
void TryGetSpawnPointsWithNoTag()
{
var spawnPointsWithNoTag = potentialSpawnPoints.Where(wp => wp.Tags.None());
if (spawnPointsWithNoTag.Any())
{
potentialSpawnPoints = spawnPointsWithNoTag.ToList();
}
}
if (potentialSpawnPoints.None())
{
if (requireTaggedSpawnPoint && spawnpointTags != null && spawnpointTags.Any())
@@ -777,7 +777,15 @@ namespace Barotrauma
var locationType = location.GetLocationTypeToDisplay();
bool includeGenericEvents = level.Type == LevelData.LevelType.LocationConnection || !locationType.IgnoreGenericEvents;
if (includeGenericEvents && eventSet.LocationTypeIdentifiers == null) { return true; }
return eventSet.LocationTypeIdentifiers != null && eventSet.LocationTypeIdentifiers.Any(identifier => identifier == locationType.Identifier);
if (eventSet.LocationTypeIdentifiers == null) { return false; }
// EventLocationType is used to have the event set consider the location id as something else, for example "city" to get events that go to city locations
bool hasMatchingEventLocationId = !locationType.EventLocationType.IsEmpty &&
eventSet.LocationTypeIdentifiers.Contains(locationType.EventLocationType);
bool hasMatchingLocationId = eventSet.LocationTypeIdentifiers.Contains(locationType.Identifier);
return hasMatchingEventLocationId || hasMatchingLocationId;
}
private Location GetEventLocation()
@@ -156,9 +156,34 @@ namespace Barotrauma
}
}
}
private int previousKillTargetsRemaining = -1;
private void TrackKillTargetCount()
{
if (requireKill.Count == 0) { return; }
if (previousKillTargetsRemaining == -1)
{
previousKillTargetsRemaining = requireKill.Count();
}
int killTargetsRemaining = requireKill.Count(c => !c.Removed && !c.IsDead && !(c.LockHands && c.Submarine == Submarine.MainSub));
// at least one of the targets have been eliminated
if (killTargetsRemaining < previousKillTargetsRemaining)
{
#if CLIENT
SteamTimelineManager.OnOutpostTargetEliminated(this);
#endif
}
previousKillTargetsRemaining = killTargetsRemaining;
}
protected override void UpdateMissionSpecific(float deltaTime)
{
TrackKillTargetCount();
if (State != HostagesKilledState)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
@@ -9,14 +9,30 @@ namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly ContentXElement itemConfig;
private readonly ContentXElement terroristItemConfig;
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
/// <summary>
/// Number of escorted characters by default.
/// </summary>
private readonly int baseEscortedCharacters;
/// <summary>
/// A scaling factor for the number of escorted characters, relative to the recommended crew size of the sub. The total amount of escorted characters is calculated as
/// baseEscortedCharacters + scalingEscortedCharacters * (RecommendedCrewSizeMin + RecommendedCrewSizeMax) / 2
/// </summary>
private readonly float scalingEscortedCharacters;
/// <summary>
/// The probability for the escorted characters to be "terrorists" (turning them hostile when the sub has progressed enough in the level).
/// A value of 0.5 would mean about half of the characters are terrorist, 1 would mean they all are. There's 20% of randomness applied to the value to make it less predictable.
/// </summary>
private readonly float terroristChance;
/// <summary>
/// Dialog tag the terrorists use in their dialog when they become hostile.
/// </summary>
private readonly string terroristAnnounceDialogTag;
private int calculatedReward;
private Submarine missionSub;
@@ -26,7 +42,6 @@ namespace Barotrauma
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
private readonly string terroristAnnounceDialogTag = string.Empty;
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
@@ -35,8 +50,10 @@ namespace Barotrauma
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag = prefab.ConfigElement.GetAttributeString("terroristannouncedialogtag", string.Empty);
terroristItemConfig = prefab.ConfigElement.GetChildElement("TerroristItems");
terroristAnnounceDialogTag =
prefab.ConfigElement.GetAttributeString("dialogterroristannounce",
prefab.ConfigElement.GetAttributeString("terroristAnnounceDialogTag", string.Empty));
CalculateReward();
}
@@ -94,35 +111,29 @@ namespace Barotrauma
randSync = Rand.RandSync.Unsynced;
}
List<HumanPrefab> humanPrefabsToSpawn = new List<HumanPrefab>();
List<(HumanPrefab humanPrefab, List<StatusEffect> statusEffects)> humanPrefabsToSpawn = new List<(HumanPrefab humanPrefab, List<StatusEffect> statusEffects)>();
foreach (ContentXElement characterElement in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
var humanPrefab = GetHumanPrefabFromElement(characterElement);
for (int i = 0; i < count; i++)
{
humanPrefabsToSpawn.Add(humanPrefab);
}
foreach (var element in characterElement.Elements())
{
if (element.NameAsIdentifier() == "statuseffect")
List<StatusEffect> characterStatusEffects = new List<StatusEffect>();
foreach (var element in characterElement.Elements())
{
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
if (!characterStatusEffects.ContainsKey(humanPrefab))
if (element.NameAsIdentifier() == "statuseffect")
{
characterStatusEffects[humanPrefab] = new List<StatusEffect> { newEffect };
var newEffect = StatusEffect.Load(element, parentDebugName: Prefab.Name.Value);
if (newEffect == null) { continue; }
characterStatusEffects.Add(newEffect);
}
else
{
characterStatusEffects[humanPrefab].Add(newEffect);
}
}
humanPrefabsToSpawn.Add((humanPrefab, characterStatusEffects));
}
}
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
foreach (var humanPrefab in humanPrefabsToSpawn)
foreach ((var humanPrefab, var statusEffectList) in humanPrefabsToSpawn)
{
if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
var jobPrefab = humanPrefab.GetJobPrefab(randSync);
@@ -136,23 +147,19 @@ namespace Barotrauma
}
}
}
foreach (var humanPrefab in humanPrefabsToSpawn)
foreach ((var humanPrefab, var statusEffectList) in humanPrefabsToSpawn)
{
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
if (spawnedCharacter.AIController is HumanAIController humanAI)
{
humanAI.InitMentalStateManager();
}
if (characterStatusEffects.TryGetValue(humanPrefab, out var statusEffectList))
foreach (var statusEffect in statusEffectList)
{
foreach (var statusEffect in statusEffectList)
{
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
statusEffect.Apply(statusEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
if (terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
@@ -256,13 +263,23 @@ namespace Barotrauma
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
if (!string.IsNullOrEmpty(terroristAnnounceDialogTag))
{
character.Speak(TextManager.Get("dialogterroristannounce").Value, null, Rand.Range(0.5f, 3f));
character.Speak(TextManager.Get(terroristAnnounceDialogTag).Value, null, Rand.Range(0.5f, 3f));
}
ContentXElement randomElement = itemConfig.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
foreach (var itemElement in terroristItemConfig.Elements())
{
HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
float levelDifficulty = Level.Loaded?.Difficulty ?? 0.0f;
var selectedItemElement = itemElement;
if (itemElement.NameAsIdentifier() == "chooserandom".ToIdentifier())
{
selectedItemElement = itemElement.Elements().GetRandomUnsynced(e => e.GetAttributeFloat(0f, "mindifficulty") <= levelDifficulty);
}
if (selectedItemElement != null)
{
if (levelDifficulty < selectedItemElement.GetAttributeFloat(0f, "mindifficulty")) { continue; }
HumanPrefab.InitializeItem(character, selectedItemElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
}
}
}
}
}
@@ -26,6 +26,7 @@ namespace Barotrauma
{
if (state != value)
{
int previousState = state;
state = value;
TryTriggerEvents(state);
#if SERVER
@@ -38,6 +39,7 @@ namespace Barotrauma
#endif
ShowMessage(State);
OnMissionStateChanged?.Invoke(this);
MissionStateChanged(previousState);
}
}
}
@@ -198,6 +200,11 @@ namespace Barotrauma
Messages = messages.ToImmutableArray();
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
if (prefab.ConfigElement.GetChildElements("Characters").Count() > 1)
{
DebugConsole.AddWarning($"Error in mission {Prefab.Identifier}: multiple <Characters> elements found. Only the first one will be used.",
contentPackage: prefab.ContentPackage);
}
}
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
@@ -214,6 +221,8 @@ namespace Barotrauma
}
return message;
}
protected virtual void MissionStateChanged(int previousState) {}
public virtual void SetLevel(LevelData level) { }
@@ -109,6 +109,8 @@ namespace Barotrauma
public readonly bool AllowRetry;
public readonly bool ShowSonarLabels;
public readonly bool ShowInMenus, ShowStartMessage;
public readonly bool IsSideObjective;
@@ -219,11 +221,12 @@ namespace Barotrauma
}
}
Reward = element.GetAttributeInt("reward", 1);
ExperienceMultiplier = element.GetAttributeFloat("experiencemultiplier", 1.0f);
AllowRetry = element.GetAttributeBool("allowretry", false);
ShowInMenus = element.GetAttributeBool("showinmenus", true);
ShowStartMessage = element.GetAttributeBool("showstartmessage", true);
Reward = element.GetAttributeInt(nameof(Reward), 1);
ExperienceMultiplier = element.GetAttributeFloat(nameof(ExperienceMultiplier), 1.0f);
AllowRetry = element.GetAttributeBool(nameof(AllowRetry), false);
ShowSonarLabels = element.GetAttributeBool(nameof(ShowSonarLabels), true);
ShowInMenus = element.GetAttributeBool(nameof(ShowInMenus), true);
ShowStartMessage = element.GetAttributeBool(nameof(ShowStartMessage), true);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
@@ -237,11 +240,12 @@ namespace Barotrauma
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt("commonness", 1);
AllowOtherMissionsInLevel = element.GetAttributeBool("allowothermissionsinlevel", true);
Commonness = element.GetAttributeInt(nameof(Commonness), 1);
AllowOtherMissionsInLevel = element.GetAttributeBool(nameof(AllowOtherMissionsInLevel), true);
if (element.GetAttribute("difficulty") != null)
{
int difficulty = element.GetAttributeInt("difficulty", MinDifficulty);
int difficulty = element.GetAttributeInt(nameof(Difficulty), MinDifficulty);
Difficulty = Math.Clamp(difficulty, MinDifficulty, MaxDifficulty);
}
MinLevelDifficulty = element.GetAttributeInt(nameof(MinLevelDifficulty), MinLevelDifficulty);
@@ -173,6 +173,17 @@ namespace Barotrauma
}
}
protected override void MissionStateChanged(int previousState)
{
// state of 1+ here means the mission is completed
if (previousState == 0 && State >= 1)
{
#if CLIENT
SteamTimelineManager.OnMonsterMissionTargetsKilled(this);
#endif
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
switch (State)
@@ -92,13 +92,38 @@ namespace Barotrauma
set
{
if (value == state) { return; }
bool wasRetrieved = Retrieved;
state = value;
#if SERVER
GameMain.Server?.UpdateMissionState(mission);
#endif
if (!wasRetrieved && Retrieved)
{
OnTargetRetrieved();
}
else if (state == RetrievalState.PickedUp)
{
OnTargetPickedUp();
}
}
}
private void OnTargetRetrieved()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetRetrieved(Item, mission);
#endif
}
private void OnTargetPickedUp()
{
if (Item == null) { return; }
#if CLIENT
SteamTimelineManager.OnMissionTargetPickedUp(Item, mission);
#endif
}
public bool Interacted;
private readonly SalvageMission mission;
@@ -469,13 +494,13 @@ namespace Barotrauma
target.Item.ExternalHighlight = true;
#endif
target.Item.UpdateTransform();
if (target.Item.CurrentHull == null)
if (target.Item.CurrentHull == null && target.Item.body != null)
{
//prevent the body from moving if it spawned outside the hulls (we don't want it e.g. falling to the bottom of a cave or into the abyss)
target.Item.body.FarseerBody.BodyType = BodyType.Kinematic;
}
}
else if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
if (target.RequiredRetrievalState == Target.RetrievalState.Interact)
{
target.Item.OnInteract += () =>
{
@@ -168,6 +168,17 @@ namespace Barotrauma
}
}
protected override void MissionStateChanged(int previousState)
{
// detect successful scanned targets increasing after scan is completed
if (previousState < State)
{
#if CLIENT
SteamTimelineManager.OnScanSuccessful(this);
#endif
}
}
private void GetScanners()
{
foreach (var startingItem in startingItems)