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()).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 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 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() 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() is Reactor reactor) { Item reactorItem = reactor.Item; ItemContainer container = reactorItem.GetComponent(); 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(); 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() is Repairable repairable) { repairable.DeteriorationSpeed = 0; } }); Item sonarItem = Item.ItemList.Find(it => it.Submarine == ghostship && it.GetComponent() != null); if (sonarItem == null) { DebugConsole.ThrowError($"No sonar found in the beacon station \"{ghostship.Info.Name}\"!"); return; } var steering = sonarItem.GetComponent(); 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(); } } }