Track LocalMods as part of monolith

This commit is contained in:
2026-06-08 18:50:16 +03:00
parent 143f2fed7c
commit 1b214b44c2
1287 changed files with 139255 additions and 1 deletions
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
namespace MoreLevelContent.Missions
{
partial class BeaconConstMission : Mission
{
private readonly LocalizedString sonarLabel;
private readonly int PriceUtility;
private readonly int PriceStructure;
private readonly int PriceElectric;
public BeaconConstMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
sonarLabel = TextManager.Get("beaconconsonarlabel");
var supplyCosts = prefab.ConfigElement.GetChildElement("supplycosts");
PriceUtility = GetPrice("supply_utility");
PriceStructure = GetPrice("supply_structural");
PriceElectric = GetPrice("supply_electrical");
string rewardText = $"‖color:gui.orange‖{string.Format(CultureInfo.InvariantCulture, "{0:N0}", GetReward(sub))}‖end‖";
description = descriptionWithoutReward.Replace("[reward]", rewardText);
int GetPrice(string supplyType) => supplyCosts.GetChildElement(supplyType).GetAttributeInt("price", 0);
}
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
public override int Reward => GetReward();
public override float GetBaseReward(Submarine sub) => GetReward();
private int GetReward()
{
LocationConnection connection = Locations[0].Connections.Find(lc => lc.Locations.Contains(Locations[1]));
var levelData = connection.LevelData.MLC();
int reward = (int)(((PriceUtility * levelData.RequestedU) +
(PriceStructure * levelData.RequestedS) +
(PriceElectric * levelData.RequestedE)) * 2.5);
return reward;
}
public override void StartMissionSpecific(Level level) => description = description.Replace("[requestedsupplies]", level.LevelData.MLC().GetRequestedSupplies());
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (level.MLC().BeaconConstructionStation == null)
{
yield break;
}
Vector2 worldPos = level.MLC().BeaconConstructionStation.WorldPosition;
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, worldPos);
}
}
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) { return; }
if (State == 0 && level.MLC().CheckSuppliesDelivered())
{
State = 1;
}
}
public override void EndMissionSpecific(bool completed)
{
if (completed && level.LevelData != null)
{
level.LevelData.IsBeaconActive = true;
level.LevelData.HasBeaconStation = true;
level.LevelData.MLC().HasBeaconConstruction = false;
}
}
public override void AdjustLevelData(LevelData levelData) => levelData.MLC().HasBeaconConstruction = true;
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => level.MLC().CheckSuppliesDelivered();
}
}
@@ -0,0 +1,338 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Resources;
using System.Xml.Linq;
namespace MoreLevelContent.Missions
{
// Shared
internal partial class CablePuzzleMission : Mission
{
/// <summary>
/// This sucks ass but we should /NEVER/ have more than one relay station in one level
/// </summary>
public static SubmarineFile SubmarineFile { get; set; }
const float INTERVAL = 2.5f;
const int RANGE_MIN = 50;
const int RANGE_MAX = 200;
const int REQUIRED_CYCLES = 2;
const float REQUIRED_CYCLE_TIME = 1;
const double CYCLE_GRACE_PERIOD = Timing.Step * 2;
private readonly LocalizedString defaultSonarLabel;
private readonly string terminalTag;
private readonly XElement _SubmarineConfig;
private Submarine _Station;
private LevelData _LevelData;
private float _Timer = INTERVAL;
public CablePuzzleMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
_SubmarineConfig = prefab.ConfigElement.GetChildElement("Submarine");
defaultSonarLabel = TextManager.Get("relaysonarlabel");
terminalTag = _SubmarineConfig.GetAttributeString("welcomemsg", "relayrepair.terminal");
SubmarineFile = null;
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (_Station == null) yield break;
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel, _Station.WorldPosition);
}
}
public override void SetLevel(LevelData level)
{
if (_LevelData != null)
{
//level already set
return;
}
_LevelData = level;
SetSub(_SubmarineConfig, Prefab);
}
public static void SetSub(XElement config, MissionPrefab prefab)
{
ContentPath subPath = config.GetAttributeContentPath("path", prefab.ContentPackage);
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the relay station mission \"{prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
SubmarineFile = file;
Log.Debug("Set relay station sub file");
}
private MemoryComponent _WpInput;
private MemoryComponent _WpTarget;
private LightComponent _WpLight;
private Terminal _WpHint;
private readonly List<SignalOperation> _Operations = new();
private readonly SignalOperation[] _Sequence = new SignalOperation[4];
private enum OperationType
{
Add,
Sub,
Mul,
Div
}
private struct SignalOperation
{
public SignalOperation(OperationType type, int value)
{
_Value = value;
_Operation = type;
}
private readonly OperationType _Operation;
private readonly int _Value;
public override string ToString() => $"{_Operation} {_Value}";
public int Run(int input)
{
switch (_Operation)
{
case OperationType.Add:
return input += _Value;
case OperationType.Sub:
return input -= _Value;
case OperationType.Mul:
return input *= _Value;
case OperationType.Div:
return input /= _Value;
}
throw new NotImplementedException();
}
}
public override void StartMissionSpecific(Level level)
{
_Station = level.MLC().RelayStation;
if (_Station == null)
{
Log.Error("Failed to spawn relay station!!");
return;
}
if (IsClient) return;
var items = _Station.GetItems(false);
var rand = MLCUtils.GetRandomFromString(_LevelData.Seed);
var memoryComps = new List<MemoryComponent>();
foreach (var item in items)
{
if (item.HasTag("wp_input")) _WpInput = item.GetComponent<MemoryComponent>();
if (item.HasTag("wp_target")) _WpTarget = item.GetComponent<MemoryComponent>();
if (item.HasTag("wp_light")) _WpLight = item.GetComponent<LightComponent>();
if (item.HasTag("wp_hint")) _WpHint = item.GetComponent<Terminal>();
if (item.HasTag("wp_add")) AddOperation(OperationType.Add);
if (item.HasTag("wp_sub")) AddOperation(OperationType.Sub);
if (item.HasTag("wp_mul")) AddOperation(OperationType.Mul);
void AddOperation(OperationType type, int low = 5, int high = 25)
{
var comp = item.GetComponent<MemoryComponent>();
if (comp == null)
{
Log.Error("Failed to find a memory component on tagged item");
return;
}
int val = rand.Next(low, high);
comp.Value = val.ToString();
memoryComps.Add(comp);
_Operations.Add(new SignalOperation(type, val));
}
}
if (_Operations.Count == 0)
{
Log.Error("Failed to collect any operations!");
return;
}
// Build random sequence
_Operations.Shuffle(rand);
_Sequence[0] = _Operations[0];
_Sequence[1] = _Operations[1];
_Sequence[2] = _Operations[2];
_Sequence[3] = _Operations[3];
int[] steps0 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
int[] steps1 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
int[] steps2 = GetStepsForInput(Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced));
_WpHint.ShowMessage = TextManager.GetWithVariables("relayrepair.terminal",
("[version]", $"${Main.Version}"),
("[station]", $"{rand.Next(100, 999)}"),
("[input0]", $"{steps0[0].ToString().PadLeft(3, '0')}"),
("[input1]", $"{steps1[0].ToString().PadLeft(3, '0')}"),
("[input2]", $"{steps2[0].ToString().PadLeft(3, '0')}"),
("[step10]", $"{steps0[1].ToString().PadLeft(3, '0')}"),
("[step20]", $"{steps0[2].ToString().PadLeft(3, '0')}"),
("[step30]", $"{steps0[3].ToString().PadLeft(3, '0')}"),
("[step11]", $"{steps1[1].ToString().PadLeft(3, '0')}"),
("[step21]", $"{steps1[2].ToString().PadLeft(3, '0')}"),
("[step31]", $"{steps1[3].ToString().PadLeft(3, '0')}"),
("[step12]", $"{steps2[1].ToString().PadLeft(3, '0')}"),
("[step22]", $"{steps2[2].ToString().PadLeft(3, '0')}"),
("[step32]", $"{steps2[3].ToString().PadLeft(3, '0')}"),
("[output0]", $"{steps0[4]}"),
("[output1]", $"{steps1[4]}"),
("[output2]", $"{steps2[4]}")
).Value;
Log.Debug("Sub created");
#if SERVER
// Have the server send these changes to the client
_WpHint.SyncHistory();
foreach (var comp in memoryComps)
{
comp.Item.CreateServerEvent(comp);
}
#endif
}
private int GetStepForInput(int input, int stepCount)
{
if (stepCount > _Sequence.Length)
{
Log.Error($"Requested sequence step ({stepCount}) is bigger then the sequence length ({_Sequence.Length})");
return 0;
}
for (int i = 0; i < stepCount; i++)
{
var operation = _Sequence[i];
input = operation.Run(input);
}
return input;
}
private int[] GetStepsForInput(int input)
{
int[] output = new int[_Sequence.Length + 1];
output[0] = input;
for (int i = 0; i < _Sequence.Length; i++)
{
input = _Sequence[i].Run(input);
output[i + 1] = input;
}
return output;
}
double successTimer = 0;
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) return;
if (_Station == null) return;
if (State == 0)
{
if (CrewInSub())
{
State = 1;
}
}
// Crew has entered the relay and is fixing it
if (State >= 1)
{
// We have the correct value
if (_WpLight.IsOn)
{
successTimer = 2f;
} else if (successTimer > 0)
{
successTimer -= deltaTime;
}
State = successTimer > 0 ? 2 : 1;
}
// Return if timer is counting
if (_Timer > 0)
{
_Timer -= deltaTime;
return;
}
var input = Rand.Range(RANGE_MIN, RANGE_MAX, Rand.RandSync.Unsynced);
_WpInput.Value = input.ToString();
_WpTarget.Value = GetStepForInput(input, 4).ToString();
#if SERVER
// Have the server send the info to the client
_WpInput.Item.CreateServerEvent(_WpInput);
_WpTarget.Item.CreateServerEvent(_WpTarget);
#endif
_Timer = INTERVAL;
bool CrewInSub()
{
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
if (crewMember.Submarine == _Station)
{
return true;
}
}
return false;
}
}
public override void EndMissionSpecific(bool completed)
{
if (completed && level.LevelData != null)
{
level.LevelData.MLC().RelayStationStatus = RelayStationStatus.Active;
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
}
}
@@ -0,0 +1,241 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.Level;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressEscortMission : DistressMission
{
private readonly XElement localCharacterConfig;
private readonly LocalizedString sonarLabel;
private readonly PositionType spawnPositionType;
private readonly bool hostile;
private readonly MissionNPCCollection missionNPCs;
private int calculatedReward;
private Submarine missionSub;
private Vector2 spawnPosition;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (State != 1)
yield return (Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel, spawnPosition);
}
}
public DistressEscortMission(MissionPrefab prefab, Location[] locations, Submarine sub)
: base(prefab, locations, sub)
{
missionSub = sub;
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
sonarLabel = TextManager.Get("missionname.distressmission");
spawnPositionType = localCharacterConfig.GetAttributeEnum("spawntype", PositionType.Cave);
hostile = localCharacterConfig.GetAttributeBool("hostile", false);
missionNPCs = new(this, localCharacterConfig);
CalculateReward();
}
private void CalculateReward()
{
if (missionSub == null)
{
calculatedReward = Prefab.Reward;
return;
}
// Disabled for now, because they make balancing the missions a pain.
int multiplier = 1;//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 float GetBaseReward(Submarine sub)
{
if (sub != missionSub)
{
missionSub = sub;
CalculateReward();
}
return calculatedReward;
}
private void InitEscort()
{
missionNPCs.Clear();
if (!Loaded.TryGetInterestingPosition(false, spawnPositionType, 0, out InterestingPosition _spawnPos))
{
Log.Error($"Failed to find a spawn position of type {spawnPositionType}! Falling back to any position.");
bool failed = Loaded.TryGetInterestingPosition(false, PositionType.MainPath | PositionType.SidePath | PositionType.Cave | PositionType.Wreck, 0, out _spawnPos);
if (failed)
{
Log.Error("Could not find ANY position to spawn distress humans at!");
spawnPosition = Vector2.Zero;
}
}
spawnPosition = _spawnPos.Position.ToVector2();
CharacterTeamType team = hostile ? CharacterTeamType.None : CharacterTeamType.FriendlyNPC;
missionNPCs.CreateHumansAtPosition(team, spawnPosition, OnCharacterCreated);
void OnCharacterCreated(Character character, XElement characterMissionConfig)
{
character.MLC().NPCElement = characterMissionConfig;
if (character.AIController is not HumanAIController humanAI) return;
// Only turn on the mental state for non-hostile divers
// so that sometimes you'll get insane divers that attack you
if (!hostile) humanAI.InitMentalStateManager();
// Force the AI to wait in the location so if they get spooked by damage they
// don't try to swim cross map to your sub to find saftey
var waitOrder = OrderPrefab.Prefabs["wait"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
humanAI.SetForcedOrder(waitOrder);
int minMoney = characterMissionConfig.GetAttributeInt("minmoney", 0);
int maxMoney = characterMissionConfig.GetAttributeInt("maxmoney", 0);
if (maxMoney > 0)
{
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
character.Wallet.Give(money);
Log.InternalDebug($"Gave {money} to {character.Name}");
}
if (characterMissionConfig.GetAttributeBool("allowordering", false))
{
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacterToCrewList(character);
#endif
}
}
}
private void InitCharacters()
{
spawnPosition = missionNPCs[0].WorldPosition;
}
public override void StartMissionSpecific(Level level)
{
if (missionNPCs.characters.Count > 0)
{
#if DEBUG
throw new Exception($"characters.Count > 0 ({missionNPCs.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.");
missionNPCs.characters.Clear();
#endif
}
if (localCharacterConfig == 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();
}
}
const float MINDIST = 2000f;
bool triggered = false;
public override void UpdateMissionSpecific(float deltaTime)
{
if (IsClient) return;
// Exit if we're client or if we're already active or if all of the characters are dead
if (missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c)))
{
State = 1;
}
if (triggered || missionNPCs.characters.Any(c => !MissionNPCCollection.IsAlive(c))) return;
foreach (var aiCharacter in missionNPCs.characters.Where(c => MissionNPCCollection.IsAlive(c)))
{
if (!triggered && ShouldActivate())
{
Activate();
return;
}
bool ShouldActivate()
{
bool shouldActivate = GameSession.GetSessionCrewCharacters(CharacterType.Player).Any(c => MissionNPCCollection.Close(c, aiCharacter, MINDIST));
return shouldActivate;
}
}
}
/// <summary>
/// Removes the forced wait order from the AI
/// and forces them into combat with the closest player
/// if they're hostile
/// </summary>
private void Activate()
{
triggered = true;
Log.Debug("Activated NPC");
foreach (var aiCharacter in missionNPCs.characters)
{
if (aiCharacter.AIController is HumanAIController humanAI)
{
humanAI.ClearForcedOrder();
if (hostile)
{
Character target = MissionNPCCollection.GetClosest(aiCharacter);
if (target != null) humanAI.AddCombatObjective(AIObjectiveCombat.CombatMode.Offensive, target);
}
}
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
if (Submarine.MainSub != null && Submarine.MainSub.AtEndExit)
{
if (hostile)
{
return triggered;
}
return missionNPCs.characters.All(c => MissionNPCCollection.Survived(c));
}
return false;
}
public override void EndMissionSpecific(bool completed)
{
if (!IsClient)
{
missionNPCs.End(completed);
}
missionNPCs.Clear();
base.EndMissionSpecific(completed);
}
}
}
@@ -0,0 +1,526 @@
using Barotrauma;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using HarmonyLib;
using System;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System.Collections.Generic;
using Steamworks.Data;
using Barotrauma.Networking;
using static MoreLevelContent.Shared.Generation.MissionGenerationDirector;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressGhostshipMission : DistressMission
{
private readonly LocalizedString defaultSonarLabel;
private readonly XElement localCharacterConfig;
private readonly XElement submarineConfig;
private readonly XElement decalConfig;
private readonly XElement damageDevices;
private readonly XElement removeItems;
private readonly XElement tagDevices;
private readonly MissionNPCCollection missionNPCs;
private readonly bool AllowStealing;
private readonly TravelTarget travelTarget;
private readonly bool reactorActive;
private readonly Level.PositionType spawnPosition;
private Submarine ghostship;
private LevelData levelData;
private TrackingSonarMarker trackingSonarMarker;
private StructureDamageTracker damageTracker;
private int salvagedReward = 0;
enum TravelTarget
{
Start,
Maintain,
End
}
public DistressGhostshipMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
// Config
submarineConfig = prefab.ConfigElement.GetChildElement("submarines");
localCharacterConfig = prefab.ConfigElement.GetChildElement("characters");
removeItems = prefab.ConfigElement.GetChildElement("removeitems");
decalConfig = prefab.ConfigElement.GetChildElement("decals");
damageDevices = prefab.ConfigElement.GetChildElement("damageDevices");
tagDevices = prefab.ConfigElement.GetChildElement("tagDevices");
// Top level attributes
travelTarget = submarineConfig.GetAttributeEnum("TravelTarget", TravelTarget.Maintain);
spawnPosition = submarineConfig.GetAttributeEnum("SpawnPosition", Level.PositionType.MainPath);
reactorActive = submarineConfig.GetAttributeBool("ReactorActive", true);
AllowStealing = submarineConfig.GetAttributeBool("AllowStealing", true);
// General
defaultSonarLabel = TextManager.Get("missionname.distressmission");
missionNPCs = new(this, localCharacterConfig);
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
public override int Reward
{
get
{
if (_SubWasSalvaged) return salvagedReward;
return 0;
}
}
private bool _SubWasSalvaged = false;
private bool SubSalvaged => ghostship.AtEndExit || ghostship.AtStartExit;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (ghostship == null) yield break;
yield return trackingSonarMarker.CurrentPosition;
}
}
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
List<(float, ContentPath)> submarines = new List<(float, ContentPath)>();
foreach (var sub in submarineConfig.GetChildElements("sub"))
{
ContentPath path = sub.GetAttributeContentPath("path", Prefab.ContentPackage);
int commenness = sub.GetAttributeInt("commonness", 0);
submarines.Add((commenness, path));
}
ContentPath subPath = ToolBox.SelectWeightedRandom(submarines, s => s.Item1, Rand.RandSync.ServerAndClient).Item2;
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
{
File = file,
Callback = OnSubCreated,
SpawnPosition = SubSpawnPosition.Path,
AutoFill = true,
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
AllowStealing = AllowStealing,
SkipItemChance = 0.75f
});
}
void OnSubCreated(Submarine submarine)
{
Random rand = new MTRandom(ToolBox.StringToInt(levelData.Seed));
ghostship = submarine;
ghostship.FlipX();
submarine.ShowSonarMarker = false;
submarine.TeamID = CharacterTeamType.FriendlyNPC;
ghostship.Info.Type = (SubmarineType)7;
submarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
SubPlacementUtils.PositionSubmarine(submarine, Level.PositionType.MainPath);
SubPlacementUtils.SetCrushDepth(submarine);
salvagedReward = (int)Math.Round(ghostship.Info.Price * 0.85f);
double minFlood = submarineConfig.GetAttributeDouble("minfloodpercentage", 0);
double maxFlood = submarineConfig.GetAttributeDouble("maxfloodpercentage", 0);
string[] floodHulls = submarineConfig.GetAttributeStringArray("floodtargets", new string[] { }, convertToLowerInvariant: true);
if (floodHulls.Length > 0 && minFlood >= 0 && maxFlood > 0)
{
List<Hull> validHulls = ghostship.GetHulls(false).Where(h => floodHulls.Contains(h.RoomName.ToLowerInvariant())).ToList();
foreach (var target in validHulls)
{
target.WaterVolume = (float)(target.Volume * ((rand.NextDouble() * (maxFlood - minFlood)) + minFlood));
Log.Debug($"Flooded hull {target.RoomName}");
}
} else
{
Log.Debug("No hulls to flood");
}
// tag all sub waypoints
submarine.TagSubmarineWaypoints("distress_ghostship");
if (tagDevices != null)
{
foreach (var item in tagDevices.Elements())
{
Identifier targetItem = item.GetAttributeIdentifier("identifier", null);
Identifier tag = item.GetAttributeIdentifier("tag", null);
var items = submarine.GetItems(false).Where(i => i.Prefab.Identifier == targetItem);
items.ForEach((i) =>
{
i.AddTag(tag);
Log.Debug(i.Tags);
});
}
}
// Init tracking sonar marker
trackingSonarMarker = new TrackingSonarMarker(30, submarine, Prefab.SonarLabel.IsNullOrEmpty() ? defaultSonarLabel : Prefab.SonarLabel);
}
private void SpawnCharacters()
{
missionNPCs.CreateHumansInSubmarine(ghostship, onCharacterCreated: (character, config) =>
{
if (character.AIController is not HumanAIController humanAI) return;
bool alive = config.GetAttributeBool("alive", true);
if (!alive)
{
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null, log: false);
}
else
{
humanAI.InitMentalStateManager();
}
int minMoney = config.GetAttributeInt("minmoney", 0);
int maxMoney = config.GetAttributeInt("maxmoney", 0);
if (maxMoney > 0)
{
int money = Rand.Range(minMoney, maxMoney, Rand.RandSync.Unsynced);
character.Wallet.Give(money);
Log.InternalDebug($"Gave {money} to {character.Name}");
}
foreach (var affliction in config.GetChildElements("affliction"))
{
string identifier = affliction.GetAttributeString("identifier", null);
LimbType limb = affliction.GetAttributeEnum("limb", LimbType.None);
float strength = affliction.GetAttributeFloat("strength", 1);
bool targetRandomLimb = affliction.GetAttributeBool("randomLimb", false);
bool randomStrength = affliction.GetAttributeBool("randomStrength", false);
int count = affliction.GetAttributeInt("count", 1);
if (AfflictionHelper.TryGetAffliction(identifier, out AfflictionPrefab prefab))
{
for (int i = 0; i < count; i++)
{
Limb targetLimb = character.AnimController.MainLimb;
if (limb != LimbType.None) targetLimb = character.AnimController.GetLimb(limb);
if (targetRandomLimb) targetLimb = character.AnimController.Limbs.GetRandomUnsynced();
if (randomStrength) strength = Rand.Range(10f, 70f, Rand.RandSync.Unsynced);
character.CharacterHealth.ApplyAffliction(targetLimb, new Affliction(prefab, strength));
}
}
else
{
Log.Error($"Unable to get affliction with identifier {identifier}");
}
}
character.CharacterHealth.ForceUpdateVisuals();
});
}
private void SpawnDecals()
{
if (decalConfig == null) return;
foreach (XElement item in decalConfig.GetChildElements("item"))
{
string prefab = item.GetAttributeString("prefab", "");
string preferedHullName = item.GetAttributeString("preferedhull", "");
int count = item.GetAttributeInt("count", 0);
if (count == 0 || string.IsNullOrWhiteSpace(prefab)) continue;
PlaceDecals(prefab, preferedHullName, count);
}
}
private void PlaceDecals(string decalName, string preferedHull, int count)
{
try
{
bool hasPreferedHull = !string.IsNullOrWhiteSpace(preferedHull);
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
List<Hull> filteredHulls = ghostship.GetHulls(false).Where(h => !h.RoomName.Contains("ballast") && !h.RoomName.Contains("airlock") && !h.IsWetRoom).ToList();
var preferedHulls = filteredHulls.Where(h => h.RoomName.ToLowerInvariant() == preferedHull.ToLowerInvariant());
for (int i = 0; i < count; i++)
{
Hull hull = filteredHulls.ToList().GetRandom(rand);
if (hasPreferedHull && preferedHull.Any())
{
hull = preferedHulls.ToList().GetRandom(rand);
Log.Debug($"Set prefered hull, roomname {hull.RoomName}");
}
Vector2 pos = new Vector2(hull.WorldPosition.X + rand.Next(-hull.RectWidth / 2, hull.RectWidth / 2), hull.WorldPosition.Y + rand.Next(-hull.RectHeight / 2, hull.RectHeight / 2));
Decal decal = hull.AddDecal(decalName, pos, 1.0f, false);
}
} catch(Exception e)
{
Log.Error(e.ToString());
}
}
private void DamageDevices()
{
if (damageDevices == null) return;
foreach (XElement device in damageDevices.GetChildElements("item"))
{
Identifier tag = device.GetAttributeIdentifier("tag", "");
int condition = device.GetAttributeInt("condition", 0);
int amount = device.GetAttributeInt("amount", 1);
bool all = device.GetAttributeBool("all", false);
if (tag.IsEmpty) continue;
DamageDevice(tag, condition, amount, all);
}
}
private void DamageDevice(Identifier tag, int condition, int amount, bool all)
{
Random rand = new MTRandom(ToolBox.StringToInt(level.Seed));
var validItems = ghostship.GetItems(false).Where(i => i.IsPlayerTeamInteractable && i.HasTag(tag)).ToList();
if (!validItems.Any()) return;
if (all)
{
validItems.ForEach(i => Damage(i));
return;
}
while(amount > 0 && validItems.Any())
{
amount--;
Item target = validItems.GetRandom(rand);
_ = validItems.Remove(target);
Damage(target);
}
void Damage(Item item)
{
if (item.GetComponent<Repairable>() is Repairable repairable)
{
item.Condition = condition;
}
Log.Debug("Damaged Device");
}
}
public override void StartMissionSpecific(Level level)
{
if (!IsClient)
{
StartServer();
InitShip();
SpawnCharacters();
}
}
const float MAX_REP_LOSS = 20f;
float _LostRep = 0;
void StartServer()
{
// Reputation stuff
damageTracker = new StructureDamageTracker(ghostship, 2.0f, 1f, 2f);
damageTracker.DamageAfterThreshold += DamageTracker_DamageAfterThreshold;
damageTracker.ThresholdCrossed += DamageTracker_ThresholdCrossed;
_LostRep = 0;
}
private void DamageTracker_ThresholdCrossed()
{
#if SERVER
GameMain.Server?.SendChatMessage(TextManager.GetServerMessage("distress.ghostship.damagenotification")?.Value, ChatMessageType.Default);
#endif
#if CLIENT
if (GameMain.IsSingleplayer)
{
GameMain.GameSession?.CrewManager?.AddSinglePlayerChatMessage(
TextManager.Get("mlc.info1")?.Value, TextManager.Get("distress.ghostship.damagenotification")?.Value,
ChatMessageType.MessageBox, null);
}
#endif
}
private void DamageTracker_DamageAfterThreshold(float amount)
{
if (_LostRep >= MAX_REP_LOSS) return;
var reputationLoss = amount * Reputation.ReputationLossPerWallDamage;
reputationLoss = Math.Min(reputationLoss, 10); // clamp rep loss to a value 0-10
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss);
_LostRep += reputationLoss;
}
void InitShip()
{
ghostship.NeutralizeBallast();
var ghostshipItems = ghostship.GetItems(alsoFromConnectedSubs: false);
if (ghostshipItems.Find(i => i.HasTag("reactor") && !i.NonInteractable)?.GetComponent<Reactor>() is Reactor reactor)
{
Item reactorItem = reactor.Item;
ItemContainer container = reactorItem.GetComponent<ItemContainer>();
if (reactorActive)
{
reactor.PowerUpImmediately();
reactor.FuelConsumptionRate = 0;
}
// ItemPrefab rod = ItemPrefab.Find(null, "fuelrod".ToIdentifier());
// Make sure the reactor doesn't explode or irradiate the bots
if (CompatabilityHelper.Instance.ReactorModInstalled) CompatabilityHelper.SetupHazReactor(reactor);
Repairable repairable = reactor.Item.GetComponent<Repairable>();
if (repairable != null)
{
repairable.DeteriorationSpeed = 0.0f;
}
}
// make sure shit doesn't break by itself
ghostshipItems.FindAll(i => i.HasTag("junctionbox") || i.HasTag("oxygengenerator")).ForEach(i =>
{
if (i.GetComponent<Repairable>() is Repairable repairable)
{
repairable.DeteriorationSpeed = 0;
}
});
Item sonarItem = Item.ItemList.Find(it => it.Submarine == ghostship && it.GetComponent<Sonar>() != null);
if (sonarItem == null)
{
DebugConsole.ThrowError($"No sonar found in the beacon station \"{ghostship.Info.Name}\"!");
return;
}
var steering = sonarItem.GetComponent<Steering>();
steering.AutoPilot = true;
switch (travelTarget)
{
case TravelTarget.Start:
steering.SetDestinationLevelStart();
break;
case TravelTarget.Maintain:
steering.MaintainPos = true;
steering.PosToMaintain = ghostship.WorldPosition;
break;
case TravelTarget.End:
steering.SetDestinationLevelEnd();
break;
}
}
readonly float detectDist = Sonar.DefaultSonarRange;
private bool finalSubSetup = false;
const float FINAL_SETUP_DELAY = 5;
float setupDelay = FINAL_SETUP_DELAY;
partial void UpdateProjSpecific(float deltaTime);
public override void UpdateMissionSpecific(float deltaTime)
{
if (ghostship == null) return;
if (!finalSubSetup && !IsClient && setupDelay <= 0)
{
SpawnDecals();
DamageDevices();
if (removeItems != null)
{
foreach (XElement item in removeItems.Elements())
{
Identifier tagToRemove = item.GetAttributeIdentifier("tag", null);
if (tagToRemove != null)
{
foreach (var itemToRemove in ghostship.GetItems(false).FindAll(i => i.HasTag(tagToRemove)))
{
Entity.Spawner.AddItemToRemoveQueue(itemToRemove);
}
}
}
}
Log.InternalDebug("Preformed final sub setup");
finalSubSetup = true;
}
if (setupDelay > 0)
{
setupDelay -= deltaTime;
}
trackingSonarMarker.Update(deltaTime);
UpdateProjSpecific(deltaTime);
bool crewMemberInSub = CrewInSub();
switch (State)
{
case 0:
float dist = Vector2.DistanceSquared(ghostship.WorldPosition, Submarine.MainSub.WorldPosition);
if (dist < detectDist * detectDist || crewMemberInSub)
{
State = 1;
Log.InternalDebug("State -> 1");
}
break;
case 1:
if (crewMemberInSub)
{
State = 2;
Log.InternalDebug("State -> 2");
}
break;
default:
break;
}
if (SubSalvaged) _SubWasSalvaged = true;
if (IsClient) return;
damageTracker.Update();
bool CrewInSub()
{
foreach (var crewMember in GameSession.GetSessionCrewCharacters(CharacterType.Player))
{
if (crewMember.Submarine == ghostship)
{
return true;
}
}
return false;
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => State == 2;
public override void EndMissionSpecific(bool completed)
{
base.EndMissionSpecific(completed);
missionNPCs.Clear();
}
}
}
@@ -0,0 +1,27 @@
using Barotrauma;
using MoreLevelContent.Shared.Data;
using System;
using System.Linq;
using System.Reflection;
namespace MoreLevelContent.Missions
{
// Shared
abstract partial class DistressMission : Mission
{
protected bool DisplayReward;
public DistressMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub) => DisplayReward = prefab.ConfigElement.GetAttributeBool("displayreward", false);
public override void EndMissionSpecific(bool completed)
{
failed = !completed;
if (completed || Submarine.MainSub.AtEndExit || Submarine.MainSub.AtStartExit)
{
if (level?.LevelData != null && Prefab.Tags.Contains("distress"))
{
level.LevelData.MLC().HasDistress = false;
}
}
}
}
}
@@ -0,0 +1,465 @@
using Barotrauma;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static HarmonyLib.Code;
namespace MoreLevelContent.Missions
{
// Shared
partial class DistressSubmarineMission : DistressMission
{
private class MonsterSet
{
public readonly HashSet<(CharacterPrefab character, Point amountRange)> MonsterPrefabs = new HashSet<(CharacterPrefab character, Point amountRange)>();
public float Commonness;
public MonsterSet(XElement element) => Commonness = element.GetAttributeFloat("commonness", 100.0f);
}
private readonly XElement localCharacterConfig;
private readonly List<MonsterSet> monsterSets = new List<MonsterSet>();
private readonly XElement submarineTypeConfig;
private readonly LocalizedString sonarLabel;
private readonly LocalizedString successCrew;
private readonly LocalizedString successSub;
private readonly LocalizedString revealedFailure;
private readonly MissionNPCCollection missionNPCs;
private readonly Dictionary<Character, int> rewardLookup = new();
private Submarine lostSubmarine;
private LevelData levelData;
private bool swarmSpawned;
private bool outsideOfSonarRange;
private bool playerSubClose;
private TrackingSonarMarker trackingSonarMarker;
private bool SubSalvaged => lostSubmarine.AtEndExit || lostSubmarine.AtStartExit;
private bool CrewResuced => missionNPCs.AnyHumanSurvived;
public DistressSubmarineMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
// Setup submarine
localCharacterConfig = prefab.ConfigElement.GetChildElement("Characters");
submarineTypeConfig = prefab.ConfigElement.GetChildElement("Submarine");
// Setup text
sonarLabel = TextManager.Get("missionname.distressmission");
successCrew = TextManager.Get(prefab.ConfigElement.GetAttributeString("crewsurviveidentifier", "distress.submarinesuccess.crew"));
successSub = TextManager.Get(prefab.ConfigElement.GetAttributeString("subsalvagedidentifer", "distress.submarinesuccess.sub"));
revealedFailure = TextManager.Get(prefab.ConfigElement.GetAttributeString("revealedFailureidentifer", "distress.submarinefail"));
// Setup monsters, copied from beacon station code
swarmSpawned = false;
foreach (var monsterElement in prefab.ConfigElement.GetChildElements("monster"))
{
if (!monsterSets.Any())
{
monsterSets.Add(new MonsterSet(monsterElement));
}
LoadMonsters(monsterElement, monsterSets[0]);
}
foreach (var monsterSetElement in prefab.ConfigElement.GetChildElements("monsters"))
{
monsterSets.Add(new MonsterSet(monsterSetElement));
foreach (var monsterElement in monsterSetElement.GetChildElements("monster"))
{
LoadMonsters(monsterElement, monsterSets.Last());
}
}
missionNPCs = new(this, localCharacterConfig);
// for campaign missions, set level at construction
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
// Allow override the default sonar label with a sonarlabel="" attribute
// public override LocalizedString SonarLabel => base.SonarLabel.IsNullOrEmpty() ? sonarLabel : base.SonarLabel;
// Display a different failure message if the shuttle was located
public override LocalizedString FailureMessage => state > 0 ? revealedFailure : base.FailureMessage;
// Display a different success message if the crew is dead or alive at the end of the level
public override LocalizedString SuccessMessage => state < 2 ? FormatReward(successCrew) : FormatReward(successSub);
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels
{
get
{
if (lostSubmarine == null) yield break;
if (outsideOfSonarRange)
{
if (trackingSonarMarker == null) yield break;
yield return trackingSonarMarker.CurrentPosition;
}
}
}
#region Reward
LocalizedString FormatReward(LocalizedString input)
{
string msg = input.Value;
msg = msg.Replace("[reward]", GetReward(null).ToString());
return msg;
}
public override float GetBaseReward(Submarine sub) => Completed ? GetRewardCompleted() : GetRewardInLevel();
int survivingCrewPayout = 0;
void CalculateSurvivingPayout(out int payout)
{
payout = 0;
foreach (var character in missionNPCs.characters)
{
if (MissionNPCCollection.Survived(character))
{
payout += rewardLookup[character];
}
}
}
int GetRewardCompleted()
{
int reward = survivingCrewPayout;
if (SubSalvaged)
{
reward += Prefab.Reward;
Log.Debug("Sub salvaged");
}
Log.Debug($"Get reward completed: {reward}");
return reward;
}
int GetRewardInLevel()
{
int reward = Prefab.Reward;
if (missionNPCs?.characters?.Count > 0)
{
foreach (var character in missionNPCs.characters)
{
// Add payout for each living character
if (MissionNPCCollection.IsAlive(character))
{
reward += rewardLookup[character];
Log.Debug($"Added {rewardLookup[character]} to reward");
}
else Log.Debug("Character is dead");
}
}
else Log.Debug("No Characters");
Log.Debug($"Reward: {reward}");
return reward;
}
#endregion
private void LoadMonsters(XElement monsterElement, MonsterSet set)
{
Identifier speciesName = monsterElement.GetAttributeIdentifier("character", Identifier.Empty);
int defaultCount = monsterElement.GetAttributeInt("count", -1);
if (defaultCount < 0)
{
defaultCount = monsterElement.GetAttributeInt("amount", 1);
}
int min = Math.Min(monsterElement.GetAttributeInt("min", defaultCount), 255);
int max = Math.Min(Math.Max(min, monsterElement.GetAttributeInt("max", defaultCount)), 255);
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
if (characterPrefab != null)
{
_ = set.MonsterPrefabs.Add((characterPrefab, new Point(min, max)));
}
else
{
DebugConsole.ThrowError($"Error in distress submarine mission \"{Prefab.Identifier}\". Could not find a character prefab with the name \"{speciesName}\".");
}
}
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
ContentPath subPath = submarineTypeConfig.GetAttributeContentPath("path", Prefab.ContentPackage);
if (subPath.IsNullOrEmpty())
{
Log.Error($"No path used for submarine for the shuttle rescue mission \"{Prefab.Identifier}\"!");
return;
}
SubmarineFile file = ContentPackageManager.EnabledPackages.All.SelectMany(p => p.GetFiles<SubmarineFile>()).Where(f => f.Path.Value == subPath).FirstOrDefault();
if (file == null)
{
Log.Error($"Failed to find submarine at path {subPath}");
return;
}
MissionGenerationDirector.RequestSubmarine(new MissionGenerationDirector.SubmarineSpawnRequest()
{
File = file,
Callback = OnSubCreated,
AllowStealing = true,
AutoFill = true,
IgnoreCrushDpeth = true,
Prefix = MissionGenerationDirector.SubmarineSpawnRequest.AutoFillPrefix.Abandoned,
SpawnPosition = MissionGenerationDirector.SubSpawnPosition.Path,
SkipItemChance = 0.5f
});
}
void OnSubCreated(Submarine submarine)
{
if (submarine == null)
{
State = -1;
DebugConsole.ThrowError("Distress submarine failed to spawn! Mission will now fail.");
return;
}
lostSubmarine = submarine;
lostSubmarine.Info.Type = SubmarineType.Player;
lostSubmarine.TeamID = CharacterTeamType.FriendlyNPC;
lostSubmarine.ShowSonarMarker = false;
lostSubmarine.PhysicsBody.FarseerBody.BodyType = FarseerPhysics.BodyType.Dynamic;
SubPlacementUtils.PositionSubmarine(lostSubmarine, Level.PositionType.SidePath | Level.PositionType.MainPath);
//make the shuttle resist at least it's spawn position + 1000m
SubPlacementUtils.SetCrushDepth(lostSubmarine);
// tag all sub waypoints
lostSubmarine.TagSubmarineWaypoints("distress_shuttle");
// Init sonar tracking
trackingSonarMarker = new TrackingSonarMarker(30, lostSubmarine, Prefab.SonarLabel.IsNullOrEmpty() ? sonarLabel : Prefab.SonarLabel);
// TriggerEvents(0);
}
public override void StartMissionSpecific(Level level)
{
if (lostSubmarine == null) return;
if (!IsClient) StartServer();
}
void StartServer()
{
SinkSub();
lostSubmarine.EnableMaintainPosition();
Item sonarItem = Item.ItemList.Find(it => it.Submarine == lostSubmarine && it.GetComponent<Sonar>() != null);
if (sonarItem != null)
{
// Always allow the lost sub sonar to run so it attracts monsters :)
Powered sonarPower = sonarItem.GetComponent<Powered>();
sonarPower.MinVoltage = 0;
var sonar = sonarItem.GetComponent<Sonar>();
var steering = sonarItem.GetComponent<Steering>();
sonar.CurrentMode = Sonar.Mode.Active;
// Notify clients of the sonar's state
#if SERVER
sonar.Item.CreateServerEvent(sonar);
#endif
}
bool givenCharge = false;
// Drain all of the batteries on the shuttle
foreach (var item in lostSubmarine.GetItems(alsoFromConnectedSubs: false).Where(i => i.HasTag("battery") && !i.NonInteractable))
{
if (item.GetComponent<PowerContainer>() is PowerContainer powerContainer)
{
// Allow fast recharging
powerContainer.MaxRechargeSpeed = powerContainer.Capacity;
// Drain batteries, give them a little bit of power
powerContainer.Charge = givenCharge ? 0 : 10;
givenCharge = true;
}
}
// TODO: Drain any rods that are inside a reactor, if the shuttle has one
// Init NPCS
missionNPCs.CreateHumansInSubmarine(lostSubmarine, onCharacterCreated: (character, config) =>
{
int payout = config.GetAttributeInt("payout", 0);
rewardLookup.Add(character, payout);
character.Info.Title = TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", payout));
((HumanAIController)character.AIController).InitMentalStateManager();
if (config.GetAttributeBool("isCaptain", false))
{
// Set shuttle captain orders
var fightIntruders = OrderPrefab.Prefabs["fightintruders"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
var repairBrokenDevices = OrderPrefab.Prefabs["repairsystems"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
var fixLeaks = OrderPrefab.Prefabs["fixleaks"].CreateInstance(OrderPrefab.OrderTargetType.Entity);
character.SetOrder(fixLeaks, true, false);
character.SetOrder(fightIntruders, true, false);
character.SetOrder(repairBrokenDevices, true, false);
Log.InternalDebug("Updated captain orders");
}
});
void SinkSub()
{
HashSet<Hull> ballastHulls = new HashSet<Hull>();
foreach (Item item in Item.ItemList)
{
if (item.Submarine != lostSubmarine) { continue; }
var pump = item.GetComponent<Pump>();
if (pump == null || item.CurrentHull == null) { continue; }
if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
pump.FlowPercentage = 0.0f;
_ = ballastHulls.Add(item.CurrentHull);
}
foreach (Hull hull in ballastHulls)
{
hull.WaterVolume = hull.Volume;
}
}
}
// state 0 = init
// state 1 = crew alive, escort to end
// state 2 = crew dead, escort sub to end
readonly float spawnDist = Sonar.DefaultSonarRange * 2;
private bool _salvedState = false;
private bool _migrate = false;
public override void UpdateMissionSpecific(float deltaTime)
{
if (State == -1 || lostSubmarine == null) return;
UpdateLastPing(deltaTime);
#if CLIENT
if (SubSalvaged && _salvedState != SubSalvaged)
{
CoroutineManager.StartCoroutine(_showMessageBox(TextManager.Get("missionheader1.distress_shiprescue"), TextManager.Get("distress.lostshuttle.atend")));
_salvedState = SubSalvaged;
}
IEnumerable<CoroutineStatus> _showMessageBox(LocalizedString header, LocalizedString message)
{
while (GUIMessageBox.VisibleBox?.UserData is RoundSummary)
{
yield return new WaitForSeconds(1.0f);
}
CreateMessageBox(header, message);
yield return CoroutineStatus.Success;
}
#endif
if (IsClient) return;
switch (state)
{
// init
case 0:
float dist = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition);
if (dist > spawnDist * spawnDist) return;
if (!swarmSpawned) SpawnSwarm();
if (playerSubClose && swarmSpawned)
{
State = missionNPCs.AnyHumanAlive ? 1 : 2;
}
break;
// crew alive
case 1:
if (!missionNPCs.AnyHumanAlive)
{
State = 2;
}
break;
// crew dead
case 2:
break;
}
}
readonly float sonarClose = Sonar.DefaultSonarRange / 0.8f;
void UpdateLastPing(float deltaTime)
{
if (Submarine.MainSub == null)
{
Log.Warn($"Skipped updating last ping as main sub was null");
return;
}
outsideOfSonarRange = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) > Sonar.DefaultSonarRange * Sonar.DefaultSonarRange;
playerSubClose = Vector2.DistanceSquared(lostSubmarine.WorldPosition, Submarine.MainSub.WorldPosition) < sonarClose * sonarClose;
trackingSonarMarker.Update(deltaTime);
}
void SpawnSwarm()
{
swarmSpawned = true;
// Find spawn position for the monsters
if (monsterSets.Count == 0) return;
Vector2 spawnPos = lostSubmarine.WorldPosition;
spawnPos.Y += lostSubmarine.GetDockedBorders().Height * 1.5f;
var monsterSet = ToolBox.SelectWeightedRandom(monsterSets, m => m.Commonness, Rand.RandSync.Unsynced);
foreach ((CharacterPrefab monsterSpecies, Point monsterCountRange) in monsterSet.MonsterPrefabs)
{
int amount = Rand.Range(monsterCountRange.X, monsterCountRange.Y + 1);
for (int i = 0; i < amount; i++)
{
_ = CoroutineManager.Invoke(() =>
{
//round ended before the coroutine finished
if (GameMain.GameSession == null || Level.Loaded == null) { return; }
Entity.Spawner.AddCharacterToSpawnQueue(monsterSpecies.Identifier, spawnPos, (Character character) =>
{
if (character.AIController is EnemyAIController controller)
{
AITarget target = missionNPCs.characters.GetRandomUnsynced().AiTarget;
if (target != null) controller.SelectTarget(target);
}
});
}, Rand.Range(0f, amount));
}
}
}
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType)
{
CalculateSurvivingPayout(out survivingCrewPayout);
return SubSalvaged || CrewResuced;
}
public override void EndMissionSpecific(bool completed)
{
if (!IsClient) missionNPCs.End(completed);
missionNPCs.Clear();
base.EndMissionSpecific(completed);
}
}
}
@@ -0,0 +1,12 @@
using Barotrauma;
using System;
namespace MoreLevelContent.Missions
{
// Shared
partial class LostCargoMission
{
}
}
@@ -0,0 +1,266 @@
using Barotrauma;
using Barotrauma.Items.Components;
using Barotrauma.MoreLevelContent.Shared.Utils;
using Barotrauma.Networking;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared;
using MoreLevelContent.Shared.Data;
using MoreLevelContent.Shared.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace MoreLevelContent.Missions
{
// Shared
partial class MissionNPCCollection
{
internal readonly List<Character> characters = new();
internal readonly Dictionary<Character, List<Item>> characterItems = new();
internal bool AllHumansAlive => characters.All(c => IsAlive(c));
internal bool AnyHumanAlive => characters.Any(c => IsAlive(c));
internal bool AnyHumanSurvived => characters.Any(c => Survived(c));
internal int AliveHumans => characters.Where(c => IsAlive(c)).Count();
internal delegate void OnCharacterCreated(Character character, XElement missionCharacterConfig);
internal Character this[int index] => characters[index];
internal MissionNPCCollection(Mission mission, XElement characterConfig)
{
this.mission = mission;
this.characterConfig = characterConfig;
}
private readonly Mission mission;
private readonly XElement characterConfig;
public void Clear()
{
characters.Clear();
characterItems.Clear();
}
public void End(bool completed)
{
foreach (Character character in characters)
{
if (character.Inventory == null) { continue; }
foreach (Item item in character.Inventory.AllItemsMod)
{
//item didn't spawn with the characters -> drop it
if (!characterItems.Any(c => c.Value.Contains(item)))
{
item.Drop(character);
}
}
}
// 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
foreach (var characterItem in characterItems)
{
if (Survived(characterItem.Key) || !completed)
{
foreach (Item item in characterItem.Value)
{
if (!item.Removed)
{
item.Remove();
}
}
}
}
}
internal void CreateHumansInSubmarine(Submarine submarine, CharacterTeamType team = CharacterTeamType.FriendlyNPC, OnCharacterCreated onCharacterCreated = null)
{
if (characterConfig == null)
{
Log.Warn("No characters");
return;
}
WayPoint explicitStayInHullPos = WayPoint.GetRandom(SpawnType.Human, null, submarine);
Rand.RandSync randSync = Rand.RandSync.Unsynced;
List<(HumanPrefab prefab, XElement config)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
foreach (XElement element in characterConfig.Elements())
{
var humanPrefab = GetHumanPrefabFromElement(element);
humanPrefabsToSpawn.Add((humanPrefab, element));
}
foreach (var (prefab, config) in humanPrefabsToSpawn)
{
var humanPrefab = prefab;
XElement characterSpecificConfig = config;
if (humanPrefab == null || humanPrefab.Job.IsEmpty || humanPrefab.Job == "any") { continue; }
var jobPrefab = humanPrefab.GetJobPrefab(randSync);
var stayPos = explicitStayInHullPos;
if (jobPrefab != null)
{
stayPos = WayPoint.GetRandom(SpawnType.Human, jobPrefab, submarine) ?? explicitStayInHullPos;
}
XElement additionalItemsElement = config?.GetChildElement("additionalitems");
ContentXElement additionalItems = new ContentXElement(null, additionalItemsElement);
Character spawnedCharacter = CreateHuman(humanPrefab, characters, characterItems, submarine, team, stayPos, additionalItems: additionalItemsElement != null ? additionalItems.Elements() : null);
spawnedCharacter.EnableDespawn = false; // don't let mission npcs despawn
spawnedCharacter.GiveIdCardTags(stayPos, false);
onCharacterCreated?.Invoke(spawnedCharacter, characterSpecificConfig);
spawnedCharacter.MLC().NPCElement = characterSpecificConfig;
#if CLIENT
if (GameMain.IsSingleplayer)
{
if (characterSpecificConfig.GetAttributeBool("allowordering", false))
{
_ = GameMain.GameSession.CrewManager.AddCharacterToCrewList(spawnedCharacter);
}
}
#endif
}
Log.Debug("end");
InitCharacters();
}
internal Character CreateHuman(HumanPrefab humanPrefab, List<Character> characters, Dictionary<Character, List<Item>> characterItems, Submarine submarine, CharacterTeamType teamType, ISpatialEntity positionToStayIn = null, bool giveTags = true, IEnumerable<ContentXElement> additionalItems = null)
{
var characterInfo = humanPrefab.CreateCharacterInfo(Rand.RandSync.Unsynced);
characterInfo.TeamID = teamType;
if (positionToStayIn == null)
{
positionToStayIn =
WayPoint.GetRandom(SpawnType.Human, characterInfo.Job?.Prefab, submarine) ??
WayPoint.GetRandom(SpawnType.Human, null, submarine);
}
Character spawnedCharacter = Character.Create(characterInfo.SpeciesName, positionToStayIn.WorldPosition, ToolBox.RandomSeed(8), characterInfo, createNetworkEvent: false);
spawnedCharacter.HumanPrefab = humanPrefab;
humanPrefab.InitializeCharacter(spawnedCharacter, positionToStayIn);
_ = humanPrefab.GiveItems(spawnedCharacter, submarine, null, createNetworkEvents: false);
foreach (var item in spawnedCharacter.Inventory.AllItems)
{
IdCard card = item.GetComponent<IdCard>();
if (card == null) continue;
card.SubmarineSpecificID = submarine.SubmarineSpecificIDTag;
}
if (additionalItems != null)
{
foreach (var additionalItem in additionalItems)
{
int amount = additionalItem.GetAttributeInt("amount", 1);
for (int i = 0; i < amount; i++)
{
HumanPrefab.InitializeItem(spawnedCharacter, additionalItem, submarine, humanPrefab, createNetworkEvents: false);
}
}
}
characters.Add(spawnedCharacter);
characterItems.Add(spawnedCharacter, spawnedCharacter.Inventory.FindAllItems(recursive: true));
return spawnedCharacter;
}
internal void GiveCharacterItem(Character character, ContentXElement itemElement, bool createNetworkEvents = true)
{
HumanPrefab.InitializeItem(character, itemElement, null, character.HumanPrefab, createNetworkEvents: createNetworkEvents);
characterItems[character] = character.Inventory.FindAllItems(recursive: true);
}
internal void CreateHumansAtPosition(CharacterTeamType team, Vector2 position, OnCharacterCreated onCharacterCreated)
{
List<(HumanPrefab, XElement)> humanPrefabsToSpawn = new List<(HumanPrefab, XElement)>();
foreach (XElement element in characterConfig?.Elements())
{
var humanPrefab = GetHumanPrefabFromElement(element);
humanPrefabsToSpawn.Add((humanPrefab, element));
}
foreach (var prefabToSpawn in humanPrefabsToSpawn)
{
var humanPrefab = prefabToSpawn.Item1;
XElement characterMissionConfig = prefabToSpawn.Item2;
Character character = CreateHumanAtPosition(humanPrefab, team, position);
character.EnableDespawn = false; // don't let mission npcs despawn
onCharacterCreated.Invoke(character, characterMissionConfig);
character.MLC().NPCElement = characterMissionConfig;
}
InitCharacters();
}
internal Character CreateHumanAtPosition(HumanPrefab humanPrefab, CharacterTeamType team, Vector2 spawnPosition)
{
Character character = CharacterUtils.CreateHuman(humanPrefab, characters, characterItems, team, spawnPosition, false);
return character;
}
private HumanPrefab GetHumanPrefabFromElement(XElement element)
{
if (element.Attribute("name") != null)
{
DebugConsole.ThrowError("Error in mission \"" + mission.Prefab.Identifier + "\" - use character identifiers instead of names to configure the characters.");
return null;
}
Identifier characterIdentifier = element.GetAttributeIdentifier("identifier", Identifier.Empty);
Identifier characterFrom = element.GetAttributeIdentifier("from", Identifier.Empty);
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;
}
private void InitCharacters()
{
Log.Debug("characterCount");
int i = 0;
foreach (XElement element in characterConfig.Elements())
{
characters[i].IsEscorted = false;
Color col = element.GetAttributeColor("color", Color.LightGreen);
characters[i].UniqueNameColor = col;
Log.Debug($"Set color to {col} for character {characters[i].Name}");
i++;
}
}
internal static bool IsAlive(Character character) => character != null && !character.Removed && !character.IsDead;
internal static bool IsCaptured(Character character) => character.LockHands;
internal static bool Survived(Character character)
{
return IsAlive(character) && character.CurrentHull?.Submarine != null &&
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine));
}
internal static bool Close(Character target, Character ai, float closeDistance)
{
float dist = Vector2.DistanceSquared(ai.WorldPosition, target.WorldPosition);
return dist < closeDistance * closeDistance;
}
internal static Character GetClosest(Character ai)
{
float closest = float.MaxValue;
Character target = null;
foreach (Character player in Character.CharacterList.Where(c => c.IsPlayer))
{
if (player.IsDead) continue; // skip dead players
float dist = Vector2.DistanceSquared(player.WorldPosition, ai.WorldPosition);
if (dist < closest)
{
closest = dist;
target = player;
}
}
return target;
}
}
}
@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Barotrauma.MoreLevelContent.Shared.Missions
{
class MissionTest
{
}
}
@@ -0,0 +1,25 @@
using Barotrauma;
using MoreLevelContent.Shared.Generation;
using MoreLevelContent.Shared;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using System;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Utils;
using System.Collections.Generic;
namespace MoreLevelContent.Missions
{
// Shared
// partial class PirateOutpostMission : Mission
// {
// public override bool DisplayAsCompleted => throw new NotImplementedException();
//
// public override bool DisplayAsFailed => throw new NotImplementedException();
//
// public override bool DetermineCompleted() => throw new NotImplementedException();
// }
}
@@ -0,0 +1,60 @@
using Barotrauma;
using Microsoft.Xna.Framework;
using MoreLevelContent.Shared.Data;
using System.Collections.Generic;
using System.Linq;
namespace MoreLevelContent.Missions
{
// Shared
internal partial class TriangulationMission : Mission
{
private readonly LocalizedString sonarLabel0;
private readonly LocalizedString sonarLabel1;
private readonly LocalizedString sonarLabel2;
private LevelData levelData;
public TriangulationMission(MissionPrefab prefab, Location[] locations, Submarine sub) : base(prefab, locations, sub)
{
sonarLabel0 = TextManager.Get("tri-point-1");
sonarLabel1 = TextManager.Get("tri-point-1");
sonarLabel2 = TextManager.Get("tri-point-1");
LevelData levelData = locations[0].Connections.Where(c => c.Locations.Contains(locations[1])).FirstOrDefault()?.LevelData ?? locations[0]?.LevelData;
if (levelData != null)
{
SetLevel(levelData);
}
}
//public override bool DetermineCompleted() => false;
public override bool DetermineCompleted(CampaignMode.TransitionType transitionType) => false;
public override IEnumerable<(LocalizedString Label, Vector2 Position)> SonarLabels => base.SonarLabels;
public override void SetLevel(LevelData level)
{
if (levelData != null)
{
//level already set
return;
}
levelData = level;
switch (level.MLC().TriangulationTarget)
{
case TriangulationTarget.None:
break;
case TriangulationTarget.MapFeature:
break;
case TriangulationTarget.PirateBase:
break;
case TriangulationTarget.Treasure:
break;
default:
break;
}
}
}
}