Files
BarotraumaModServer/LocalMods/More Level Content/CSharp/Shared/Missions/DistressSubmarineMission.cs
T
2026-06-09 00:42:10 +03:00

466 lines
19 KiBLFS
C#
Executable File

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);
}
}
}