From fbb7daed00e783f192bcf94d31a00c31fb5dd9d9 Mon Sep 17 00:00:00 2001 From: Joonas Rikkonen Date: Fri, 3 May 2019 13:41:23 +0300 Subject: [PATCH] (0c3442136) Merge branch 'dev' into docking-interface --- .../BarotraumaClient/ClientCode.projitems | 5 + .../Source/Characters/Animation/Ragdoll.cs | 11 +- .../Source/Characters/Character.cs | 44 +- .../Source/Characters/CharacterHUD.cs | 15 +- .../Characters/Health/CharacterHealth.cs | 11 + Barotrauma/BarotraumaClient/Source/GUI/GUI.cs | 14 +- .../BarotraumaClient/Source/GUI/GUIButton.cs | 5 + .../Source/GUI/GUIComponent.cs | 73 ++- .../Source/GUI/GUIMessageBox.cs | 6 + .../BarotraumaClient/Source/GUI/GUITextBox.cs | 4 +- .../Source/GUI/VideoPlayer.cs | 89 ++-- .../BarotraumaClient/Source/GameMain.cs | 31 +- .../Source/GameSession/CrewManager.cs | 101 +++- .../GameModes/Tutorials/BasicTutorial.cs | 2 +- .../GameModes/Tutorials/CaptainTutorial.cs | 39 +- .../GameModes/Tutorials/ContextualTutorial.cs | 277 +--------- .../GameModes/Tutorials/DoctorTutorial.cs | 2 +- .../GameModes/Tutorials/EngineerTutorial.cs | 15 +- .../GameModes/Tutorials/MechanicTutorial.cs | 13 +- .../GameModes/Tutorials/ScenarioTutorial.cs | 228 ++++++++- .../GameModes/Tutorials/Tutorial.cs | 484 ++++++++++++++++-- .../GameModes/Tutorials/TutorialMode.cs | 10 +- .../Components/Machines/Deconstructor.cs | 7 +- .../Items/Components/Machines/Fabricator.cs | 75 ++- .../Source/Items/Components/Machines/Pump.cs | 5 +- .../Items/Components/Machines/Reactor.cs | 18 + .../Source/Items/Components/Machines/Sonar.cs | 4 + .../Items/Components/Machines/Steering.cs | 14 +- .../Source/Items/Components/Repairable.cs | 4 + .../Items/Components/Signal/Connection.cs | 36 +- .../Components/Signal/CustomInterface.cs | 19 + .../Source/Items/Inventory.cs | 7 +- .../BarotraumaClient/Source/Items/Item.cs | 2 +- .../BarotraumaClient/Source/Map/Hull.cs | 135 ----- .../BarotraumaClient/Source/Map/Structure.cs | 18 +- .../Source/Screens/CampaignSetupUI.cs | 17 +- .../Source/Screens/MainMenuScreen.cs | 29 +- .../BarotraumaShared/SharedCode.projitems | 1 + .../BarotraumaShared/SharedContent.projitems | 69 +++ .../Source/Characters/AI/EnemyAIController.cs | 2 + .../Source/Characters/Character.cs | 10 +- .../Source/Characters/CharacterInfo.cs | 25 +- .../Source/Items/Components/ItemComponent.cs | 5 +- .../Components/Machines/Deconstructor.cs | 5 + .../Items/Components/Machines/Fabricator.cs | 28 +- .../Source/Items/Inventory.cs | 10 + .../BarotraumaShared/Source/Map/Explosion.cs | 12 +- .../BarotraumaShared/Source/Map/FireSource.cs | 20 +- .../Source/Map/Levels/Level.cs | 37 +- .../BarotraumaShared/Source/Map/Submarine.cs | 8 +- .../Source/Serialization/XMLExtensions.cs | 10 +- .../BarotraumaShared/Source/TextManager.cs | 44 ++ .../Submarines/TutorialOutpost.sub | Bin 24554 -> 24492 bytes 53 files changed, 1442 insertions(+), 713 deletions(-) diff --git a/Barotrauma/BarotraumaClient/ClientCode.projitems b/Barotrauma/BarotraumaClient/ClientCode.projitems index 1b79fd2eb..8360a00c7 100644 --- a/Barotrauma/BarotraumaClient/ClientCode.projitems +++ b/Barotrauma/BarotraumaClient/ClientCode.projitems @@ -43,8 +43,13 @@ + + + + + diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs index 864fd5996..667d29d62 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Animation/Ragdoll.cs @@ -440,14 +440,15 @@ namespace Barotrauma return; } - //foreach (Limb limb in Limbs) - //{ - // limb.Draw(spriteBatch, cam); - //} + Color? color = null; + if (character.ExternalHighlight) + { + color = Color.Lerp(Color.White, Color.OrangeRed, (float)Math.Sin(Timing.TotalTime * 3.5f)); + } for (int i = 0; i < limbs.Length; i++) { - inversedLimbDrawOrder[i].Draw(spriteBatch, cam); + inversedLimbDrawOrder[i].Draw(spriteBatch, cam, color); } LimbJoints.ForEach(j => j.Draw(spriteBatch)); } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs index 5e58e5b4a..89c0898de 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Character.cs @@ -111,6 +111,33 @@ namespace Barotrauma get { return gibEmitters; } } + public class ObjectiveEntity + { + public Entity Entity; + public Sprite Sprite; + public Color Color; + + public ObjectiveEntity(Entity entity, Sprite sprite, Color? color = null) + { + Entity = entity; + Sprite = sprite; + if (color.HasValue) + { + Color = color.Value; + } + else + { + Color = Color.White; + } + } + } + + private List activeObjectiveEntities = new List(); + public IEnumerable ActiveObjectiveEntities + { + get { return activeObjectiveEntities; } + } + partial void InitProjSpecific(XDocument doc) { soundInterval = doc.Root.GetAttributeFloat("soundinterval", 10.0f); @@ -140,9 +167,6 @@ namespace Barotrauma } } - hudProgressBars = new Dictionary(); - } - partial void UpdateLimbLightSource(Limb limb) { if (limb.LightSource != null) @@ -735,6 +759,20 @@ namespace Barotrauma soundTimer = soundInterval; } + public void AddActiveObjectiveEntity(Entity entity, Sprite sprite, Color? color = null) + { + if (activeObjectiveEntities.Any(aoe => aoe.Entity == entity)) return; + ObjectiveEntity objectiveEntity = new ObjectiveEntity(entity, sprite, color); + activeObjectiveEntities.Add(objectiveEntity); + } + + public void RemoveActiveObjectiveEntity(Entity entity) + { + ObjectiveEntity found = activeObjectiveEntities.Find(aoe => aoe.Entity == entity); + if (found == null) return; + activeObjectiveEntities.Remove(found); + } + partial void ImplodeFX() { Vector2 centerOfMass = AnimController.GetCenterOfMass(); diff --git a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs index a2137e660..441ae6a6e 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/CharacterHUD.cs @@ -168,7 +168,12 @@ namespace Barotrauma DrawOrderIndicator(spriteBatch, cam, character, character.CurrentOrder, 1.0f); } } - + + foreach (Character.ObjectiveEntity objectiveEntity in character.ActiveObjectiveEntities) + { + DrawObjectiveIndicator(spriteBatch, cam, character, objectiveEntity, 1.0f); + } + foreach (Item brokenItem in brokenItems) { float dist = Vector2.Distance(character.WorldPosition, brokenItem.WorldPosition); @@ -371,5 +376,13 @@ namespace Barotrauma orderIndicatorCount[target] = orderIndicatorCount[target] + 1; } + + private static void DrawObjectiveIndicator(SpriteBatch spriteBatch, Camera cam, Character character, Character.ObjectiveEntity objectiveEntity, float iconAlpha = 1.0f) + { + if (objectiveEntity == null) return; + + Vector2 drawPos = objectiveEntity.Entity.WorldPosition;// + Vector2.UnitX * objectiveEntity.Sprite.size.X * 1.5f; + GUI.DrawIndicator(spriteBatch, drawPos, cam, 100.0f, objectiveEntity.Sprite, objectiveEntity.Color * iconAlpha); + } } } diff --git a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs index 3799e1756..34ebcb336 100644 --- a/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaClient/Source/Characters/Health/CharacterHealth.cs @@ -136,6 +136,17 @@ namespace Barotrauma } } + public GUIButton CPRButton + { + get { return cprButton; } + } + + public float HealthBarPulsateTimer + { + get { return healthBarPulsateTimer; } + set { healthBarPulsateTimer = MathHelper.Clamp(value, 0.0f, 10.0f); } + } + static CharacterHealth() { damageOverlay = new Sprite("Content/UI/damageOverlay.png", Vector2.Zero); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs index dddd37a0f..90d96a1dd 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUI.cs @@ -129,6 +129,8 @@ namespace Barotrauma get { return pauseMenuOpen; } } + public static bool PreventPauseMenuToggle = false; + public static Color ScreenOverlayColor { get; @@ -1413,6 +1415,7 @@ namespace Barotrauma public static void TogglePauseMenu() { if (Screen.Selected == GameMain.MainMenuScreen) return; + if (PreventPauseMenuToggle) return; settingsMenuOpen = false; @@ -1546,9 +1549,16 @@ namespace Barotrauma if (GameMain.GameSession != null) { - if (ContextualTutorial.Initialized && GameMain.GameSession.GameMode is SinglePlayerCampaign) + if (Tutorial.Initialized) { - ((SinglePlayerCampaign)GameMain.GameSession.GameMode).ContextualTutorial.Stop(); + if (GameMain.GameSession.GameMode is SinglePlayerCampaign) + { + ((SinglePlayerCampaign)GameMain.GameSession.GameMode).ContextualTutorial.Stop(); + } + else + { + ((TutorialMode)GameMain.GameSession.GameMode).Tutorial.Stop(); + } } if (GameSettings.SendUserStatistics) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs index 7917d904f..c4746fa69 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIButton.cs @@ -168,6 +168,11 @@ namespace Barotrauma if (frame != null) frame.ApplyStyle(style); } + public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, Vector2? flashRectInflate = null) + { + Frame.Flash(color, flashDuration, useRectangleFlash, flashRectInflate); + } + protected override void Draw(SpriteBatch spriteBatch) { //do nothing diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs index 8d0a7e1b2..641e793e5 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIComponent.cs @@ -121,25 +121,22 @@ namespace Barotrauma protected Color selectedColor; protected Color pressedColor; + private CoroutineHandle pulsateCoroutine; + protected ComponentState state; protected Color flashColor; protected float flashDuration = 1.5f; + private bool useRectangleFlash; + public float FlashTimer + { + get { return flashTimer; } + } protected float flashTimer; private Vector2 flashRectInflate; public bool IgnoreLayoutGroups; - public bool IgnoreLayoutGroups; - - public bool IgnoreLayoutGroups; - - public bool IgnoreLayoutGroups; - - public bool IgnoreLayoutGroups; - - public bool IgnoreLayoutGroups; - public virtual ScalableFont Font { get; @@ -272,6 +269,8 @@ namespace Barotrauma set { pressedColor = value; } } + public bool ExternalHighlight = false; + private RectTransform rectTransform; public RectTransform RectTransform { @@ -446,11 +445,21 @@ namespace Barotrauma int flashCycleCount = (int)Math.Max(flashDuration, 1); float flashCycleDuration = flashDuration / flashCycleCount; + Rectangle flashRect = Rect; + flashRect.Inflate(flashRectInflate.X, flashRectInflate.Y); + //MathHelper.Pi * 0.8f -> the curve goes from 144 deg to 0, //i.e. quickly bumps up from almost full brightness to full and then fades out - GUI.UIGlow.Draw(spriteBatch, - rect, - flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f)); + if (!useRectangleFlash) + { + GUI.UIGlow.Draw(spriteBatch, + flashRect, + flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f)); + } + else + { + GUI.DrawRectangle(spriteBatch, flashRect, flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f), true); + } } } @@ -475,6 +484,7 @@ namespace Barotrauma toolTipBlock.WrappedText.Split('\n').Length * 18 + 7); toolTipBlock.userData = toolTip; } + toolTipBlock.SetTextPos(); toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom); if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10) @@ -498,9 +508,11 @@ namespace Barotrauma color = new Color(color.R / 255.0f, color.G / 255.0f, color.B / 255.0f, a); } - public virtual void Flash(Color? color = null, float flashDuration = 1.5f) + public virtual void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, Vector2? flashRectInflate = null) { flashTimer = flashDuration; + this.flashRectInflate = flashRectInflate ?? Vector2.Zero; + this.useRectangleFlash = useRectangleFlash; this.flashDuration = flashDuration; flashColor = (color == null) ? Color.Red : (Color)color; } @@ -518,12 +530,9 @@ namespace Barotrauma while (t < duration) { t += CoroutineManager.DeltaTime; - SetAlpha(MathHelper.Lerp(startA, to, t / duration)); - yield return CoroutineStatus.Running; } - toolTipBlock.SetTextPos(); SetAlpha(to); @@ -534,9 +543,31 @@ namespace Barotrauma yield return CoroutineStatus.Success; } + + public void Pulsate(Vector2 startScale, Vector2 endScale, float duration) + { + if (CoroutineManager.IsCoroutineRunning(pulsateCoroutine)) + { + return; + } + pulsateCoroutine = CoroutineManager.StartCoroutine(DoPulsate(startScale, endScale, duration), "Pulsate" + ToString()); + } + + private IEnumerable DoPulsate(Vector2 startScale, Vector2 endScale, float duration) + { + float t = 0.0f; + while (t < duration) + { + t += CoroutineManager.DeltaTime; + RectTransform.LocalScale = Vector2.Lerp(startScale, endScale, (float)Math.Sin(t / duration * MathHelper.Pi)); + yield return CoroutineStatus.Running; + } + RectTransform.LocalScale = startScale; + yield return CoroutineStatus.Success; + } #endregion - protected virtual void SetAlpha(float a) + public virtual void ApplyStyle(GUIComponentStyle style) { if (style == null) return; @@ -549,11 +580,7 @@ namespace Barotrauma OutlineColor = style.OutlineColor; - public virtual void Flash(Color? color = null, float flashDuration = 1.5f) - { - flashTimer = flashDuration; - this.flashDuration = flashDuration; - flashColor = (color == null) ? Color.Red : (Color)color; + this.style = style; } } } diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs index de8bc615c..4a9a3bac7 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUIMessageBox.cs @@ -71,6 +71,12 @@ namespace Barotrauma Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; Tag = tag; + InnerFrame = new GUIFrame(new RectTransform(new Point(width, height), RectTransform, Anchor.Center) { IsFixedSize = false }, style: null); + GUI.Style.Apply(InnerFrame, "", this); + + Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 }; + Tag = tag; + if (height == 0) { string wrappedText = ToolBox.WrapText(text, Content.Rect.Width, GUI.Font); diff --git a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs index 73fcd1b3a..886e5aa2c 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/GUITextBox.cs @@ -365,9 +365,9 @@ namespace Barotrauma OnDeselected?.Invoke(this, Keys.None); } - public override void Flash(Color? color = null, float flashDuration = 1.5f) + public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, Vector2? flashRectOffset = null) { - textBlock.Flash(color, flashDuration); + textBlock.Flash(color, flashDuration, useRectangleFlash, flashRectOffset); } protected override void Update(float deltaTime) diff --git a/Barotrauma/BarotraumaClient/Source/GUI/VideoPlayer.cs b/Barotrauma/BarotraumaClient/Source/GUI/VideoPlayer.cs index bb327ac69..83a9e98f6 100644 --- a/Barotrauma/BarotraumaClient/Source/GUI/VideoPlayer.cs +++ b/Barotrauma/BarotraumaClient/Source/GUI/VideoPlayer.cs @@ -10,9 +10,10 @@ namespace Barotrauma { class VideoPlayer { + public bool IsPlaying; + private Video currentVideo; private string filePath; - private bool isPlaying; private GUIFrame background, videoFrame, textFrame; private GUITextBlock title, textContent, objectiveTitle, objectiveText; @@ -29,7 +30,9 @@ namespace Barotrauma private readonly int objectiveFrameHeight = 60; private readonly int textHeight = 25; - public struct TextSettings + private bool useTextOnRightSide = false; + + public class TextSettings { public string Text; public int Width; @@ -41,7 +44,7 @@ namespace Barotrauma } } - public struct VideoSettings + public class VideoSettings { public string File; @@ -62,7 +65,14 @@ namespace Barotrauma background = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas, Anchor.Center), "InnerFrame", backgroundColor); videoFrame = new GUIFrame(new RectTransform(Point.Zero, background.RectTransform, Anchor.Center, Pivot.Center), "SonarFrame"); - textFrame = new GUIFrame(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), "TextFrame"); + if (useTextOnRightSide) + { + textFrame = new GUIFrame(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.CenterLeft, Pivot.CenterLeft), "TextFrame"); + } + else + { + textFrame = new GUIFrame(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), "TextFrame"); + } videoView = new GUICustomComponent(new RectTransform(Point.Zero, videoFrame.RectTransform, Anchor.Center), (spriteBatch, guiCustomComponent) => { DrawVideo(spriteBatch, guiCustomComponent.Rect); }); title = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.VideoTitleFont, textColor: new Color(253, 174, 0), textAlignment: Alignment.Left); @@ -70,7 +80,7 @@ namespace Barotrauma textContent = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopLeft, Pivot.TopLeft), string.Empty, font: GUI.Font, textAlignment: Alignment.TopLeft); objectiveTitle = new GUITextBlock(new RectTransform(new Vector2(1f, 0f), textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.ObjectiveTitleFont, textAlignment: Alignment.CenterRight, textColor: Color.White); - objectiveTitle.Text = TextManager.Get("NewObjective"); + objectiveTitle.Text = TextManager.Get("Tutorial.NewObjective"); objectiveText = new GUITextBlock(new RectTransform(Point.Zero, textFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter), string.Empty, font: GUI.ObjectiveNameFont, textColor: new Color(4, 180, 108), textAlignment: Alignment.CenterRight); objectiveTitle.Visible = objectiveText.Visible = false; @@ -78,12 +88,12 @@ namespace Barotrauma public void Play() { - isPlaying = true; + IsPlaying = true; } public void Stop() { - isPlaying = false; + IsPlaying = false; if (currentVideo == null) return; currentVideo.Dispose(); currentVideo = null; @@ -99,13 +109,6 @@ namespace Barotrauma public void Update() { if (currentVideo == null) return; - - if (PlayerInput.KeyHit(Keys.Enter) || PlayerInput.KeyHit(Keys.Escape)) - { - DisposeVideo(null, null); - return; - } - if (currentVideo.IsPlaying) return; currentVideo.Dispose(); @@ -115,7 +118,7 @@ namespace Barotrauma public void AddToGUIUpdateList(bool ignoreChildren = false, int order = 0) { - if (!isPlaying) return; + if (!IsPlaying) return; background.AddToGUIUpdateList(ignoreChildren, order); } @@ -139,7 +142,7 @@ namespace Barotrauma currentVideo = CreateVideo(scaledVideoResolution); title.Text = TextManager.Get(contentId); - textContent.Text = textSettings.Text; + textContent.Text = textSettings != null ? textSettings.Text : string.Empty; objectiveText.Text = objective; AdjustFrames(videoSettings, textSettings); @@ -165,7 +168,8 @@ namespace Barotrauma title.TextScale = textContent.TextScale = objectiveText.TextScale = objectiveTitle.TextScale = GUI.Scale; int scaledBorderSize = (int)(borderSize * GUI.Scale); - int scaledTextWidth = (int)(textSettings.Width * GUI.Scale); + int scaledTextWidth = 0; + if (textSettings != null) scaledTextWidth = useTextOnRightSide ? (int)(textSettings.Width * GUI.Scale) : scaledVideoResolution.X / 2; int scaledTitleHeight = (int)(titleHeight * GUI.Scale); int scaledTextHeight = (int)(textHeight * GUI.Scale); int scaledObjectiveFrameHeight = (int)(objectiveFrameHeight * GUI.Scale); @@ -180,13 +184,21 @@ namespace Barotrauma title.RectTransform.NonScaledSize += new Point(scaledTextWidth, scaledTitleHeight); title.RectTransform.AbsoluteOffset = new Point((int)(5 * GUI.Scale), (int)(10 * GUI.Scale)); - if (!string.IsNullOrEmpty(textSettings.Text)) + if (textSettings != null && !string.IsNullOrEmpty(textSettings.Text)) { textSettings.Text = ToolBox.WrapText(textSettings.Text, scaledTextWidth, GUI.Font); int wrappedHeight = textSettings.Text.Split('\n').Length * scaledTextHeight; textFrame.RectTransform.NonScaledSize += new Point(scaledTextWidth + scaledBorderSize, wrappedHeight + scaledBorderSize + scaledButtonSize.Y + scaledTitleHeight); - textFrame.RectTransform.AbsoluteOffset = new Point(scaledVideoResolution.X + scaledBorderSize * 2, 0); + + if (useTextOnRightSide) + { + textFrame.RectTransform.AbsoluteOffset = new Point(scaledVideoResolution.X + scaledBorderSize * 2, 0); + } + else + { + textFrame.RectTransform.AbsoluteOffset = new Point(0, scaledVideoResolution.Y + scaledBorderSize * 2); + } textContent.RectTransform.NonScaledSize += new Point(scaledTextWidth, wrappedHeight); textContent.RectTransform.AbsoluteOffset = new Point(0, scaledBorderSize + scaledTitleHeight); @@ -209,22 +221,41 @@ namespace Barotrauma objectiveTitle.Visible = objectiveText.Visible = false; } - int totalFrameWidth = videoFrame.Rect.Width + textFrame.Rect.Width + scaledBorderSize * 2; - int xOffset = videoFrame.Rect.Width / 2 + scaledBorderSize - (videoFrame.Rect.Width / 2 - textFrame.Rect.Width / 2); - - - videoFrame.RectTransform.AbsoluteOffset = new Point(-xOffset, (int)(50 * GUI.Scale)); - if (okButton != null) { textFrame.RemoveChild(okButton); okButton = null; } - okButton = new GUIButton(new RectTransform(scaledButtonSize, textFrame.RectTransform, Anchor.BottomRight, Pivot.BottomRight) { AbsoluteOffset = new Point(scaledBorderSize, scaledBorderSize) }, TextManager.Get("OK")) + if (textSettings != null) { - OnClicked = DisposeVideo - }; + if (useTextOnRightSide) + { + int totalFrameWidth = videoFrame.Rect.Width + textFrame.Rect.Width + scaledBorderSize * 2; + int xOffset = videoFrame.Rect.Width / 2 + scaledBorderSize - (videoFrame.Rect.Width / 2 - textFrame.Rect.Width / 2); + videoFrame.RectTransform.AbsoluteOffset = new Point(-xOffset, (int)(50 * GUI.Scale)); + } + else + { + int totalFrameHeight = videoFrame.Rect.Height + textFrame.Rect.Height + scaledBorderSize * 2; + int yOffset = videoFrame.Rect.Height / 2 + scaledBorderSize - (videoFrame.Rect.Height / 2 - textFrame.Rect.Height / 2); + videoFrame.RectTransform.AbsoluteOffset = new Point(0, -yOffset); + } + + okButton = new GUIButton(new RectTransform(scaledButtonSize, textFrame.RectTransform, Anchor.BottomRight, Pivot.BottomRight) { AbsoluteOffset = new Point(scaledBorderSize, scaledBorderSize) }, TextManager.Get("OK")) + { + OnClicked = DisposeVideo + }; + } + else + { + videoFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(100 * GUI.Scale)); + + okButton = new GUIButton(new RectTransform(scaledButtonSize, videoFrame.RectTransform, Anchor.TopLeft, Pivot.Center), TextManager.Get("Back")) + { + OnClicked = DisposeVideo + }; + } } private Video CreateVideo(Point resolution) @@ -245,7 +276,7 @@ namespace Barotrauma private void DrawVideo(SpriteBatch spriteBatch, Rectangle rect) { - if (!isPlaying) return; + if (!IsPlaying) return; spriteBatch.Draw(currentVideo.GetTexture(), rect, Color.White); } diff --git a/Barotrauma/BarotraumaClient/Source/GameMain.cs b/Barotrauma/BarotraumaClient/Source/GameMain.cs index 6ea5cbce7..8777ff743 100644 --- a/Barotrauma/BarotraumaClient/Source/GameMain.cs +++ b/Barotrauma/BarotraumaClient/Source/GameMain.cs @@ -159,14 +159,10 @@ namespace Barotrauma public GameMain() { -#if !DEBUG && OSX - // Use a separate path for content that's editable due to macOS's .app bundles crashing when edited during runtime + #if !DEBUG && OSX string macPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library/Barotrauma"); Directory.SetCurrentDirectory(macPath); - Content.RootDirectory = macPath + "/Content"; -#else - Content.RootDirectory = "Content"; -#endif + #endif GraphicsDeviceManager = new GraphicsDeviceManager(this); @@ -188,6 +184,10 @@ namespace Barotrauma GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); + GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window); + + + PerformanceCounter = new PerformanceCounter(); PerformanceCounter = new PerformanceCounter(); @@ -638,8 +638,12 @@ namespace Barotrauma { ((GUIMessageBox)GUIMessageBox.VisibleBox).Close(); } + else if (Tutorial.Initialized && Tutorial.ContentRunning) + { + (GameMain.GameSession.GameMode as TutorialMode).Tutorial.CloseActiveContentGUI(); + } else if ((Character.Controlled?.SelectedConstruction == null || !Character.Controlled.SelectedConstruction.ActiveHUDs.Any(ic => ic.GuiFrame != null)) - && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null) + && Inventory.SelectedSlot == null && CharacterHealth.OpenHealthWindow == null) { // Otherwise toggle pausing, unless another window/interface is open. GUI.TogglePauseMenu(); @@ -647,7 +651,7 @@ namespace Barotrauma } GUI.ClearUpdateList(); - paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || ContextualTutorial.ContentRunning) && + paused = (DebugConsole.IsOpen || GUI.PauseMenuOpen || GUI.SettingsMenuOpen || Tutorial.ContentRunning) && (NetworkMember == null || !NetworkMember.GameStarted); Screen.Selected.AddToGUIUpdateList(); @@ -666,9 +670,9 @@ namespace Barotrauma { Screen.Selected.Update(Timing.Step); } - else if (ContextualTutorial.Initialized && ContextualTutorial.ContentRunning && GameSession.GameMode is SinglePlayerCampaign) + else if (Tutorial.Initialized && Tutorial.ContentRunning) { - (GameSession.GameMode as SinglePlayerCampaign).ContextualTutorial.Update((float)Timing.Step); + (GameSession.GameMode as TutorialMode).Update((float)Timing.Step); } if (NetworkMember != null) @@ -790,6 +794,13 @@ namespace Barotrauma Config.SaveNewPlayerConfig(); } + msgBox.Text.RectTransform.MaxSize = new Point(int.MaxValue, msgBox.Text.Rect.Height); + linkHolder.RectTransform.MaxSize = new Point(int.MaxValue, linkHolder.Rect.Height); + msgBox.RectTransform.MinSize = new Point(0, msgBox.Rect.Height + linkHolder.Rect.Height + msgBox.Buttons.First().Rect.Height * 8); + Config.EditorDisclaimerShown = true; + Config.SaveNewPlayerConfig(); + } + // ToDo: Move texts/links to localization, when possible. public void ShowBugReporter() { diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs index 01b7c98e9..dad1414cb 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/CrewManager.cs @@ -49,6 +49,8 @@ namespace Barotrauma private GUIComponent orderTargetFrame, orderTargetFrameShadow; + public bool AllowCharacterSwitch = true; + public bool ToggleCrewAreaOpen { get { return toggleCrewAreaOpen; } @@ -112,6 +114,41 @@ namespace Barotrauma }; scrollButtonDown.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipVertically); + if (isSinglePlayer) + { + ChatBox = new ChatBox(guiFrame, isSinglePlayer: true) + { + OnEnterMessage = (textbox, text) => + { + if (Character.Controlled?.Info == null) + { + textbox.Deselect(); + textbox.Text = ""; + return true; + } + + textbox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default]; + + if (!string.IsNullOrWhiteSpace(text)) + { + string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg); + AddSinglePlayerChatMessage( + Character.Controlled.Info.Name, + msg, + ((msgCommand == "r" || msgCommand == "radio") && ChatMessage.CanUseRadio(Character.Controlled)) ? ChatMessageType.Radio : ChatMessageType.Default, + Character.Controlled); + var headset = GetHeadset(Character.Controlled, true); + if (headset != null && headset.CanTransmit()) + { + headset.TransmitSignal(stepsTaken: 0, signal: msg, source: headset.Item, sender: Character.Controlled, sendToChat: false); + } + } + textbox.Deselect(); + textbox.Text = ""; + return true; + } + }; + var characterInfo = new CharacterInfo(subElement); characterInfos.Add(characterInfo); foreach (XElement invElement in subElement.Elements()) @@ -173,16 +210,6 @@ namespace Barotrauma ToggleCrewAreaOpen = GameMain.Config.CrewMenuOpen; } - - #endregion - - #region Character list management - - public Rectangle GetCharacterListArea() - { - return characterListBox.Rect; - } - partial void InitProjectSpecific() { guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUICanvas.Instance), null, Color.Transparent) @@ -571,7 +598,10 @@ namespace Barotrauma orderButtonFrame.RectTransform; var btn = new GUIButton(new RectTransform(new Point(iconSize, iconSize), btnParent, Anchor.CenterLeft), - style: null); + style: null) + { + UserData = order + }; new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlow") { @@ -663,6 +693,7 @@ namespace Barotrauma /// public bool CharacterClicked(GUIComponent component, object selection) { + if (!AllowCharacterSwitch) { return false; } Character character = selection as Character; if (character == null || character.IsDead || character.IsUnconscious) return false; SelectCharacter(character); @@ -703,8 +734,17 @@ namespace Barotrauma { characterListBox.BarScroll = roundedPos; } + } - return false; + public void AddCharacterInfo(CharacterInfo characterInfo) + { + if (characterInfos.Contains(characterInfo)) + { + DebugConsole.ThrowError("Tried to add the same character info to CrewManager twice.\n" + Environment.StackTrace); + return; + } + + characterInfos.Add(characterInfo); } private IEnumerable KillCharacterAnim(GUIComponent component) @@ -905,6 +945,22 @@ namespace Barotrauma } } } + + character.SetOrder(order, option, orderGiver, speak: orderGiver != character); + if (IsSinglePlayer) + { + orderGiver?.Speak( + order.GetChatMessage(character.Name, orderGiver.CurrentHull?.DisplayName, givingOrderToSelf: character == orderGiver, orderOption: option), null); + } + else if (orderGiver != null) + { + OrderChatMessage msg = new OrderChatMessage(order, option, order.TargetItemComponent?.Item, character, orderGiver); + if (GameMain.Client != null) + { + GameMain.Client.SendChatMessage(msg); + } + } + DisplayCharacterOrder(character, order); } /// @@ -1059,6 +1115,24 @@ namespace Barotrauma color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f); } + public void HighlightOrderButton(Character character, string orderAiTag, Color color, Vector2? flashRectInflate = null) + { + var order = Order.PrefabList.Find(o => o.AITag == orderAiTag); + if (order == null) + { + DebugConsole.ThrowError("Could not find an order with the AI tag \"" + orderAiTag + "\".\n" + Environment.StackTrace); + return; + } + var characterElement = characterListBox.Content.FindChild(character); + GUIButton orderBtn = characterElement.FindChild(order, recursive: true) as GUIButton; + if (orderBtn.Frame.FlashTimer <= 0) + { + orderBtn.Flash(color, 1.5f, false, flashRectInflate); + } + + //orderBtn.Pulsate(Vector2.One, Vector2.One * 2.0f, 1.5f); + } + #region Updating and drawing the UI private void DrawMiniMapOverlay(SpriteBatch spriteBatch, GUICustomComponent container) @@ -1116,6 +1190,7 @@ namespace Barotrauma public void SelectNextCharacter() { + if (!AllowCharacterSwitch) { return; } if (GameMain.IsMultiplayer) { return; } if (characters.None()) { return; } SelectCharacter(characters[TryAdjustIndex(1)]); @@ -1123,6 +1198,7 @@ namespace Barotrauma public void SelectPreviousCharacter() { + if (!AllowCharacterSwitch) { return; } if (GameMain.IsMultiplayer) { return; } if (characters.None()) { return; } SelectCharacter(characters[TryAdjustIndex(-1)]); @@ -1130,6 +1206,7 @@ namespace Barotrauma private void SelectCharacter(Character character) { + if (!AllowCharacterSwitch) { return; } //make the previously selected character wait in place for some time //(so they don't immediately start idling and walking away from their station) if (Character.Controlled?.AIController?.ObjectiveManager != null) diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs index 95f619e22..0a0677484 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/BasicTutorial.cs @@ -303,7 +303,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(1.0f); - infoBox = CreateInfoFrame("Uh-oh... Something enormous just appeared on the sonar."); + infoBox = CreateInfoFrame("", "Uh-oh... Something enormous just appeared on the sonar."); List windows = new List(); foreach (Structure s in Structure.WallList) diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs index 00ae9403c..35520cad9 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/CaptainTutorial.cs @@ -148,7 +148,7 @@ namespace Barotrauma.Tutorials TriggerTutorialSegment(0); do { - yield return null; + yield return new WaitForSeconds(1.5f); GameMain.GameSession.CrewManager.HighlightOrderButton(captain_medic, "follow", highlightColor, new Vector2(5, 5)); } while (!HasOrder(captain_medic, "follow")); @@ -163,31 +163,28 @@ namespace Barotrauma.Tutorials GameMain.GameSession.CrewManager.AddCharacter(captain_mechanic); do { - yield return null; + yield return new WaitForSeconds(1.5f); GameMain.GameSession.CrewManager.HighlightOrderButton(captain_mechanic, "repairsystems", highlightColor, new Vector2(5, 5)); - HighlightOrderOption("jobspecific"); } - while (!HasOrder(captain_mechanic, "repairsystems", "jobspecific")); + while (!HasOrder(captain_mechanic, "repairsystems")); RemoveCompletedObjective(segments[1]); yield return new WaitForSeconds(2f); TriggerTutorialSegment(2); GameMain.GameSession.CrewManager.AddCharacter(captain_security); do { - yield return null; + yield return new WaitForSeconds(1.5f); GameMain.GameSession.CrewManager.HighlightOrderButton(captain_security, "operateweapons", highlightColor, new Vector2(5, 5)); - HighlightOrderOption("fireatwill"); } - while (!HasOrder(captain_security, "operateweapons", "fireatwill")); + while (!HasOrder(captain_security, "operateweapons")); RemoveCompletedObjective(segments[2]); yield return new WaitForSeconds(4f); TriggerTutorialSegment(3); GameMain.GameSession.CrewManager.AddCharacter(captain_engineer); do { - yield return null; + yield return new WaitForSeconds(1.5f); GameMain.GameSession.CrewManager.HighlightOrderButton(captain_engineer, "operatereactor", highlightColor, new Vector2(5, 5)); - HighlightOrderOption("powerup"); } while (!HasOrder(captain_engineer, "operatereactor", "powerup")); RemoveCompletedObjective(segments[3]); @@ -239,30 +236,6 @@ namespace Barotrauma.Tutorials CoroutineManager.StartCoroutine(TutorialCompleted()); } - private void HighlightOrderOption(string option) - { - if (GameMain.GameSession.CrewManager.OrderOptionButtons.Count == 0) return; - var order = GameMain.GameSession.CrewManager.OrderOptionButtons[0].UserData as Order; - - int orderIndex = 0; - for (int i = 0; i < GameMain.GameSession.CrewManager.OrderOptionButtons.Count; i++) - { - if (orderIndex >= order.Options.Length) - { - orderIndex = 0; - } - if (order.Options[orderIndex] == option) - { - if (GameMain.GameSession.CrewManager.OrderOptionButtons[i].Frame.FlashTimer <= 0) - { - GameMain.GameSession.CrewManager.OrderOptionButtons[i].Frame.Flash(highlightColor); - } - } - - orderIndex++; - } - } - private bool IsSelectedItem(Item item) { return captain?.SelectedConstruction == item; diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ContextualTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ContextualTutorial.cs index d8ac5bc80..b883c43ed 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ContextualTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ContextualTutorial.cs @@ -4,22 +4,12 @@ using System; using Microsoft.Xna.Framework; using Barotrauma.Items.Components; using System.Linq; -using Microsoft.Xna.Framework.Input; namespace Barotrauma.Tutorials { class ContextualTutorial : Tutorial { public static bool Selected = false; - public static bool ContentRunning = false; - public static bool Initialized = false; - - private enum ContentTypes { None = 0, Video = 1, TextOnly = 2 }; - - private TutorialSegment activeSegment; - private List segments; - - private VideoPlayer videoPlayer; private Steering navConsole; private Reactor reactor; @@ -33,83 +23,29 @@ namespace Barotrauma.Tutorials private List> characterTimeOnSonar; private float requiredTimeOnSonar = 5f; - private bool started = false; - private string playableContentPath; - private float tutorialTimer; private bool disableTutorialOnDeficiencyFound = true; - private GUIFrame holderFrame, objectiveFrame; - private List activeObjectives = new List(); - private string objectiveTranslated; - private float floodTutorialTimer = 0.0f; private const float floodTutorialDelay = 2.0f; private float medicalTutorialTimer = 0.0f; private const float medicalTutorialDelay = 2.0f; - private Point screenResolution; - private float prevUIScale; - - private class TutorialSegment - { - public string Id; - public string Objective; - public ContentTypes ContentType; - public XElement TextContent; - public XElement VideoContent; - public bool IsTriggered; - public GUIButton ReplayButton; - public GUITextBlock LinkedTitle, LinkedText; - - public TutorialSegment(XElement config) - { - Id = config.GetAttributeString("id", "Missing ID"); - Objective = TextManager.Get(config.GetAttributeString("objective", string.Empty), true); - Enum.TryParse(config.GetAttributeString("contenttype", "None"), true, out ContentType); - IsTriggered = config.GetAttributeBool("istriggered", false); - - switch (ContentType) - { - case ContentTypes.None: - break; - case ContentTypes.Video: - VideoContent = config.Element("Video"); - TextContent = config.Element("Text"); - break; - case ContentTypes.TextOnly: - TextContent = config.Element("Text"); - break; - } - } - } - public ContextualTutorial(XElement element) : base(element) { - playableContentPath = element.GetAttributeString("playablecontentpath", ""); - segments = new List(); - - foreach (var segment in element.Elements("Segment")) - { - segments.Add(new TutorialSegment(segment)); - } - Name = "ContextualTutorial"; } public override void Initialize() { + base.Initialize(); + for (int i = 0; i < segments.Count; i++) { segments[i].IsTriggered = false; } - if (Initialized) return; - Initialized = true; - - base.Initialize(); - videoPlayer = new VideoPlayer(); characterTimeOnSonar = new List>(); } @@ -167,10 +103,7 @@ namespace Barotrauma.Tutorials base.Start(); injuredMember = null; - activeObjectives.Clear(); - objectiveTranslated = TextManager.Get("Objective"); - CreateObjectiveFrame(); - activeSegment = null; + activeContentSegment = null; tutorialTimer = floodTutorialTimer = medicalTutorialTimer = 0.0f; subStartingPosition = Vector2.Zero; characterTimeOnSonar.Clear(); @@ -183,10 +116,10 @@ namespace Barotrauma.Tutorials #if DEBUG if (reactor == null || navConsole == null || sonar == null) { - infoBox = CreateInfoFrame("Submarine not compatible with the tutorial:" + infoBox = CreateInfoFrame("Error", "Submarine not compatible with the tutorial:" + "\nReactor - " + (reactor != null ? "OK" : "Tag 'reactor' not found") + "\nNavigation Console - " + (navConsole != null ? "OK" : "Tag 'command' not found") - + "\nSonar - " + (sonar != null ? "OK" : "Not found under Navigation Console"), true); + + "\nSonar - " + (sonar != null ? "OK" : "Not found under Navigation Console"), hasButton: true); CoroutineManager.StartCoroutine(WaitForErrorClosed()); return; } @@ -222,62 +155,15 @@ namespace Barotrauma.Tutorials } #endif - public void Stop() + public override void Stop() { - started = ContentRunning = Initialized = false; - videoPlayer.Remove(); - videoPlayer = null; + base.Stop(); characterTimeOnSonar = null; } - private void CreateObjectiveFrame() - { - holderFrame = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center)); - objectiveFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ObjectiveAnchor, holderFrame.RectTransform), style: null); - - for (int i = 0; i < activeObjectives.Count; i++) - { - CreateObjectiveGUI(activeObjectives[i], i); - } - - screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); - prevUIScale = GUI.Scale; - } - - public override void AddToGUIUpdateList() - { - if (videoPlayer != null) - { - videoPlayer.AddToGUIUpdateList(order: 100); - } - - if (GUI.DisableHUD) return; - if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) - { - CreateObjectiveFrame(); - } - - if (objectiveFrame != null && activeObjectives.Count > 0) - { - objectiveFrame.AddToGUIUpdateList(order: -1); - } - base.AddToGUIUpdateList(); - } - public override void Update(float deltaTime) { - if (videoPlayer != null) - { - videoPlayer.Update(); - } - - if (infoBox != null) - { - if (PlayerInput.KeyHit(Keys.Enter) || PlayerInput.KeyHit(Keys.Escape)) - { - CloseInfoFrame(null, null); - } - } + base.Update(deltaTime); if (!started || ContentRunning) return; @@ -285,93 +171,12 @@ namespace Barotrauma.Tutorials for (int i = 0; i < segments.Count; i++) { - if (segments[i].IsTriggered || activeObjectives.Contains(segments[i])) continue; + if (segments[i].IsTriggered || HasObjective(segments[i])) continue; if (CheckContextualTutorials(i, deltaTime)) // Found a relevant tutorial, halt finding new ones { break; } } - - for (int i = 0; i < activeObjectives.Count; i++) - { - CheckActiveObjectives(activeObjectives[i], deltaTime); - } - } - - private void ClosePreTextAndTriggerVideoCallback() - { - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(activeSegment.VideoContent), new VideoPlayer.TextSettings(activeSegment.VideoContent), activeSegment.Id, true, activeSegment.Objective, CurrentSegmentStopCallback); - } - - private void CurrentSegmentStopCallback() - { - if (!string.IsNullOrEmpty(activeSegment.Objective)) - { - AddNewObjective(activeSegment); - } - - activeSegment = null; - ContentRunning = false; - } - - private void AddNewObjective(TutorialSegment segment) - { - activeObjectives.Add(segment); - CreateObjectiveGUI(segment, activeObjectives.Count - 1); - } - - private void CreateObjectiveGUI(TutorialSegment segment, int index) - { - Point replayButtonSize = new Point((int)(GUI.ObjectiveNameFont.MeasureString(segment.Objective).X * GUI.Scale), (int)(GUI.ObjectiveNameFont.MeasureString(segment.Objective).Y * 1.45f * GUI.Scale)); - - segment.ReplayButton = new GUIButton(new RectTransform(replayButtonSize, objectiveFrame.RectTransform, Anchor.TopRight, Pivot.TopRight) { AbsoluteOffset = new Point(0, (replayButtonSize.Y + (int)(20f * GUI.Scale)) * index) }, style: null); - segment.ReplayButton.OnClicked += (GUIButton btn, object userdata) => - { - ReplaySegmentVideo(segment); - return true; - }; - - int yOffset = (int)((GUI.ObjectiveNameFont.MeasureString(objectiveTranslated).Y / 2f + 5) * GUI.Scale); - segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point(replayButtonSize.X, yOffset), segment.ReplayButton.RectTransform, Anchor.Center, Pivot.BottomCenter) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }, objectiveTranslated, textColor: Color.White, font: GUI.ObjectiveTitleFont, textAlignment: Alignment.CenterRight); - segment.LinkedText = new GUITextBlock(new RectTransform(new Point(replayButtonSize.X, yOffset), segment.ReplayButton.RectTransform, Anchor.Center, Pivot.TopCenter) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }, segment.Objective, textColor: new Color(4, 180, 108), font: GUI.ObjectiveNameFont, textAlignment: Alignment.CenterRight); - - segment.LinkedTitle.TextScale = segment.LinkedText.TextScale = GUI.Scale; - - segment.LinkedTitle.Color = segment.LinkedTitle.HoverColor = segment.LinkedTitle.PressedColor = segment.LinkedTitle.SelectedColor = Color.Transparent; - segment.LinkedText.Color = segment.LinkedText.HoverColor = segment.LinkedText.PressedColor = segment.LinkedText.SelectedColor = Color.Transparent; - segment.ReplayButton.Color = segment.ReplayButton.HoverColor = segment.ReplayButton.PressedColor = segment.ReplayButton.SelectedColor = Color.Transparent; - } - - private void RemoveCompletedObjective(TutorialSegment objective) - { - objective.IsTriggered = true; - - int checkMarkHeight = (int)(objective.ReplayButton.Rect.Height * 1.2f); - int checkMarkWidth = (int)(checkMarkHeight * 0.93f); - - Color color = new Color(4, 180, 108); - RectTransform rectTA = new RectTransform(new Point(checkMarkWidth, checkMarkHeight), objective.ReplayButton.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft); - rectTA.AbsoluteOffset = new Point(-rectTA.Rect.Width - 5, 0); - GUIImage checkmark = new GUIImage(rectTA, "CheckMark"); - checkmark.Color = color; - - RectTransform rectTB = new RectTransform(new Vector2(1.1f, .8f), objective.LinkedText.RectTransform, Anchor.Center, Pivot.Center); - GUIImage stroke = new GUIImage(rectTB, "Stroke"); - stroke.Color = color; - - CoroutineManager.StartCoroutine(WaitForObjectiveEnd(objective)); - } - - private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) - { - yield return new WaitForSeconds(2.0f); - objectiveFrame.RemoveChild(objective.ReplayButton); - activeObjectives.Remove(objective); - - for (int i = 0; i < activeObjectives.Count; i++) - { - activeObjectives[i].ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjectives[i].ReplayButton.Rect.Height + 20) * i); - } } private bool CheckContextualTutorials(int index, float deltaTime) @@ -526,17 +331,7 @@ namespace Barotrauma.Tutorials return true; } - private bool HasObjective(string objectiveName) - { - for (int i = 0; i < activeObjectives.Count; i++) - { - if (activeObjectives[i].Id == objectiveName) return true; - } - - return false; - } - - private void CheckActiveObjectives(TutorialSegment objective, float deltaTime) + protected override void CheckActiveObjectives(TutorialSegment objective, float deltaTime) { switch(objective.Id) { @@ -704,50 +499,9 @@ namespace Barotrauma.Tutorials return characterTimeOnSonar.Find(ct => ct.Second >= requiredTimeOnSonar && !ct.First.IsDead) != null; } - private void TriggerTutorialSegment(int index, params object[] args) + protected override void TriggerTutorialSegment(int index, params object[] args) { - Inventory.draggingItem = null; - ContentRunning = true; - activeSegment = segments[index]; - - string tutorialText = TextManager.GetFormatted(activeSegment.TextContent.GetAttributeString("tag", ""), true, args); - string objectiveText = string.Empty; - - if (!string.IsNullOrEmpty(activeSegment.Objective)) - { - if (args.Length == 0) - { - objectiveText = activeSegment.Objective; - } - else - { - objectiveText = string.Format(activeSegment.Objective, args); - } - - activeSegment.Objective = objectiveText; - } - else - { - activeSegment.IsTriggered = true; // Complete at this stage only if no related objective - } - - switch (activeSegment.ContentType) - { - case ContentTypes.None: - break; - case ContentTypes.Video: - infoBox = CreateInfoFrame(TextManager.Get(activeSegment.Id), tutorialText, - activeSegment.TextContent.GetAttributeInt("width", 300), - activeSegment.TextContent.GetAttributeInt("height", 80), - activeSegment.TextContent.GetAttributeString("anchor", "Center"), true, ClosePreTextAndTriggerVideoCallback); - break; - case ContentTypes.TextOnly: - infoBox = CreateInfoFrame(TextManager.Get(activeSegment.Id), tutorialText, - activeSegment.TextContent.GetAttributeInt("width", 300), - activeSegment.TextContent.GetAttributeInt("height", 80), - activeSegment.TextContent.GetAttributeString("anchor", "Center"), true, CurrentSegmentStopCallback); - break; - } + base.TriggerTutorialSegment(index, args); for (int i = 0; i < segments.Count; i++) { @@ -757,13 +511,6 @@ namespace Barotrauma.Tutorials CoroutineManager.StartCoroutine(WaitToStop()); // Completed } - private void ReplaySegmentVideo(TutorialSegment segment) - { - if (ContentRunning) return; - ContentRunning = true; - videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); - } - private IEnumerable WaitToStop() { while (ContentRunning) yield return null; diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs index b372f675b..8ad220618 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/DoctorTutorial.cs @@ -170,7 +170,7 @@ namespace Barotrauma.Tutorials yield return new WaitForSeconds(2.0f); }*/ - TriggerTutorialSegment(0, GameMain.Config.KeyBind(InputType.Select), GameMain.Config.KeyBind(InputType.Deselect)); // Medical supplies objective + TriggerTutorialSegment(0, GameMain.Config.KeyBind(InputType.Use), GameMain.Config.KeyBind(InputType.Deselect)); // Medical supplies objective do { diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs index f230542a1..7cedf8fad 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/EngineerTutorial.cs @@ -272,7 +272,7 @@ namespace Barotrauma.Tutorials } yield return null; - } while (!engineer_equipmentCabinet.Inventory.IsEmpty()); // Wait until looted + } while (engineer.Inventory.FindItemByIdentifier("screwdriver") == null || engineer.Inventory.FindItemByIdentifier("redwire") == null || engineer.Inventory.FindItemByIdentifier("bluewire") == null); // Wait until looted RemoveCompletedObjective(segments[0]); SetHighlight(engineer_equipmentCabinet.Item, false); SetHighlight(engineer_reactor.Item, true); @@ -286,7 +286,6 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(engineer_reactor.Item)) { - engineer_reactor.AutoTempSlider.BarScrollValue = 1.0f; if (engineer_reactor.OnOffSwitch.FlashTimer <= 0) { engineer_reactor.OnOffSwitch.Flash(highlightColor, 1.5f, false); @@ -298,7 +297,6 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(engineer_reactor.Item) && engineer_reactor.Item.OwnInventory.slots != null) { - engineer_reactor.AutoTempSlider.BarScrollValue = 1.0f; HighlightInventorySlot(engineer.Inventory, "fuelrod", highlightColor, 0.5f, 0.5f, 0f); for (int i = 0; i < engineer_reactor.Item.OwnInventory.slots.Length; i++) @@ -313,7 +311,6 @@ namespace Barotrauma.Tutorials { if (IsSelectedItem(engineer_reactor.Item)) { - engineer_reactor.AutoTempSlider.BarScrollValue = 1.0f; if (engineer_reactor.FissionRateScrollBar.FlashTimer <= 0) { engineer_reactor.FissionRateScrollBar.Flash(highlightColor, 1.5f); @@ -339,16 +336,6 @@ namespace Barotrauma.Tutorials } yield return null; } while (!engineer_reactor.AutoTemp); - - float wait = 1.5f; - do - { - yield return new WaitForSeconds(0.1f); - wait -= 0.1f; - engineer_reactor.AutoTempSlider.BarScrollValue = 0.0f; - } while (wait > 0.0f); - engineer.SelectedConstruction = null; - engineer_reactor.CanBeSelected = false; RemoveCompletedObjective(segments[1]); SetHighlight(engineer_reactor.Item, false); SetHighlight(engineer_brokenJunctionBox, true); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs index 6cc78431d..72dca0c66 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/MechanicTutorial.cs @@ -317,14 +317,11 @@ namespace Barotrauma.Tutorials SetHighlight(mechanic_workingPump.Item, true); do { - yield return null; - if (IsSelectedItem(mechanic_brokenPump.Item)) + if (mechanic_workingPump.IsActiveSlider.FlashTimer <= 0) { - if (mechanic_workingPump.IsActiveSlider.FlashTimer <= 0) - { - mechanic_workingPump.IsActiveSlider.Flash(uiHighlightColor, 1.5f, true); - } + mechanic_workingPump.IsActiveSlider.Flash(uiHighlightColor, 1.5f, true); } + yield return null; } while (mechanic_workingPump.FlowPercentage >= 0 || !mechanic_workingPump.IsActive); // Highlight until draining SetHighlight(mechanic_workingPump.Item, false); do { yield return null; } while (mechanic_brokenhull_1.WaterPercentage > waterVolumeBeforeOpening); // Unlock door once drained @@ -508,12 +505,11 @@ namespace Barotrauma.Tutorials mechanic.AddActiveObjectiveEntity(mechanic_brokenWall_2, mechanic_repairIcon, mechanic_repairIconColor); do { yield return null; } while (WallHasDamagedSections(mechanic_brokenWall_2)); mechanic.RemoveActiveObjectiveEntity(mechanic_brokenWall_2); - TriggerTutorialSegment(9, GameMain.Config.KeyBind(InputType.Use)); // Repairing machinery (pump) + TriggerTutorialSegment(9, GameMain.Config.KeyBind(InputType.Select)); // Repairing machinery (pump) SetHighlight(mechanic_brokenPump.Item, true); Repairable repairablePumpComponent = mechanic_brokenPump.Item.GetComponent(); do { - yield return null; if (!mechanic_brokenPump.Item.IsFullCondition) { if (!mechanic.HasEquippedItem("wrench")) @@ -528,6 +524,7 @@ namespace Barotrauma.Tutorials } } } + yield return null; } while (!mechanic_brokenPump.Item.IsFullCondition || mechanic_brokenPump.FlowPercentage >= 0 || !mechanic_brokenPump.IsActive); RemoveCompletedObjective(segments[9]); SetHighlight(mechanic_brokenPump.Item, false); diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs index 025ae979e..cbff14dca 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/ScenarioTutorial.cs @@ -1,44 +1,118 @@ -using System; +using Barotrauma.Items.Components; +using Microsoft.Xna.Framework; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Xml.Linq; namespace Barotrauma.Tutorials { class ScenarioTutorial : Tutorial { + private CoroutineHandle tutorialCoroutine; + private Character character; + private string spawnSub; private SpawnType spawnPointType; private string submarinePath; + private string startOutpostPath; + private string endOutpostPath; private string levelSeed; + private string levelParams; + + private Submarine startOutpost = null; + private Submarine endOutpost = null; + private bool currentTutorialCompleted = false; + private float fadeOutTime = 3f; + protected float waitBeforeFade = 4f; + + // Colors + protected Color highlightColor = Color.OrangeRed; + protected Color uiHighlightColor = new Color(150, 50, 0); + protected Color buttonHighlightColor = new Color(255, 100, 0); + protected Color inaccessibleColor = Color.Red; + protected Color accessibleColor = Color.Green; public ScenarioTutorial(XElement element) : base(element) { submarinePath = element.GetAttributeString("submarinepath", ""); + startOutpostPath = element.GetAttributeString("startoutpostpath", ""); + endOutpostPath = element.GetAttributeString("endoutpostpath", ""); + levelSeed = element.GetAttributeString("levelseed", "tuto"); - Enum.TryParse(element.GetAttributeString("spawnpointtype", "Human"), true, out spawnPointType); + levelParams = element.GetAttributeString("levelparams", ""); + + spawnSub = element.GetAttributeString("spawnsub", ""); + Enum.TryParse(element.GetAttributeString("spawnpointtype", "Human"), true, out spawnPointType); } public override void Initialize() { base.Initialize(); + currentTutorialCompleted = false; GameMain.Instance.ShowLoading(Loading()); } + private IEnumerable Loading() + { + Submarine.MainSub = Submarine.Load(submarinePath, "", true); + + LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Name == levelParams); + + yield return CoroutineStatus.Running; + + GameMain.GameSession = new GameSession(Submarine.MainSub, "", + GameModePreset.List.Find(g => g.Identifier == "tutorial")); + (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; + + if (generationParams != null) + { + Biome biome = LevelGenerationParams.GetBiomes().Find(b => generationParams.AllowedBiomes.Contains(b)); + + if (startOutpostPath != string.Empty) + { + startOutpost = Submarine.Load(startOutpostPath, "", false); + } + + if (endOutpostPath != string.Empty) + { + endOutpost = Submarine.Load(endOutpostPath, "", false); + } + + Level tutorialLevel = new Level(levelSeed, 0, 0, generationParams, biome, startOutpost, endOutpost); + GameMain.GameSession.StartRound(tutorialLevel); + } + else + { + GameMain.GameSession.StartRound(levelSeed); + } + + GameMain.GameSession.EventManager.Events.Clear(); + GameMain.GameSession.EventManager.Enabled = false; + GameMain.GameScreen.Select(); + + yield return CoroutineStatus.Success; + } + public override void Start() { base.Start(); - WayPoint wayPoint = WayPoint.GetRandom(spawnPointType, null); + Submarine.MainSub.GodMode = true; + + CharacterInfo charInfo = configElement.Element("Character") == null ? + new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")) : + new CharacterInfo(configElement.Element("Character")); + + WayPoint wayPoint = GetSpawnPoint(charInfo); + if (wayPoint == null) { DebugConsole.ThrowError("A waypoint with the spawntype \"" + spawnPointType + "\" is required for the tutorial event"); return; } - CharacterInfo charInfo = configElement.Element("Character") == null ? - new CharacterInfo(Character.HumanConfigFile, "", JobPrefab.List.Find(jp => jp.Identifier == "engineer")) : - new CharacterInfo(configElement.Element("Character")); - character = Character.Create(charInfo, wayPoint.WorldPosition, "", false, false); Character.Controlled = character; character.GiveJobItems(null); @@ -52,22 +126,82 @@ namespace Barotrauma.Tutorials idCard.AddTag("com"); idCard.AddTag("eng"); - CoroutineManager.StartCoroutine(UpdateState()); + tutorialCoroutine = CoroutineManager.StartCoroutine(UpdateState()); } - private IEnumerable Loading() + public override void AddToGUIUpdateList() { - Submarine.MainSub = Submarine.Load(submarinePath, "", true); - yield return CoroutineStatus.Running; + if (!currentTutorialCompleted) + { + base.AddToGUIUpdateList(); + } + } - GameMain.GameSession = new GameSession(Submarine.MainSub, "", - GameModePreset.List.Find(g => g.Identifier == "tutorial")); - (GameMain.GameSession.GameMode as TutorialMode).tutorial = this; - GameMain.GameSession.StartRound(levelSeed); - GameMain.GameSession.EventManager.Events.Clear(); - GameMain.GameScreen.Select(); + private WayPoint GetSpawnPoint(CharacterInfo charInfo) + { + Submarine spawnSub = null; - yield return CoroutineStatus.Success; + if (this.spawnSub != string.Empty) + { + switch (this.spawnSub) + { + case "startoutpost": + spawnSub = startOutpost; + break; + + case "endoutpost": + spawnSub = endOutpost; + break; + + default: + spawnSub = Submarine.MainSub; + break; + } + } + + return WayPoint.GetRandom(spawnPointType, charInfo.Job, spawnSub); + } + + protected bool HasOrder(Character character, string aiTag, string option = null) + { + if (character.CurrentOrder?.AITag == aiTag) + { + if (option == null) + { + return true; + } + else + { + HumanAIController humanAI = character.AIController as HumanAIController; + return humanAI.CurrentOrderOption == option; + } + } + + return false; + } + + protected void SetHighlight(Item item, bool state) + { + if (item.ExternalHighlight == state) return; + item.SpriteColor = (state) ? highlightColor : Color.White; + item.ExternalHighlight = state; + } + + protected void SetHighlight(Structure structure, bool state) + { + structure.SpriteColor = (state) ? highlightColor : Color.White; + structure.ExternalHighlight = state; + } + + protected void SetHighlight(Character character, bool state) + { + character.ExternalHighlight = state; + } + + protected void SetDoorAccess(Door door, LightComponent light, bool state) + { + if (state && door != null) door.requiredItems.Clear(); + if (light != null) light.LightColor = (state) ? accessibleColor : inaccessibleColor; } public override void Update(float deltaTime) @@ -75,27 +209,47 @@ namespace Barotrauma.Tutorials base.Update(deltaTime); if (character != null) { - if (Character.Controlled == null) + if (character.Oxygen < 1) { - CoroutineManager.StopCoroutines("TutorialMode.UpdateState"); + character.Oxygen = 1; + } + if (character.IsDead) + { + CoroutineManager.StartCoroutine(Dead()); + } + else if (Character.Controlled == null) + { + if (tutorialCoroutine != null) + { + CoroutineManager.StopCoroutines(tutorialCoroutine); + } infoBox = null; } else if (Character.Controlled.IsDead) { - Character.Controlled = null; - - CoroutineManager.StopCoroutines("TutorialMode.UpdateState"); - infoBox = null; CoroutineManager.StartCoroutine(Dead()); } } } + public override void Stop() + { + if (tutorialCoroutine != null) + { + CoroutineManager.StopCoroutines(tutorialCoroutine); + } + base.Stop(); + } + private IEnumerable Dead() { + GUI.PreventPauseMenuToggle = true; + Character.Controlled = character = null; + Stop(); + yield return new WaitForSeconds(3.0f); - var messageBox = new GUIMessageBox("You have died", "Do you want to try again?", new string[] { "Yes", "No" }); + var messageBox = new GUIMessageBox(TextManager.Get("Tutorial.TryAgainHeader"), TextManager.Get("Tutorial.TryAgain"), new string[] { TextManager.Get("Yes"), TextManager.Get("No") }); messageBox.Buttons[0].OnClicked += Restart; messageBox.Buttons[0].OnClicked += messageBox.Close; @@ -106,5 +260,29 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } + + protected IEnumerable TutorialCompleted() + { + GUI.PreventPauseMenuToggle = true; + + Character.Controlled.ClearInputs(); + Character.Controlled = null; + + yield return new WaitForSeconds(waitBeforeFade); + + var endCinematic = new RoundEndCinematic(Submarine.MainSub, GameMain.GameScreen.Cam, fadeOutTime); + currentTutorialCompleted = Completed = true; + while (endCinematic.Running) yield return null; + Stop(); + GameMain.MainMenuScreen.ReturnToMainMenu(null, null); + } + + protected void Heal(Character character) + { + character.SetAllDamage(0.0f, 0.0f, 0.0f); + character.Oxygen = 100.0f; + character.Bloodloss = 0.0f; + character.SetStun(0.0f, true); + } } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/Tutorial.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/Tutorial.cs index 69a5e98c9..22cc125a4 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/Tutorial.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/Tutorial.cs @@ -1,15 +1,23 @@ using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Reflection; +using System.Linq; using System.Xml.Linq; +using Barotrauma.Items.Components; +using Barotrauma.Extensions; namespace Barotrauma.Tutorials { abstract class Tutorial { + #region Tutorial variables + public static bool Initialized = false; + public static bool ContentRunning = false; public static List Tutorials; + protected bool started = false; protected GUIComponent infoBox; private Action infoBoxClosedCallback; protected XElement configElement; @@ -17,6 +25,54 @@ namespace Barotrauma.Tutorials private enum TutorialType { None, Scenario, Contextual }; private TutorialType tutorialType = TutorialType.None; + protected VideoPlayer videoPlayer; + protected enum TutorialContentTypes { None = 0, Video = 1, ManualVideo = 2, TextOnly = 3 }; + protected string playableContentPath; + protected Point screenResolution; + protected float prevUIScale; + + private GUIFrame holderFrame, objectiveFrame; + private List activeObjectives = new List(); + private string objectiveTranslated; + + protected TutorialSegment activeContentSegment; + protected List segments; + + protected class TutorialSegment + { + public string Id; + public string Objective; + public TutorialContentTypes ContentType; + public XElement TextContent; + public XElement VideoContent; + public bool IsTriggered; + public GUIButton ReplayButton; + public GUITextBlock LinkedTitle, LinkedText; + public object[] Args; + + public TutorialSegment(XElement config) + { + Id = config.GetAttributeString("id", "Missing ID"); + Objective = TextManager.Get(config.GetAttributeString("objective", string.Empty), true); + Enum.TryParse(config.GetAttributeString("contenttype", "None"), true, out ContentType); + IsTriggered = config.GetAttributeBool("istriggered", false); + + switch (ContentType) + { + case TutorialContentTypes.None: + break; + case TutorialContentTypes.Video: + case TutorialContentTypes.ManualVideo: + VideoContent = config.Element("Video"); + TextContent = config.Element("Text"); + break; + case TutorialContentTypes.TextOnly: + TextContent = config.Element("Text"); + break; + } + } + } + public string Name { get; @@ -34,7 +90,9 @@ namespace Barotrauma.Tutorials GameMain.Config.SaveNewPlayerConfig(); } } + #endregion + #region Tutorial Controls public static void Init() { Tutorials = new List(); @@ -107,26 +165,85 @@ namespace Barotrauma.Tutorials Name = element.GetAttributeString("name", "Unnamed"); completed = GameMain.Config.CompletedTutorialNames.Contains(Name); Enum.TryParse(element.GetAttributeString("tutorialtype", "Scenario"), true, out tutorialType); + playableContentPath = element.GetAttributeString("playablecontentpath", ""); + + segments = new List(); + + foreach (var segment in element.Elements("Segment")) + { + segments.Add(new TutorialSegment(segment)); + } } public virtual void Initialize() { - + if (Initialized) return; + Initialized = true; + videoPlayer = new VideoPlayer(); } public virtual void Start() { - + activeObjectives.Clear(); + objectiveTranslated = TextManager.Get("Tutorial.Objective"); + CreateObjectiveFrame(); + + // Setup doors: Clear all requirements, unless the door is setup as locked. + foreach (var item in Item.ItemList) + { + var door = item.GetComponent(); + if (door != null) + { + if (door.requiredItems.Values.None(ris => ris.None(ri => ri.Identifiers.None(i => i == "locked")))) + { + door.requiredItems.Clear(); + } + } + } } public virtual void AddToGUIUpdateList() { + if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale) + { + CreateObjectiveFrame(); + } + + if (objectiveFrame != null && activeObjectives.Count > 0) + { + objectiveFrame.AddToGUIUpdateList(order: -1); + } + if (infoBox != null) infoBox.AddToGUIUpdateList(order: 100); + if (videoPlayer != null) videoPlayer.AddToGUIUpdateList(order: 100); } public virtual void Update(float deltaTime) { - + if (videoPlayer != null) + { + videoPlayer.Update(); + } + + if (activeObjectives != null) + { + for (int i = 0; i < activeObjectives.Count; i++) + { + CheckActiveObjectives(activeObjectives[i], deltaTime); + } + } + } + + public void CloseActiveContentGUI() + { + if (videoPlayer.IsPlaying) + { + videoPlayer.Stop(); + } + else if (infoBox != null) + { + CloseInfoFrame(null, null); + } } public virtual IEnumerable UpdateState() @@ -134,6 +251,233 @@ namespace Barotrauma.Tutorials yield return CoroutineStatus.Success; } + protected bool Restart(GUIButton button, object obj) + { + GUI.PreventPauseMenuToggle = false; + TutorialMode.StartTutorial(this); + return true; + } + + protected virtual void TriggerTutorialSegment(int index, params object[] args) + { + Inventory.draggingItem = null; + ContentRunning = true; + activeContentSegment = segments[index]; + segments[index].Args = args; + + string tutorialText = TextManager.GetFormatted(activeContentSegment.TextContent.GetAttributeString("tag", ""), true, args); + tutorialText = TextManager.ParseInputTypes(tutorialText); + string objectiveText = string.Empty; + + if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + { + if (args.Length == 0) + { + objectiveText = activeContentSegment.Objective; + } + else + { + objectiveText = string.Format(activeContentSegment.Objective, args); + } + objectiveText = TextManager.ParseInputTypes(objectiveText); + activeContentSegment.Objective = objectiveText; + } + else + { + activeContentSegment.IsTriggered = true; // Complete at this stage only if no related objective + } + + + switch (activeContentSegment.ContentType) + { + case TutorialContentTypes.None: + break; + case TutorialContentTypes.Video: + infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, + activeContentSegment.TextContent.GetAttributeInt("width", 300), + activeContentSegment.TextContent.GetAttributeInt("height", 80), + activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, () => LoadVideo(activeContentSegment)); + break; + case TutorialContentTypes.ManualVideo: + infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, + activeContentSegment.TextContent.GetAttributeInt("width", 300), + activeContentSegment.TextContent.GetAttributeInt("height", 80), + activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment, () => LoadVideo(activeContentSegment, false)); + break; + case TutorialContentTypes.TextOnly: + infoBox = CreateInfoFrame(TextManager.Get(activeContentSegment.Id), tutorialText, + activeContentSegment.TextContent.GetAttributeInt("width", 300), + activeContentSegment.TextContent.GetAttributeInt("height", 80), + activeContentSegment.TextContent.GetAttributeString("anchor", "Center"), true, StopCurrentContentSegment); + break; + } + } + + public virtual void Stop() + { + started = ContentRunning = Initialized = false; + infoBox = null; + if (videoPlayer != null) + { + videoPlayer.Remove(); + videoPlayer = null; + } + } + #endregion + + #region Objectives + private void CreateObjectiveFrame() + { + holderFrame = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight), GUI.Canvas, Anchor.Center)); + objectiveFrame = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.ObjectiveAnchor, holderFrame.RectTransform), style: null); + + for (int i = 0; i < activeObjectives.Count; i++) + { + CreateObjectiveGUI(activeObjectives[i], i, activeObjectives[i].ContentType); + } + + screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); + prevUIScale = GUI.Scale; + } + + protected void StopCurrentContentSegment() + { + if (!string.IsNullOrEmpty(activeContentSegment.Objective)) + { + AddNewObjective(activeContentSegment, activeContentSegment.ContentType); + } + + activeContentSegment = null; + ContentRunning = false; + } + + protected virtual void CheckActiveObjectives(TutorialSegment objective, float deltaTime) + { + + } + + protected bool HasObjective(TutorialSegment segment) + { + return activeObjectives.Contains(segment); + } + + protected void AddNewObjective(TutorialSegment segment, TutorialContentTypes type) + { + activeObjectives.Add(segment); + CreateObjectiveGUI(segment, activeObjectives.Count - 1, type); + } + + private void CreateObjectiveGUI(TutorialSegment segment, int index, TutorialContentTypes type) + { + Point replayButtonSize = new Point((int)(GUI.ObjectiveNameFont.MeasureString(segment.Objective).X * GUI.Scale), (int)(GUI.ObjectiveNameFont.MeasureString(segment.Objective).Y * 1.45f * GUI.Scale)); + + segment.ReplayButton = new GUIButton(new RectTransform(replayButtonSize, objectiveFrame.RectTransform, Anchor.TopRight, Pivot.TopRight) { AbsoluteOffset = new Point(0, (replayButtonSize.Y + (int)(20f * GUI.Scale)) * index) }, style: null); + segment.ReplayButton.OnClicked += (GUIButton btn, object userdata) => + { + if (type == TutorialContentTypes.Video) + { + ReplaySegmentVideo(segment); + } + else + { + ShowSegmentText(segment); + } + return true; + }; + + string objectiveText = TextManager.ParseInputTypes(objectiveTranslated); + int yOffset = (int)((GUI.ObjectiveNameFont.MeasureString(objectiveText).Y / 2f + 5) * GUI.Scale); + segment.LinkedTitle = new GUITextBlock(new RectTransform(new Point((int)GUI.ObjectiveNameFont.MeasureString(objectiveText).X, yOffset), segment.ReplayButton.RectTransform, Anchor.CenterRight, Pivot.BottomRight) { AbsoluteOffset = new Point((int)(-10 * GUI.Scale), 0) }, + objectiveText, textColor: Color.White, font: GUI.ObjectiveTitleFont, textAlignment: Alignment.CenterRight); + segment.LinkedText = new GUITextBlock(new RectTransform(new Point(replayButtonSize.X, yOffset), segment.ReplayButton.RectTransform, Anchor.Center, Pivot.TopCenter) { AbsoluteOffset = new Point((int)(10 * GUI.Scale), 0) }, + TextManager.ParseInputTypes(segment.Objective), textColor: new Color(4, 180, 108), font: GUI.ObjectiveNameFont, textAlignment: Alignment.CenterRight); + + segment.LinkedTitle.TextScale = segment.LinkedText.TextScale = GUI.Scale; + + segment.LinkedTitle.Color = segment.LinkedTitle.HoverColor = segment.LinkedTitle.PressedColor = segment.LinkedTitle.SelectedColor = Color.Transparent; + segment.LinkedText.Color = segment.LinkedText.HoverColor = segment.LinkedText.PressedColor = segment.LinkedText.SelectedColor = Color.Transparent; + segment.ReplayButton.Color = segment.ReplayButton.HoverColor = segment.ReplayButton.PressedColor = segment.ReplayButton.SelectedColor = Color.Transparent; + } + + private void ReplaySegmentVideo(TutorialSegment segment) + { + if (ContentRunning) return; + ContentRunning = true; + videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, callback: () => ContentRunning = false); + } + + private void ShowSegmentText(TutorialSegment segment) + { + if (ContentRunning) return; + Inventory.draggingItem = null; + ContentRunning = true; + + string tutorialText = TextManager.GetFormatted(segment.TextContent.GetAttributeString("tag", ""), true, segment.Args); + + Action videoAction = null; + + if (segment.ContentType != TutorialContentTypes.TextOnly) + { + videoAction = () => LoadVideo(segment, false); + } + + infoBox = CreateInfoFrame(TextManager.Get(segment.Id), tutorialText, + segment.TextContent.GetAttributeInt("width", 300), + segment.TextContent.GetAttributeInt("height", 80), + segment.TextContent.GetAttributeString("anchor", "Center"), true, () => ContentRunning = false, videoAction); + } + + protected void RemoveCompletedObjective(TutorialSegment segment) + { + if (!HasObjective(segment)) return; + segment.IsTriggered = true; + segment.ReplayButton.OnClicked = null; + + int checkMarkHeight = (int)(segment.ReplayButton.Rect.Height * 1.2f); + int checkMarkWidth = (int)(checkMarkHeight * 0.93f); + + Color color = new Color(4, 180, 108); + + int objectiveTextWidth = segment.LinkedText.Rect.Width; + int objectiveTitleWidth = segment.LinkedTitle.Rect.Width; + + RectTransform rectTA; + if (objectiveTextWidth > objectiveTitleWidth) + { + rectTA = new RectTransform(new Point(checkMarkWidth, checkMarkHeight), segment.ReplayButton.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft); + rectTA.AbsoluteOffset = new Point(-rectTA.Rect.Width - 5, 0); + } + else + { + rectTA = new RectTransform(new Point(checkMarkWidth, checkMarkHeight), segment.ReplayButton.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft); + rectTA.AbsoluteOffset = new Point(-rectTA.Rect.Width - 5 - (objectiveTitleWidth), 0); + } + + GUIImage checkmark = new GUIImage(rectTA, "CheckMark"); + checkmark.Color = checkmark.SelectedColor = checkmark.HoverColor = checkmark.PressedColor = color; + + RectTransform rectTB = new RectTransform(new Vector2(1.1f, .8f), segment.LinkedText.RectTransform, Anchor.Center, Pivot.Center); + GUIImage stroke = new GUIImage(rectTB, "Stroke"); + stroke.Color = stroke.SelectedColor = stroke.HoverColor = stroke.PressedColor = color; + + CoroutineManager.StartCoroutine(WaitForObjectiveEnd(segment)); + } + + private IEnumerable WaitForObjectiveEnd(TutorialSegment objective) + { + yield return new WaitForSeconds(2.0f); + objectiveFrame.RemoveChild(objective.ReplayButton); + activeObjectives.Remove(objective); + + for (int i = 0; i < activeObjectives.Count; i++) + { + activeObjectives[i].ReplayButton.RectTransform.AbsoluteOffset = new Point(0, (activeObjectives[i].ReplayButton.Rect.Height + 20) * i); + } + } + + #endregion + + #region InfoFrame protected bool CloseInfoFrame(GUIButton button, object userData) { infoBox = null; @@ -141,73 +485,71 @@ namespace Barotrauma.Tutorials return true; } - protected GUIComponent CreateInfoFrame(string text, bool hasButton = false, Action callback = null) + protected GUIComponent CreateInfoFrame(string title, string text, int width = 300, int height = 80, string anchorStr = "", bool hasButton = false, Action callback = null, Action showVideo = null) { - int width = 300; - int height = hasButton ? 110 : 80; + if (hasButton) height += 60; - string wrappedText = ToolBox.WrapText(text, width, GUI.Font); - - height += wrappedText.Split('\n').Length * 25; - - var infoBlock = new GUIFrame(new RectTransform(new Point(width, height), GUI.Canvas, Anchor.TopRight) { AbsoluteOffset = new Point(20) }); - infoBlock.Flash(Color.Green); - - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 0.7f), infoBlock.RectTransform, Anchor.Center), - text, wrap: true); - - infoBoxClosedCallback = callback; - - if (hasButton) - { - var okButton = new GUIButton(new RectTransform(new Point(160, 50), infoBlock.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, -10) }, - TextManager.Get("OK")) - { - OnClicked = CloseInfoFrame - }; - } - - GUI.PlayUISound(GUISoundType.UIMessage); - - return infoBlock; - } - - protected GUIComponent CreateInfoFrame(string title, string text, int width, int height, string anchorStr, bool hasButton = false, Action callback = null) - { - if (hasButton) height += 30; - - string wrappedText = ToolBox.WrapText(text, width, GUI.Font); - - height += wrappedText.Split('\n').Length * 25; + float textScale = GUI.Scale; + string wrappedText = ToolBox.WrapText(text, width, GUI.Font, textScale); + height += (int)(GUI.Font.MeasureString(wrappedText).Y * textScale + 50); if (title.Length > 0) { height += 35; } Anchor anchor = Anchor.TopRight; - Enum.TryParse(anchorStr, out anchor); + + if (anchorStr != string.Empty) + { + Enum.TryParse(anchorStr, out anchor); + } var infoBlock = new GUIFrame(new RectTransform(new Point((int)(width * GUI.Scale), (int)(height * GUI.Scale)), GUI.Canvas, anchor) { AbsoluteOffset = new Point(20) }); infoBlock.Flash(Color.Green); + var infoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.8f), infoBlock.RectTransform, Anchor.Center)) + { + Stretch = true, + RelativeSpacing = 0.05f + }; + if (title.Length > 0) { - var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1f, .35f), infoBlock.RectTransform, Anchor.TopCenter, - Pivot.TopCenter), title, font: GUI.VideoTitleFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); - titleBlock.TextScale = GUI.Scale; + var titleBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), + title, font: GUI.VideoTitleFont, textAlignment: Alignment.Center, textColor: new Color(253, 174, 0)); + titleBlock.TextScale = textScale; } - var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.9f, 1f), infoBlock.RectTransform, Anchor.BottomCenter), + var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), infoContent.RectTransform), text, wrap: true); - textBlock.TextScale = GUI.Scale; + textBlock.TextScale = textScale; infoBoxClosedCallback = callback; if (hasButton) { - var okButton = new GUIButton(new RectTransform(new Point(160, 50), infoBlock.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, -10) }, - TextManager.Get("OK")) + var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), infoContent.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true) + { + Stretch = true, + RelativeSpacing = 0.1f + }; + + if (showVideo != null) + { + var videoButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("Video"), style: "GUIButtonLarge") + { + OnClicked = (GUIButton button, object obj) => + { + showVideo(); + return true; + } + }; + } + + var okButton = new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f), buttonContainer.RectTransform), + TextManager.Get("OK"), style: "GUIButtonLarge") { OnClicked = CloseInfoFrame }; @@ -217,11 +559,53 @@ namespace Barotrauma.Tutorials return infoBlock; } + #endregion - protected bool Restart(GUIButton button, object obj) + #region Video + protected void LoadVideo(TutorialSegment segment, bool showText = true) { - TutorialMode.StartTutorial(this); - return true; + if (videoPlayer == null) videoPlayer = new VideoPlayer(); + if (showText) + { + videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), new VideoPlayer.TextSettings(segment.VideoContent), segment.Id, true, segment.Objective, StopCurrentContentSegment); + } + else + { + videoPlayer.LoadContent(playableContentPath, new VideoPlayer.VideoSettings(segment.VideoContent), null, segment.Id, true, segment.Objective, null); + } } + #endregion + + #region Highlights + protected void HighlightInventorySlot(Inventory inventory, string identifier, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + { + if (inventory.slots == null) { return; } + for (int i = 0; i < inventory.Items.Length; i++) + { + if (inventory.Items[i] != null && inventory.Items[i].Prefab.Identifier == identifier) + { + HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); + } + } + } + + protected void HighlightInventorySlotWithTag(Inventory inventory, string tag, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + { + if (inventory.slots == null) { return; } + for (int i = 0; i < inventory.Items.Length; i++) + { + if (inventory.Items[i] != null && inventory.Items[i].HasTag(tag)) + { + HighlightInventorySlot(inventory, i, color, fadeInDuration, fadeOutDuration, scaleUpAmount); + } + } + } + + protected void HighlightInventorySlot(Inventory inventory, int index, Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount) + { + if (inventory.slots == null || index < 0 || inventory.slots[index].HighlightTimer > 0) return; + inventory.slots[index].ShowBorderHighlight(color, fadeInDuration, fadeOutDuration, scaleUpAmount); + } + #endregion } } diff --git a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/TutorialMode.cs b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/TutorialMode.cs index 942168cfb..407f742e9 100644 --- a/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/TutorialMode.cs +++ b/Barotrauma/BarotraumaClient/Source/GameSession/GameModes/Tutorials/TutorialMode.cs @@ -4,7 +4,7 @@ namespace Barotrauma { class TutorialMode : GameMode { - public Tutorial tutorial; + public Tutorial Tutorial; public static void StartTutorial(Tutorial tutorial) { @@ -19,18 +19,20 @@ namespace Barotrauma public override void Start() { base.Start(); - tutorial.Start(); + GameMain.GameSession.CrewManager = new CrewManager(true); + Tutorial.Start(); } public override void AddToGUIUpdateList() { - tutorial.AddToGUIUpdateList(); + base.AddToGUIUpdateList(); + Tutorial.AddToGUIUpdateList(); } public override void Update(float deltaTime) { base.Update(deltaTime); - tutorial.Update(deltaTime); + Tutorial.Update(deltaTime); } } } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs index 85150941e..cb8889292 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Deconstructor.cs @@ -9,6 +9,10 @@ namespace Barotrauma.Items.Components { partial class Deconstructor : Powered, IServerSerializable, IClientSerializable { + public GUIButton ActivateButton + { + get { return activateButton; } + } private GUIButton activateButton; private GUIComponent inputInventoryHolder, outputInventoryHolder; private GUICustomComponent inputInventoryOverlay; @@ -44,7 +48,6 @@ namespace Barotrauma.Items.Components Visible = false, CanBeFocused = false }; - outputInventoryHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.3f), paddedFrame.RectTransform), style: null); } @@ -71,7 +74,7 @@ namespace Barotrauma.Items.Components public override void UpdateHUD(Character character, float deltaTime, Camera cam) { inSufficientPowerWarning.Visible = powerConsumption > 0 && voltage < minVoltage; - activateButton.Enabled = !inSufficientPowerWarning.Visible; + //activateButton.Enabled = !inSufficientPowerWarning.Visible; } private bool ToggleActive(GUIButton button, object obj) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs index 4dc0b89a1..f01906f3d 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Fabricator.cs @@ -15,6 +15,10 @@ namespace Barotrauma.Items.Components private GUIFrame selectedItemFrame; + public GUIButton ActivateButton + { + get { return activateButton; } + } private GUIButton activateButton; private GUITextBox itemFilterBox; @@ -22,6 +26,10 @@ namespace Barotrauma.Items.Components private GUIComponent inputInventoryHolder, outputInventoryHolder; private GUICustomComponent inputInventoryOverlay, outputInventoryOverlay; + public FabricationRecipe SelectedItem + { + get { return selectedItem; } + } private FabricationRecipe selectedItem; private GUIComponent inSufficientPowerWarning; @@ -73,7 +81,31 @@ namespace Barotrauma.Items.Components { CanBeFocused = false }; - + + CreateRecipes(); + + activateButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.07f), paddedFrame.RectTransform), + TextManager.Get("FabricatorCreate"), style: "GUIButtonLarge") + { + OnClicked = StartButtonClicked, + UserData = selectedItem, + Enabled = false + }; + + inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), TextManager.Get("FabricatorNoPower"), + textColor: Color.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow") + { + HoverColor = Color.Black, + IgnoreLayoutGroups = true, + Visible = false, + CanBeFocused = false + }; + } + + partial void CreateRecipes() + { + itemList.Content.RectTransform.ClearChildren(); + foreach (FabricationRecipe fi in fabricationRecipes) { GUIFrame frame = new GUIFrame(new RectTransform(new Point(itemList.Rect.Width, 30), itemList.Content.RectTransform), style: null) @@ -101,23 +133,6 @@ namespace Barotrauma.Items.Components }; } } - - activateButton = new GUIButton(new RectTransform(new Vector2(0.8f, 0.07f), paddedFrame.RectTransform), - TextManager.Get("FabricatorCreate"), style: "GUIButtonLarge") - { - OnClicked = StartButtonClicked, - UserData = selectedItem, - Enabled = false - }; - - inSufficientPowerWarning = new GUITextBlock(new RectTransform(Vector2.One, activateButton.RectTransform), TextManager.Get("FabricatorNoPower"), - textColor: Color.Orange, textAlignment: Alignment.Center, color: Color.Black, style: "OuterGlow") - { - HoverColor = Color.Black, - IgnoreLayoutGroups = true, - Visible = false, - CanBeFocused = false - }; } partial void OnItemLoadedProjSpecific() @@ -241,6 +256,7 @@ namespace Barotrauma.Items.Components } } } + private void DrawOutputOverLay(SpriteBatch spriteBatch, GUICustomComponent overlayComponent) { overlayComponent.RectTransform.SetAsLastChild(); @@ -363,6 +379,29 @@ namespace Barotrauma.Items.Components return true; } + public void HighlightRecipe(string identifier, Color color) + { + foreach (GUIComponent child in itemList.Content.Children) + { + FabricationRecipe recipe = child.UserData as FabricationRecipe; + if (recipe?.DisplayName == null) { continue; } + if (recipe.TargetItem.Identifier == identifier) + { + if (child.FlashTimer > 0.0f) return; + child.Flash(color, 1.5f, false); + + for (int i = 0; i < child.CountChildren; i++) + { + var grandChild = child.GetChild(i); + if (grandChild is GUITextBlock) continue; + grandChild.Flash(color, 1.5f, false); + } + + return; + } + } + } + private bool StartButtonClicked(GUIButton button, object obj) { if (selectedItem == null) { return false; } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs index b13727818..5ef0e010c 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Pump.cs @@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components { partial class Pump : Powered, IServerSerializable, IClientSerializable { + public GUIScrollBar IsActiveSlider + { + get { return isActiveSlider; } + } private GUIScrollBar isActiveSlider; private GUIScrollBar pumpSpeedSlider; private GUITickBox powerIndicator; @@ -49,7 +53,6 @@ namespace Barotrauma.Items.Components }; var sliderHandle = isActiveSlider.GetChild(); sliderHandle.RectTransform.NonScaledSize = new Point(84, sliderHandle.Rect.Height); - isActiveSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { bool active = scrollBar.BarScroll < 0.5f; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs index 893943a62..b4d53d0e0 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Reactor.cs @@ -11,7 +11,16 @@ namespace Barotrauma.Items.Components { partial class Reactor : Powered, IServerSerializable, IClientSerializable { + public GUIScrollBar AutoTempSlider + { + get { return autoTempSlider; } + } private GUIScrollBar autoTempSlider; + + public GUIScrollBar OnOffSwitch + { + get { return onOffSwitch; } + } private GUIScrollBar onOffSwitch; private const int GraphSize = 25; @@ -27,7 +36,16 @@ namespace Barotrauma.Items.Components private Sprite graphLine; + public GUIScrollBar FissionRateScrollBar + { + get { return fissionRateScrollBar; } + } private GUIScrollBar fissionRateScrollBar; + + public GUIScrollBar TurbineOutputScrollBar + { + get { return turbineOutputScrollBar; } + } private GUIScrollBar turbineOutputScrollBar; private float[] outputGraph = new float[GraphSize]; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs index 17c3ee623..2b7fa07bd 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Sonar.cs @@ -20,6 +20,10 @@ namespace Barotrauma.Items.Components private bool unsentChanges; private float networkUpdateTimer; + public GUITickBox ActiveTickBox + { + get { return activeTickBox; } + } private GUITickBox activeTickBox, passiveTickBox; private GUITextBlock signalWarningText; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs index f96f11335..e0fc23544 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Machines/Steering.cs @@ -329,10 +329,7 @@ namespace Barotrauma.Items.Components int x = rect.X; int y = rect.Y; - //docking interface ---------------------------------------------------- - dockingContainer = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GuiFrame.RectTransform, Anchor.BottomLeft) - { MinSize = new Point(150, 0), AbsoluteOffset = new Point((int)(viewSize * 0.9f), 0) }, style: null); - var paddedDockingContainer = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), dockingContainer.RectTransform, Anchor.Center), style: null); + GUI.DrawRectangle(spriteBatch, new Rectangle((int)pos.X - 3 / 2, (int)pos.Y - 3, 6, 6), (SteeringPath.CurrentNode == wp) ? Color.LightGreen : Color.Green, false); Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); Vector2 displaySubPos = (-sonar.DisplayOffset * sonar.Zoom) / sonar.Range * sonar.DisplayRadius * sonar.Zoom; @@ -393,15 +390,6 @@ namespace Barotrauma.Items.Components -targetVelocity.Y * 0.9f * (float)Math.Sqrt(1.0f - 0.5f * unitTargetVel.X * unitTargetVel.X)); steeringPos += displaySubPos; - public void DrawHUD(SpriteBatch spriteBatch, Rectangle rect) - { - int width = rect.Width, height = rect.Height; - int x = rect.X; - int y = rect.Y; - - if (voltage < minVoltage && currPowerConsumption > 0.0f) return; - - Rectangle velRect = new Rectangle(x + 20, y + 20, width - 40, height - 40); GUI.DrawLine(spriteBatch, displaySubPos, steeringPos, diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs index ddd2ffb29..5cde6d3ec 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Repairable.cs @@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components { partial class Repairable : ItemComponent, IDrawableComponent { + public GUIButton RepairButton + { + get { return repairButton; } + } private GUIButton repairButton; private GUIProgressBar progressBar; diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Connection.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Connection.cs index 53c247abe..f5a3c8999 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Connection.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/Connection.cs @@ -18,6 +18,14 @@ namespace Barotrauma.Items.Components private static Wire draggingConnected; + private Color flashColor; + private float flashDuration = 1.5f; + public float FlashTimer + { + get { return flashTimer; } + } + private float flashTimer; + public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Character character) { Rectangle panelRect = panel.GuiFrame.Rect; @@ -174,14 +182,38 @@ namespace Barotrauma.Items.Components } } } - + + if (flashTimer > 0.0f) + { + //the number of flashes depends on the duration, 1 flash per 1 full second + int flashCycleCount = (int)Math.Max(flashDuration, 1); + float flashCycleDuration = flashDuration / flashCycleCount; + + //MathHelper.Pi * 0.8f -> the curve goes from 144 deg to 0, + //i.e. quickly bumps up from almost full brightness to full and then fades out + connectionSpriteHighlight.Draw(spriteBatch, position, flashColor * (float)Math.Sin(flashTimer % flashCycleDuration / flashCycleDuration * MathHelper.Pi * 0.8f)); + } + if (Wires.Any(w => w != null && w != draggingConnected)) { int screwIndex = (int)Math.Floor(position.Y / 30.0f) % screwSprites.Count; screwSprites[screwIndex].Draw(spriteBatch, position); } } - + + public void Flash(Color? color = null, float flashDuration = 1.5f) + { + flashTimer = flashDuration; + this.flashDuration = flashDuration; + flashColor = (color == null) ? Color.Red : (Color)color; + } + + public void UpdateFlashTimer(float deltaTime) + { + if (flashTimer <= 0) return; + flashTimer -= deltaTime; + } + private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Item item, Vector2 end, Vector2 start, bool mouseIn, Wire equippedWire, ConnectionPanel panel, string label) { if (draggingConnected == wire) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs index fe1fbc065..124992e2c 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Components/Signal/CustomInterface.cs @@ -92,6 +92,25 @@ namespace Barotrauma.Items.Components } } + public void HighlightElement(int index, Color color, float duration, float pulsateAmount = 0.0f) + { + if (index < 0 || index >= uiElements.Count) { return; } + uiElements[index].Flash(color, duration); + + if (pulsateAmount > 0.0f) + { + if (uiElements[index] is GUIButton button) + { + button.Frame.Pulsate(Vector2.One, Vector2.One * (1.0f + pulsateAmount), duration); + button.Frame.RectTransform.SetPosition(Anchor.Center); + } + else + { + uiElements[index].Pulsate(Vector2.One, Vector2.One * (1.0f + pulsateAmount), duration); + } + } + } + partial void UpdateLabelsProjSpecific() { for (int i = 0; i < labels.Length && i < uiElements.Count; i++) diff --git a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs index 30eaf8155..8fb30030a 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Inventory.cs @@ -26,6 +26,7 @@ namespace Barotrauma public Color Color; public Color HighlightColor; + public float HighlightScaleUpAmount; private CoroutineHandle highlightCoroutine; public float HighlightTimer; @@ -80,7 +81,7 @@ namespace Barotrauma return rect.Contains(PlayerInput.MousePosition); } - public void ShowBorderHighlight(Color color, float fadeInDuration, float fadeOutDuration) + public void ShowBorderHighlight(Color color, float fadeInDuration, float fadeOutDuration, float scaleUpAmount = 0.5f) { if (highlightCoroutine != null) { @@ -88,6 +89,7 @@ namespace Barotrauma highlightCoroutine = null; } + HighlightScaleUpAmount = scaleUpAmount; highlightCoroutine = CoroutineManager.StartCoroutine(UpdateBorderHighlight(color, fadeInDuration, fadeOutDuration)); } @@ -800,8 +802,7 @@ namespace Barotrauma if (slot.HighlightColor.A > 0) { - float scaleUpAmount = 0.5f; - float inflateAmount = (slot.HighlightColor.A / 255.0f) * scaleUpAmount * 0.5f; + float inflateAmount = (slot.HighlightColor.A / 255.0f) * slot.HighlightScaleUpAmount * 0.5f; rect.Inflate(rect.Width * inflateAmount, rect.Height * inflateAmount); } diff --git a/Barotrauma/BarotraumaClient/Source/Items/Item.cs b/Barotrauma/BarotraumaClient/Source/Items/Item.cs index 501f023e5..2c5e3b5cc 100644 --- a/Barotrauma/BarotraumaClient/Source/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/Source/Items/Item.cs @@ -184,7 +184,7 @@ namespace Barotrauma if (!Visible || (!editing && hiddenInGame)) return; if (editing && !ShowItems) return; - Color color = isHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? Color.Orange : GetSpriteColor(); + Color color = IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen ? Color.Orange : GetSpriteColor(); //if (IsSelected && editing) color = Color.Lerp(color, Color.Gold, 0.5f); BrokenItemSprite fadeInBrokenSprite = null; diff --git a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs index 0f5c15c60..252662461 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Hull.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Hull.cs @@ -326,141 +326,6 @@ namespace Barotrauma Color.Green, width: 2); } } - - foreach (MapEntity e in linkedTo) - { - if (e is Hull) - { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : - new Rectangle( - (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), - linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); - - //center of the hull - Rectangle currentHullRect = Submarine == null ? - WorldRect : - new Rectangle( - (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), - WorldRect.Width, WorldRect.Height); - - GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - Color.Green, width: 2); - } - } - - foreach (MapEntity e in linkedTo) - { - if (e is Hull) - { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : - new Rectangle( - (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), - linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); - - //center of the hull - Rectangle currentHullRect = Submarine == null ? - WorldRect : - new Rectangle( - (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), - WorldRect.Width, WorldRect.Height); - - GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - Color.Green, width: 2); - } - } - - foreach (MapEntity e in linkedTo) - { - if (e is Hull) - { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : - new Rectangle( - (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), - linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); - - //center of the hull - Rectangle currentHullRect = Submarine == null ? - WorldRect : - new Rectangle( - (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), - WorldRect.Width, WorldRect.Height); - - GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - Color.Green, width: 2); - } - } - - foreach (MapEntity e in linkedTo) - { - if (e is Hull) - { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : - new Rectangle( - (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), - linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); - - //center of the hull - Rectangle currentHullRect = Submarine == null ? - WorldRect : - new Rectangle( - (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), - WorldRect.Width, WorldRect.Height); - - GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - Color.Green, width: 2); - } - } - - foreach (MapEntity e in linkedTo) - { - if (e is Hull) - { - Hull linkedHull = (Hull)e; - Rectangle connectedHullRect = e.Submarine == null ? - linkedHull.rect : - new Rectangle( - (int)(Submarine.DrawPosition.X + linkedHull.WorldPosition.X), - (int)(Submarine.DrawPosition.Y + linkedHull.WorldPosition.Y), - linkedHull.WorldRect.Width, linkedHull.WorldRect.Height); - - //center of the hull - Rectangle currentHullRect = Submarine == null ? - WorldRect : - new Rectangle( - (int)(Submarine.DrawPosition.X + WorldPosition.X), - (int)(Submarine.DrawPosition.Y + WorldPosition.Y), - WorldRect.Width, WorldRect.Height); - - GUI.DrawLine(spriteBatch, - new Vector2(currentHullRect.X, -currentHullRect.Y), - new Vector2(connectedHullRect.X, -connectedHullRect.Y), - Color.Green, width: 2); - } - } } public static void UpdateVertices(GraphicsDevice graphicsDevice, Camera cam, WaterRenderer renderer) diff --git a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs index 8d893af08..08a9b5ae0 100644 --- a/Barotrauma/BarotraumaClient/Source/Map/Structure.cs +++ b/Barotrauma/BarotraumaClient/Source/Map/Structure.cs @@ -42,7 +42,21 @@ namespace Barotrauma MathHelper.Clamp(value.Y, 0.01f, 10)); } } - + + private string specialTag; + [Editable, Serialize("", true)] + public string SpecialTag + { + get { return specialTag; } + set { specialTag = value; } + } + + // Only for testing in the debug build. Not saved. +#if DEBUG + [Editable, Serialize(true, false)] +#endif + public bool DrawTiled { get; protected set; } = true; + protected Vector2 textureOffset = Vector2.Zero; [Editable(MinValueFloat = -1000f, MaxValueFloat = 1000f, ValueStep = 10f), Serialize("0.0, 0.0", true)] public Vector2 TextureOffset @@ -187,7 +201,7 @@ namespace Barotrauma if (HasBody && !ShowWalls) return; } - Color color = isHighlighted ? Color.Orange : spriteColor; + Color color = IsHighlighted ? Color.Orange : spriteColor; if (IsSelected && editing) { //color = Color.Lerp(color, Color.Gold, 0.5f); diff --git a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs index d4d830e8b..835d09937 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/CampaignSetupUI.cs @@ -20,6 +20,8 @@ namespace Barotrauma private GUILayoutGroup subPreviewContainer; + private GUILayoutGroup subPreviewContainer; + private GUIButton loadGameButton; public Action StartNewGame; @@ -68,12 +70,6 @@ namespace Barotrauma new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed") + ":"); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); - if (!isMultiplayer) - { - contextualTutorialBox = new GUITickBox(new RectTransform(new Point(32, 32), leftColumn.RectTransform), TextManager.Get("TutorialActive")); - UpdateTutorialSelection(); - } - new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.02f), leftColumn.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SelectedSub") + ":"); var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform), isHorizontal: true) { @@ -400,14 +396,7 @@ namespace Barotrauma }, Enabled = false }; - } - - public void UpdateTutorialSelection() - { - if (isMultiplayer) return; - Tutorial contextualTutorial = Tutorial.Tutorials.Find(t => t is ContextualTutorial); - contextualTutorialBox.Selected = (contextualTutorial != null) ? !GameMain.Config.CompletedTutorialNames.Contains(contextualTutorial.Name) : true; - } + } private bool SelectSaveFile(GUIComponent component, object obj) { diff --git a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs index f838dfc6e..503a23bc5 100644 --- a/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/Source/Screens/MainMenuScreen.cs @@ -59,10 +59,10 @@ namespace Barotrauma Stretch = true, RelativeSpacing = 0.02f }; - + // === CAMPAIGN var campaignHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1.0f), parent: buttonsParent.RectTransform) { RelativeOffset = new Vector2(0.1f, 0.0f) }, isHorizontal: true); - + new GUIImage(new RectTransform(new Vector2(0.2f, 0.7f), campaignHolder.RectTransform), "MainMenuCampaignIcon") { CanBeFocused = false @@ -84,6 +84,17 @@ namespace Barotrauma RelativeSpacing = 0.035f }; + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), "Tutorial", textAlignment: Alignment.Left, style: "MainMenuGUIButton") + { + ForceUpperCase = true, + UserData = Tab.Tutorials, + OnClicked = (tb, userdata) => + { + SelectTab(tb, userdata); + return true; + } + }; + new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), campaignList.RectTransform), TextManager.Get("LoadGameButton"), textAlignment: Alignment.Left, style: "MainMenuGUIButton") { ForceUpperCase = true, @@ -309,6 +320,7 @@ namespace Barotrauma false, null, ""); foreach (Tutorial tutorial in Tutorial.Tutorials) { + if (tutorial is ContextualTutorial) continue; var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.Name, textAlignment: Alignment.Center, font: GUI.LargeFont) { UserData = tutorial @@ -320,8 +332,6 @@ namespace Barotrauma return true; }; - UpdateTutorialList(); - this.game = game; } #endregion @@ -341,10 +351,6 @@ namespace Barotrauma ResetButtonStates(null); - UpdateTutorialList(); - - ResetButtonStates(null); - GameAnalyticsManager.SetCustomDimension01(""); } @@ -390,7 +396,6 @@ namespace Barotrauma case Tab.NewGame: campaignSetupUI.CreateDefaultSaveName(); campaignSetupUI.RandomizeSeed(); - campaignSetupUI.UpdateTutorialSelection(); campaignSetupUI.UpdateSubList(Submarine.SavedSubmarines); break; case Tab.LoadGame: @@ -407,6 +412,7 @@ namespace Barotrauma case Tab.HostServer: break; case Tab.Tutorials: + UpdateTutorialList(); break; case Tab.CharacterEditor: Submarine.MainSub = null; @@ -437,6 +443,8 @@ namespace Barotrauma public bool ReturnToMainMenu(GUIButton button, object obj) { + GUI.PreventPauseMenuToggle = false; + if (Selected != this) { Select(); @@ -772,8 +780,7 @@ namespace Barotrauma } selectedSub = new Submarine(Path.Combine(SaveUtil.TempPath, selectedSub.Name + ".sub"), ""); - - ContextualTutorial.Selected = campaignSetupUI.TutorialSelected; + GameMain.GameSession = new GameSession(selectedSub, saveName, GameModePreset.List.Find(g => g.Identifier == "singleplayercampaign")); (GameMain.GameSession.GameMode as CampaignMode).GenerateMap(mapSeed); diff --git a/Barotrauma/BarotraumaShared/SharedCode.projitems b/Barotrauma/BarotraumaShared/SharedCode.projitems index f15f9dbd1..05589e12a 100644 --- a/Barotrauma/BarotraumaShared/SharedCode.projitems +++ b/Barotrauma/BarotraumaShared/SharedCode.projitems @@ -181,6 +181,7 @@ + diff --git a/Barotrauma/BarotraumaShared/SharedContent.projitems b/Barotrauma/BarotraumaShared/SharedContent.projitems index dfe87235b..989c0f30e 100644 --- a/Barotrauma/BarotraumaShared/SharedContent.projitems +++ b/Barotrauma/BarotraumaShared/SharedContent.projitems @@ -445,9 +445,24 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -475,6 +490,54 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -2112,6 +2175,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs index 232f2d24f..3a6fed89d 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/AI/EnemyAIController.cs @@ -1057,6 +1057,8 @@ namespace Barotrauma private bool IsProperlyLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub && SelectedAiTarget?.Entity == wallTarget?.Structure; + private bool IsProperlyLatchedOnSub => LatchOntoAI != null && LatchOntoAI.IsAttachedToSub && SelectedAiTarget?.Entity == wallTarget?.Structure; + //goes through all the AItargets, evaluates how preferable it is to attack the target, //whether the Character can see/hear the target and chooses the most preferable target within //sight/hearing range diff --git a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs index 9004982fc..13a4ec2e2 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/Character.cs @@ -426,7 +426,7 @@ namespace Barotrauma } } - private bool canSpeak; + public bool CanSpeak; private bool speechImpedimentSet; @@ -436,7 +436,7 @@ namespace Barotrauma { get { - if (!canSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f; + if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f; return speechImpediment; } set @@ -710,7 +710,7 @@ namespace Barotrauma displayName = TextManager.Get($"Character.{Path.GetFileName(Path.GetDirectoryName(file))}", true); IsHumanoid = doc.Root.GetAttributeBool("humanoid", false); - canSpeak = doc.Root.GetAttributeBool("canspeak", false); + CanSpeak = doc.Root.GetAttributeBool("canspeak", false); needsAir = doc.Root.GetAttributeBool("needsair", false); Noise = doc.Root.GetAttributeFloat("noise", 100f); @@ -2571,6 +2571,10 @@ namespace Barotrauma GameMain.GameSession?.CrewManager?.RemoveCharacter(this); #endif +#if CLIENT + GameMain.GameSession?.CrewManager?.RemoveCharacter(this); +#endif + #if CLIENT GameMain.GameSession?.CrewManager?.RemoveCharacter(this); #endif diff --git a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs index 8dcf98fce..cb7df70ec 100644 --- a/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/Source/Characters/CharacterInfo.cs @@ -399,7 +399,7 @@ namespace Barotrauma { ID = idCounter; idCounter++; - Name = element.GetAttributeString("name", "unnamed"); + Name = element.GetAttributeString("name", ""); string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant(); File = element.GetAttributeString("file", ""); SourceElement = GetConfig(File).Root; @@ -423,6 +423,29 @@ namespace Barotrauma element.GetAttributeInt("beardindex", -1), element.GetAttributeInt("moustacheindex", -1), element.GetAttributeInt("faceattachmentindex", -1)); + + if (string.IsNullOrEmpty(Name)) + { + if (SourceElement.Element("name") != null) + { + string firstNamePath = SourceElement.Element("name").GetAttributeString("firstname", ""); + if (firstNamePath != "") + { + firstNamePath = firstNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + Name = ToolBox.GetRandomLine(firstNamePath); + } + + string lastNamePath = SourceElement.Element("name").GetAttributeString("lastname", ""); + if (lastNamePath != "") + { + lastNamePath = lastNamePath.Replace("[GENDER]", (Head.gender == Gender.Female) ? "female" : "male"); + if (Name != "") Name += " "; + Name += ToolBox.GetRandomLine(lastNamePath); + } + } + } + + StartItemsGiven = element.GetAttributeBool("startitemsgiven", false); string personalityName = element.GetAttributeString("personality", ""); ragdollFileName = element.GetAttributeString("ragdoll", string.Empty); diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs index 831c07c39..fa9ff9e17 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/ItemComponent.cs @@ -801,10 +801,7 @@ namespace Barotrauma.Items.Components string msg = TextManager.Get(Msg, true); if (msg != null) { - foreach (InputType inputType in Enum.GetValues(typeof(InputType))) - { - msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBind(inputType).ToString()); - } + msg = TextManager.ParseInputTypes(msg); DisplayMsg = msg; } else diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs index 95476dc1f..fb4392e3c 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Deconstructor.cs @@ -13,6 +13,11 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; + public ItemContainer InputContainer + { + get { return inputContainer; } + } + public ItemContainer OutputContainer { get { return outputContainer; } diff --git a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs index 7ade916d4..cd1b0e081 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Components/Machines/Fabricator.cs @@ -23,6 +23,16 @@ namespace Barotrauma.Items.Components private ItemContainer inputContainer, outputContainer; + public ItemContainer InputContainer + { + get { return inputContainer; } + } + + public ItemContainer OutputContainer + { + get { return outputContainer; } + } + private float progressState; public Fabricator(Item item, XElement element) @@ -98,7 +108,23 @@ namespace Barotrauma.Items.Components { return (picker != null); } - + + public void RemoveFabricationRecipes(List allowedIdentifiers) + { + for (int i = 0; i < fabricationRecipes.Count; i++) + { + if (!allowedIdentifiers.Contains(fabricationRecipes[i].TargetItem.Identifier)) + { + fabricationRecipes.RemoveAt(i); + i--; + } + } + + CreateRecipes(); + } + + partial void CreateRecipes(); + private void StartFabricating(FabricationRecipe selectedItem, Character user) { if (selectedItem == null) return; diff --git a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs index e6e6a9541..85d352153 100644 --- a/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/Source/Items/Inventory.cs @@ -206,6 +206,16 @@ namespace Barotrauma return true; } + public bool IsFull() + { + for (int i = 0; i < capacity; i++) + { + if (Items[i] == null) return false; + } + + return true; + } + protected bool TrySwapping(int index, Item item, Character user, bool createNetworkEvent) { if (item?.ParentInventory == null || Items[index] == null) return false; diff --git a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs index a73ac8907..40f2acd92 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Explosion.cs @@ -40,7 +40,7 @@ namespace Barotrauma flames = true; underwaterBubble = true; } - + public Explosion(XElement element, string parentDebugName) { attack = new Attack(element, parentDebugName + ", Explosion"); @@ -62,6 +62,16 @@ namespace Barotrauma CameraShake = element.GetAttributeFloat("camerashake", attack.Range * 0.1f); } + public void DisableParticles() + { + sparks = false; + shockwave = false; + smoke = false; + flash = false; + flames = false; + underwaterBubble = false; + } + public List> GetRecentExplosions(float maxSecondsAgo) { return prevExplosions.FindAll(e => e.Third >= Timing.TotalTime - maxSecondsAgo); diff --git a/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs b/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs index d78a6fdc8..f3b97de1c 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/FireSource.cs @@ -14,15 +14,15 @@ namespace Barotrauma { const float OxygenConsumption = 50.0f; const float GrowSpeed = 5.0f; - - private Hull hull; - private Vector2 position; - private Vector2 size; + protected Hull hull; + + protected Vector2 position; + protected Vector2 size; private Entity Submarine; - private bool removed; + protected bool removed; #if CLIENT private List burnDecals = new List(); @@ -182,6 +182,16 @@ namespace Barotrauma } } + protected virtual void ReduceOxygen(float deltaTime) + { + hull.Oxygen -= size.X * deltaTime * OxygenConsumption; + } + + protected virtual void AdjustXPos(float growModifier, float deltaTime) + { + position.X -= GrowSpeed * growModifier * 0.5f * deltaTime; + } + partial void UpdateProjSpecific(float growModifier); private void OnChangeHull(Vector2 pos, Hull particleHull) diff --git a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs index 5bace8352..695b56df0 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Levels/Level.cs @@ -138,6 +138,9 @@ namespace Barotrauma public Submarine StartOutpost { get; private set; } public Submarine EndOutpost { get; private set; } + private Submarine preSelectedStartOutpost; + private Submarine preSelectedEndOutpost; + public string Seed { get { return seed; } @@ -209,7 +212,7 @@ namespace Barotrauma /// /// A scalar between 0-100 /// A scalar between 0-1 (0 = the minimum width defined in the generation params is used, 1 = the max width is used) - public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome) + public Level(string seed, float difficulty, float sizeFactor, LevelGenerationParams generationParams, Biome biome, Submarine startOutpost = null, Submarine endOutPost = null) : base(null) { @@ -225,6 +228,9 @@ namespace Barotrauma (width / GridCellSize) * GridCellSize, (generationParams.Height / GridCellSize) * GridCellSize); + preSelectedStartOutpost = startOutpost; + preSelectedEndOutpost = endOutPost; + //remove from entity dictionary base.Remove(); } @@ -1510,14 +1516,24 @@ namespace Barotrauma continue; } - //only create a starting outpost in campaign mode - if (GameMain.GameSession?.GameMode as CampaignMode == null && ((i == 0) == !Mirrored)) + //only create a starting outpost in campaign and tutorial modes + if (!IsModeStartOutpostCompatible() && ((i == 0) == !Mirrored)) { continue; } - - string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server); - var outpost = new Submarine(outpostFile, tryLoad: false); + + Submarine outpost = null; + + if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) + { + string outpostFile = outpostFiles.GetRandom(Rand.RandSync.Server); + outpost = new Submarine(outpostFile, tryLoad: false); + } + else + { + outpost = (i == 0) ? preSelectedStartOutpost : preSelectedEndOutpost; + } + outpost.Load(unloadPrevious: false); outpost.MakeOutpost(); @@ -1569,6 +1585,15 @@ namespace Barotrauma } } + private bool IsModeStartOutpostCompatible() + { +#if CLIENT + return GameMain.GameSession?.GameMode as CampaignMode != null || GameMain.GameSession?.GameMode as TutorialMode != null; +#else + return GameMain.GameSession?.GameMode as CampaignMode != null; +#endif + } + public override void Remove() { base.Remove(); diff --git a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs index ab486666c..b686f5dac 100644 --- a/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs +++ b/Barotrauma/BarotraumaShared/Source/Map/Submarine.cs @@ -431,6 +431,12 @@ namespace Barotrauma else if (ic is Pickable pickable) { //prevent picking up (or deattaching) items +#if CLIENT + if (GameMain.GameSession.GameMode is TutorialMode) + { + continue; + } +#endif pickable.CanBePicked = false; pickable.CanBeSelected = false; } @@ -1426,7 +1432,7 @@ namespace Barotrauma doc.Root.Add(new XAttribute("md5hash", hash.Hash)); if (previewImage != null) { - doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); + //doc.Root.Add(new XAttribute("previewimage", Convert.ToBase64String(previewImage.ToArray()))); } try diff --git a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs index 8f9090ae9..10b3691c6 100644 --- a/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/Source/Serialization/XMLExtensions.cs @@ -46,7 +46,15 @@ namespace Barotrauma if (File.Exists(filePath)) { - doc = XDocument.Load(filePath, LoadOptions.SetBaseUri); + try + { + doc = XDocument.Load(filePath, LoadOptions.SetBaseUri); + } + catch + { + return null; + } + if (doc.Root == null) return null; } diff --git a/Barotrauma/BarotraumaShared/Source/TextManager.cs b/Barotrauma/BarotraumaShared/Source/TextManager.cs index 52df9ac7b..9a0e023c7 100644 --- a/Barotrauma/BarotraumaShared/Source/TextManager.cs +++ b/Barotrauma/BarotraumaShared/Source/TextManager.cs @@ -79,6 +79,40 @@ namespace Barotrauma } public static string Get(string textTag, bool returnNull = false) + { + if (!textPacks.ContainsKey(Language)) + { + DebugConsole.ThrowError("No text packs available for the selected language (" + Language + ")! Switching to English..."); + Language = "English"; + if (!textPacks.ContainsKey(Language)) + { + throw new Exception("No text packs available in English!"); + } + } + } + + public static string GetFormatted(string textTag, bool returnNull = false, params object[] args) + { + string text = Get(textTag, returnNull); + + if (text == null || text.Length == 0) + { + if (returnNull) + { + return null; + } + else + { + DebugConsole.ThrowError("Text \"" + textTag + "\" not found."); + return textTag; + } + } + + return string.Format(text, args); + } + + // Format: ServerMessage.Identifier1/ServerMessage.Indentifier2~[variable1]=value~[variable2]=value + public static string GetServerMessage(string serverMessage) { if (!textPacks.ContainsKey(Language)) { @@ -118,6 +152,16 @@ namespace Barotrauma } } + public static string ParseInputTypes(string text) + { + foreach (InputType inputType in Enum.GetValues(typeof(InputType))) + { + text = text.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameMain.Config.KeyBind(inputType).ToString()); + text = text.Replace("[InputType." + inputType.ToString() + "]", GameMain.Config.KeyBind(inputType).ToString()); + } + return text; + } + public static string GetFormatted(string textTag, bool returnNull = false, params object[] args) { string text = Get(textTag, returnNull); diff --git a/Barotrauma/BarotraumaShared/Submarines/TutorialOutpost.sub b/Barotrauma/BarotraumaShared/Submarines/TutorialOutpost.sub index 3e5085c2eb23e0312a2c1bdb2134799bf21a22ca..79e598573f57eccb3ddbc77e2ec9943ac221024c 100644 GIT binary patch literal 24492 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 literal 24554 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 TC#%^a{U857=Iby$p6v$!*?PIm