4 using Microsoft.Xna.Framework;
6 using System.Collections.Generic;
11 static class ObjectiveManager
15 public readonly record
struct Text(
17 int Width = DefaultWidth,
18 int Height = DefaultHeight,
19 Anchor Anchor = Anchor.Center);
21 public readonly record
struct Video(
24 int Width = DefaultWidth,
25 int Height = DefaultHeight)
27 public string FileName => Path.GetFileName(FullPath.CleanUpPath());
28 public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath());
31 private const int DefaultWidth = 450;
32 private const int DefaultHeight = 80;
39 public readonly Identifier
Id;
56 return new Segment(
id, objectiveTextTag, autoPlayVideo, textContent, videoContent);
61 return new Segment(
id, objectiveTextTag, onClickObjective);
66 return new Segment(
id, objectiveTextTag);
69 private Segment(Identifier
id, Identifier objectiveTextTag,
AutoPlayVideo autoPlayVideo,
Text textContent =
default,
Video videoContent =
default)
72 ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value));
79 private Segment(Identifier
id, Identifier objectiveTextTag, Action onClickObjective)
82 ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value));
87 private Segment(Identifier
id, Identifier objectiveTextTag)
90 ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag).Fallback(objectiveTextTag.Value));
101 private readonly record
struct ScreenSettings(
102 Point ScreenResolution =
default,
103 float UiScale =
default,
104 WindowMode WindowMode =
default)
106 public bool HaveChanged() =>
107 GameMain.GraphicsWidth != ScreenResolution.X ||
108 GameMain.GraphicsHeight != ScreenResolution.Y ||
109 GUI.Scale != UiScale ||
110 GameSettings.CurrentConfig.Graphics.DisplayMode != WindowMode;
113 private const float ObjectiveComponentAnimationTime = 1.5f;
115 public static bool ContentRunning {
get;
private set; }
117 public static VideoPlayer VideoPlayer {
get; } =
new VideoPlayer();
119 private static Segment ActiveContentSegment {
get;
set; }
121 private readonly
static List<Segment> activeObjectives =
new List<Segment>();
122 private static GUIComponent infoBox;
123 private static Action infoBoxClosedCallback;
124 private static ScreenSettings screenSettings;
125 private static GUILayoutGroup objectiveGroup;
126 private static LocalizedString objectiveTextTranslated;
128 public static void AddToGUIUpdateList()
130 if (screenSettings.HaveChanged())
132 CreateObjectiveFrame();
134 if (activeObjectives.Count > 0 && GameMain.GameSession?.Campaign is not { ShowCampaignUI: true })
136 objectiveGroup?.AddToGUIUpdateList(order: -1);
138 infoBox?.AddToGUIUpdateList(order: 100);
139 VideoPlayer.AddToGUIUpdateList(order: 100);
142 public static bool IsSegmentActive(Identifier segmentId)
144 return activeObjectives.Any(o => o.Id == segmentId);
147 public static void TriggerSegment(Segment segment,
bool connectObjective =
false)
151 activeObjectives.Add(segment);
152 AddToObjectiveList(segment, connectObjective);
156 Inventory.DraggingItems.Clear();
157 ContentRunning =
true;
158 ActiveContentSegment = segment;
160 var title = TextManager.Get(segment.Id);
161 LocalizedString text = TextManager.GetFormatted(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value);
162 text = TextManager.ParseInputTypes(text);
164 switch (segment.AutoPlayVideo)
167 infoBox = CreateInfoFrame(
170 segment.TextContent.Width,
171 segment.TextContent.Height,
172 segment.TextContent.Anchor,
174 onInfoBoxClosed: LoadActiveContentVideo);
177 infoBox = CreateInfoFrame(
180 segment.TextContent.Width,
181 segment.TextContent.Height,
182 segment.TextContent.Anchor,
184 onInfoBoxClosed: StopCurrentContentSegment,
185 onVideoButtonClicked: LoadActiveContentVideo);
190 public static void CompleteSegment(Identifier segmentId)
192 if (GetActiveObjective(segmentId) is not Segment segment || !segment.CanBeCompleted || segment.IsCompleted)
196 CompleteSegment(segment, failed:
false);
199 public static void FailSegment(Identifier segmentId)
201 if (GetActiveObjective(segmentId) is not Segment segment)
205 CompleteSegment(segment, failed:
true);
208 private static void CompleteSegment(Segment segment,
bool failed =
false)
212 if (!MarkSegmentFailed(segment)) {
return; }
216 if (!MarkSegmentCompleted(segment)) {
return; }
218 if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
220 GameAnalyticsManager.AddDesignEvent($
"Tutorial:{tutorialMode.Tutorial?.Identifier}:{segment.Id}:{(failed ? "Failed
" : "Completed
")}");
224 private static bool MarkSegmentCompleted(Segment segment,
bool flash =
true)
226 return MarkSegment(segment,
"ObjectiveIndicatorCompleted", flash, flashColor: GUIStyle.Green);
229 private static bool MarkSegmentFailed(Segment segment,
bool flash =
true)
231 return MarkSegment(segment,
"MissionFailedIcon", flash, flashColor: GUIStyle.Red);
234 private static bool MarkSegment(Segment segment,
string iconStyleName,
bool flash, Color flashColor)
236 segment.IsCompleted =
true;
237 if (GUIStyle.GetComponentStyle(iconStyleName) is GUIComponentStyle style)
239 if (segment.ObjectiveStateIndicator.Style == style)
243 segment.ObjectiveStateIndicator.ApplyStyle(style);
247 segment.ObjectiveStateIndicator.Parent.Flash(color: flashColor, flashDuration: 0.35f, useRectangleFlash:
true);
249 segment.ObjectiveButton.OnClicked =
null;
250 segment.ObjectiveButton.CanBeFocused =
false;
254 public static void RemoveSegment(Identifier segmentId)
256 if (GetActiveObjective(segmentId) is not Segment segment)
260 segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime,
false);
261 segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime,
false);
262 var parent = segment.LinkedTextBlock.Parent;
263 parent.FadeOut(ObjectiveComponentAnimationTime,
true, onRemove: () =>
265 activeObjectives.Remove(segment);
266 objectiveGroup?.Recalculate();
268 parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime);
269 segment.ObjectiveButton.OnClicked =
null;
270 segment.ObjectiveButton.CanBeFocused =
false;
273 public static void CloseActiveContentGUI()
275 if (VideoPlayer.IsPlaying)
279 else if (infoBox !=
null)
285 public static void ClearContent()
287 ContentRunning =
false;
291 public static void ResetUI()
293 ContentRunning =
false;
295 VideoPlayer.Remove();
299 private static Segment GetActiveObjective(Identifier
id) => activeObjectives.FirstOrDefault(s => s.Id ==
id);
301 public static void ResetObjectives()
303 activeObjectives.Clear();
304 ActiveContentSegment =
null;
305 CreateObjectiveFrame();
311 private static void CreateObjectiveFrame()
313 var objectiveListFrame =
new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.TutorialObjectiveListArea, GUI.Canvas), style:
null)
317 objectiveGroup =
new GUILayoutGroup(
new RectTransform(Vector2.One, objectiveListFrame.RectTransform))
319 AbsoluteSpacing = (int)GUIStyle.Font.LineHeight
321 for (
int i = 0; i < activeObjectives.Count; i++)
323 AddToObjectiveList(activeObjectives[i], useExistingIndex:
true);
325 screenSettings =
new ScreenSettings(
new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Scale, GameSettings.CurrentConfig.Graphics.DisplayMode);
331 private static void StopCurrentContentSegment()
333 if (!ActiveContentSegment.ObjectiveText.IsNullOrEmpty())
335 activeObjectives.Add(ActiveContentSegment);
336 AddToObjectiveList(ActiveContentSegment);
338 ContentRunning =
false;
339 ActiveContentSegment =
null;
345 private static void AddToObjectiveList(Segment segment,
bool connectExisting =
false,
bool useExistingIndex =
false)
349 if (activeObjectives.Find(o => o.Id == segment.Id) is { } existingSegment)
351 existingSegment.ConnectMessageBox(segment);
352 SetButtonBehavior(existingSegment);
357 var frameRt =
new RectTransform(
new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform)
359 MinSize =
new Point(0, objectiveGroup.AbsoluteSpacing)
361 Segment parentSegment = activeObjectives.FirstOrDefault(s => s.Id == segment.ParentId);
362 if (parentSegment is not
null)
365 int childIndex = useExistingIndex ? activeObjectives.IndexOf(segment) :
366 activeObjectives.IndexOf(parentSegment) + activeObjectives.Count(s => s.ParentId == segment.ParentId);
367 if (objectiveGroup.RectTransform.GetChildIndex(frameRt) != childIndex)
369 if (childIndex < 0 || childIndex >= frameRt.Parent.CountChildren)
371 DebugConsole.ThrowError(
372 $
"Error in {nameof(ObjectiveManager.AddToObjectiveList)}. " +
373 $
"Failed to reposition an objective in the list. Text \"{segment.ObjectiveText}\", parentId: {segment.ParentId}, childIndex: {childIndex}");
377 frameRt.RepositionChildInHierarchy(childIndex);
378 activeObjectives.Remove(segment);
379 activeObjectives.Insert(childIndex, segment);
383 frameRt.AbsoluteOffset = GetObjectiveHiddenPosition();
385 var frame =
new GUIFrame(frameRt, style:
null)
390 objectiveGroup.Recalculate();
392 int textWidth = parentSegment is
null ? frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing
393 : frameRt.Rect.Width - 2 * objectiveGroup.AbsoluteSpacing;
394 segment.LinkedTextBlock =
new GUITextBlock(
395 new RectTransform(
new Point(textWidth, 0), frame.RectTransform, anchor:
Anchor.TopRight),
396 TextManager.ParseInputTypes(segment.ObjectiveText),
399 var size =
new Point(segment.LinkedTextBlock.Rect.Width, segment.LinkedTextBlock.Rect.Height);
400 segment.LinkedTextBlock.RectTransform.NonScaledSize = size;
401 segment.LinkedTextBlock.RectTransform.MinSize = size;
402 segment.LinkedTextBlock.RectTransform.MaxSize = size;
403 segment.LinkedTextBlock.RectTransform.IsFixedSize =
true;
404 frame.RectTransform.Resize(
new Point(frame.Rect.Width, segment.LinkedTextBlock.RectTransform.Rect.Height), resizeChildren:
false);
405 frame.RectTransform.IsFixedSize =
true;
407 var indicatorRt =
new RectTransform(
new Point(objectiveGroup.AbsoluteSpacing), frame.RectTransform, isFixedSize:
true);
408 if (parentSegment is not
null)
410 indicatorRt.AbsoluteOffset =
new Point(objectiveGroup.AbsoluteSpacing, 0);
412 segment.ObjectiveStateIndicator =
new GUIImage(indicatorRt,
"ObjectiveIndicatorIncomplete");
414 SetTransparent(segment.LinkedTextBlock);
416 objectiveTextTranslated ??= TextManager.Get(
"Tutorial.Objective");
417 segment.ObjectiveButton =
new GUIButton(
new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform,
Anchor.TopLeft,
Pivot.TopLeft), style:
null)
419 ToolTip = objectiveTextTranslated
421 SetButtonBehavior(segment);
422 SetTransparent(segment.ObjectiveButton);
424 frameRt.MoveOverTime(
new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate());
427 if (!segment.IsCompleted && GameMain.GameSession?.Campaign?.CampaignMetadata is CampaignMetadata data && data.GetBoolean(segment.Id))
429 MarkSegmentCompleted(segment, flash:
false);
432 static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent;
434 void SetButtonBehavior(Segment segment)
436 segment.ObjectiveButton.CanBeFocused = segment.SegmentType !=
SegmentType.Objective;
437 segment.ObjectiveButton.OnClicked = (GUIButton btn,
object userdata) =>
443 ReplaySegmentVideo(segment);
447 ShowSegmentText(segment);
450 else if (segment.SegmentType ==
SegmentType.MessageBox)
452 segment.OnClickObjective?.Invoke();
459 private static void ReplaySegmentVideo(Segment segment)
461 if (ContentRunning) {
return; }
462 Inventory.DraggingItems.Clear();
463 ContentRunning =
true;
467 private static void ShowSegmentText(Segment segment)
469 if (ContentRunning) {
return; }
470 Inventory.DraggingItems.Clear();
471 ContentRunning =
true;
472 ActiveContentSegment = segment;
473 infoBox = CreateInfoFrame(
474 TextManager.Get(segment.Id).Fallback(segment.Id.Value),
475 TextManager.Get(segment.TextContent.Tag).Fallback(segment.TextContent.Tag.Value),
476 segment.TextContent.Width,
477 segment.TextContent.Height,
478 segment.TextContent.Anchor,
480 onInfoBoxClosed: () => ContentRunning =
false,
481 onVideoButtonClicked: () => LoadVideo(segment));
484 private static Point GetObjectiveHiddenPosition(RectTransform rt =
null)
486 return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0);
489 public static Segment GetObjective(Identifier identifier)
491 return activeObjectives.FirstOrDefault(o => o.Id == identifier);
494 public static bool AllActiveObjectivesCompleted()
496 return activeObjectives.None() || activeObjectives.All(o => !o.CanBeCompleted || o.IsCompleted);
499 public static bool AnyObjectives => activeObjectives.Any();
505 private static void CloseInfoFrame() => CloseInfoFrame(
null,
null);
507 private static bool CloseInfoFrame(GUIButton button,
object userData)
510 infoBoxClosedCallback?.Invoke();
517 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)
524 width = (int)(width * GUI.Scale);
525 height = (int)(height * GUI.Scale);
527 LocalizedString wrappedText = ToolBox.WrapText(text, width, GUIStyle.Font);
528 height += (int)GUIStyle.Font.MeasureString(wrappedText).Y;
530 if (title.Length > 0)
532 height += (int)GUIStyle.Font.MeasureString(title).Y + (int)(150 * GUI.Scale);
535 var background =
new GUIFrame(
new RectTransform(
new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas,
Anchor.Center), style:
"GUIBackgroundBlocker");
537 var infoBlock =
new GUIFrame(
new RectTransform(
new Point(width, height), background.RectTransform, anchor));
538 infoBlock.Flash(GUIStyle.Green);
540 var infoContent =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.9f, 0.9f), infoBlock.RectTransform,
Anchor.Center))
546 if (title.Length > 0)
548 var titleBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), infoContent.RectTransform),
549 title, font: GUIStyle.LargeFont, textAlignment: Alignment.Center, textColor:
new Color(253, 174, 0));
550 titleBlock.RectTransform.IsFixedSize =
true;
553 text = RichString.Rich(text);
554 GUITextBlock textBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap:
true);
556 textBlock.RectTransform.IsFixedSize =
true;
557 infoBoxClosedCallback = onInfoBoxClosed;
561 var buttonContainer =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.15f), infoContent.RectTransform), isHorizontal:
true)
563 RelativeSpacing = 0.1f
565 buttonContainer.RectTransform.IsFixedSize =
true;
567 if (onVideoButtonClicked !=
null)
569 buttonContainer.Stretch =
true;
570 var videoButton =
new GUIButton(
new RectTransform(
new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
571 TextManager.Get(
"Video"), style:
"GUIButtonLarge")
573 OnClicked = (GUIButton button,
object obj) =>
575 onVideoButtonClicked();
582 buttonContainer.Stretch =
false;
583 buttonContainer.ChildAnchor =
Anchor.Center;
586 var okButton =
new GUIButton(
new RectTransform(
new Vector2(0.4f, 1.0f), buttonContainer.RectTransform),
587 TextManager.Get(
"OK"), style:
"GUIButtonLarge")
589 OnClicked = CloseInfoFrame
593 infoBlock.RectTransform.NonScaledSize =
new Point(infoBlock.Rect.Width, (
int)(infoContent.Children.Sum(c => c.Rect.Height + infoContent.AbsoluteSpacing) / infoContent.RectTransform.RelativeSize.Y));
604 private static void LoadVideo(Segment segment)
608 VideoPlayer.LoadContent(
609 contentPath: segment.VideoContent.ContentPath,
610 videoSettings:
new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
611 textSettings:
new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width),
612 contentId: segment.Id,
614 objective: segment.ObjectiveText,
615 onStop: StopCurrentContentSegment);
619 VideoPlayer.LoadContent(
620 contentPath: segment.VideoContent.ContentPath,
621 videoSettings:
new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
623 contentId: segment.Id,
625 objective:
string.Empty);
629 private static void LoadActiveContentVideo() => LoadVideo(ActiveContentSegment);
LocalizedString ObjectiveText
GUIButton ObjectiveButton
readonly Text TextContent
void ConnectMessageBox(Segment messageBoxSegment)
GUIImage ObjectiveStateIndicator
static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent=default, Video videoContent=default)
readonly record struct Video(string FullPath, Identifier TextTag, int Width=DefaultWidth, int Height=DefaultHeight)
readonly record struct Text(Identifier Tag, int Width=DefaultWidth, int Height=DefaultHeight, Anchor Anchor=Anchor.Center)
static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
readonly Video VideoContent
static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag)
GUITextBlock LinkedTextBlock
readonly AutoPlayVideo AutoPlayVideo