using Barotrauma.Extensions; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma { class SinglePlayerCampaign : CampaignMode { public const int MinimumInitialMoney = 0; public override bool Paused { get { return ForceMapUI || CoroutineManager.IsCoroutineRunning("LevelTransition") || ShowCampaignUI && CampaignUI.SelectedTab == InteractionType.Map || (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown); } } public override void UpdateWhilePaused(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && roundSummary.ContinueButton != null && roundSummary.ContinueButton.Visible) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } } SlideshowPlayer?.UpdateManually(deltaTime); CrewManager.ChatBox?.Update(deltaTime); CrewManager.UpdateReports(); } private float endTimer; private bool savedOnStart; private bool gameOver; private Character lastControlledCharacter; private bool showCampaignResetText; public override bool PurchasedHullRepairs { get { return PurchasedHullRepairsInLatestSave; } set { PurchasedHullRepairsInLatestSave = value; } } public override bool PurchasedLostShuttles { get { return PurchasedLostShuttlesInLatestSave; } set { PurchasedLostShuttlesInLatestSave = value; } } public override bool PurchasedItemRepairs { get { return PurchasedItemRepairsInLatestSave; } set { PurchasedItemRepairsInLatestSave = value; } } #region Constructors/initialization /// /// Instantiates a new single player campaign /// private SinglePlayerCampaign(string mapSeed, CampaignSettings settings) : base(GameModePreset.SinglePlayerCampaign, settings) { UpgradeManager = new UpgradeManager(this); Settings = settings; InitFactions(); map = new Map(this, mapSeed); foreach (JobPrefab jobPrefab in JobPrefab.Prefabs) { for (int i = 0; i < jobPrefab.InitialCount; i++) { var variant = Rand.Range(0, jobPrefab.Variants); CrewManager.AddCharacterInfo(new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: jobPrefab, variant: variant)); } } InitUI(); } /// /// Loads a previously saved single player campaign from XML /// private SinglePlayerCampaign(XElement element) : base(GameModePreset.SinglePlayerCampaign, CampaignSettings.Empty) { IsFirstRound = false; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "metadata": CampaignMetadata.Load(subElement); break; } } InitFactions(); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case CampaignSettings.LowerCaseSaveElementName: Settings = new CampaignSettings(subElement); break; case "crew": GameMain.GameSession.CrewManager = new CrewManager(subElement, true); ActiveOrdersElement = subElement.GetChildElement("activeorders"); break; case "map": map = Map.Load(this, subElement); break; } } LoadSaveSharedSingleAndMultiplayer(element); UpgradeManager ??= new UpgradeManager(this); InitUI(); if (map == null) { throw new System.Exception("Failed to load the campaign save file (saved with an older, incompatible version of Barotrauma)."); } savedOnStart = true; } /// /// Start a completely new single player campaign /// public static SinglePlayerCampaign StartNew(string mapSeed, CampaignSettings startingSettings) => new SinglePlayerCampaign(mapSeed, startingSettings); /// /// Load a previously saved single player campaign from xml /// /// /// public static SinglePlayerCampaign Load(XElement element) => new SinglePlayerCampaign(element); private void InitUI() { CreateEndRoundButton(); campaignUIContainer = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "InnerGlow", color: Color.Black); CampaignUI = new CampaignUI(this, campaignUIContainer) { StartRound = () => { TryEndRound(); } }; endRoundButton = CreateEndRoundButton(); endRoundButton.OnClicked = (btn, userdata) => { TryEndRoundWithFuelCheck( onConfirm: () => TryEndRound(), onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); }); return true; }; } public override void HUDScaleChanged() { CreateEndRoundButton(); } #endregion public override void Start() { base.Start(); CargoManager.CreatePurchasedItems(); UpgradeManager.ApplyUpgrades(); UpgradeManager.SanityCheckUpgrades(); if (!savedOnStart) { GUI.SetSavingIndicatorState(true); SaveUtil.SaveGame(GameMain.GameSession.DataPath, isSavingOnLoading: true); savedOnStart = true; } CrewDead = false; endTimer = 5.0f; CrewManager.InitSinglePlayerRound(); LoadPets(); LoadActiveOrders(); CargoManager.InitPurchasedIDCards(); GUI.DisableSavingIndicatorDelayed(); } protected override void LoadInitialLevel() { //no level loaded yet -> show a loading screen and load the current location (outpost) GameMain.Instance.ShowLoading( DoLoadInitialLevel(map.SelectedConnection?.LevelData ?? map.CurrentLocation.LevelData, mirror: map.CurrentLocation != map.SelectedConnection?.Locations[0])); } private IEnumerable DoLoadInitialLevel(LevelData level, bool mirror) { GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost()); GameMain.GameScreen.Select(); CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition"); yield return CoroutineStatus.Success; } private IEnumerable DoInitialCameraTransition() { while (GameMain.Instance.LoadingScreenOpen) { yield return CoroutineStatus.Running; } Character prevControlled = Character.Controlled; if (prevControlled?.AIController != null) { prevControlled.AIController.Enabled = false; } Character.Controlled = null; prevControlled?.ClearInputs(); GUI.DisableHUD = true; while (GameMain.Instance.LoadingScreenOpen) { yield return CoroutineStatus.Running; } if (IsFirstRound || showCampaignResetText) { if (SlideshowPrefab.Prefabs.TryGet("campaignstart".ToIdentifier(), out var slideshow)) { SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow); } var subToFocusTo = GameMain.GameSession.Level.StartOutpost ?? Submarine.MainSub; var borders = subToFocusTo.GetDockedBorders(); borders.Location += subToFocusTo.WorldPosition.ToPoint(); GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2); float startZoom = 0.8f / ((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X); GameMain.GameScreen.Cam.Zoom = GameMain.GameScreen.Cam.MinZoom = Math.Min(startZoom, GameMain.GameScreen.Cam.MinZoom); while (SlideshowPlayer != null && !SlideshowPlayer.LastTextShown) { GUI.PreventPauseMenuToggle = true; yield return CoroutineStatus.Running; } GUI.PreventPauseMenuToggle = false; var transition = new CameraTransition(prevControlled, GameMain.GameScreen.Cam, null, null, fadeOut: false, losFadeIn: true, waitDuration: 1, panDuration: 5, startZoom: startZoom, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = false }; while (transition.Running) { yield return CoroutineStatus.Running; } showCampaignResetText = false; } else { ISpatialEntity transitionTarget; transitionTarget = (ISpatialEntity)prevControlled ?? Submarine.MainSub; var transition = new CameraTransition(transitionTarget, GameMain.GameScreen.Cam, null, null, fadeOut: false, losFadeIn: prevControlled != null, panDuration: 5, startZoom: 0.5f, endZoom: 1.0f) { AllowInterrupt = true, RemoveControlFromCharacter = false }; while (transition.Running) { yield return CoroutineStatus.Running; } } if (prevControlled != null) { prevControlled.SelectedItem = prevControlled.SelectedSecondaryItem = null; if (prevControlled.AIController != null) { prevControlled.AIController.Enabled = true; } } if (prevControlled != null) { Character.Controlled = prevControlled; } GUI.DisableHUD = false; yield return CoroutineStatus.Success; } protected override IEnumerable DoLevelTransition(TransitionType transitionType, LevelData newLevel, Submarine leavingSub, bool mirror) { NextLevel = newLevel; bool success = CrewManager.GetCharacters().Any(c => !c.IsDead); SoundPlayer.OverrideMusicType = (success ? "endround" : "crewdead").ToIdentifier(); SoundPlayer.OverrideMusicDuration = 18.0f; GUI.SetSavingIndicatorState(success); CrewDead = false; if (success) { // Event history must be registered before ending the round or it will be cleared GameMain.GameSession.EventManager.StoreEventDataAtRoundEnd(); } GameMain.GameSession.EndRound("", transitionType); var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton; RoundSummary roundSummary = null; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary) { roundSummary = GUIMessageBox.VisibleBox?.UserData as RoundSummary; } if (continueButton != null) { continueButton.Visible = false; } lastControlledCharacter = Character.Controlled; Character.Controlled = null; switch (transitionType) { case TransitionType.None: throw new InvalidOperationException("Level transition failed (no transitions available)."); case TransitionType.ReturnToPreviousLocation: //deselect destination on map map.SelectLocation(-1); break; case TransitionType.ProgressToNextLocation: Map.MoveToNextLocation(); TotalPassedLevels++; break; case TransitionType.ProgressToNextEmptyLocation: Map.Visit(Map.CurrentLocation); TotalPassedLevels++; break; case TransitionType.End: EndCampaign(); IsFirstRound = true; break; } Map.ProgressWorld(this, transitionType, GameMain.GameSession.RoundDuration); GUI.ClearMessages(); //-------------------------------------- if (transitionType != TransitionType.End) { var endTransition = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, transitionType == TransitionType.LeaveLocation ? Alignment.BottomCenter : Alignment.Center, fadeOut: false, panDuration: EndTransitionDuration); Location portraitLocation = Map.SelectedLocation ?? Map.CurrentLocation; overlaySprite = portraitLocation.Type.GetPortrait(portraitLocation.PortraitId); float fadeOutDuration = endTransition.PanDuration; float t = 0.0f; while (t < fadeOutDuration || endTransition.Running) { t += CoroutineManager.DeltaTime; overlayColor = Color.Lerp(Color.Transparent, Color.White, t / fadeOutDuration); yield return CoroutineStatus.Running; } overlayColor = Color.White; yield return CoroutineStatus.Running; //-------------------------------------- if (success) { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.DataPath); } else { PendingSubmarineSwitch = null; EnableRoundSummaryGameOverState(); } CrewManager?.ClearCurrentOrders(); SelectSummaryScreen(roundSummary, newLevel, mirror, () => { GameMain.GameScreen.Select(); if (continueButton != null) { continueButton.Visible = true; } GUI.DisableHUD = false; GUI.ClearCursorWait(); overlayColor = Color.Transparent; }); } GUI.SetSavingIndicatorState(false); yield return CoroutineStatus.Success; } protected override void EndCampaignProjSpecific() { GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); SaveUtil.SaveGame(GameMain.GameSession.DataPath); GameMain.CampaignEndScreen.Select(); GUI.DisableHUD = false; GameMain.CampaignEndScreen.OnFinished = () => { showCampaignResetText = true; LoadInitialLevel(); IsFirstRound = true; }; } public override void Update(float deltaTime) { if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition") || gameOver) { return; } base.Update(deltaTime); SlideshowPlayer?.UpdateManually(deltaTime); Map?.Radiation?.UpdateRadiation(deltaTime); if (PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { ShowCampaignUI = false; if (GUIMessageBox.VisibleBox?.UserData is RoundSummary roundSummary && roundSummary.ContinueButton != null && roundSummary.ContinueButton.Visible) { GUIMessageBox.MessageBoxes.Remove(GUIMessageBox.VisibleBox); } } if (ShowCampaignUI || ForceMapUI) { Character.DisableControls = true; } if (!GUI.DisableHUD && !GUI.DisableUpperHUD) { endRoundButton.UpdateManually(deltaTime); if (CoroutineManager.IsCoroutineRunning("LevelTransition") || ForceMapUI) { return; } } if (Level.Loaded.Type == LevelData.LevelType.Outpost) { KeepCharactersCloseToOutpost(deltaTime); if (wasDocked) { var connectedSubs = Submarine.MainSub.GetConnectedSubs(); bool isDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); if (!isDocked) { //undocked from outpost, need to choose a destination ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); } } else if (Level.Loaded.IsEndBiome) { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); if (transitionType == TransitionType.ProgressToNextLocation) { LoadNewLevel(); } } else { //force the map to open if the sub is somehow not at the start of the outpost level //UNLESS the level has specific exit points, in that case the sub needs to get to those if (!Submarine.MainSub.AtStartExit && /*there should normally always be a start outpost in outpost levels, * but that might not always be the case e.g. mods or outdated saves (see #13042)*/ Level.Loaded.StartOutpost is not { ExitPoints.Count: > 0 }) { ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); } } } else { var transitionType = GetAvailableTransition(out _, out Submarine leavingSub); if (Level.Loaded.IsEndBiome && transitionType == TransitionType.ProgressToNextLocation) { LoadNewLevel(); } else if (transitionType == TransitionType.ProgressToNextLocation && Level.Loaded.EndOutpost != null && Level.Loaded.EndOutpost.DockedTo.Contains(leavingSub)) { LoadNewLevel(); } else if (transitionType == TransitionType.ReturnToPreviousLocation && Level.Loaded.StartOutpost != null && Level.Loaded.StartOutpost.DockedTo.Contains(leavingSub)) { LoadNewLevel(); } else if (transitionType == TransitionType.None && CampaignUI.SelectedTab == InteractionType.Map) { ShowCampaignUI = false; } HintManager.OnAvailableTransition(transitionType); } if (!CrewDead) { if (CrewManager.GetCharacters().None(c => !c.IsDead && !CrewManager.IsFired(c))) { CrewDead = true; } } else { endTimer -= deltaTime; if (endTimer <= 0.0f) { GameOver(); } } } private bool TryEndRound() { var transitionType = GetAvailableTransition(out LevelData nextLevel, out Submarine leavingSub); if (leavingSub == null || transitionType == TransitionType.None) { return false; } if (nextLevel == null) { //no level selected -> force the player to select one ForceMapUI = true; CampaignUI.SelectTab(InteractionType.Map); map.SelectLocation(-1); return false; } else if (transitionType == TransitionType.ProgressToNextEmptyLocation) { Map.SetLocation(Map.Locations.IndexOf(Level.Loaded.EndLocation ?? Map.CurrentLocation)); } var subsToLeaveBehind = GetSubsToLeaveBehind(leavingSub); if (subsToLeaveBehind.Any()) { LocalizedString msg = TextManager.Get(subsToLeaveBehind.Count == 1 ? "LeaveSubBehind" : "LeaveSubsBehind"); var msgBox = new GUIMessageBox(TextManager.Get("Warning"), msg, new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked += (btn, userdata) => { LoadNewLevel(); return true; } ; msgBox.Buttons[0].OnClicked += msgBox.Close; msgBox.Buttons[0].UserData = Submarine.Loaded.FindAll(s => !subsToLeaveBehind.Contains(s)); msgBox.Buttons[1].OnClicked += msgBox.Close; } else { LoadNewLevel(); } return true; } private void GameOver() { gameOver = true; GameMain.GameSession.EndRound("", transitionType: TransitionType.None); EnableRoundSummaryGameOverState(); } private void EnableRoundSummaryGameOverState() { var roundSummary = GameMain.GameSession.RoundSummary; if (roundSummary != null) { roundSummary.ContinueButton.Visible = false; roundSummary.ContinueButton.IgnoreLayoutGroups = true; new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), roundSummary.ButtonArea.RectTransform), TextManager.Get("QuitButton")) { OnClicked = (GUIButton button, object obj) => { GameMain.MainMenuScreen.Select(); GUIMessageBox.MessageBoxes.Remove(roundSummary.Frame); return true; } }; new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), roundSummary.ButtonArea.RectTransform), TextManager.Get("LoadGameButton")) { OnClicked = (GUIButton button, object obj) => { GameMain.GameSession.LoadPreviousSave(); GUIMessageBox.MessageBoxes.Remove(roundSummary.Frame); return true; } }; } } public override void Save(XElement element, bool isSavingOnLoading) { XElement modeElement = new XElement("SinglePlayerCampaign", new XAttribute("purchasedlostshuttles", PurchasedLostShuttles), new XAttribute("purchasedhullrepairs", PurchasedHullRepairs), new XAttribute("purchaseditemrepairs", PurchasedItemRepairs), new XAttribute("cheatsenabled", CheatsEnabled)); modeElement.Add(Settings.Save()); modeElement.Add(SaveStats()); if (GameMain.GameSession?.EventManager != null) { modeElement.Add(GameMain.GameSession?.EventManager.Save()); } foreach ((CharacterTeamType team, Identifier unlockedRecipe) in GameMain.GameSession.UnlockedRecipes) { modeElement.Add( new XElement("unlockedrecipe", new XAttribute("identifier", unlockedRecipe), new XAttribute("team", team))); } //save and remove all items that are in someone's inventory so they don't get included in the sub file as well foreach (Character c in Character.CharacterList) { if (c.Info == null) { continue; } if (c.IsDead) { CrewManager.RemoveCharacterInfo(c.Info); } c.Info.LastControlled = c == lastControlledCharacter; c.Info.HealthData = new XElement("health"); c.CharacterHealth.Save(c.Info.HealthData); if (c.Inventory != null) { c.Info.InventoryData = new XElement("inventory"); c.SaveInventory(); c.Inventory?.DeleteAllItems(); } c.Info.SaveOrderData(); } SavePets(modeElement); var crewManagerElement = CrewManager.Save(modeElement); SaveActiveOrders(crewManagerElement); CampaignMetadata.Save(modeElement); Map.Save(modeElement); CargoManager?.SavePurchasedItems(modeElement); UpgradeManager?.Save(modeElement); modeElement.Add(Bank.Save()); element.Add(modeElement); } } }