Build 0.20.4.0

This commit is contained in:
Markus Isberg
2022-11-11 17:57:23 +02:00
parent edaf4b09fe
commit 54712b5dc9
201 changed files with 7618 additions and 2020 deletions
@@ -4,6 +4,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -203,6 +204,10 @@ namespace Barotrauma
}
break;
}
if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted())
{
endRoundButton.Visible = false;
}
if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; }
@@ -266,7 +271,7 @@ namespace Barotrauma
Rand.ThreadId = Thread.CurrentThread.ManagedThreadId;
try
{
GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror);
GameMain.GameSession.StartRound(newLevel, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost());
}
catch (Exception e)
{
@@ -283,6 +288,18 @@ namespace Barotrauma
return loadTask;
}
protected SubmarineInfo GetPredefinedStartOutpost()
{
if (Map?.CurrentLocation?.Type?.GetForcedOutpostGenerationParams() is OutpostGenerationParams parameters && !parameters.OutpostFilePath.IsNullOrEmpty())
{
return new SubmarineInfo(parameters.OutpostFilePath.Value)
{
OutpostGenerationParams = parameters
};
}
return null;
}
partial void NPCInteractProjSpecific(Character npc, Character interactor)
{
if (npc == null || interactor == null) { return; }
@@ -265,8 +265,8 @@ namespace Barotrauma
private IEnumerable<CoroutineStatus> DoLoadInitialLevel(LevelData level, bool mirror)
{
GameMain.GameSession.StartRound(level,
mirrorLevel: mirror);
GameMain.GameSession.StartRound(level, mirrorLevel: mirror, startOutpost: GetPredefinedStartOutpost());
GameMain.GameScreen.Select();
CoroutineManager.StartCoroutine(DoInitialCameraTransition(), "SinglePlayerCampaign.DoInitialCameraTransition");
@@ -1,7 +1,5 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -25,107 +23,9 @@ namespace Barotrauma.Tutorials
#region Tutorial variables
public readonly Identifier Identifier;
public LocalizedString DisplayName { get; }
public bool ContentRunning { get; private set; }
private GUIComponent infoBox;
private Action infoBoxClosedCallback;
private VideoPlayer videoPlayer;
private Point screenResolution;
private WindowMode windowMode;
private float prevUIScale;
private GUILayoutGroup objectiveGroup;
private readonly LocalizedString objectiveTextTranslated;
private readonly List<Segment> ActiveObjectives = new List<Segment>();
private const float ObjectiveComponentAnimationTime = 1.5f;
private Segment ActiveContentSegment { get; set; }
public class Segment
{
public readonly record struct Text(
Identifier Tag,
int Width = DefaultWidth,
int Height = DefaultHeight,
Anchor Anchor = Anchor.Center);
public readonly record struct Video(
string FullPath,
Identifier TextTag,
int Width = DefaultWidth,
int Height = DefaultHeight)
{
public string FileName => Path.GetFileName(FullPath.CleanUpPath());
public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath());
}
private const int DefaultWidth = 450;
private const int DefaultHeight = 80;
public GUIImage ObjectiveStateIndicator;
public GUIButton ObjectiveButton;
public GUITextBlock LinkedTextBlock;
public LocalizedString ObjectiveText;
public readonly Identifier Id;
public readonly Text TextContent;
public readonly Video VideoContent;
public readonly AutoPlayVideo AutoPlayVideo;
public Action OnClickObjective;
public TutorialSegmentType SegmentType { get; private set; }
public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent);
}
public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
return new Segment(id, objectiveTextTag, onClickObjective);
}
public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag)
{
return new Segment(id, objectiveTextTag);
}
private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
AutoPlayVideo = autoPlayVideo;
TextContent = textContent;
VideoContent = videoContent;
SegmentType = TutorialSegmentType.InfoBox;
}
private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
OnClickObjective = onClickObjective;
SegmentType = TutorialSegmentType.MessageBox;
}
private Segment(Identifier id, Identifier objectiveTextTag)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
SegmentType = TutorialSegmentType.Objective;
}
public void ConnectMessageBox(Segment messageBoxSegment)
{
SegmentType = TutorialSegmentType.MessageBox;
OnClickObjective = messageBoxSegment.OnClickObjective;
}
}
public LocalizedString Description { get; }
private bool completed;
public bool Completed
@@ -163,6 +63,8 @@ namespace Barotrauma.Tutorials
public readonly List<(Entity entity, Identifier iconStyle)> Icons = new List<(Entity entity, Identifier iconStyle)>();
public bool Paused { get; private set; }
#endregion
#region Tutorial Controls
@@ -171,8 +73,7 @@ namespace Barotrauma.Tutorials
{
Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier();
DisplayName = TextManager.Get(Identifier);
objectiveTextTranslated = TextManager.Get("Tutorial.Objective");
Description = TextManager.Get($"tutorial.{prefab.Identifier}.description");
TutorialPrefab = prefab;
eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier);
}
@@ -260,35 +161,26 @@ namespace Barotrauma.Tutorials
tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState());
Initialize();
GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch;
GameMain.GameSession.CrewManager.AutoHideCrewList();
if (Character.Controlled?.Inventory is CharacterInventory inventory)
{
foreach (Item item in inventory.AllItemsMod)
{
if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; }
item.Unequip(Character.Controlled);
Character.Controlled.Inventory.RemoveItem(item);
}
}
yield return CoroutineStatus.Success;
}
private void Initialize()
{
GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch;
GameMain.GameSession.CrewManager.AutoHideCrewList();
if (Character.Controlled is Character character)
{
foreach (Item item in character.Inventory.AllItemsMod)
{
if (item.HasTag(TutorialPrefab.StartingItemTags)) { continue; }
item.Unequip(character);
character.Inventory.RemoveItem(item);
}
}
}
public void Start()
{
videoPlayer = new VideoPlayer();
GameMain.Instance.ShowLoading(Loading());
ActiveObjectives.Clear();
ActiveContentSegment = null;
CreateObjectiveFrame();
ObjectiveManager.ResetObjectives();
// Setup doors: Clear all requirements, unless the door is setup as locked.
foreach (var item in Item.ItemList)
@@ -304,24 +196,8 @@ namespace Barotrauma.Tutorials
}
}
public void AddToGUIUpdateList()
{
if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale || GameSettings.CurrentConfig.Graphics.DisplayMode != windowMode)
{
CreateObjectiveFrame();
}
if (ActiveObjectives.Count > 0)
{
objectiveGroup?.AddToGUIUpdateList(order: -1);
}
infoBox?.AddToGUIUpdateList(order: 100);
videoPlayer?.AddToGUIUpdateList(order: 100);
}
public void Update()
{
videoPlayer?.Update();
if (character != null)
{
if (character.Oxygen < 1)
@@ -342,8 +218,7 @@ namespace Barotrauma.Tutorials
{
GUI.PreventPauseMenuToggle = false;
}
ContentRunning = false;
infoBox = null;
ObjectiveManager.ClearContent();
}
else
{
@@ -374,18 +249,6 @@ namespace Barotrauma.Tutorials
yield return CoroutineStatus.Success;
}
public void CloseActiveContentGUI()
{
if (videoPlayer.IsPlaying)
{
videoPlayer.Stop();
}
else if (infoBox != null)
{
CloseInfoFrame();
}
}
public IEnumerable<CoroutineStatus> UpdateState()
{
while (GameMain.Instance.LoadingScreenOpen || Level.Loaded == null || Level.Loaded.Generating)
@@ -432,13 +295,56 @@ namespace Barotrauma.Tutorials
yield return new WaitForSeconds(WaitBeforeFade);
Action onEnd = () => GameMain.MainMenuScreen.ReturnToMainMenu(null, null);
TutorialPrefab nextTutorialPrefab = null;
bool displayEndMessage =
TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Restart ||
(TutorialPrefab.EndMessage.EndType == TutorialPrefab.EndType.Continue && TutorialPrefab.Prefabs.TryGet(TutorialPrefab.EndMessage.NextTutorialIdentifier, out nextTutorialPrefab));
if (displayEndMessage)
{
Paused = true;
var endingMessageBox = new GUIMessageBox(
headerText: "",
text: TextManager.Get($"{Identifier}.completed"),
buttons: new LocalizedString[]
{
TextManager.Get(nextTutorialPrefab is null ? "restart" : "campaigncontinue"),
TextManager.Get("pausemenuquit")
});
endingMessageBox.Buttons[0].OnClicked += (_, _) =>
{
if (nextTutorialPrefab is null)
{
onEnd = () => Restart(null, null);
}
else
{
onEnd = () =>
{
GameMain.MainMenuScreen.ReturnToMainMenu(null, null);
new Tutorial(nextTutorialPrefab).Start();
};
}
return true;
};
endingMessageBox.Buttons[0].OnClicked += endingMessageBox.Close;
endingMessageBox.Buttons[0].OnClicked += (_, _) => Paused = false;
endingMessageBox.Buttons[1].OnClicked += endingMessageBox.Close;
endingMessageBox.Buttons[1].OnClicked += (_, _) => Paused = false;
}
while (Paused) { yield return CoroutineStatus.Running; }
var endCinematic = new CameraTransition(Submarine.MainSub, GameMain.GameScreen.Cam, null, Alignment.Center, panDuration: FadeOutTime);
Completed = true;
while (endCinematic.Running) { yield return CoroutineStatus.Running; }
Stop();
GameMain.MainMenuScreen.ReturnToMainMenu(null, null);
onEnd();
}
}
@@ -450,379 +356,15 @@ namespace Barotrauma.Tutorials
return true;
}
public void TriggerTutorialSegment(Segment segment, bool connectObjective = false)
{
if (segment.SegmentType != TutorialSegmentType.InfoBox)
{
ActiveObjectives.Add(segment);
AddToObjectiveList(segment, connectObjective);
return;
}
Inventory.DraggingItems.Clear();
ContentRunning = true;
ActiveContentSegment = segment;
var title = TextManager.Get(segment.Id);
LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag);
tutorialText = TextManager.ParseInputTypes(tutorialText);
switch (segment.AutoPlayVideo)
{
case AutoPlayVideo.Yes:
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: LoadActiveContentVideo);
break;
case AutoPlayVideo.No:
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: StopCurrentContentSegment,
onVideoButtonClicked: LoadActiveContentVideo);
break;
}
}
public void CompleteTutorialSegment(Identifier segmentId)
{
if (GetActiveObjective(segmentId) is not Segment segment)
{
DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return;
}
if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style)
{
//return if already completed
if (segment.ObjectiveStateIndicator.Style == style) { return; }
segment.ObjectiveStateIndicator.ApplyStyle(style);
}
segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true);
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
GameAnalyticsManager.AddDesignEvent($"Tutorial:{Identifier}:{segmentId}:Completed");
}
public void RemoveTutorialSegment(Identifier segmentId)
{
if (GetActiveObjective(segmentId) is not Segment segment)
{
DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return;
}
segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false);
segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false);
var parent = segment.LinkedTextBlock.Parent;
parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () =>
{
ActiveObjectives.Remove(segment);
objectiveGroup?.Recalculate();
});
parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime);
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
}
private Segment GetActiveObjective(Identifier id) => ActiveObjectives.FirstOrDefault(s => s.Id == id);
public void Stop()
{
if (tutorialCoroutine != null)
{
CoroutineManager.StopCoroutines(tutorialCoroutine);
}
ContentRunning = false;
infoBox = null;
videoPlayer?.Remove();
ObjectiveManager.ResetUI();
}
#endregion
#region Objectives
/// <summary>
/// Create the objective list that holds the objectives (called on start and on resolution change)
/// </summary>
private void CreateObjectiveFrame()
{
var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null);
objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform))
{
AbsoluteSpacing = (int)GUIStyle.Font.LineHeight
};
for (int i = 0; i < ActiveObjectives.Count; i++)
{
AddToObjectiveList(ActiveObjectives[i]);
}
screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
windowMode = GameSettings.CurrentConfig.Graphics.DisplayMode;
prevUIScale = GUI.Scale;
}
/// <summary>
/// Stops content running and adds the active segment to the objective list
/// </summary>
private void StopCurrentContentSegment()
{
if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty())
{
ActiveObjectives.Add(ActiveContentSegment);
AddToObjectiveList(ActiveContentSegment);
}
ContentRunning = false;
ActiveContentSegment = null;
}
/// <summary>
/// Adds the segment to the objective list
/// </summary>
private void AddToObjectiveList(Segment segment, bool connectExisting = false)
{
if (connectExisting)
{
if (ActiveObjectives.Find(o => o.Id == segment.Id) is { } existingSegment)
{
existingSegment.ConnectMessageBox(segment);
SetButtonBehavior(existingSegment);
}
return;
}
var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform)
{
AbsoluteOffset = GetObjectiveHiddenPosition(),
MinSize = new Point(0, objectiveGroup.AbsoluteSpacing)
};
var frame = new GUIFrame(frameRt, style: null)
{
CanBeFocused = true
};
objectiveGroup.Recalculate();
segment.LinkedTextBlock = new GUITextBlock(
new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight),
TextManager.ParseInputTypes(segment.ObjectiveText),
wrap: true);
var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height);
segment.LinkedTextBlock.RectTransform.NonScaledSize = size;
segment.LinkedTextBlock.RectTransform.MinSize = size;
segment.LinkedTextBlock.RectTransform.MaxSize = size;
segment.LinkedTextBlock.RectTransform.IsFixedSize = true;
frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false);
frame.RectTransform.IsFixedSize = true;
var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true);
segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete");
SetTransparent(segment.LinkedTextBlock);
segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null)
{
ToolTip = objectiveTextTranslated
};
SetButtonBehavior(segment);
SetTransparent(segment.ObjectiveButton);
frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate());
static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent;
void SetButtonBehavior(Segment segment)
{
segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective;
segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) =>
{
if (segment.SegmentType == TutorialSegmentType.InfoBox)
{
if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{
ReplaySegmentVideo(segment);
}
else
{
ShowSegmentText(segment);
}
}
else if (segment.SegmentType == TutorialSegmentType.MessageBox)
{
segment.OnClickObjective?.Invoke();
}
return true;
};
}
}
private void ReplaySegmentVideo(Segment segment)
{
if (ContentRunning) { return; }
Inventory.DraggingItems.Clear();
ContentRunning = true;
LoadVideo(segment);
}
private void ShowSegmentText(Segment segment)
{
if (ContentRunning) { return; }
Inventory.DraggingItems.Clear();
ContentRunning = true;
ActiveContentSegment = segment;
infoBox = CreateInfoFrame(
TextManager.Get(segment.Id),
TextManager.Get(segment.TextContent.Tag),
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: () => ContentRunning = false,
onVideoButtonClicked: () => LoadVideo(segment));
}
private Point GetObjectiveHiddenPosition(RectTransform rt = null)
{
return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0);
}
#endregion
#region InfoFrame
private void CloseInfoFrame() => CloseInfoFrame(null, null);
private bool CloseInfoFrame(GUIButton button, object userData)
{
infoBox = null;
infoBoxClosedCallback?.Invoke();
return true;
}
/// <summary>
// Creates and displays a tutorial info box
/// </summary>
private GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null)
{
if (hasButton)
{
height += 60;
}
width = (int)(width * GUI.Scale);
height = (int)(height * GUI.Scale);
LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font);
height += (int)GUIStyle.Font.MeasureString(wrappedText).Y;
if (title.Length > 0)
{
height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale);
}
var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker");
var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor));
infoBlock.Flash(GUIStyle.Green);
var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center))
{
Stretch = true,
AbsoluteSpacing = 5
};
if (title.Length > 0)
{
var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform),
title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0));
titleBlock.RectTransform.IsFixedSize = true;
}
text = RichString.Rich(text);
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true);
textBlock.RectTransform.IsFixedSize = true;
infoBoxClosedCallback = onInfoBoxClosed;
if (hasButton)
{
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.1f
};
buttonContainer.RectTransform.IsFixedSize = true;
if (onVideoButtonClicked != null)
{
buttonContainer.Stretch = true;
var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
TextManager.Get("Video"), style: "GUIButtonLarge")
{
OnClicked = (GUIButton button, object obj) =>
{
onVideoButtonClicked();
return true;
}
};
}
else
{
buttonContainer.Stretch = false;
buttonContainer.ChildAnchor = Anchor.Center;
}
var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
TextManager.Get("OK"), style: "GUIButtonLarge")
{
OnClicked = CloseInfoFrame
};
}
infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y));
SoundPlayer.PlayUISound(GUISoundType.UIMessage);
return background;
}
#endregion
#region Video
private void LoadVideo(Segment segment)
{
videoPlayer ??= new VideoPlayer();
if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{
videoPlayer.LoadContent(
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width),
contentId: segment.Id,
startPlayback: true,
objective: segment.ObjectiveText,
onStop: StopCurrentContentSegment);
}
else
{
videoPlayer.LoadContent(
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: null,
contentId: segment.Id,
startPlayback: true,
objective: string.Empty);
}
}
private void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment);
#endregion
}
}
@@ -6,6 +6,8 @@ namespace Barotrauma
{
public Tutorial Tutorial;
public override bool Paused => Tutorial.Paused;
public TutorialMode(GameModePreset preset) : base(preset) { }
public override void Start()
@@ -19,12 +21,6 @@ namespace Barotrauma
}
}
public override void AddToGUIUpdateList()
{
base.AddToGUIUpdateList();
Tutorial.AddToGUIUpdateList();
}
public override void Update(float deltaTime)
{
base.Update(deltaTime);
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Tutorials;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
@@ -128,8 +129,9 @@ namespace Barotrauma
if (GUI.DisableHUD) { return; }
GameMode?.AddToGUIUpdateList();
tabMenu?.AddToGUIUpdateList();
ObjectiveManager.AddToGUIUpdateList();
if ((!(GameMode is CampaignMode campaign) || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) &&
if ((GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI)) &&
!CoroutineManager.IsCoroutineRunning("LevelTransition") && !CoroutineManager.IsCoroutineRunning("SubmarineTransition"))
{
if (topLeftButtonGroup == null)
@@ -223,6 +225,7 @@ namespace Barotrauma
}
HintManager.Update();
ObjectiveManager.VideoPlayer.Update();
}
public void SetRespawnInfo(bool visible, string text, Color textColor, bool buttonsVisible, bool waitForNextRoundRespawn)
@@ -1,6 +1,7 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Tutorials;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
@@ -209,7 +210,7 @@ namespace Barotrauma
{
if (item.CurrentHull == null) { continue; }
if (item.GetComponent<Pump>() == null) { continue; }
if (!item.HasTag("ballast")) { continue; }
if (!item.HasTag("ballast") && !item.CurrentHull.RoomName.Contains("ballast", StringComparison.OrdinalIgnoreCase)) { continue; }
BallastHulls.Add(item.CurrentHull);
}
}
@@ -383,6 +384,34 @@ namespace Barotrauma
IgnoreReminder("tabmenu");
}
public static void OnObtainedItem(Character character, Item item)
{
if (!CanDisplayHints()) { return; }
if (character != Character.Controlled || item == null) { return; }
if (DisplayHint($"onobtaineditem.{item.Prefab.Identifier}".ToIdentifier())) { return; }
foreach (Identifier tag in item.GetTags())
{
if (DisplayHint($"onobtaineditem.{tag}".ToIdentifier())) { return; }
}
if ((item.HasTag("geneticmaterial") && character.Inventory.FindItemByTag("geneticdevice".ToIdentifier(), recursive: true) != null) ||
(item.HasTag("geneticdevice") && character.Inventory.FindItemByTag("geneticmaterial".ToIdentifier(), recursive: true) != null))
{
if (DisplayHint($"geneticmaterial.useinstructions".ToIdentifier())) { return; }
}
}
public static void OnStartDeconstructing(Character character, Deconstructor deconstructor)
{
if (!CanDisplayHints()) { return; }
if (character != Character.Controlled || deconstructor == null) { return; }
if (deconstructor.InputContainer.Inventory.AllItems.All(it => it.GetComponent<GeneticMaterial>() is not null))
{
DisplayHint($"geneticmaterial.onrefiningorcombining".ToIdentifier());
}
}
public static void OnStoleItem(Character character, Item item)
{
if (!CanDisplayHints()) { return; }
@@ -507,7 +536,7 @@ namespace Barotrauma
if (!CanDisplayHints()) { return; }
if (character != Character.Controlled) { return; }
// Could make this more generic if there will ever be any other status effect related hints
if (!(component is Repairable) || actionType != ActionType.OnFailure) { return; }
if (component is not Repairable || actionType != ActionType.OnFailure) { return; }
DisplayHint("onrepairfailed".ToIdentifier());
}
@@ -563,7 +592,7 @@ namespace Barotrauma
foreach (var me in gap.linkedTo)
{
if (me == Character.Controlled.CurrentHull) { continue; }
if (!(me is Hull adjacentHull)) { continue; }
if (me is not Hull adjacentHull) { continue; }
if (!IsOnFriendlySub()) { continue; }
if (IsWearingDivingSuit()) { continue; }
if (adjacentHull.LethalPressure > 5.0f && DisplayHint("onadjacenthull.highpressure".ToIdentifier())) { return; }
@@ -720,6 +749,7 @@ namespace Barotrauma
if (requireControllingCharacter && Character.Controlled == null) { return false; }
var gameMode = GameMain.GameSession?.GameMode;
if (!(gameMode is CampaignMode || gameMode is MissionMode)) { return false; }
if (ObjectiveManager.AnyObjectives) { return false; }
if (requireGameScreen && Screen.Selected != GameMain.GameScreen) { return false; }
return true;
}
@@ -9,7 +9,7 @@ using Barotrauma.Networking;
namespace Barotrauma
{
internal partial class MedicalClinic
internal sealed partial class MedicalClinic
{
public enum RequestResult
{
@@ -19,63 +19,11 @@ namespace Barotrauma
Timeout
}
public readonly struct RequestAction<T>
{
public readonly Action<T> Callback;
public readonly DateTimeOffset Timeout;
public RequestAction(Action<T> callback, DateTimeOffset timeout)
{
Callback = callback;
Timeout = timeout;
}
}
public readonly struct AfflictionRequest
{
public readonly RequestResult Result;
public readonly ImmutableArray<NetAffliction> Afflictions;
public AfflictionRequest(RequestResult result, ImmutableArray<NetAffliction> afflictions)
{
Result = result;
Afflictions = afflictions;
}
}
public readonly struct PendingRequest
{
public readonly RequestResult Result;
public readonly ImmutableArray<NetCrewMember> CrewMembers;
public PendingRequest(RequestResult result, ImmutableArray<NetCrewMember> crewMembers)
{
Result = result;
CrewMembers = crewMembers;
}
}
public readonly struct CallbackOnlyRequest
{
public readonly RequestResult Result;
public CallbackOnlyRequest(RequestResult result)
{
Result = result;
}
}
public readonly struct HealRequest
{
public readonly RequestResult Result;
public readonly HealRequestResult HealResult;
public HealRequest(RequestResult result, HealRequestResult healResult)
{
Result = result;
HealResult = healResult;
}
}
public readonly record struct RequestAction<T>(Action<T> Callback, DateTimeOffset Timeout);
public readonly record struct AfflictionRequest(RequestResult Result, ImmutableArray<NetAffliction> Afflictions);
public readonly record struct PendingRequest(RequestResult Result, NetCollection<NetCrewMember> CrewMembers);
public readonly record struct CallbackOnlyRequest(RequestResult Result);
public readonly record struct HealRequest(RequestResult Result, HealRequestResult HealResult);
private readonly List<RequestAction<AfflictionRequest>> afflictionRequests = new List<RequestAction<AfflictionRequest>>();
private readonly List<RequestAction<PendingRequest>> pendingHealRequests = new List<RequestAction<PendingRequest>>();
@@ -96,7 +44,7 @@ namespace Barotrauma
}
#endif
if (!(info is { Character: { CharacterHealth: { } health } }))
if (info is not { Character.CharacterHealth: { } health })
{
onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray<NetAffliction>.Empty));
return;
@@ -123,14 +71,14 @@ namespace Barotrauma
public void Update(float deltaTime)
{
DateTimeOffset now = DateTimeOffset.Now;
UpdateQueue(afflictionRequests, now, onTimeout: callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray<NetAffliction>.Empty)); });
UpdateQueue(pendingHealRequests, now, onTimeout: callback => { callback(new PendingRequest(RequestResult.Timeout, ImmutableArray<NetCrewMember>.Empty)); });
UpdateQueue(healAllRequests, now, onTimeout: callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); });
UpdateQueue(afflictionRequests, now, onTimeout: static callback => { callback(new AfflictionRequest(RequestResult.Timeout, ImmutableArray<NetAffliction>.Empty)); });
UpdateQueue(pendingHealRequests, now, onTimeout: static callback => { callback(new PendingRequest(RequestResult.Timeout, NetCollection<NetCrewMember>.Empty)); });
UpdateQueue(healAllRequests, now, onTimeout: static callback => { callback(new HealRequest(RequestResult.Timeout, HealRequestResult.Unknown)); });
UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
}
public bool IsAfflictionPending(NetCrewMember character, NetAffliction affliction)
@@ -148,9 +96,9 @@ namespace Barotrauma
private static bool TryDequeue<T>(List<RequestAction<T>> requestQueue, out Action<T> result)
{
RequestAction<T>? first = requestQueue.FirstOrNull();
if (!(first is { } action))
if (first is not { } action)
{
result = _ => { };
result = static _ => { };
return false;
}
@@ -191,11 +139,25 @@ namespace Barotrauma
private static int GetPing()
{
if (GameMain.IsSingleplayer || !(GameMain.Client?.Name is { } ownName) || !(GameMain.NetworkMember?.ConnectedClients is { } clients)) { return 0; }
if (GameMain.IsSingleplayer || GameMain.Client?.Name is not { } ownName || GameMain.NetworkMember?.ConnectedClients is not { } clients) { return 0; }
return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
}
public void TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
}
public void HealAllButtonAction(Action<HealRequest> onReceived)
{
if (GameMain.IsSingleplayer)
@@ -296,8 +258,11 @@ namespace Barotrauma
private void NewAdditonReceived(IReadMessage inc, MessageFlag flag)
{
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
InsertPendingCrewMember(crewMember);
var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
foreach (var crewMember in crewMembers)
{
InsertPendingCrewMember(crewMember);
}
if (flag == MessageFlag.Response && TryDequeue(addRequests, out var callback))
{
callback(new CallbackOnlyRequest(RequestResult.Success));
@@ -318,11 +283,7 @@ namespace Barotrauma
private static void SendAfflictionRequest(CharacterInfo info)
{
INetSerializableStruct crewMember = new NetCrewMember
{
CharacterInfo = info,
Afflictions = Array.Empty<NetAffliction>()
};
INetSerializableStruct crewMember = new NetCrewMember(info);
ClientSend(crewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable);
}
@@ -337,17 +298,17 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
if (TryDequeue(afflictionRequests, out var callback))
{
RequestResult result = crewMember.CharacterInfoID == 0 ? RequestResult.Error : RequestResult.Success;
RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success;
callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
}
}
private void PendingRequestReceived(IReadMessage inc)
{
NetPendingCrew pendingCrew = INetSerializableStruct.Read<NetPendingCrew>(inc);
var pendingCrew = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
if (TryDequeue(pendingHealRequests, out var callback))
{
callback(new PendingRequest(RequestResult.Success, pendingCrew.CrewMembers.ToImmutableArray()));
callback(new PendingRequest(RequestResult.Success, pendingCrew));
}
}
@@ -0,0 +1,599 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Tutorials;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma;
static class ObjectiveManager
{
public class Segment
{
public readonly record struct Text(
Identifier Tag,
int Width = DefaultWidth,
int Height = DefaultHeight,
Anchor Anchor = Anchor.Center);
public readonly record struct Video(
string FullPath,
Identifier TextTag,
int Width = DefaultWidth,
int Height = DefaultHeight)
{
public string FileName => Path.GetFileName(FullPath.CleanUpPath());
public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath());
}
private const int DefaultWidth = 450;
private const int DefaultHeight = 80;
public GUIImage ObjectiveStateIndicator;
public GUIButton ObjectiveButton;
public GUITextBlock LinkedTextBlock;
public LocalizedString ObjectiveText;
public readonly Identifier Id;
public readonly Text TextContent;
public readonly Video VideoContent;
public readonly AutoPlayVideo AutoPlayVideo;
public Action OnClickObjective;
public bool IsCompleted { get; set; }
public bool CanBeCompleted { get; set; }
public Identifier ParentId { get; set; }
public TutorialSegmentType SegmentType { get; private set; }
public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent);
}
public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
return new Segment(id, objectiveTextTag, onClickObjective);
}
public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag)
{
return new Segment(id, objectiveTextTag);
}
private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
AutoPlayVideo = autoPlayVideo;
TextContent = textContent;
VideoContent = videoContent;
SegmentType = TutorialSegmentType.InfoBox;
}
private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
OnClickObjective = onClickObjective;
SegmentType = TutorialSegmentType.MessageBox;
}
private Segment(Identifier id, Identifier objectiveTextTag)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
SegmentType = TutorialSegmentType.Objective;
}
public void ConnectMessageBox(Segment messageBoxSegment)
{
SegmentType = TutorialSegmentType.MessageBox;
OnClickObjective = messageBoxSegment.OnClickObjective;
}
}
private readonly record struct ScreenSettings(
Point ScreenResolution = default,
float UiScale = default,
WindowMode WindowMode = default)
{
public bool HaveChanged() =>
GameMain.GraphicsWidth != ScreenResolution.X ||
GameMain.GraphicsHeight != ScreenResolution.Y ||
GUI.Scale != UiScale ||
GameSettings.CurrentConfig.Graphics.DisplayMode != WindowMode;
};
private const float ObjectiveComponentAnimationTime = 1.5f;
public static bool ContentRunning { get; private set; }
public static VideoPlayer VideoPlayer { get; } = new VideoPlayer();
private static Segment ActiveContentSegment { get; set; }
private readonly static List<Segment> activeObjectives = new List<Segment>();
private static GUIComponent infoBox;
private static Action infoBoxClosedCallback;
private static ScreenSettings screenSettings;
private static GUILayoutGroup objectiveGroup;
private static LocalizedString objectiveTextTranslated;
public static void AddToGUIUpdateList()
{
if (screenSettings.HaveChanged())
{
CreateObjectiveFrame();
}
if (activeObjectives.Count > 0 && GameMain.GameSession?.Campaign is not { ShowCampaignUI: true })
{
objectiveGroup?.AddToGUIUpdateList(order: -1);
}
infoBox?.AddToGUIUpdateList(order: 100);
VideoPlayer.AddToGUIUpdateList(order: 100);
}
public static void TriggerTutorialSegment(Segment segment, bool connectObjective = false)
{
if (segment.SegmentType != TutorialSegmentType.InfoBox)
{
activeObjectives.Add(segment);
AddToObjectiveList(segment, connectObjective);
return;
}
Inventory.DraggingItems.Clear();
ContentRunning = true;
ActiveContentSegment = segment;
var title = TextManager.Get(segment.Id);
LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag);
tutorialText = TextManager.ParseInputTypes(tutorialText);
switch (segment.AutoPlayVideo)
{
case AutoPlayVideo.Yes:
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: LoadActiveContentVideo);
break;
case AutoPlayVideo.No:
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: StopCurrentContentSegment,
onVideoButtonClicked: LoadActiveContentVideo);
break;
}
}
public static void CompleteTutorialSegment(Identifier segmentId)
{
if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted)
{
return;
}
if (!MarkSegmentCompleted(segment))
{
return;
}
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
GameAnalyticsManager.AddDesignEvent($"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segmentId}:Completed");
}
else if (GameMain.GameSession?.GameMode is CampaignMode campaign)
{
GameAnalyticsManager.AddDesignEvent($"Tutorial:CampaignMode:{segmentId}:Completed");
campaign?.CampaignMetadata?.SetValue(segmentId, true);
}
}
public static bool MarkSegmentCompleted(Segment segment, bool flash = true)
{
segment.IsCompleted = true;
if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style)
{
if (segment.ObjectiveStateIndicator.Style == style)
{
return false;
}
segment.ObjectiveStateIndicator.ApplyStyle(style);
}
if (flash)
{
segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true);
}
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
return true;
}
public static void RemoveTutorialSegment(Identifier segmentId)
{
if (GetActiveObjective(segmentId) is not Segment segment)
{
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{tutorialMode.Tutorial?.Identifier}\" but it isn't active!");
}
return;
}
segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false);
segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false);
var parent = segment.LinkedTextBlock.Parent;
parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () =>
{
activeObjectives.Remove(segment);
objectiveGroup?.Recalculate();
});
parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime);
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
}
public static void CloseActiveContentGUI()
{
if (VideoPlayer.IsPlaying)
{
VideoPlayer.Stop();
}
else if (infoBox != null)
{
CloseInfoFrame();
}
}
public static void ClearContent()
{
ContentRunning = false;
infoBox = null;
}
public static void ResetUI()
{
ContentRunning = false;
infoBox = null;
VideoPlayer.Remove();
}
#region Objectives
private static Segment GetActiveObjective(Identifier id) => activeObjectives.FirstOrDefault(s => s.Id == id);
public static void ResetObjectives()
{
activeObjectives.Clear();
ActiveContentSegment = null;
CreateObjectiveFrame();
}
/// <summary>
/// Create the objective list that holds the objectives (called on start and on resolution change)
/// </summary>
private static void CreateObjectiveFrame()
{
var objectiveListFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style: null)
{
CanBeFocused = false
};
objectiveGroup = new GUILayoutGroup(new RectTransform(Vector2.One, objectiveListFrame.RectTransform))
{
AbsoluteSpacing = (int)GUIStyle.Font.LineHeight
};
for (int i = 0; i < activeObjectives.Count; i++)
{
AddToObjectiveList(activeObjectives[i]);
}
screenSettings = new ScreenSettings(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode);
}
/// <summary>
/// Stops content running and adds the active segment to the objective list
/// </summary>
private static void StopCurrentContentSegment()
{
if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty())
{
activeObjectives.Add(ActiveContentSegment);
AddToObjectiveList(ActiveContentSegment);
}
ContentRunning = false;
ActiveContentSegment = null;
}
/// <summary>
/// Adds the segment to the objective list
/// </summary>
private static void AddToObjectiveList(Segment segment, bool connectExisting = false)
{
if (connectExisting)
{
if (activeObjectives.Find(o => o.Id == segment.Id) is { } existingSegment)
{
existingSegment.ConnectMessageBox(segment);
SetButtonBehavior(existingSegment);
}
return;
}
var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform)
{
MinSize = new Point(0, objectiveGroup.AbsoluteSpacing)
};
Segment parentSegment = activeObjectives.FirstOrDefault(s => s.Id == segment.ParentId);
if (parentSegment is not null)
{
// Add this child as the last child in case there are other existing children already
int totalChildren = activeObjectives.Count(s => s.ParentId == segment.ParentId);
int childIndex = activeObjectives.IndexOf(parentSegment) + totalChildren;
if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex)
{
frameRt.RepositionChildInHierarchy(childIndex);
activeObjectives.Remove(segment);
activeObjectives.Insert(childIndex, segment);
}
}
frameRt.AbsoluteOffset = GetObjectiveHiddenPosition();
var frame = new GUIFrame(frameRt, style: null)
{
CanBeFocused = true
};
objectiveGroup.Recalculate();
int textWidth = parentSegment is null ? frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing
: frameRt.Rect.Width - 2 * objectiveGroup.AbsoluteSpacing;
segment.LinkedTextBlock = new GUITextBlock(
new RectTransform(new Point(textWidth, 0), frame.RectTransform, anchor: Anchor.TopRight),
TextManager.ParseInputTypes(segment.ObjectiveText),
wrap: true);
var size = new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height);
segment.LinkedTextBlock.RectTransform.NonScaledSize = size;
segment.LinkedTextBlock.RectTransform.MinSize = size;
segment.LinkedTextBlock.RectTransform.MaxSize = size;
segment.LinkedTextBlock.RectTransform.IsFixedSize = true;
frame.RectTransform.Resize(new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren: false);
frame.RectTransform.IsFixedSize = true;
var indicatorRt = new RectTransform(new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize: true);
if (parentSegment is not null)
{
indicatorRt.AbsoluteOffset = new Point(objectiveGroup.AbsoluteSpacing, 0);
}
segment.ObjectiveStateIndicator = new GUIImage(indicatorRt, "ObjectiveIndicatorIncomplete");
SetTransparent(segment.LinkedTextBlock);
objectiveTextTranslated ??= TextManager.Get("Tutorial.Objective");
segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null)
{
ToolTip = objectiveTextTranslated
};
SetButtonBehavior(segment);
SetTransparent(segment.ObjectiveButton);
frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate());
// Check if the objective has already been completed in the campaign
if (!segment.IsCompleted && GameMain.GameSession?.Campaign?.CampaignMetadata is CampaignMetadata data && data.GetBoolean(segment.Id))
{
MarkSegmentCompleted(segment, flash: false);
}
static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent;
void SetButtonBehavior(Segment segment)
{
segment.ObjectiveButton.CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective;
segment.ObjectiveButton.OnClicked = (GUIButton btn, object userdata) =>
{
if (segment.SegmentType == TutorialSegmentType.InfoBox)
{
if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{
ReplaySegmentVideo(segment);
}
else
{
ShowSegmentText(segment);
}
}
else if (segment.SegmentType == TutorialSegmentType.MessageBox)
{
segment.OnClickObjective?.Invoke();
}
return true;
};
}
}
private static void ReplaySegmentVideo(Segment segment)
{
if (ContentRunning) { return; }
Inventory.DraggingItems.Clear();
ContentRunning = true;
LoadVideo(segment);
}
private static void ShowSegmentText(Segment segment)
{
if (ContentRunning) { return; }
Inventory.DraggingItems.Clear();
ContentRunning = true;
ActiveContentSegment = segment;
infoBox = CreateInfoFrame(
TextManager.Get(segment.Id),
TextManager.Get(segment.TextContent.Tag),
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: () => ContentRunning = false,
onVideoButtonClicked: () => LoadVideo(segment));
}
private static Point GetObjectiveHiddenPosition(RectTransform rt = null)
{
return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0);
}
public static Segment GetObjective(Identifier identifier)
{
return activeObjectives.FirstOrDefault(o => o.Id == identifier);
}
public static bool AllActiveObjectivesCompleted()
{
return activeObjectives.None() || activeObjectives.All(o => !o.CanBeCompleted || o.IsCompleted);
}
public static bool AnyObjectives => activeObjectives.Any();
#endregion
#region InfoFrame
private static void CloseInfoFrame() => CloseInfoFrame(null, null);
private static bool CloseInfoFrame(GUIButton button, object userData)
{
infoBox = null;
infoBoxClosedCallback?.Invoke();
return true;
}
/// <summary>
// Creates and displays a tutorial info box
/// </summary>
private static GUIComponent CreateInfoFrame(LocalizedString title, LocalizedString text, int width = 300, int height = 80, Anchor anchor = Anchor.TopRight, bool hasButton = false, Action onInfoBoxClosed = null, Action onVideoButtonClicked = null)
{
if (hasButton)
{
height += 60;
}
width = (int)(width * GUI.Scale);
height = (int)(height * GUI.Scale);
LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font);
height += (int)GUIStyle.Font.MeasureString(wrappedText).Y;
if (title.Length > 0)
{
height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale);
}
var background = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker");
var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), background.RectTransform, anchor));
infoBlock.Flash(GUIStyle.Green);
var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), infoBlock.RectTransform, Anchor.Center))
{
Stretch = true,
AbsoluteSpacing = 5
};
if (title.Length > 0)
{
var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform),
title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0));
titleBlock.RectTransform.IsFixedSize = true;
}
text = RichString.Rich(text);
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true);
textBlock.RectTransform.IsFixedSize = true;
infoBoxClosedCallback = onInfoBoxClosed;
if (hasButton)
{
var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal: true)
{
RelativeSpacing = 0.1f
};
buttonContainer.RectTransform.IsFixedSize = true;
if (onVideoButtonClicked != null)
{
buttonContainer.Stretch = true;
var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
TextManager.Get("Video"), style: "GUIButtonLarge")
{
OnClicked = (GUIButton button, object obj) =>
{
onVideoButtonClicked();
return true;
}
};
}
else
{
buttonContainer.Stretch = false;
buttonContainer.ChildAnchor = Anchor.Center;
}
var okButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
TextManager.Get("OK"), style: "GUIButtonLarge")
{
OnClicked = CloseInfoFrame
};
}
infoBlock.RectTransform.NonScaledSize = new Point(infoBlock.Rect.Width, (int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y));
SoundPlayer.PlayUISound(GUISoundType.UIMessage);
return background;
}
#endregion
#region Video
private static void LoadVideo(Segment segment)
{
if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{
VideoPlayer.LoadContent(
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width),
contentId: segment.Id,
startPlayback: true,
objective: segment.ObjectiveText,
onStop: StopCurrentContentSegment);
}
else
{
VideoPlayer.LoadContent(
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: null,
contentId: segment.Id,
startPlayback: true,
objective: string.Empty);
}
}
private static void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment);
#endregion
}