527 lines
22 KiBLFS
C#
Executable File
527 lines
22 KiBLFS
C#
Executable File
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();
|
|
}
|
|
}
|
|
}
|