v0.14.6.0

This commit is contained in:
Joonas Rikkonen
2021-06-17 17:54:52 +03:00
parent 3f324b14e8
commit c27e2ea5ab
348 changed files with 13156 additions and 4266 deletions
@@ -1,4 +1,5 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,6 +16,10 @@ namespace Barotrauma
protected readonly HashSet<Character> requireKill = new HashSet<Character>();
protected readonly HashSet<Character> requireRescue = new HashSet<Character>();
private readonly string itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
protected const int HostagesKilledState = 5;
private readonly string hostagesKilledMessage;
@@ -33,15 +38,55 @@ namespace Barotrauma
}
}
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return Targets.Select(t => t.WorldPosition);
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
protected bool wasDocked;
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
public AbandonedOutpostMission(MissionPrefab prefab, Location[] locations, Submarine sub) :
base(prefab, locations, sub)
{
characterConfig = prefab.ConfigElement.Element("Characters");
string msgTag = prefab.ConfigElement.GetAttributeString("hostageskilledmessage", "");
hostagesKilledMessage = TextManager.Get(msgTag, returnNull: true) ?? msgTag;
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
}
protected override void StartMissionSpecific(Level level)
@@ -52,8 +97,13 @@ namespace Barotrauma
characterItems.Clear();
requireKill.Clear();
requireRescue.Clear();
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
InitItems(submarine);
if (!IsClient)
{
InitCharacters(submarine);
@@ -62,56 +112,101 @@ namespace Barotrauma
wasDocked = Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost);
}
private void InitItems(Submarine submarine)
{
if (!string.IsNullOrEmpty(itemTag))
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
string itemIdentifier = element.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null && wp.CurrentHull.Rect.Width > 100)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X + 50, wp.CurrentHull.WorldRect.Right - 50),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
}
private void InitCharacters(Submarine submarine)
{
characters.Clear();
characterItems.Clear();
if (characterConfig == null) { return; }
foreach (XElement element in characterConfig.Elements())
{
if (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
if (characterConfig != null)
{
foreach (XElement element in characterConfig.Elements())
{
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 (GameMain.NetworkMember == null && element.GetAttributeBool("multiplayeronly", false)) { continue; }
if (element.Attribute("identifier") != null && element.Attribute("from") != null)
{
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
int defaultCount = element.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + characterIdentifier + "\" not found");
continue;
defaultCount = element.GetAttributeInt("amount", 1);
}
for (int i = 0; i < count; i++)
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)
{
LoadHuman(humanPrefab, element, submarine);
HumanPrefab humanPrefab = GetHumanPrefabFromElement(element);
for (int i = 0; i < count; i++)
{
LoadHuman(humanPrefab, element, submarine);
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
else
{
string speciesName = element.GetAttributeString("character", element.GetAttributeString("identifier", ""));
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn a character for abandoned outpost mission: character prefab \"" + speciesName + "\" not found");
continue;
}
for (int i = 0; i < count; i++)
{
LoadMonster(characterPrefab, element, submarine);
}
}
}
}
}
private void LoadHuman(HumanPrefab humanPrefab, XElement element, Submarine submarine)
@@ -128,32 +223,27 @@ namespace Barotrauma
spawnPos = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
var characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobPrefab: humanPrefab.GetJobPrefab(Rand.RandSync.Server), randSync: Rand.RandSync.Server);
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, spawnPos.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
if (element.GetAttributeBool("requirerescue", false))
{
requireRescue.Add(spawnedCharacter);
spawnedCharacter.TeamID = CharacterTeamType.FriendlyNPC;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
else
{
spawnedCharacter.TeamID = CharacterTeamType.None;
}
humanPrefab.InitializeCharacter(spawnedCharacter, spawnPos);
humanPrefab.GiveItems(spawnedCharacter, Submarine.MainSub, Rand.RandSync.Server, createNetworkEvents: false);
bool requiresRescue = element.GetAttributeBool("requirerescue", false);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, requiresRescue ? CharacterTeamType.FriendlyNPC : CharacterTeamType.None, spawnPos, giveTags: true);
if (spawnPos is WayPoint wp)
{
spawnedCharacter.GiveIdCardTags(wp);
}
if (requiresRescue)
{
requireRescue.Add(spawnedCharacter);
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
#endif
}
if (element.GetAttributeBool("requirekill", false))
{
requireKill.Add(spawnedCharacter);
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
}
private void LoadMonster(CharacterPrefab monsterPrefab, XElement element, Submarine submarine)
@@ -187,7 +277,7 @@ namespace Barotrauma
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (State != HostagesKilledState)
{
@@ -215,7 +305,8 @@ namespace Barotrauma
{
case 0:
if (requireKill.All(c => c.Removed || c.IsDead) &&
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
@@ -13,7 +13,7 @@ namespace Barotrauma
private Point monsterCountRange;
private readonly string sonarLabel;
public BeaconMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
public BeaconMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
swarmSpawned = false;
@@ -53,7 +53,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
if (!swarmSpawned && level.CheckBeaconActive())
@@ -15,17 +15,112 @@ namespace Barotrauma
private readonly Dictionary<Item, UInt16> parentInventoryIDs = new Dictionary<Item, UInt16>();
private readonly Dictionary<Item, byte> parentItemContainerIndices = new Dictionary<Item, byte>();
private int requiredDeliveryAmount;
private float requiredDeliveryAmount;
public CargoMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
private readonly List<(XElement element, ItemContainer container)> itemsToSpawn = new List<(XElement element, ItemContainer container)>();
private int? rewardPerCrate;
private int calculatedReward;
private int maxItemCount;
private Submarine sub;
public override string Description
{
get
{
if (Submarine.MainSub != sub)
{
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(Submarine.MainSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
return description;
}
}
public CargoMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
this.sub = sub;
itemConfig = prefab.ConfigElement.Element("Items");
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
requiredDeliveryAmount = Math.Min(prefab.ConfigElement.GetAttributeFloat("requireddeliveryamount", 0.98f), 1.0f);
DetermineCargo();
}
private void DetermineCargo()
{
if (this.sub == null || itemConfig == null)
{
calculatedReward = Prefab.Reward;
return;
}
itemsToSpawn.Clear();
List<(ItemContainer container, int freeSlots)> containers = sub.GetCargoContainers();
containers.Sort((c1, c2) => { return c2.container.Capacity.CompareTo(c1.container.Capacity); });
maxItemCount = 0;
foreach (XElement subElement in itemConfig.Elements())
{
int maxCount = subElement.GetAttributeInt("maxcount", 10);
maxItemCount += maxCount;
}
for (int i = 0; i < containers.Count; i++)
{
foreach (XElement subElement in itemConfig.Elements())
{
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))
{
containers[i] = (containers[i].container, containers[i].freeSlots - 1);
itemsToSpawn.Add((subElement, containers[i].container));
if (itemsToSpawn.Count(it => it.element == subElement) >= maxCount) { break; }
}
}
}
if (!itemsToSpawn.Any())
{
itemsToSpawn.Add((itemConfig.Elements().First(), null));
}
calculatedReward = 0;
foreach (var itemToSpawn in itemsToSpawn)
{
int price = itemToSpawn.element.GetAttributeInt("reward", Prefab.Reward / itemsToSpawn.Count);
if (rewardPerCrate.HasValue)
{
if (price != rewardPerCrate.Value) { rewardPerCrate = -1; }
}
else
{
rewardPerCrate = price;
}
calculatedReward += price;
}
if (rewardPerCrate.HasValue && rewardPerCrate < 0) { rewardPerCrate = null; }
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetReward(Submarine sub)
{
if (sub != this.sub)
{
this.sub = sub;
DetermineCargo();
}
return calculatedReward;
}
private void InitItems()
{
this.sub = Submarine.MainSub;
DetermineCargo();
items.Clear();
parentInventoryIDs.Clear();
parentItemContainerIndices.Clear();
@@ -36,20 +131,15 @@ namespace Barotrauma
return;
}
foreach (XElement subElement in itemConfig.Elements())
foreach (var (element, container) in itemsToSpawn)
{
LoadItemAsChild(subElement, null);
LoadItemAsChild(element, container?.Item);
}
if (requiredDeliveryAmount == 0) { requiredDeliveryAmount = items.Count; }
if (requiredDeliveryAmount > items.Count)
{
DebugConsole.AddWarning($"Error in mission \"{Prefab.Identifier}\". Required delivery amount is {requiredDeliveryAmount} but there's only {items.Count} items to deliver.");
requiredDeliveryAmount = items.Count;
}
if (requiredDeliveryAmount <= 0.0f) { requiredDeliveryAmount = 1.0f; }
}
private void LoadItemAsChild(XElement element, Item parent)
private ItemPrefab FindItemPrefab(XElement element)
{
ItemPrefab itemPrefab;
if (element.Attribute("name") != null)
@@ -60,7 +150,6 @@ namespace Barotrauma
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
return;
}
}
else
@@ -70,15 +159,15 @@ namespace Barotrauma
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
return;
}
}
return itemPrefab;
}
if (itemPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found");
return;
}
private void LoadItemAsChild(XElement element, Item parent)
{
ItemPrefab itemPrefab = FindItemPrefab(element);
WayPoint cargoSpawnPos = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub, useSyncedRand: true);
if (cargoSpawnPos == null)
@@ -88,7 +177,6 @@ namespace Barotrauma
}
var cargoRoom = cargoSpawnPos.CurrentHull;
if (cargoRoom == null)
{
DebugConsole.ThrowError("A waypoint marked as Cargo must be placed inside a room!");
@@ -140,7 +228,7 @@ namespace Barotrauma
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
int deliveredItemCount = items.Count(i => i.CurrentHull != null && !i.Removed && i.Condition > 0.0f);
if (deliveredItemCount >= requiredDeliveryAmount)
if (deliveredItemCount / (float)items.Count >= requiredDeliveryAmount)
{
GiveReward();
completed = true;
@@ -1,5 +1,4 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using System.Collections.Generic;
namespace Barotrauma
@@ -7,6 +6,7 @@ namespace Barotrauma
partial class CombatMission : Mission
{
private Submarine[] subs;
// TODO: not used
private List<Character>[] crews;
private readonly string[] descriptions;
@@ -45,8 +45,8 @@ namespace Barotrauma
}
}
public CombatMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public CombatMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
descriptions = new string[]
{
@@ -0,0 +1,344 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
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 XElement characterConfig;
private readonly XElement itemConfig;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
private readonly int baseEscortedCharacters;
private readonly float scalingEscortedCharacters;
private readonly float terroristChance;
private int calculatedReward;
private Submarine missionSub;
private Character vipCharacter;
private readonly List<Character> terroristCharacters = new List<Character>();
private bool terroristsShouldAct = false;
private float terroristDistanceSquared;
private const string TerroristTeamChangeIdentifier = "terrorist";
public EscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
missionSub = sub;
characterConfig = prefab.ConfigElement.Element("Characters");
baseEscortedCharacters = prefab.ConfigElement.GetAttributeInt("baseescortedcharacters", 1);
scalingEscortedCharacters = prefab.ConfigElement.GetAttributeFloat("scalingescortedcharacters", 0);
terroristChance = prefab.ConfigElement.GetAttributeFloat("terroristchance", 0);
itemConfig = prefab.ConfigElement.Element("TerroristItems");
CalculateReward();
}
private void CalculateReward()
{
if (missionSub == null)
{
calculatedReward = Prefab.Reward;
return;
}
int multiplier = CalculateScalingEscortedCharacterCount();
calculatedReward = Prefab.Reward * multiplier;
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", GetReward(missionSub))}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
}
public override int GetReward(Submarine sub)
{
if (sub != missionSub)
{
missionSub = sub;
CalculateReward();
}
return calculatedReward;
}
int CalculateScalingEscortedCharacterCount(bool inMission = false)
{
if (missionSub == null || missionSub.Info == null) // UI logic failing to get the correct value is not important, but the mission logic must succeed
{
if (inMission)
{
DebugConsole.ThrowError("MainSub was null when trying to retrieve submarine size for determining escorted character count!");
}
return 1;
}
return (int)Math.Round(baseEscortedCharacters + scalingEscortedCharacters * (missionSub.Info.RecommendedCrewSizeMin + missionSub.Info.RecommendedCrewSizeMax) / 2);
}
private void InitEscort()
{
characters.Clear();
characterItems.Clear();
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub);
Rand.RandSync randSync = Rand.RandSync.Server;
if (terroristChance > 0f)
{
// in terrorist missions, reroll characters each retry to avoid confusion as to who the terrorists are
randSync = Rand.RandSync.Unsynced;
}
//if any of the escortees have a job defined, try to use a spawnpoint designated for that job
foreach (XElement element in characterConfig.Elements())
{
var humanPrefab = GetHumanPrefabFromElement(element);
if (humanPrefab == null || string.IsNullOrEmpty(humanPrefab.Job) || humanPrefab.Job.Equals("any", StringComparison.OrdinalIgnoreCase)) { continue; }
var jobPrefab = humanPrefab.GetJobPrefab();
if (jobPrefab != null)
{
var jobSpecificSpawnPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, Submarine.MainSub);
if (jobSpecificSpawnPos != null)
{
explicitStayInHullPos = jobSpecificSpawnPos;
break;
}
}
}
foreach (XElement element in characterConfig.Elements())
{
int count = CalculateScalingEscortedCharacterCount(inMission: true);
for (int i = 0; i < count; i++)
{
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(element), characters, characterItems, Submarine.MainSub, CharacterTeamType.FriendlyNPC, explicitStayInHullPos, humanPrefabRandSync: randSync);
if (spawnedCharacter.AIController is HumanAIController humanAI)
{
humanAI.InitMentalStateManager();
}
}
}
if (terroristChance > 0f)
{
int terroristCount = (int)Math.Ceiling(terroristChance * Rand.Range(0.8f, 1.2f) * characters.Count);
terroristCount = Math.Clamp(terroristCount, 1, characters.Count);
terroristCharacters.Clear();
characters.GetRange(0, terroristCount).ForEach(c => terroristCharacters.Add(c));
terroristDistanceSquared = Vector2.DistanceSquared(Level.Loaded.StartPosition, Level.Loaded.EndPosition) * Rand.Range(0.35f, 0.65f);
#if DEBUG
DebugConsole.AddWarning("Terrorists will trigger at range " + Math.Sqrt(terroristDistanceSquared));
foreach (Character character in terroristCharacters)
{
DebugConsole.AddWarning(character.Name + " is a terrorist.");
}
#endif
}
}
private void InitCharacters()
{
int scalingCharacterCount = CalculateScalingEscortedCharacterCount(inMission: true);
if (scalingCharacterCount * characterConfig.Elements().Count() != characters.Count)
{
DebugConsole.AddWarning("Character count did not match expected character count in InitCharacters of EscortMission");
return;
}
int i = 0;
foreach (XElement 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
characters[k + i].IsEscorted = true;
if (escortIdentifier != string.Empty)
{
if (escortIdentifier == "vip")
{
vipCharacter = characters[k + i];
}
}
characters[k + i].UniqueNameColor = element.GetAttributeColor("color", Color.LightGreen);
}
i++;
}
}
protected override void StartMissionSpecific(Level level)
{
if (characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({characters.Count})");
#else
DebugConsole.AddWarning("Character list was not empty at the start of a escort mission. The mission instance may not have been ended correctly on previous rounds.");
characters.Clear();
#endif
}
if (characterConfig == null)
{
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
return;
}
// to ensure single missions run without issues, default to mainsub
if (missionSub == null)
{
missionSub = Submarine.MainSub;
CalculateReward();
}
if (!IsClient)
{
InitEscort();
InitCharacters();
}
}
void TryToTriggerTerrorists()
{
if (terroristsShouldAct)
{
// decoupled from range check to prevent from weirdness if players handcuff a terrorist and move backwards
foreach (Character character in terroristCharacters)
{
if (character.HasTeamChange(TerroristTeamChangeIdentifier))
{
// already triggered
continue;
}
if (IsAlive(character) && !character.IsIncapacitated && !character.LockHands)
{
character.TryAddNewTeamChange(TerroristTeamChangeIdentifier, new ActiveTeamChange(CharacterTeamType.None, ActiveTeamChange.TeamChangePriorities.Willful, aggressiveBehavior: true));
character.Speak(TextManager.Get("dialogterroristannounce"), null, Rand.Range(0.5f, 3f));
XElement randomElement = itemConfig.Elements().GetRandom(e => e.GetAttributeFloat(0f, "mindifficulty") <= Level.Loaded.Difficulty);
if (randomElement != null)
{
HumanPrefab.InitializeItem(character, randomElement, character.Submarine, humanPrefab: null, createNetworkEvents: true);
}
}
}
}
else if (Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, Level.Loaded.EndPosition) < terroristDistanceSquared)
{
foreach (Character character in terroristCharacters)
{
if (character.AIController is HumanAIController humanAI)
{
humanAI.ObjectiveManager.AddObjective(new AIObjectiveEscapeHandcuffs(character, humanAI.ObjectiveManager, shouldSwitchTeams: false, beginInstantly: true));
}
}
terroristsShouldAct = true;
}
}
bool NonTerroristsStillAlive(IEnumerable<Character> characterList)
{
return characterList.All(c => terroristCharacters.Contains(c) || IsAlive(c));
}
protected override void UpdateMissionSpecific(float deltaTime)
{
if (!IsClient)
{
int newState = State;
TryToTriggerTerrorists();
switch (State)
{
case 0: // base
if (!NonTerroristsStillAlive(characters))
{
newState = 1;
}
if (terroristCharacters.Any() && terroristCharacters.All(c => !IsAlive(c)))
{
newState = 2;
}
break;
case 1: // failure
break;
case 2: // terrorists killed
if (!NonTerroristsStillAlive(characters))
{
newState = 1;
}
break;
}
State = newState;
}
}
private bool Survived(Character character)
{
return IsAlive(character) && character.CurrentHull != null && character.CurrentHull.Submarine == Submarine.MainSub;
}
private bool IsAlive(Character character)
{
return character != null && !character.Removed && !character.IsDead;
}
private bool IsCaptured(Character character)
{
return character.LockHands && character.HasTeamChange(TerroristTeamChangeIdentifier);
}
public override void End()
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
bool terroristsSurvived = terroristCharacters.Any(c => Survived(c) && !IsCaptured(c));
bool friendliesSurvived = characters.Except(terroristCharacters).All(c => Survived(c));
bool vipDied = false;
// this logic is currently irrelevant, as the mission is failed regardless of who dies
if (vipCharacter != null)
{
vipDied = !Survived(vipCharacter);
}
if (friendliesSurvived && !terroristsSurvived && !vipDied)
{
GiveReward();
completed = true;
}
}
// characters that survived will take their items with them, in case players tried to be crafty and steal them
// this needs to run here in case players abort the mission by going back home
// TODO: I think this might feel like a bug.
foreach (var characterItem in characterItems)
{
if (Survived(characterItem.Key) || !completed)
{
foreach (Item item in characterItem.Value)
{
if (!item.Removed)
{
item.Remove();
}
}
}
}
characters.Clear();
characterItems.Clear();
failed = !completed;
}
}
}
@@ -0,0 +1,28 @@
using Barotrauma.Networking;
namespace Barotrauma
{
partial class GoToMission : Mission
{
public GoToMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
}
protected override void UpdateMissionSpecific(float deltaTime)
{
State = 1;
}
#if CLIENT
public override void ClientReadInitial(IReadMessage msg)
{
}
#elif SERVER
public override void ServerWriteInitial(IWriteMessage msg, Client c)
{
}
#endif
}
}
@@ -26,7 +26,7 @@ namespace Barotrauma
}
}
public MineralMission(MissionPrefab prefab, Location[] locations) : base(prefab, locations)
public MineralMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
var configElement = prefab.ConfigElement.Element("Items");
foreach (var c in configElement.GetChildElements("Item"))
@@ -115,7 +115,7 @@ namespace Barotrauma
FindRelevantLevelResources();
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
switch (State)
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -22,6 +23,7 @@ namespace Barotrauma
if (state != value)
{
state = value;
TryTriggerEvents(state);
#if SERVER
GameMain.Server?.UpdateMissionState(this, state);
#endif
@@ -61,14 +63,19 @@ namespace Barotrauma
//private set { description = value; }
}
protected string descriptionWithoutReward;
public virtual bool AllowUndocking
{
get { return true; }
}
public int Reward
public virtual int Reward
{
get { return Prefab.Reward; }
get
{
return Prefab.Reward;
}
}
public Dictionary<string, float> ReputationRewards
@@ -92,6 +99,16 @@ namespace Barotrauma
get { return true; }
}
public virtual int TeamCount
{
get { return 1; }
}
public virtual SubmarineInfo EnemySubmarineInfo
{
get { return null; }
}
public virtual IEnumerable<Vector2> SonarPositions
{
get { return Enumerable.Empty<Vector2>(); }
@@ -112,8 +129,22 @@ namespace Barotrauma
{
get { return Prefab.Difficulty; }
}
private class DelayedTriggerEvent
{
public readonly MissionPrefab.TriggerEvent TriggerEvent;
public float Delay;
public DelayedTriggerEvent(MissionPrefab.TriggerEvent triggerEvent, float delay)
{
TriggerEvent = triggerEvent;
Delay = delay;
}
}
private List<DelayedTriggerEvent> delayedTriggerEvents = new List<DelayedTriggerEvent>();
public Mission(MissionPrefab prefab, Location[] locations)
public Mission(MissionPrefab prefab, Location[] locations, Submarine sub)
{
System.Diagnostics.Debug.Assert(locations.Length == 2);
@@ -138,8 +169,12 @@ namespace Barotrauma
Messages[m] = Messages[m].Replace("[location" + (n + 1) + "]", locationName);
}
}
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", Reward)}‖end‖";
if (description != null) { description = description.Replace("[reward]", rewardText); }
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
if (description != null)
{
descriptionWithoutReward = description;
description = description.Replace("[reward]", rewardText);
}
if (successMessage != null) { successMessage = successMessage.Replace("[reward]", rewardText); }
if (failureMessage != null) { failureMessage = failureMessage.Replace("[reward]", rewardText); }
for (int m = 0; m < Messages.Count; m++)
@@ -147,6 +182,9 @@ namespace Barotrauma
Messages[m] = Messages[m].Replace("[reward]", rewardText);
}
}
public virtual void SetDifficulty(float difficulty) { }
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
{
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
@@ -181,7 +219,7 @@ namespace Barotrauma
{
if (randomNumber <= missionPrefab.Commonness)
{
return missionPrefab.Instantiate(locations);
return missionPrefab.Instantiate(locations, Submarine.MainSub);
}
randomNumber -= missionPrefab.Commonness;
}
@@ -189,11 +227,18 @@ namespace Barotrauma
return null;
}
public virtual int GetReward(Submarine sub)
{
return Prefab.Reward;
}
public void Start(Level level)
{
state = 0;
#if CLIENT
shownMessages.Clear();
#endif
delayedTriggerEvents.Clear();
foreach (string categoryToShow in Prefab.UnhideEntitySubCategories)
{
foreach (MapEntity entityToShow in MapEntity.mapEntityList.Where(me => me.prefab?.HasSubCategory(categoryToShow) ?? false))
@@ -202,12 +247,27 @@ namespace Barotrauma
}
}
this.level = level;
TryTriggerEvents(0);
StartMissionSpecific(level);
}
protected virtual void StartMissionSpecific(Level level) { }
public virtual void Update(float deltaTime) { }
public void Update(float deltaTime)
{
for (int i = delayedTriggerEvents.Count - 1; i>=0;i--)
{
delayedTriggerEvents[i].Delay -= deltaTime;
if (delayedTriggerEvents[i].Delay <= 0.0f)
{
TriggerEvent(delayedTriggerEvents[i].TriggerEvent);
delayedTriggerEvents.RemoveAt(i);
}
}
UpdateMissionSpecific(deltaTime);
}
protected virtual void UpdateMissionSpecific(float deltaTime) { }
protected void ShowMessage(int missionState)
{
@@ -216,6 +276,57 @@ namespace Barotrauma
partial void ShowMessageProjSpecific(int missionState);
private void TryTriggerEvents(int state)
{
foreach (var triggerEvent in Prefab.TriggerEvents)
{
if (triggerEvent.State == state)
{
TryTriggerEvent(triggerEvent);
}
}
}
/// <summary>
/// Triggers the event or adds it to the delayedTriggerEvents it if it has a delay
/// </summary>
private void TryTriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
if (trigger.Delay > 0)
{
if (!delayedTriggerEvents.Any(t => t.TriggerEvent == trigger))
{
delayedTriggerEvents.Add(new DelayedTriggerEvent(trigger, trigger.Delay));
}
}
else
{
TriggerEvent(trigger);
}
}
/// <summary>
/// Triggers the event immediately, ignoring any delays
/// </summary>
private void TriggerEvent(MissionPrefab.TriggerEvent trigger)
{
if (trigger.CampaignOnly && GameMain.GameSession?.Campaign == null) { return; }
var eventPrefab = EventSet.GetAllEventPrefabs().Find(p => p.Identifier.Equals(trigger.EventIdentifier, StringComparison.OrdinalIgnoreCase));
if (eventPrefab == null)
{
DebugConsole.ThrowError($"Mission \"{Name}\" failed to trigger an event (couldn't find an event with the identifier \"{trigger.EventIdentifier}\").");
return;
}
if (GameMain.GameSession?.EventManager != null)
{
var newEvent = eventPrefab.CreateInstance();
GameMain.GameSession.EventManager.ActiveEvents.Add(newEvent);
newEvent.Init(true);
}
}
/// <summary>
/// End the mission and give a reward if it was completed successfully
/// </summary>
@@ -232,7 +343,7 @@ namespace Barotrauma
public void GiveReward()
{
if (!(GameMain.GameSession.GameMode is CampaignMode campaign)) { return; }
campaign.Money += Reward;
campaign.Money += GetReward(Submarine.MainSub);
foreach (KeyValuePair<string, float> reputationReward in ReputationRewards)
{
@@ -287,5 +398,47 @@ namespace Barotrauma
}
public virtual void AdjustLevelData(LevelData levelData) { }
// putting these here since both escort and pirate missions need them. could be tucked away into another class that they can inherit from (or use composition)
protected HumanPrefab GetHumanPrefabFromElement(XElement element)
{
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + Name + "\" - use character identifiers instead of names to configure the characters.");
return null;
}
string characterIdentifier = element.GetAttributeString("identifier", "");
string characterFrom = element.GetAttributeString("from", "");
HumanPrefab humanPrefab = NPCSet.Get(characterFrom, characterIdentifier);
if (humanPrefab == null)
{
DebugConsole.ThrowError("Couldn't spawn character for mission: character prefab \"" + characterIdentifier + "\" not found");
return null;
}
return humanPrefab;
}
protected Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, Rand.RandSync humanPrefabRandSync = Rand.RandSync.Server, bool giveTags = true)
{
if (positionToStayIn == null)
{
positionToStayIn = WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
var characterInfo = humanPrefab.GetCharacterInfo(Rand.RandSync.Server) ?? new CharacterInfo(CharacterPrefab.HumanSpeciesName, npcIdentifier: humanPrefab.Identifier, jobPrefab: humanPrefab.GetJobPrefab(humanPrefabRandSync), randSync: humanPrefabRandSync);
characterInfo.TeamID = teamType;
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.Prefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
humanPrefab.GiveItems(spawnedCharacter, submarine, Rand.RandSync.Server, createNetworkEvents: false);
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
}
}
@@ -18,10 +18,11 @@ namespace Barotrauma
Nest = 0x10,
Mineral = 0x20,
Combat = 0x40,
OutpostDestroy = 0x80,
OutpostRescue = 0x100,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | OutpostDestroy | OutpostRescue
AbandonedOutpost = 0x80,
Escort = 0x100,
Pirate = 0x200,
GoTo = 0x400,
All = Salvage | Monster | Cargo | Beacon | Nest | Mineral | Combat | AbandonedOutpost | Escort | Pirate | GoTo
}
partial class MissionPrefab
@@ -36,13 +37,17 @@ namespace Barotrauma
{ MissionType.Beacon, typeof(BeaconMission) },
{ MissionType.Nest, typeof(NestMission) },
{ MissionType.Mineral, typeof(MineralMission) },
{ MissionType.OutpostDestroy, typeof(OutpostDestroyMission) },
{ MissionType.OutpostRescue, typeof(AbandonedOutpostMission) },
{ MissionType.AbandonedOutpost, typeof(AbandonedOutpostMission) },
{ MissionType.Escort, typeof(EscortMission) },
{ MissionType.Pirate, typeof(PirateMission) },
{ MissionType.GoTo, typeof(GoToMission) }
};
public static readonly Dictionary<MissionType, Type> PvPMissionClasses = new Dictionary<MissionType, Type>()
{
{ MissionType.Combat, typeof(CombatMission) }
};
public static readonly HashSet<MissionType> HiddenMissionClasses = new HashSet<MissionType>() { MissionType.GoTo };
private readonly ConstructorInfo constructor;
@@ -84,6 +89,8 @@ namespace Barotrauma
public readonly bool IsSideObjective;
public readonly bool RequireWreck;
/// <summary>
/// The mission can only be received when travelling from Pair.First to Pair.Second
/// </summary>
@@ -99,6 +106,28 @@ namespace Barotrauma
/// </summary>
public readonly List<string> UnhideEntitySubCategories = new List<string>();
public class TriggerEvent
{
[Serialize("", true)]
public string EventIdentifier { get; private set; }
[Serialize(0, true)]
public int State { get; private set; }
[Serialize(0.0f, true)]
public float Delay { get; private set; }
[Serialize(false, true)]
public bool CampaignOnly { get; private set; }
public TriggerEvent(XElement element)
{
SerializableProperty.DeserializeProperties(this, element);
}
}
public readonly List<TriggerEvent> TriggerEvents = new List<TriggerEvent>();
public LocationTypeChange LocationTypeChangeOnCompleted;
public readonly XElement ConfigElement;
@@ -157,6 +186,7 @@ namespace Barotrauma
Reward = element.GetAttributeInt("reward", 1);
AllowRetry = element.GetAttributeBool("allowretry", false);
IsSideObjective = element.GetAttributeBool("sideobjective", false);
RequireWreck = element.GetAttributeBool("requirewreck", false);
Commonness = element.GetAttributeInt("commonness", 1);
if (element.GetAttribute("difficulty") != null)
{
@@ -269,10 +299,19 @@ namespace Barotrauma
DataRewards.Add(Tuple.Create(identifier, value, operation));
}
break;
case "triggerevent":
TriggerEvents.Add(new TriggerEvent(subElement));
break;
}
}
string missionTypeName = element.GetAttributeString("type", "");
//backwards compatibility
if (missionTypeName.Equals("outpostdestroy", StringComparison.OrdinalIgnoreCase) || missionTypeName.Equals("outpostrescue", StringComparison.OrdinalIgnoreCase))
{
missionTypeName = "AbandonedOutpost";
}
if (!Enum.TryParse(missionTypeName, out Type))
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
@@ -286,16 +325,20 @@ namespace Barotrauma
if (CoOpMissionClasses.ContainsKey(Type))
{
constructor = CoOpMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
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[]) });
constructor = PvPMissionClasses[Type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]), typeof(Submarine) });
}
else
{
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - unsupported mission type \"" + Type.ToString() + "\"");
}
if (constructor == null)
{
DebugConsole.ThrowError($"Failed to find a constructor for the mission type \"{Type}\"!");
}
InitProjSpecific(element);
}
@@ -333,9 +376,9 @@ namespace Barotrauma
return false;
}
public Mission Instantiate(Location[] locations)
public Mission Instantiate(Location[] locations, Submarine sub)
{
return constructor?.Invoke(new object[] { this, locations }) as Mission;
return constructor?.Invoke(new object[] { this, locations, sub }) as Mission;
}
}
}
@@ -33,8 +33,8 @@ namespace Barotrauma
}
}
public MonsterMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public MonsterMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
string speciesName = prefab.ConfigElement.GetAttributeString("monsterfile", null);
if (!string.IsNullOrEmpty(speciesName))
@@ -160,7 +160,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
switch (State)
{
@@ -46,8 +46,8 @@ namespace Barotrauma
}
}
public NestMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public NestMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
itemConfig = prefab.ConfigElement.Element("Items");
@@ -216,7 +216,7 @@ namespace Barotrauma
level.LevelObjectManager.PlaceNestObjects(level, cave, nestPosition, nestObjectRadius, nestObjectAmount);
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient)
{
@@ -1,165 +0,0 @@
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class OutpostDestroyMission : AbandonedOutpostMission
{
private readonly string itemTag;
private readonly XElement itemConfig;
private readonly List<Item> items = new List<Item>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
if (State > 0)
{
return Enumerable.Empty<Vector2>();
}
else
{
return Targets.Select(t => t.WorldPosition);
}
}
}
private IEnumerable<Entity> Targets
{
get
{
if (State > 0)
{
return Enumerable.Empty<Entity>();
}
else
{
if (items.Any())
{
return items.Where(it => !it.Removed && it.Condition > 0.0f).Cast<Entity>().Concat(requireKill.Where(c => !c.Removed && !c.IsDead)).Concat(requireRescue);
}
else
{
return requireKill.Concat(requireRescue);
}
}
}
}
public OutpostDestroyMission(MissionPrefab prefab, Location[] locations) :
base(prefab, locations)
{
itemConfig = prefab.ConfigElement.Element("Items");
itemTag = prefab.ConfigElement.GetAttributeString("targetitem", "");
}
protected override void StartMissionSpecific(Level level)
{
items.Clear();
#if SERVER
spawnedItems.Clear();
#endif
if (!string.IsNullOrEmpty(itemTag))
{
var itemsToDestroy = Item.ItemList.FindAll(it => it.Submarine?.Info.Type != SubmarineType.Player && it.HasTag(itemTag));
if (!itemsToDestroy.Any())
{
DebugConsole.ThrowError($"Error in mission \"{Prefab.Identifier}\". Could not find an item with the tag \"{itemTag}\".");
}
else
{
items.AddRange(itemsToDestroy);
}
}
if (itemConfig != null && !IsClient)
{
foreach (XElement element in itemConfig.Elements())
{
string itemIdentifier = element.GetAttributeString("identifier", "");
if (!(MapEntityPrefab.Find(null, itemIdentifier) is ItemPrefab itemPrefab))
{
DebugConsole.ThrowError("Couldn't spawn item for outpost destroy mission: item prefab \"" + itemIdentifier + "\" not found");
continue;
}
string[] moduleFlags = element.GetAttributeStringArray("moduleflags", null);
string[] spawnPointTags = element.GetAttributeStringArray("spawnpointtags", null);
ISpatialEntity spawnPoint = SpawnAction.GetSpawnPos(
SpawnAction.SpawnLocationType.Outpost, SpawnType.Human | SpawnType.Enemy,
moduleFlags, spawnPointTags, element.GetAttributeBool("asfaraspossible", false));
if (spawnPoint == null)
{
var submarine = Submarine.Loaded.Find(s => s.Info.Type == SubmarineType.Outpost) ?? Submarine.MainSub;
spawnPoint = submarine.GetHulls(alsoFromConnectedSubs: false).GetRandom();
}
Vector2 spawnPos = spawnPoint.WorldPosition;
if (spawnPoint is WayPoint wp && wp.CurrentHull != null)
{
spawnPos = new Vector2(
MathHelper.Clamp(wp.WorldPosition.X + Rand.Range(-200, 200), wp.CurrentHull.WorldRect.X, wp.CurrentHull.WorldRect.Right),
wp.CurrentHull.WorldRect.Y - wp.CurrentHull.Rect.Height + 16.0f);
}
var item = new Item(itemPrefab, spawnPos, null);
items.Add(item);
#if SERVER
spawnedItems.Add(item);
#endif
}
}
base.StartMissionSpecific(level);
}
public override void Update(float deltaTime)
{
if (requireRescue.Any(r => r.Removed || r.IsDead))
{
#if SERVER
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
GameMain.Server.EndGame();
}
#endif
return;
}
switch (state)
{
case 0:
if (items.Any())
{
if (items.All(it => it.Removed || it.Condition <= 0.0f) &&
requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
else
{
if (requireKill.All(c => c.Removed || c.IsDead) &&
requireRescue.All(c => c.Submarine?.Info.Type == SubmarineType.Player))
{
State = 1;
}
}
break;
#if SERVER
case 1:
if (!(GameMain.GameSession.GameMode is CampaignMode) && GameMain.Server != null)
{
if (!Submarine.MainSub.AtStartExit || (wasDocked && !Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost)))
{
GameMain.Server.EndGame();
State = 2;
}
}
break;
#endif
}
}
}
}
@@ -0,0 +1,395 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class PirateMission : Mission
{
private readonly XElement submarineTypeConfig;
private readonly XElement characterConfig;
private readonly XElement characterTypeConfig;
private readonly float addedMissionDifficultyPerPlayer;
private float missionDifficulty;
private int alternateReward;
private Submarine enemySub;
private readonly List<Character> characters = new List<Character>();
private readonly Dictionary<Character, List<Item>> characterItems = new Dictionary<Character, List<Item>>();
// 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;
private float pirateSightingUpdateTimer;
private Vector2? lastSighting;
public override int TeamCount => 2;
private bool outsideOfSonarRange;
private readonly List<Vector2> patrolPositions = new List<Vector2>();
public override IEnumerable<Vector2> SonarPositions
{
get
{
var empty = Enumerable.Empty<Vector2>();
if (outsideOfSonarRange)
{
return State switch
{
0 => patrolPositions,
1 => lastSighting.HasValue ? lastSighting.Value.ToEnumerable() : empty,
_ => empty,
};
}
else
{
return empty;
}
}
}
public override int GetReward(Submarine sub)
{
return alternateReward;
}
private SubmarineInfo submarineInfo;
public override SubmarineInfo EnemySubmarineInfo
{
get
{
return submarineInfo;
}
}
// these values could also be defined within the mission XML
private const float RandomnessModifier = 25;
private const float ShipRandomnessModifier = 15;
private const float MaxDifficulty = 100;
public PirateMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
submarineTypeConfig = prefab.ConfigElement.Element("SubmarineTypes");
characterConfig = prefab.ConfigElement.Element("Characters");
characterTypeConfig = prefab.ConfigElement.Element("CharacterTypes");
addedMissionDifficultyPerPlayer = prefab.ConfigElement.GetAttributeFloat("addedmissiondifficultyperplayer", 0);
// for campaign missions, set difficulty at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
SetDifficulty(levelData?.Difficulty ?? Level.Loaded?.Difficulty ?? 0f);
}
public override void SetDifficulty(float difficulty)
{
if (missionDifficulty > 0f)
{
// difficulty already set
return;
}
missionDifficulty = difficulty;
XElement submarineConfig = GetRandomDifficultyModifiedElement(submarineTypeConfig, missionDifficulty, ShipRandomnessModifier);
alternateReward = submarineConfig.GetAttributeInt("alternatereward", Reward);
string rewardText = $"‖color:gui.orange‖{string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:N0}", alternateReward)}‖end‖";
if (descriptionWithoutReward != null) { description = descriptionWithoutReward.Replace("[reward]", rewardText); }
string submarinePath = submarineConfig.GetAttributeString("path", string.Empty);
if (submarinePath == string.Empty)
{
DebugConsole.ThrowError($"No path used for submarine for the pirate mission \"{Prefab.Identifier}\"!");
return;
}
// maybe a little redundant
var contentFile = ContentPackage.GetFilesOfType(GameMain.Config.AllEnabledPackages, ContentType.EnemySubmarine).FirstOrDefault(x => x.Path == submarinePath);
if (contentFile == null)
{
DebugConsole.ThrowError($"No submarine file found from the path {submarinePath}!");
return;
}
submarineInfo = new SubmarineInfo(contentFile.Path);
}
private float GetDifficultyModifiedValue(float preferredDifficulty, float levelDifficulty, float randomnessModifier)
{
return Math.Abs(levelDifficulty - preferredDifficulty + (Rand.Range(-randomnessModifier, randomnessModifier, Rand.RandSync.Server)));
}
private int GetDifficultyModifiedAmount(int minAmount, int maxAmount, float levelDifficulty)
{
return Math.Max((int)Math.Round(minAmount + (maxAmount - minAmount) * ((levelDifficulty + Rand.Range(-RandomnessModifier, RandomnessModifier, Rand.RandSync.Server)) / MaxDifficulty)), minAmount);
}
private XElement GetRandomDifficultyModifiedElement(XElement parentElement, float levelDifficulty, float randomnessModifier)
{
// look for the element that is closest to our difficulty, with some randomness
XElement bestElement = null;
float bestValue = float.MaxValue;
foreach (XElement element in parentElement.Elements())
{
float applicabilityValue = GetDifficultyModifiedValue(element.GetAttributeFloat(0f, "preferreddifficulty"), levelDifficulty, randomnessModifier);
if (applicabilityValue < bestValue)
{
bestElement = element;
bestValue = applicabilityValue;
}
}
return bestElement;
}
private void CreateMissionPositions(out Vector2 preferredSpawnPos)
{
Vector2 patrolPos = enemySub.WorldPosition;
Point subSize = enemySub.GetDockedBorders().Size;
if (!Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out preferredSpawnPos))
{
DebugConsole.ThrowError("Could not spawn pirate submarine in an interesting location! " + this);
}
if (!Level.Loaded.TryGetInterestingPositionAwayFromPoint(true, Level.PositionType.MainPath | Level.PositionType.SidePath, Level.Loaded.Size.X * 0.3f, out patrolPos, preferredSpawnPos, minDistFromPoint: 10000f))
{
DebugConsole.ThrowError("Could not give pirate submarine an interesting location to patrol to! " + this);
}
patrolPos = enemySub.FindSpawnPos(patrolPos, subSize);
patrolPositions.Add(patrolPos);
patrolPositions.Add(preferredSpawnPos);
if (!IsClient)
{
PathFinder pathFinder = new PathFinder(WayPoint.WayPointList, false);
var path = pathFinder.FindPath(ConvertUnits.ToSimUnits(patrolPos), ConvertUnits.ToSimUnits(preferredSpawnPos));
if (!path.Unreachable)
{
preferredSpawnPos = path.Nodes[Rand.Range(0, path.Nodes.Count - 1)].WorldPosition; // spawn the sub in a random point in the path if possible
}
int graceDistance = 500; // the sub still spawns awkwardly close to walls, so this helps. could also be given as a parameter instead
preferredSpawnPos = enemySub.FindSpawnPos(preferredSpawnPos, new Point(subSize.X + graceDistance, subSize.Y + graceDistance));
}
}
private void InitPirateShip(Vector2 spawnPos)
{
enemySub.NeutralizeBallast();
if (enemySub.GetItems(alsoFromConnectedSubs: false).Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
reactor.PowerUpImmediately();
}
enemySub.EnableMaintainPosition();
enemySub.TeamID = CharacterTeamType.None;
//make the enemy sub withstand atleast the same depth as the player sub
enemySub.RealWorldCrushDepth = Math.Max(enemySub.RealWorldCrushDepth, Submarine.MainSub.RealWorldCrushDepth);
}
private void InitPirates()
{
characters.Clear();
characterItems.Clear();
if (characterConfig == null)
{
DebugConsole.ThrowError("Failed to initialize characters for escort mission (characterConfig == null)");
return;
}
int playerCount = 1;
#if SERVER
playerCount = GameMain.Server.ConnectedClients.Where(c => !c.SpectateOnly || !GameMain.Server.ServerSettings.AllowSpectating).Count();
#endif
float enemyCreationDifficulty = missionDifficulty + playerCount * addedMissionDifficultyPerPlayer;
bool commanderAssigned = false;
foreach (XElement element in characterConfig.Elements())
{
// 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);
for (int i = 0; i < amountCreated; i++)
{
XElement characterType = characterTypeConfig.Elements().Where(e => e.GetAttributeString("typeidentifier", string.Empty) == element.GetAttributeString("typeidentifier", string.Empty)).FirstOrDefault();
if (characterType == null)
{
DebugConsole.ThrowError($"No character types defined in CharacterTypes for a declared type identifier in mission \"{Prefab.Identifier}\".");
return;
}
XElement variantElement = GetRandomDifficultyModifiedElement(characterType, enemyCreationDifficulty, RandomnessModifier);
Character spawnedCharacter = CreateHuman(GetHumanPrefabFromElement(variantElement), characters, characterItems, enemySub, CharacterTeamType.None, null);
if (!commanderAssigned)
{
bool isCommander = variantElement.GetAttributeBool("iscommander", false);
if (isCommander && spawnedCharacter.AIController is HumanAIController humanAIController)
{
humanAIController.InitShipCommandManager();
foreach (var patrolPos in patrolPositions)
{
humanAIController.ShipCommandManager.patrolPositions.Add(patrolPos);
}
commanderAssigned = true;
}
}
foreach (Item item in spawnedCharacter.Inventory.AllItems)
{
if (item?.Prefab.Identifier == "idcard")
{
item.AddTag("id_pirate");
}
}
}
}
}
protected override void StartMissionSpecific(Level level)
{
if (characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({characters.Count})");
#else
DebugConsole.AddWarning("Character list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
characters.Clear();
#endif
}
if (patrolPositions.Count > 0)
{
#if DEBUG
throw new Exception($"patrolPositions.Count > 0 ({patrolPositions.Count})");
#else
DebugConsole.AddWarning("Patrol point list was not empty at the start of a pirate mission. The mission instance may not have been ended correctly on previous rounds.");
patrolPositions.Clear();
#endif
}
enemySub = Submarine.MainSubs[1];
if (enemySub == null)
{
DebugConsole.ThrowError($"Enemy Submarine was not created. SubmarineInfo is likely not defined.");
// TODO: should we set the state to something here?
return;
}
Vector2 spawnPos = Level.Loaded.EndPosition; // in case TryGetInterestingPosition fails, though this should not happen
CreateMissionPositions(out spawnPos); // patrol positions are not explicitly replicated, instead they are acquired the same way the server acquires them
#if DEBUG
if (IsClient)
{
DebugConsole.NewMessage("The patrol positions set by client were: ");
}
else
{
DebugConsole.NewMessage("The patrol positions set by server were: ");
}
foreach (var patrolPos in patrolPositions)
{
DebugConsole.NewMessage("Patrol pos: " + patrolPos);
}
#endif
if (!IsClient)
{
InitPirateShip(spawnPos);
}
enemySub.SetPosition(spawnPos);
// 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
enemySub.FlipX();
enemySub.ShowSonarMarker = false;
if (!IsClient)
{
InitPirates();
}
}
protected override void UpdateMissionSpecific(float deltaTime)
{
int newState = State;
float sqrSonarRange = MathUtils.Pow2(Sonar.DefaultSonarRange);
outsideOfSonarRange = Vector2.DistanceSquared(enemySub.WorldPosition, Submarine.MainSub.WorldPosition) > sqrSonarRange;
if (State < 2 && CheckWinState())
{
newState = 2;
}
else
{
switch (State)
{
case 0:
for (int i = patrolPositions.Count - 1; i >= 0; i--)
{
if (Vector2.DistanceSquared(patrolPositions[i], Submarine.MainSub.WorldPosition) < sqrSonarRange)
{
patrolPositions.RemoveAt(i);
}
}
if (!outsideOfSonarRange || patrolPositions.None())
{
newState = 1;
}
break;
case 1:
if (outsideOfSonarRange)
{
if (lastSighting.HasValue && Vector2.DistanceSquared(lastSighting.Value, Submarine.MainSub.WorldPosition) < sqrSonarRange)
{
lastSighting = null;
}
pirateSightingUpdateTimer -= deltaTime;
if (pirateSightingUpdateTimer < 0)
{
pirateSightingUpdateTimer = pirateSightingUpdateFrequency;
lastSighting = enemySub.WorldPosition;
}
}
else
{
lastSighting = enemySub.WorldPosition;
pirateSightingUpdateTimer = 0;
}
break;
}
}
State = newState;
}
private bool CheckWinState() => !IsClient && (characters.All(m => !Survived(m)));
private bool Survived(Character character)
{
return character != null && !character.Removed && !character.IsDead;
}
public override void End()
{
if (state == 2)
{
GiveReward();
completed = true;
}
characters.Clear();
characterItems.Clear();
failed = !completed;
}
}
}
@@ -43,8 +43,8 @@ namespace Barotrauma
}
}
public SalvageMission(MissionPrefab prefab, Location[] locations)
: base(prefab, locations)
public SalvageMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
containerTag = prefab.ConfigElement.GetAttributeString("containertag", "");
@@ -205,7 +205,7 @@ namespace Barotrauma
}
}
public override void Update(float deltaTime)
protected override void UpdateMissionSpecific(float deltaTime)
{
if (item == null)
{