466 lines
19 KiBLFS
C#
Executable File
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);
|
|
}
|
|
}
|
|
}
|