Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -1,6 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -9,10 +8,6 @@ namespace Barotrauma
{
partial class AbandonedOutpostMission : Mission
{
private readonly XElement characterConfig;
protected readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
@@ -27,9 +22,9 @@ namespace Barotrauma
private const float EndDelay = 5.0f;
private float endTimer;
private bool allowOrderingRescuees;
private readonly bool allowOrderingRescuees;
public override bool AllowRespawn => false;
public override bool AllowRespawning => false;
public override bool AllowUndocking
{
@@ -82,8 +77,6 @@ namespace Barotrauma
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
allowOrderingRescuees = prefab.ConfigElement.GetAttributeBool(nameof(allowOrderingRescuees), true);
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
@@ -97,8 +90,6 @@ namespace Barotrauma
{
failed = false;
endTimer = 0.0f;
characters.Clear();
characterItems.Clear();
requireKill.Clear();
requireRescue.Clear();
items.Clear();
@@ -165,141 +156,7 @@ namespace Barotrauma
}
}
}
private void InitCharacters(Submarine submarine)
{
characters.Clear();
characterItems.Clear();
if (characterConfig != null)
{
foreach (XElement element in characterConfig.Elements())
{
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a human character for abandoned outpost mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
contentPackage: Prefab.ContentPackage);
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a character for abandoned outpost mission: character prefab \"{speciesName}\" not found",
contentPackage: Prefab.ContentPackage);
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
}
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, spawnPointType,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, teamId, spawnPos);
if (Level.Loaded?.StartOutpost?.Info is { } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
}
}
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
}
if (requiresRescue)
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController humanAi)
{
var order = OrderPrefab.Prefabs["fightintruders"]
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
}
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
if (spawnedCharacter.Inventory != null)
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
{
enemyAi.UnattackableSubmarines.Add(submarine);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (State != HostagesKilledState)
@@ -316,7 +173,7 @@ namespace Barotrauma
if (endTimer > EndDelay)
{
#if SERVER
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
@@ -337,7 +194,7 @@ namespace Barotrauma
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
if (GameMain.GameSession.GameMode is not CampaignMode && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
@@ -360,5 +217,45 @@ namespace Barotrauma
{
failed = !completed && requireRescue.Any(r => r.Removed || r.IsDead);
}
protected override void InitCharacter(Character character, XElement element)
{
base.InitCharacter(character, element);
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(character);
}
}
protected override Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
Character spawnedCharacter = base.LoadHuman(humanPrefab, element, submarine);
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
if (requiresRescue)
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
if (allowOrderingRescuees)
{
GameMain.GameSession.CrewManager?.AddCharacterToCrewList(spawnedCharacter);
}
#endif
}
else if (TimesAttempted > 0 && spawnedCharacter.AIController is HumanAIController)
{
var order = OrderPrefab.Prefabs["fightintruders"]
.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: spawnedCharacter)
.WithManualPriority(CharacterInfo.HighestManualOrderPriority);
spawnedCharacter.SetOrder(order, isNewOrder: true, speak: false);
}
// Overrides the team change set in the base method.
var teamId = element.GetAttributeEnum("teamid", requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None);
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
if (teamId != originalTeam)
{
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
}
return spawnedCharacter;
}
}
}
@@ -160,7 +160,9 @@ namespace Barotrauma
#if DEBUG || UNSTABLE
if (State == 1 && !level.CheckBeaconActive())
{
DebugConsole.ThrowError("Beacon became inactive!");
DebugConsole.ThrowError(
"Debug/unstable only error message: beacon became inactive mid-mission after it had been activated! If this happened unexpectedly while you were away from the beacon, it may be a sign of a bug."+
" If possible, please try to check what caused the beacon to go inactive.");
State = 2;
}
#endif
@@ -150,7 +150,7 @@ namespace Barotrauma
int maxCount = subElement.GetAttributeInt("maxcount", 10);
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { continue; }
ItemPrefab itemPrefab = FindItemPrefab(subElement);
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanBePut(itemPrefab))
while (containers[i].freeSlots > 0 && containers[i].container.Inventory.CanProbablyBePut(itemPrefab))
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
itemsToSpawn.Add((subElement, containers[i].container));
@@ -211,7 +211,7 @@ namespace Barotrauma
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetBaseReward(Submarine sub)
public override float GetBaseReward(Submarine sub)
{
// If we are not at the location of the mission, skip the calculation of the reward
if (GameMain.GameSession?.StartLocation != Locations[0])
@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -10,12 +11,59 @@ namespace Barotrauma
private readonly LocalizedString[] descriptions;
private static LocalizedString[] teamNames = { "Team A", "Team B" };
public override bool AllowRespawn
private readonly bool allowRespawning;
enum WinCondition
{
get { return false; }
/// <summary>
/// The winner is the team with the last living player(s)
/// </summary>
LastManStanding,
/// <summary>
/// The team who reaches a specific number of kills (determined by WinScore) is the winner
/// </summary>
KillCount,
/// <summary>
/// The team who controls a specific submarine (can be a ruin, outpost or a beacon station too) for some time (determined by WinScore) is the winner
/// </summary>
ControlSubmarine
}
private CharacterTeamType Winner
private readonly WinCondition winCondition;
public override bool AllowRespawning
{
get => allowRespawning;
}
private Submarine targetSubmarine;
private LocalizedString targetSubmarineSonarLabel;
/// <summary>
/// Which type of submarine the team needs to stay in control of to win
/// </summary>
public TagAction.SubType TargetSubmarineType { get; set; }
public readonly int PointsPerKill;
/// <summary>
/// The score required to win the mission.
/// </summary>
public int WinScore => GameMain.NetworkMember?.ServerSettings.WinScorePvP ?? 10;
/// <summary>
/// Is the winner determined by some kind of a scoring mechanism?
/// </summary>
public bool HasWinScore =>
winCondition != WinCondition.LastManStanding || PointsPerKill != 0;
/// <summary>
/// Scores of both teams. What the scoring represents depends on how the mission is configured (kills, time in control of a beacon station?)
/// </summary>
public readonly int[] Scores = new int[2];
public static CharacterTeamType Winner
{
get
{
@@ -46,6 +94,27 @@ namespace Barotrauma
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
allowRespawning = prefab.ConfigElement.GetAttributeBool(nameof(AllowRespawning), false);
winCondition = prefab.ConfigElement.GetAttributeEnum(nameof(WinCondition),
allowRespawning ? WinCondition.KillCount : WinCondition.LastManStanding);
PointsPerKill = prefab.ConfigElement.GetAttributeInt(nameof(PointsPerKill), 0);
TargetSubmarineType = prefab.ConfigElement.GetAttributeEnum(nameof(TargetSubmarineType), TagAction.SubType.Any);
string sonarTag = prefab.ConfigElement.GetAttributeString(nameof(targetSubmarineSonarLabel), string.Empty);
if (!sonarTag.IsNullOrEmpty())
{
targetSubmarineSonarLabel = TextManager.Get(sonarTag);
}
if (allowRespawning && winCondition == WinCondition.LastManStanding)
{
DebugConsole.ThrowError($"Error in mission {prefab.Identifier}: win condition cannot be \"last man standing\" when respawning is enabled.",
contentPackage: prefab.ContentPackage);
}
descriptions = new LocalizedString[]
{
TextManager.Get("MissionDescriptionNeutral." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("descriptionneutral", "")),
@@ -57,15 +126,23 @@ namespace Barotrauma
{
for (int n = 0; n < 2; n++)
{
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].DisplayName);
descriptions[i] =
descriptions[i]
.Replace($"[location{n + 1}]", locations[n].DisplayName)
.Replace("[winscore]", WinScore.ToString());
}
}
teamNames = new LocalizedString[]
{
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname1", "Team A")),
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(prefab.ConfigElement.GetAttributeString("teamname2", "Team B"))
TextManager.Get("MissionTeam1." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname1", "missionteam1.pvpmission"))),
TextManager.Get("MissionTeam2." + prefab.TextIdentifier).Fallback(TextManager.Get(prefab.ConfigElement.GetAttributeString("teamname2", "missionteam2.pvpmission"))),
};
if (winCondition == WinCondition.KillCount && PointsPerKill == 0)
{
DebugConsole.AddWarning($"Potential error in mission {Prefab.Identifier}: win condition is kill count, but {nameof(PointsPerKill)} is set to 0.");
}
}
public static LocalizedString GetTeamName(CharacterTeamType teamID)
@@ -82,9 +159,9 @@ namespace Barotrauma
return "Invalid Team";
}
public bool IsInWinningTeam(Character character)
public static bool IsInWinningTeam(Character character)
{
return character != null &&
return character != null &&
Winner != CharacterTeamType.None &&
Winner == character.TeamID;
}
@@ -99,19 +176,32 @@ namespace Barotrauma
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
if (Prefab.LoadSubmarines)
{
subs[0].NeutralizeBallast();
subs[0].TeamID = CharacterTeamType.Team1;
subs[0].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team1);
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
subs[1].FlipX();
subs[1].NeutralizeBallast();
subs[1].TeamID = CharacterTeamType.Team2;
subs[1].GetConnectedSubs().ForEach(s => s.TeamID = CharacterTeamType.Team2);
GameSession.PlaceSubAtInitialPosition(subs[1], level, placeAtStart: false);
subs[1].FlipX();
}
#if SERVER
crews = new List<Character>[] { new List<Character>(), new List<Character>() };
roundEndTimer = RoundEndDuration;
#endif
if (TargetSubmarineType != TagAction.SubType.Any)
{
targetSubmarine = Submarine.Loaded.FirstOrDefault(s => TagAction.SubmarineTypeMatches(s, TargetSubmarineType));
if (targetSubmarine == null)
{
DebugConsole.ThrowError($"Error in mission {Prefab.Identifier}: could not find a submarine of the type {TargetSubmarineType}.",
contentPackage: Prefab.ContentPackage);
}
}
}
protected override bool DetermineCompleted()
@@ -4,17 +4,13 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class EscortMission : Mission
{
private readonly ContentXElement characterConfig;
private readonly ContentXElement itemConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
private readonly int baseEscortedCharacters;
@@ -36,7 +32,6 @@ namespace Barotrauma
: base(prefab, locations, sub)
{
missionSub = sub;
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
@@ -61,7 +56,7 @@ namespace Barotrauma
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetBaseReward(Submarine sub)
public override float GetBaseReward(Submarine sub)
{
if (sub != missionSub)
{
@@ -192,7 +187,6 @@ namespace Barotrauma
foreach (ContentXElement element in characterConfig.Elements())
{
string escortIdentifier = element.GetAttributeString("escortidentifier", string.Empty);
string colorIdentifier = element.GetAttributeString("color", string.Empty);
for (int k = 0; k < scalingCharacterCount; k++)
{
// for each element defined, we need to initialize that type of character equal to the scaling escorted character count
@@ -111,7 +111,7 @@ namespace Barotrauma
get { return failed; }
}
public virtual bool AllowRespawn
public virtual bool AllowRespawning
{
get { return true; }
}
@@ -161,6 +161,10 @@ namespace Barotrauma
private readonly List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Action<Mission> OnMissionStateChanged;
protected readonly ContentXElement characterConfig;
protected readonly List<Character> characters = new List<Character>();
protected readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
{
@@ -192,6 +196,8 @@ namespace Barotrauma
messages[m] = ReplaceVariablesInMissionMessage(messages[m], sub);
}
Messages = messages.ToImmutableArray();
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
}
public LocalizedString ReplaceVariablesInMissionMessage(LocalizedString message, Submarine sub, bool replaceReward = true)
@@ -211,21 +217,21 @@ namespace Barotrauma
public virtual void SetLevel(LevelData level) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer, difficultyLevel);
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionTypes, isSinglePlayer, difficultyLevel);
}
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false, float? difficultyLevel = null)
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, IEnumerable<Identifier> missionTypes, bool isSinglePlayer = false, float? difficultyLevel = null)
{
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
if (missionType == MissionType.None)
if (missionTypes.None())
{
return null;
}
else
{
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => m.Type.HasAnyFlag(missionType)));
allowedMissions.AddRange(MissionPrefab.Prefabs.Where(m => missionTypes.Contains(m.Type)));
}
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
if (requireCorrectLocationType)
@@ -244,25 +250,179 @@ namespace Barotrauma
/// <summary>
/// Calculates the base reward, can be overridden for different mission types
/// </summary>
public virtual int GetBaseReward(Submarine sub)
public virtual float GetBaseReward(Submarine sub)
{
return Prefab.Reward;
}
/// <summary>
/// Calculates the available reward, taking into account universal modifiers such as campaign settings
/// Calculates the available monetary reward, taking into account universal modifiers such as campaign settings.
/// </summary>
public int GetReward(Submarine sub)
{
int reward = GetBaseReward(sub);
float reward = GetBaseReward(sub);
// Some modifiers should apply universally to all implementations of GetBaseReward
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
reward = (int)Math.Round(reward * campaign.Settings.MissionRewardMultiplier);
reward *= campaign.Settings.MissionRewardMultiplier;
}
return (int)Math.Round(reward);
}
/// <summary>
/// Call to load character elements to be spawned. Has to be implemented (and synced) separately per each mission.
/// </summary>
protected void InitCharacters(Submarine submarine)
{
characters.Clear();
characterItems.Clear();
return reward;
if (characterConfig != null)
{
foreach (XElement element in characterConfig.Elements())
{
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = element.GetAttributeInt("amount", 1);
}
int min = Math.Min(element.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, element.GetAttributeInt("max", defaultCount)), 255);
int count = Rand.Range(min, max + 1);
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a human character for a mission: human prefab \"{element.GetAttributeString("identifier", string.Empty)}\" not found",
contentPackage: Prefab.ContentPackage);
continue;
}
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
Identifier speciesName = element.GetAttributeIdentifier("character", element.GetAttributeIdentifier("identifier", Identifier.Empty));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn a character for a mission: character prefab \"{speciesName}\" not found",
contentPackage: Prefab.ContentPackage);
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
}
private SpawnAction.SpawnLocationType GetSpawnLocationTypeFromSubmarineType(Submarine sub)
{
return sub.Info.Type switch
{
SubmarineType.Outpost or SubmarineType.OutpostModule => SpawnAction.SpawnLocationType.Outpost,
SubmarineType.Wreck => SpawnAction.SpawnLocationType.Wreck,
SubmarineType.Ruin => SpawnAction.SpawnLocationType.Ruin,
SubmarineType.BeaconStation => SpawnAction.SpawnLocationType.BeaconStation,
SubmarineType.Player => SpawnAction.SpawnLocationType.MainSub,
_ => SpawnAction.SpawnLocationType.Any
};
}
protected virtual Character LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
var spawnPointType = element.GetAttributeEnum("spawnpointtype", SpawnType.Human);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(
GetSpawnLocationTypeFromSubmarineType(submarine), spawnPointType,
moduleFlags ?? humanPrefab.GetModuleFlags(),
spawnPointTags ?? humanPrefab.GetSpawnPointTags(),
element.GetAttributeBool("asfaraspossible", false));
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
var teamId = element.GetAttributeEnum("teamid", CharacterTeamType.None);
var originalTeam = Level.Loaded.StartOutpost?.TeamID ?? teamId;
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, originalTeam, spawnPos);
//consider the NPC to be "originally" from the team of the outpost it spawns in, and change it to the desired (hostile) team afterwards
//that allows the NPC to fight intruders and otherwise function in the outpost if the mission is configured to spawn the hostile NPCs in a friendly outpost
if (teamId != originalTeam)
{
spawnedCharacter.SetOriginalTeamAndChangeTeam(teamId);
}
if (element.GetAttribute("color") != null)
{
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
}
if (submarine.Info is { IsOutpost: true } outPostInfo)
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, humanPrefab.Identifier);
foreach (Identifier tag in humanPrefab.GetTags())
{
outPostInfo.AddOutpostNPCIdentifierOrTag(spawnedCharacter, tag);
}
}
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
}
InitCharacter(spawnedCharacter, element);
return spawnedCharacter;
}
protected virtual Character LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
{
Identifier[] moduleFlags = element.GetAttributeIdentifierArray("moduleflags", null);
Identifier[] spawnPointTags = element.GetAttributeIdentifierArray("spawnpointtags", null);
ISpatialEntity spawnPos = SpawnAction.GetSpawnPos(SpawnAction.SpawnLocationType.Outpost, SpawnType.Enemy, moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
spawnPos ??= submarine.GetHulls(alsoFromConnectedSubs: false).GetRandomUnsynced();
Character spawnedCharacter = Character.Create(monsterPrefab.Identifier, spawnPos.WorldPosition, ToolBox.RandomSeed(8), createNetworkEvent: false);
characters.Add(spawnedCharacter);
if (spawnedCharacter.Inventory != null)
{
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
if (submarine != null && spawnedCharacter.AIController is EnemyAIController enemyAi)
{
enemyAi.UnattackableSubmarines.Add(submarine);
enemyAi.UnattackableSubmarines.Add(Submarine.MainSub);
foreach (Submarine sub in Submarine.MainSub.DockedTo)
{
enemyAi.UnattackableSubmarines.Add(sub);
}
}
InitCharacter(spawnedCharacter, element);
return spawnedCharacter;
}
protected virtual void InitCharacter(Character character, XElement element)
{
if (element.GetAttributeBool(Tags.IgnoredByAI.Value, false))
{
character.AddAbilityFlag(AbilityFlags.IgnoredByEnemyAI);
}
float playDeadProbability = element.GetAttributeFloat("playdeadprobability", -1);
if (playDeadProbability >= 0)
{
character.EvaluatePlayDeadProbability(playDeadProbability);
}
float huskProbability = element.GetAttributeFloat("huskprobability", 0);
if (huskProbability > 0 && Rand.Value() <= huskProbability)
{
character.TurnIntoHusk();
}
else if (element.GetAttributeBool("corpse", false))
{
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
}
}
public void Start(Level level)
@@ -350,11 +510,12 @@ namespace Barotrauma
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier == trigger.EventIdentifier);
//clients are not allowed to trigger events, they're handled by the server
if (GameMain.NetworkMember is { IsClient: true }) { return; }
EventPrefab eventPrefab = EventPrefab.FindEventPrefab(trigger.EventIdentifier, trigger.EventTag, Prefab.ContentPackage);
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").",
contentPackage: Prefab.ContentPackage);
DebugConsole.ThrowError($"Mission {Prefab.Identifier} failed to trigger an event (identifier: {trigger.EventIdentifier}, tag: {trigger.EventTag}).", contentPackage: Prefab.ContentPackage);
return;
}
if (GameMain.GameSession?.EventManager != null)
@@ -427,15 +588,23 @@ namespace Barotrauma
finalReward = (int)(reward * missionMoneyGainMultiplier.Value);
}
private float CalculateDifficultyXPMultiplier()
{
const float minMissionDifficulty = 1;
const float maxMissionDifficulty = 4;
const float maxXpBonus = 1.3f;
float selectedMissionDifficulty = MathUtils.InverseLerp(minMissionDifficulty, maxMissionDifficulty, Prefab.Difficulty.GetValueOrDefault());
float xpBonusMultiplier = MathHelper.Lerp(1.0f, maxXpBonus, selectedMissionDifficulty);
return xpBonusMultiplier;
}
private void GiveReward()
{
if (GameMain.GameSession.GameMode is not CampaignMode campaign) { return; }
int reward = GetReward(Submarine.MainSub);
float baseExperienceGain = reward * 0.09f;
float difficultyMultiplier = 1 + level.Difficulty / 100f;
baseExperienceGain *= difficultyMultiplier;
float xpReward = GetBaseReward(Submarine.MainSub) * Prefab.ExperienceMultiplier * campaign.Settings.ExperienceRewardMultiplier;
float xpGain = xpReward * level.LevelData.Biome.ExperienceFromMissionRewards * CalculateDifficultyXPMultiplier();
IEnumerable<Character> crewCharacters = GameSession.GetSessionCrewCharacters(CharacterType.Both);
@@ -443,7 +612,7 @@ namespace Barotrauma
var experienceGainMultiplier = new AbilityMissionExperienceGainMultiplier(this, 1f, character: null);
crewCharacters.ForEach(c => experienceGainMultiplier.Value += c.GetStatValue(StatTypes.MissionExperienceGainMultiplier));
DistributeExperienceToCrew(crewCharacters, (int)(baseExperienceGain * experienceGainMultiplier.Value));
DistributeExperienceToCrew(crewCharacters, (int)(xpGain * experienceGainMultiplier.Value));
CalculateFinalReward(Submarine.MainSub);
#if SERVER
@@ -567,7 +736,7 @@ namespace Barotrauma
Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier, contentPackageToLogInError: Prefab.ContentPackage);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Couldn't spawn character for mission: character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
@@ -1,4 +1,5 @@
using System;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@@ -7,52 +8,42 @@ using System.Xml.Linq;
namespace Barotrauma
{
[Flags]
public enum MissionType
{
None = 0x0,
Salvage = 0x1,
Monster = 0x2,
Cargo = 0x4,
Beacon = 0x8,
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
AbandonedOutpost = 0x80,
Escort = 0x100,
Pirate = 0x200,
GoTo = 0x400,
ScanAlienRuins = 0x800,
EliminateTargets = 0x1000,
End = 0x2000,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo | ScanAlienRuins | EliminateTargets | End
}
partial class MissionPrefab : PrefabWithUintIdentifier
{
public static readonly PrefabCollection<MissionPrefab> Prefabs = new PrefabCollection<MissionPrefab>();
public static readonly Dictionary<MissionType, Type> CoOpMissionClasses = new Dictionary<MissionType, Type>()
/// <summary>
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
/// </summary>
public static readonly Dictionary<Identifier, Type> CoOpMissionClasses = new Dictionary<Identifier, Type>()
{
{ MissionType.Salvage, typeof(SalvageMission) },
{ MissionType.Monster, typeof(MonsterMission) },
{ MissionType.Cargo, typeof(CargoMission) },
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) },
{ MissionType.ScanAlienRuins, typeof(ScanMission) },
{ MissionType.EliminateTargets, typeof(EliminateTargetsMission) },
{ MissionType.End, typeof(EndMission) }
{ "Salvage".ToIdentifier(), typeof(SalvageMission) },
{ "Monster".ToIdentifier(), typeof(MonsterMission) },
{ "Cargo".ToIdentifier(), typeof(CargoMission) },
{ "Beacon".ToIdentifier(), typeof(BeaconMission) },
{ "Nest".ToIdentifier(), typeof(NestMission) },
{ "Mineral".ToIdentifier(), typeof(MineralMission) },
{ "AbandonedOutpost".ToIdentifier(), typeof(AbandonedOutpostMission) },
{ "Escort".ToIdentifier(), typeof(EscortMission) },
{ "Pirate".ToIdentifier(), typeof(PirateMission) },
{ "GoTo".ToIdentifier(), typeof(GoToMission) },
{ "ScanAlienRuins".ToIdentifier(), typeof(ScanMission) },
{ "EliminateTargets".ToIdentifier(), typeof(EliminateTargetsMission) },
{ "End".ToIdentifier(), typeof(EndMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
/// <summary>
/// The keys here are for backwards compatibility, tying the old mission types to the appropriate class.
/// Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string.
/// </summary>
public static readonly Dictionary<Identifier, Type> PvPMissionClasses = new Dictionary<Identifier, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
{ "Combat".ToIdentifier(), typeof(CombatMission) }
};
public static readonly HashSet<Identifier> HiddenMissionTypes = new HashSet<Identifier>() { "GoTo".ToIdentifier(), "End".ToIdentifier() };
public class ReputationReward
{
public readonly Identifier FactionIdentifier;
@@ -67,11 +58,11 @@ namespace Barotrauma
}
}
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo, MissionType.End };
private readonly ConstructorInfo constructor;
public readonly MissionType Type;
public readonly Identifier Type;
public readonly Type MissionClass;
public readonly bool MultiplayerOnly, SingleplayerOnly;
@@ -110,6 +101,8 @@ namespace Barotrauma
public readonly int Reward;
public readonly float ExperienceMultiplier;
// The titles and bodies of the popup messages during the mission, shown when the state of the mission changes. The order matters.
public readonly ImmutableArray<LocalizedString> Headers;
public readonly ImmutableArray<LocalizedString> Messages;
@@ -122,7 +115,21 @@ namespace Barotrauma
public readonly bool AllowOtherMissionsInLevel;
public readonly bool RequireWreck, RequireRuin, RequireThalamusWreck;
public readonly bool RequireWreck, RequireRuin, RequireBeaconStation, RequireThalamusWreck;
public readonly bool SpawnBeaconStationInMiddle;
public readonly bool AllowOutpostNPCs;
public readonly Identifier ForceOutpostGenerationParameters;
public readonly RespawnMode? ForceRespawnMode;
/// <summary>
/// If set, the players can choose which outpost is used for the mission (selected from the outposts that have this tag). Only works in multiplayer.
/// </summary>
public readonly Identifier AllowOutpostSelectionFromTag;
public readonly bool LoadSubmarines = true;
/// <summary>
/// If enabled, locations this mission takes place in cannot change their type
@@ -157,7 +164,10 @@ namespace Barotrauma
public class TriggerEvent
{
[Serialize("", IsPropertySaveable.Yes)]
public string EventIdentifier { get; private set; }
public Identifier EventIdentifier { get; private set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier EventTag { get; private set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int State { get; private set; }
@@ -210,16 +220,21 @@ 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);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
RequireRuin = element.GetAttributeBool("requireruin", false);
RequireThalamusWreck = element.GetAttributeBool("requirethalamuswreck", false);
RequireWreck = element.GetAttributeBool(nameof(RequireWreck), false);
RequireThalamusWreck = element.GetAttributeBool(nameof(RequireThalamusWreck), false);
RequireRuin = element.GetAttributeBool(nameof(RequireRuin), false);
RequireBeaconStation = element.GetAttributeBool(nameof(RequireBeaconStation), false);
SpawnBeaconStationInMiddle = element.GetAttributeBool(nameof(SpawnBeaconStationInMiddle), false);
if (RequireThalamusWreck) { RequireWreck = true; }
LoadSubmarines = element.GetAttributeBool(nameof(LoadSubmarines), true);
BlockLocationTypeChanges = element.GetAttributeBool(nameof(BlockLocationTypeChanges), false);
RequiredLocationFaction = element.GetAttributeIdentifier(nameof(RequiredLocationFaction), Identifier.Empty);
Commonness = element.GetAttributeInt("commonness", 1);
@@ -234,6 +249,15 @@ namespace Barotrauma
MinLevelDifficulty = Math.Clamp(MinLevelDifficulty, 0, Math.Min(MaxLevelDifficulty, 100));
MaxLevelDifficulty = Math.Clamp(MaxLevelDifficulty, Math.Max(MinLevelDifficulty, 0), 100);
AllowOutpostNPCs = element.GetAttributeBool(nameof(AllowOutpostNPCs), true);
ForceOutpostGenerationParameters = element.GetAttributeIdentifier(nameof(ForceOutpostGenerationParameters), Identifier.Empty);
AllowOutpostSelectionFromTag = element.GetAttributeIdentifier(nameof(AllowOutpostSelectionFromTag), Identifier.Empty);
if (element.GetAttribute(nameof(ForceRespawnMode)) != null)
{
ForceRespawnMode = element.GetAttributeEnum(nameof(ForceRespawnMode), RespawnMode.MidRound);
}
ShowProgressBar = element.GetAttributeBool(nameof(ShowProgressBar), false);
ShowProgressInNumbers = element.GetAttributeBool(nameof(ShowProgressInNumbers), false);
MaxProgressState = element.GetAttributeInt(nameof(MaxProgressState), 1);
@@ -362,47 +386,23 @@ namespace Barotrauma
Messages = messages.ToImmutableArray();
ReputationRewards = reputationRewards.ToImmutableList();
Identifier missionTypeName = element.GetAttributeIdentifier("type", Identifier.Empty);
//backwards compatibility
if (missionTypeName == "outpostdestroy" || missionTypeName == "outpostrescue")
{
missionTypeName = nameof(MissionType.AbandonedOutpost).ToIdentifier();
}
else if (missionTypeName == "clearalienruins")
{
missionTypeName = nameof(MissionType.EliminateTargets).ToIdentifier();
}
if (!Enum.TryParse(missionTypeName.Value, true, out Type))
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
return;
}
if (Type == MissionType.None)
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
return;
}
MissionClass = FindMissionClass(element);
Type = element.GetAttributeIdentifier(nameof(Type), Identifier.Empty);
#if DEBUG
if (Type == MissionType.Monster && SonarLabel.IsNullOrEmpty())
if (MissionClass == typeof(MonsterMission) && SonarLabel.IsNullOrEmpty())
{
DebugConsole.AddWarning($"Potential error in mission prefab \"{Identifier}\" - sonar label not set.");
}
#endif
if (CoOpMissionClasses.ContainsKey(Type))
if (!LoadSubmarines && MissionClass != typeof(CombatMission))
{
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else if (PvPMissionClasses.ContainsKey(Type))
{
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else
{
DebugConsole.ThrowErrorLocalized("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
DebugConsole.AddWarning($"Potential error in mission {Identifier}: Disabling submarines is only intended for combat missions taking place in an outpost, and may lead to issues in other types of missions.",
contentPackage: element.ContentPackage);
}
constructor = FindMissionConstructor(element, MissionClass);
if (constructor == null)
{
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!",
@@ -411,6 +411,67 @@ namespace Barotrauma
InitProjSpecific(element);
}
private Type FindMissionClass(ContentXElement element)
{
Type type;
Identifier typeName = element.NameAsIdentifier();
type = TryGetClass(typeName.RemoveFromEnd("Mission"));
if (type == null)
{
//backwards compatibility: the actual mission class used to be defined by the "type" attribute,
//Now the mission class is defined by the name of the mission element, and the type can be any arbitrary string,
//but if we failed to find the class based on the name, let's try the type attribute.
Identifier typeNameLegacy = (element.GetAttributeIdentifier("type", Identifier.Empty)).ToIdentifier();
if (typeNameLegacy == "OutpostDestroy" || typeNameLegacy == "OutpostRescue")
{
typeNameLegacy = "AbandonedOutpost".ToIdentifier();
}
else if (typeNameLegacy == "clearalienruins")
{
typeNameLegacy = "EliminateTargets".ToIdentifier();
}
type = TryGetClass(typeNameLegacy) ?? TryGetClass(typeNameLegacy.AppendIfMissing("Mission"));
if (type == null)
{
DebugConsole.ThrowError($"Failed to find the mission type \"{typeNameLegacy}\" for the mission {Identifier}.",
contentPackage: element.ContentPackage);
return null;
}
}
static Type TryGetClass(Identifier typeName)
{
if (CoOpMissionClasses.TryGetValue(typeName, out Type coOpMissionClass))
{
return coOpMissionClass;
}
else if (PvPMissionClasses.TryGetValue(typeName, out Type pvpMissionClass))
{
return pvpMissionClass;
}
return null;
}
return type;
}
private ConstructorInfo FindMissionConstructor(ContentXElement element, Type missionClass)
{
ConstructorInfo constructor;
if (missionClass == null) { return null; }
if (missionClass != typeof(Mission) && !missionClass.IsSubclassOf(typeof(Mission))) { return null; }
constructor = missionClass.GetConstructor(new Type[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
if (constructor == null)
{
DebugConsole.ThrowError(
$"Could not find the constructor of the mission type \"{missionClass}\" for the mission {Identifier}",
contentPackage: element.ContentPackage);
return null;
}
return constructor;
}
partial void InitProjSpecific(ContentXElement element);
@@ -424,7 +485,7 @@ namespace Barotrauma
}
return
AllowedLocationTypes.Any(lt => lt == "any") ||
AllowedLocationTypes.Any(lt => lt == "anyoutpost" && from.HasOutpost()) ||
AllowedLocationTypes.Any(lt => lt == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost) ||
AllowedLocationTypes.Any(lt => lt == from.Type.Identifier);
}
@@ -432,11 +493,11 @@ namespace Barotrauma
{
if (fromType == "any" ||
fromType == from.Type.Identifier ||
(fromType == "anyoutpost" && from.HasOutpost() && from.Type.Identifier != "abandoned"))
(fromType == Barotrauma.Tags.AnyOutpost && from.HasOutpost() && from.Type.IsAnyOutpost && from.Type.Identifier != "abandoned"))
{
if (toType == "any" ||
toType == to.Type.Identifier ||
(toType == "anyoutpost" && to.HasOutpost() && to.Type.Identifier != "abandoned"))
(toType == Barotrauma.Tags.AnyOutpost && to.HasOutpost() && to.Type.IsAnyOutpost && to.Type.Identifier != "abandoned"))
{
return true;
}
@@ -461,5 +522,28 @@ namespace Barotrauma
{
DisposeProjectSpecific();
}
/// <summary>
/// Returns all mission types that can be selected e.g. in the server lobby, excluding any special, hidden ones like EndMission
/// (the mission at the end of the campaign)
/// </summary>
public static IEnumerable<Identifier> GetAllMultiplayerSelectableMissionTypes()
{
List<Identifier> missionTypes = new List<Identifier>();
foreach (var missionPrefab in Prefabs)
{
if (missionPrefab.Commonness <= 0.0f) { continue; }
if (missionPrefab.SingleplayerOnly) { continue; }
if (HiddenMissionTypes.Contains(missionPrefab.Type))
{
continue;
}
if (!missionTypes.Contains(missionPrefab.Type))
{
missionTypes.Add(missionPrefab.Type);
}
}
return missionTypes.OrderBy(t => t.Value);
}
}
}
@@ -12,7 +12,6 @@ namespace Barotrauma
partial class PirateMission : Mission
{
private readonly ContentXElement submarineTypeConfig;
private readonly ContentXElement characterConfig;
private readonly ContentXElement characterTypeConfig;
private readonly float addedMissionDifficultyPerPlayer;
@@ -22,8 +21,8 @@ namespace Barotrauma
private Identifier factionIdentifier;
private Submarine enemySub;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly Dictionary<HumanPrefab, List<StatusEffect>> characterStatusEffects = new Dictionary<HumanPrefab, List<StatusEffect>>();
// Update the last sighting periodically so that the players can find the pirate sub even if they have lost the track of it.
private readonly float pirateSightingUpdateFrequency = 30;
@@ -68,7 +67,7 @@ namespace Barotrauma
}
}
public override int GetBaseReward(Submarine sub)
public override float GetBaseReward(Submarine sub)
{
return alternateReward;
}
@@ -92,18 +91,29 @@ namespace Barotrauma
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
submarineTypeConfig = prefab.ConfigElement.GetChildElement("SubmarineTypes");
characterConfig = prefab.ConfigElement.GetChildElement("Characters");
characterTypeConfig = prefab.ConfigElement.GetChildElement("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
factionIdentifier = prefab.ConfigElement.GetAttributeIdentifier("faction", Identifier.Empty);
//make sure all referenced character types are defined
foreach (XElement characterElement in characterConfig.Elements())
{
var characterId = characterElement.GetAttributeString("typeidentifier", string.Empty);
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId);
Identifier typeId = characterElement.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
if (typeId.IsEmpty)
{
if (characterElement.GetAttributeIdentifier("identifier", Identifier.Empty).IsEmpty)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character element with neither a typeidentifier or identifier ({characterElement.ToString()}).",
contentPackage: Prefab.ContentPackage);
}
continue;
}
var characterTypeElement = characterTypeConfig.Elements().FirstOrDefault(e =>
e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == typeId);
if (characterTypeElement == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{characterId}\".",
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Could not find a character type element for the character \"{typeId}\".",
contentPackage: Prefab.ContentPackage);
}
}
@@ -114,7 +124,7 @@ namespace Barotrauma
{
Identifier characterIdentifier = characterElement.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = characterElement.GetAttributeIdentifier("from", Identifier.Empty);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier, contentPackageToLogInError: Prefab.ContentPackage);
if (humanPrefab == null)
{
DebugConsole.ThrowError($"Error in mission \"{prefab.Identifier}\". Character prefab \"{characterIdentifier}\" not found in the NPC set \"{characterFrom}\".",
@@ -143,37 +153,47 @@ namespace Barotrauma
levelData = level;
missionDifficulty = level?.Difficulty ?? 0;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", Identifier.Empty);
//no specific sub configured, choose a random one
if (submarineTypeConfig == null)
{
submarineInfo = GetRandomDifficultyModifiedSubmarine(missionDifficulty, ShipRandomnessModifier);
alternateReward = (int)submarineInfo.EnemySubmarineInfo.Reward;
}
else
{
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
factionIdentifier = submarineConfig.GetAttributeIdentifier("faction", factionIdentifier);
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (submarinePath.IsNullOrEmpty())
{
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
contentPackage: Prefab.ContentPackage);
return;
}
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!",
contentPackage: Prefab.ContentPackage);
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
ContentPath submarinePath = submarineConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (submarinePath.IsNullOrEmpty())
{
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!",
contentPackage: Prefab.ContentPackage);
return;
}
BaseSubFile contentFile =
GetSubFile<EnemySubmarineFile>(submarinePath) ??
GetSubFile<SubmarineFile>(submarinePath);
BaseSubFile GetSubFile<T>(ContentPath path) where T : BaseSubFile
{
return ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<T>()).FirstOrDefault(f => f.Path == submarinePath);
}
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!",
contentPackage: Prefab.ContentPackage);
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path.Value);
}
private static float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier, Random rand)
@@ -185,6 +205,33 @@ namespace Barotrauma
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * (levelDifficulty + MathHelper.Lerp(-RandomnessModifier, RandomnessModifier, (float)rand.NextDouble())) / MaxDifficulty), minAmount);
}
private SubmarineInfo GetRandomDifficultyModifiedSubmarine(float levelDifficulty, float randomnessModifier)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
// look for the saved submarine that is closest to our difficulty, with some randomness
SubmarineInfo bestSubmarine = null;
float bestValue = float.MaxValue;
var submarineInfos = SubmarineInfo.SavedSubmarines.Where(i => i.IsEnemySubmarine);
foreach (SubmarineInfo submarineInfo in submarineInfos)
{
if (!Prefab.Tags.Any(t => submarineInfo.EnemySubmarineInfo.MissionTags.Contains(t))) { continue; }
float applicabilityValue = GetDifficultyModifiedValue(submarineInfo.EnemySubmarineInfo.PreferredDifficulty, levelDifficulty, randomnessModifier, rand);
if (applicabilityValue < bestValue)
{
bestSubmarine = submarineInfo;
bestValue = applicabilityValue;
}
}
if (bestSubmarine == null)
{
DebugConsole.ThrowError("No EnemySubmarine found that matches the mission's tags!");
return SubmarineInfo.SavedSubmarines.First(i => i.IsEnemySubmarine);
}
return bestSubmarine;
}
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
@@ -300,30 +347,54 @@ namespace Barotrauma
bool commanderAssigned = false;
foreach (ContentXElement element in characterConfig.Elements())
{
//there's two ways to define the characters in pirate missions
//1. "the normal way", referring to a human prefab
Identifier humanPrefabId = element.GetAttributeIdentifier("identifier", Identifier.Empty);
//2. the strange way it was initially implemented and the way the vanilla missions work: using a reference to a "character type" in the mission, which refers to a human prefab
Identifier characterTypeId = element.GetAttributeIdentifier("typeidentifier", Identifier.Empty);
int minAmount = element.GetAttributeInt("minamount", 0);
int maxAmount = element.GetAttributeInt("maxamount", 0);
// it is possible to get more than the "max" amount of characters if the modified difficulty is high enough; this is intentional
// if necessary, another "hard max" value could be used to clamp the value for performance/gameplay concerns
int amountCreated = GetDifficultyModifiedAmount(element.GetAttributeInt("minamount", 0), element.GetAttributeInt("maxamount", 0), enemyCreationDifficulty, rand);
var characterId = element.GetAttributeString("typeidentifier", string.Empty);
int amountCreated = minAmount == 0 && maxAmount == 0 ?
//default to 1 character if amount is not defined
1 :
//otherwise choose a value between min and max based on difficulty
GetDifficultyModifiedAmount(minAmount, maxAmount, enemyCreationDifficulty, rand);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == characterId).FirstOrDefault();
if (characterType == null)
HumanPrefab humanPrefab = null;
bool isCommander = false;
if (!characterTypeId.IsEmpty)
{
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
contentPackage: element.ContentPackage);
return;
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeIdentifier("typeidentifier", Identifier.Empty) == characterTypeId).FirstOrDefault();
if (characterType == null)
{
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".",
contentPackage: element.ContentPackage);
return;
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
humanPrefab = GetHumanPrefabFromElement(variantElement);
isCommander = variantElement.GetAttributeBool("iscommander", false);
}
else if (!humanPrefabId.IsEmpty)
{
humanPrefab = GetHumanPrefabFromElement(element);
isCommander = element.GetAttributeBool("iscommander", false);
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
var humanPrefab = GetHumanPrefabFromElement(variantElement);
if (humanPrefab == null) { continue; }
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, enemySub, CharacterTeamType.None, null);
if (element.GetAttribute("color") != null)
{
spawnedCharacter.UniqueNameColor = element.GetAttributeColor("color", Color.Red);
}
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.InitShipCommandManager();
@@ -335,6 +406,15 @@ namespace Barotrauma
}
}
foreach (var subElement in element.Elements())
{
if (subElement.NameAsIdentifier() == "statuseffect")
{
var newEffect = StatusEffect.Load(subElement, parentDebugName: Prefab.Name.Value);
newEffect?.Apply(newEffect.type, 1.0f, spawnedCharacter, spawnedCharacter);
}
}
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.GetComponent<IdCard>() != null)
@@ -395,10 +475,7 @@ namespace Barotrauma
}
#endif
enemySub.SetPosition(spawnPos);
if (!IsClient)
{
InitPirateShip();
}
InitPirateShip();
// flipping the sub on the frame it is moved into place must be done after it's been moved, or it breaks item connections in the submarine
// creating the pirates has to be done after the sub has been flipped, or it seems to break the AI pathing
@@ -39,6 +39,12 @@ namespace Barotrauma
public readonly Identifier ContainerTag;
public readonly Identifier ExistingItemTag;
/// <summary>
/// If true, target location indicator points to the submarine where the target is inside when the target is not yet found. Not used, if target is not inside any submarine.
/// When enabled, the indicator is hidden when the player is inside the target submarine.
/// </summary>
public readonly bool PointToSub;
public readonly bool RemoveItem;
public readonly LocalizedString SonarLabel;
@@ -55,6 +61,8 @@ namespace Barotrauma
public readonly RetrievalState RequiredRetrievalState;
public readonly bool HideLabelAfterRetrieved;
public readonly bool HideLabelWhenFound;
public readonly bool HideLabelWhenNotFound;
public bool Retrieved
{
@@ -115,6 +123,9 @@ namespace Barotrauma
RequiredRetrievalState = element.GetAttributeEnum("requireretrieval", parentTarget?.RequiredRetrievalState ?? RetrievalState.RetrievedToSub);
AllowContinueBeforeRetrieved = element.GetAttributeBool("allowcontinuebeforeretrieved", parentTarget != null);
HideLabelAfterRetrieved = element.GetAttributeBool("hidelabelafterretrieved", parentTarget?.HideLabelAfterRetrieved ?? false);
HideLabelWhenFound = element.GetAttributeBool(nameof(HideLabelWhenFound), parentTarget?.HideLabelWhenFound ?? false);
HideLabelWhenNotFound = element.GetAttributeBool(nameof(HideLabelWhenNotFound), parentTarget?.HideLabelWhenNotFound ?? false);
PointToSub = element.GetAttributeBool(nameof(PointToSub), parentTarget?.PointToSub ?? false);
RequireInsideOriginalContainer = element.GetAttributeBool("requireinsideoriginalcontainer", false);
string sonarLabelTag = element.GetAttributeString("sonarlabel", "");
@@ -203,6 +214,8 @@ namespace Barotrauma
/// What percentage of targets need to be retrieved for the mission to complete (0.0 - 1.0). Defaults to 0.98.
/// </summary>
private readonly float requiredDeliveryAmount;
private LocalizedString pickedUpMessage;
/// <summary>
/// Message displayed when at least one of the targets is retrieved, but the mission is not complete yet.
@@ -225,8 +238,26 @@ namespace Barotrauma
foreach (var target in targets)
{
if (target.Retrieved && target.HideLabelAfterRetrieved) { continue; }
if (target.Item != null && !target.Item.Removed)
if (target.State is Target.RetrievalState.None)
{
if (target.HideLabelWhenNotFound) { continue; }
}
else if (target.HideLabelWhenFound)
{
continue;
}
if (target.Item is { Removed: false })
{
if (target.PointToSub && target.Item.Submarine is Submarine targetSub && target.State == Target.RetrievalState.None)
{
if (Character.Controlled is Character playerCharacter && playerCharacter.Submarine != targetSub)
{
// The target is not in the same sub as the player -> point to the target submarine (instead of the item position).
// When inside the target sub, don't show anything.
yield return (target.SonarLabel, targetSub.WorldPosition);
}
continue;
}
if (target.Item.ParentInventory?.Owner is Item parentItem)
{
bool insideParentItem = false;
@@ -238,7 +269,7 @@ namespace Barotrauma
break;
}
}
//if the item is inside another target that has it's own sonar label, no need to show one on this item
//if the item is inside another target that has its own sonar label, no need to show one on this item
if (insideParentItem) { continue; }
}
@@ -263,6 +294,7 @@ namespace Barotrauma
partiallyRetrievedMessage = GetMessage(nameof(partiallyRetrievedMessage));
allRetrievedMessage = GetMessage(nameof(allRetrievedMessage));
pickedUpMessage = GetMessage(nameof(pickedUpMessage));
foreach (ContentXElement subElement in prefab.ConfigElement.Elements())
{
@@ -311,7 +343,11 @@ namespace Barotrauma
targets.Add(target);
foreach (ContentXElement subElement in chosenElement.Elements())
{
LoadTarget(subElement, parentTarget: target);
if (subElement.NameAsIdentifier() == "target" ||
subElement.NameAsIdentifier() == "chooserandom")
{
LoadTarget(subElement, parentTarget: target);
}
}
}
}
@@ -337,6 +373,17 @@ namespace Barotrauma
#if SERVER
spawnInfo.Clear();
#endif
if (!IsClient)
{
// First spawn any possible characters, so that we can use their items as targets.
Target firstTarget = targets.First();
var submarine = Submarine.Loaded.Find(s => IsValidSubmarine(s, firstTarget.SpawnPositionType));
if (submarine != null)
{
InitCharacters(submarine);
}
}
foreach (var target in targets)
{
bool usedExistingItem = false;
@@ -376,39 +423,36 @@ namespace Barotrauma
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
case Level.PositionType.SidePath:
case Level.PositionType.AbyssCave:
target.Item = suitableItems.FirstOrDefault(it => Vector2.DistanceSquared(it.WorldPosition, position) < 1000.0f);
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
case Level.PositionType.Abyss:
target.Item = suitableItems.FirstOrDefault(it => Level.IsPositionInAbyss(it.WorldPosition));
break;
case Level.PositionType.Ruin:
case Level.PositionType.Wreck:
case Level.PositionType.Outpost:
case Level.PositionType.BeaconStation:
foreach (Item it in suitableItems)
{
if (it.Submarine?.Info == null) { continue; }
if (target.SpawnPositionType == Level.PositionType.Ruin && it.Submarine.Info.Type != SubmarineType.Ruin) { continue; }
if (target.SpawnPositionType == Level.PositionType.Wreck && it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
if (target.SpawnPositionType == Level.PositionType.Outpost && it.Submarine.Info.Type != SubmarineType.Outpost) { continue; }
Rectangle worldBorders = it.Submarine.Borders;
worldBorders.Location += it.Submarine.WorldPosition.ToPoint();
if (it.Submarine is not Submarine sub) { continue; }
if (!IsValidSubmarine(sub, target.SpawnPositionType)) { continue; }
Rectangle worldBorders = sub.Borders;
worldBorders.Location += sub.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, it.WorldPosition))
{
target.Item = it;
#if SERVER
usedExistingItem = true;
#endif
break;
}
}
break;
default:
target.Item = suitableItems.FirstOrDefault();
#if SERVER
usedExistingItem = target.Item != null;
#endif
break;
}
#if SERVER
usedExistingItem = target.Item != null;
#endif
}
if (target.Item == null)
@@ -460,19 +504,7 @@ namespace Barotrauma
{
if (!it.HasTag(target.ContainerTag)) { continue; }
if (!it.IsPlayerTeamInteractable) { continue; }
switch (target.SpawnPositionType)
{
case Level.PositionType.Cave:
case Level.PositionType.MainPath:
if (it.Submarine != null) { continue; }
break;
case Level.PositionType.Ruin:
if (it.Submarine?.Info == null || !it.Submarine.Info.IsRuin) { continue; }
break;
case Level.PositionType.Wreck:
if (it.Submarine?.Info == null || it.Submarine.Info.Type != SubmarineType.Wreck) { continue; }
break;
}
if (!IsValidSubmarine(it.Submarine, target.SpawnPositionType)) { continue; }
var itemContainer = it.GetComponent<ItemContainer>();
if (itemContainer != null && itemContainer.Inventory.CanBePut(target.Item)) { validContainers.Add(itemContainer); }
}
@@ -534,6 +566,26 @@ namespace Barotrauma
}
}
}
private static bool IsValidSubmarine(Submarine sub, Level.PositionType spawnPosType)
{
if (sub == null)
{
return spawnPosType switch
{
Level.PositionType.Ruin or Level.PositionType.Wreck or Level.PositionType.BeaconStation or Level.PositionType.Outpost => false,
_ => true
};
}
return spawnPosType switch
{
Level.PositionType.Ruin => sub.Info.IsRuin,
Level.PositionType.Wreck => sub.Info.IsWreck,
Level.PositionType.BeaconStation => sub.Info.IsBeacon,
Level.PositionType.Outpost => sub.Info.IsOutpost,
_ => false
};
}
protected override void UpdateMissionSpecific(float deltaTime)
{
@@ -567,48 +619,45 @@ namespace Barotrauma
switch (target.State)
{
case Target.RetrievalState.None:
if (target.Interacted)
{
if (target.Interacted)
{
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.RootContainer ?? target.Item;
if (root.ParentInventory?.Owner is Character character && character.TeamID == CharacterTeamType.Team1)
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
}
if (inPlayerSub)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
TrySetRetrievalState(Target.RetrievalState.Interact);
}
var root = target.Item?.RootContainer ?? target.Item;
if (root.ParentInventory?.Owner is Character { TeamID: CharacterTeamType.Team1 })
{
TrySetRetrievalState(Target.RetrievalState.PickedUp);
#if CLIENT
TryShowPickedUpMessage();
#endif
}
if (inPlayerSub)
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
break;
case Target.RetrievalState.PickedUp:
case Target.RetrievalState.RetrievedToSub:
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character { TeamID: CharacterTeamType.Team1 } character)
{
bool inPlayerInventory = false;
bool playerInFriendlySub = false;
if (rootInventoryOwner is Character character && character.TeamID == CharacterTeamType.Team1)
inPlayerInventory = true;
if (character.Submarine != null)
{
inPlayerInventory = true;
if (character.Submarine != null)
{
playerInFriendlySub =
character.IsInFriendlySub ||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
}
}
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
else
{
target.State = Target.RetrievalState.PickedUp;
playerInFriendlySub =
character.IsInFriendlySub ||
(character.Submarine == Level.Loaded?.StartOutpost && Level.IsLoadedFriendlyOutpost && GameMain.GameSession?.Campaign.CurrentLocation is not { IsFactionHostile: true });
}
}
if (inPlayerSub || (inPlayerInventory && playerInFriendlySub))
{
TrySetRetrievalState(Target.RetrievalState.RetrievedToSub);
}
else
{
target.State = Target.RetrievalState.PickedUp;
}
break;
}
@@ -617,7 +666,7 @@ namespace Barotrauma
if (retrievalState < target.State || target.State == retrievalState) { return; }
bool wasRetrieved = target.Retrieved;
target.State = retrievalState;
//increment the mission state if the target became retrieved
//increment the mission state if the target became retrieved
if (!wasRetrieved && target.Retrieved)
{
State = Math.Max(i + 1, State);
@@ -641,7 +690,7 @@ namespace Barotrauma
{
if (requiredDeliveryAmount < 1.0f)
{
return targets.Count(t => IsTargetRetrieved(t)) / (float)targets.Count >= requiredDeliveryAmount;
return targets.Count(IsTargetRetrieved) / (float)targets.Count >= requiredDeliveryAmount;
}
else
{
@@ -675,7 +724,7 @@ namespace Barotrauma
}
foreach (var target in targetsToRemove)
{
if (target.Item != null && !target.Item.Removed)
if (target.Item is { Removed: false })
{
target.Item.Remove();
}
@@ -1,3 +1,4 @@
using System;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.RuinGeneration;
@@ -20,23 +21,14 @@ namespace Barotrauma
private readonly Dictionary<WayPoint, bool> scanTargets = new Dictionary<WayPoint, bool>();
private readonly HashSet<WayPoint> newTargetsScanned = new HashSet<WayPoint>();
private readonly float minTargetDistance;
private Ruin TargetRuin { get; set; }
private bool AllTargetsScanned
{
get
{
return scanTargets.Any() && scanTargets.All(kvp => kvp.Value);
}
}
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State > 0 || scanTargets.None())
if (AllTargetsScanned())
{
return Enumerable.Empty<(LocalizedString Label, Vector2 Position)>();
}
@@ -234,24 +226,19 @@ namespace Barotrauma
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
switch (State)
{
case 0:
if (AllTargetsScanned)
{
State = 1;
}
break;
}
// Allow the state to be set higher with MissionStateAction, but not lower.
State = Math.Max(State, scanTargets.Count(kvp => kvp.Value));
}
protected override bool DetermineCompleted() => State > 0;
private bool AllTargetsScanned() => State >= targetsToScan;
protected override bool DetermineCompleted() => AllTargetsScanned();
protected override void EndMissionSpecific(bool completed)
{
foreach (var scanner in scanners)
{
if (scanner.Item != null && !scanner.Item.Removed)
if (scanner.Item is { Removed: false })
{
scanner.OnScanStarted -= OnScanStarted;
scanner.OnScanCompleted -= OnScanCompleted;
@@ -259,7 +246,7 @@ namespace Barotrauma
}
}
Reset();
failed = !completed && state > 0;
failed = !completed;
}
}
}