From eec240dd56a7b27e3b22592af47673edcfa722b6 Mon Sep 17 00:00:00 2001 From: Pyrdacor Date: Thu, 10 Sep 2020 16:10:15 +0200 Subject: [PATCH] Added first version of text popup map events --- Ambermoon.Common/Rect.cs | 5 ++ Ambermoon.Core/Game.cs | 39 ++++++++- Ambermoon.Core/MapExtensions.cs | 8 ++ Ambermoon.Core/UI/Global.cs | 2 +- Ambermoon.Core/UI/ItemGrid.cs | 2 +- Ambermoon.Core/UI/Layout.cs | 31 +++++++- Ambermoon.Core/UI/Popup.cs | 16 +++- Ambermoon.Data.Common/IText.cs | 8 ++ Ambermoon.Data.Common/MapEvent.cs | 6 +- .../Serialization/MapReader.cs | 10 ++- Ambermoon.Data.Legacy/Text.cs | 79 ++++++++++++++++++- Ambermoon.Renderer.OpenGL/RenderText.cs | 4 +- 12 files changed, 195 insertions(+), 15 deletions(-) diff --git a/Ambermoon.Common/Rect.cs b/Ambermoon.Common/Rect.cs index b4de6686..28009fbb 100644 --- a/Ambermoon.Common/Rect.cs +++ b/Ambermoon.Common/Rect.cs @@ -71,6 +71,11 @@ public static Rect Create(Position center, Size size) return new Rect(center.X - size.Width / 2, center.Y - size.Height / 2, size.Width, size.Height); } + public static Rect GetCentered(Rect rect, Rect outerRect) + { + return Create(outerRect.Center, rect.Size); + } + public void Clip(int left, int top, int right, int bottom) { if (Left < left) diff --git a/Ambermoon.Core/Game.cs b/Ambermoon.Core/Game.cs index 5d4a3163..b36c484e 100644 --- a/Ambermoon.Core/Game.cs +++ b/Ambermoon.Core/Game.cs @@ -94,10 +94,26 @@ class TimedGameEvent PartyMember CurrentCaster { get; set; } = null; public Map Map => !ingame ? null : is3D ? renderMap3D?.Map : renderMap2D?.Map; readonly bool[] keys = new bool[Enum.GetValues().Length]; + bool inputEnable = true; /// /// The 3x3 buttons will always be enabled! /// - public bool InputEnable { get; set; } = true; + public bool InputEnable + { + get => inputEnable; + set + { + if (inputEnable == value) + return; + + inputEnable = value; + clickMoveActive = false; + UntrapMouse(); + + if (!inputEnable) + ResetMoveKeys(); + } + } bool leftMouseDown = false; bool clickMoveActive = false; Rect trapMouseArea = null; @@ -856,7 +872,7 @@ void UpdateCursor(Position cursorPosition, MouseButtons buttons) { if (layout.PopupActive) { - var cursorType = CursorType.Sword; + var cursorType = layout.PopupClickToClose ? CursorType.Click : CursorType.Sword; layout.Hover(renderView.ScreenToGame(cursorPosition), ref cursorType); CursorType = cursorType; } @@ -1262,6 +1278,25 @@ internal void ShowChest(ChestMapEvent chestMapEvent) }); } + internal void ShowTextPopup(PopupTextEvent popupTextEvent) + { + if (popupTextEvent.HasImage) + { + // Those always use a custom layout + } + else + { + // Simple text popup + /*var popup = layout.OpenPopup(mapViewArea.Position, (mapViewArea.Width - 32) / 7, 10, true, true); // TODO: sizes and position + popup.AddText(new Rect(mapViewArea.Position + new Position(16, 16), new Size(mapViewArea.Width - 32, 10 * 7)), + Map.Texts[(int)popupTextEvent.TextIndex], TextColor.Gray);*/ + var text = renderView.TextProcessor.ProcessText(Map.Texts[(int)popupTextEvent.TextIndex], nameProvider, dictionary); + layout.OpenTextPopup(text, () => InputEnable = true, true, true); + InputEnable = false; + CursorType = CursorType.Click; + } + } + internal void SetActivePartyMember(int index) { var partyMember = GetPartyMember(index); diff --git a/Ambermoon.Core/MapExtensions.cs b/Ambermoon.Core/MapExtensions.cs index c081d990..bc78a877 100644 --- a/Ambermoon.Core/MapExtensions.cs +++ b/Ambermoon.Core/MapExtensions.cs @@ -43,6 +43,14 @@ public static class MapExtensions game.ShowChest(chestMapEvent); break; } + case MapEventType.PopupText: + { + if (!(mapEvent is PopupTextEvent popupTextEvent)) + throw new AmbermoonException(ExceptionScope.Data, "Invalid text popup event."); + + game.ShowTextPopup(popupTextEvent); + break; + } case MapEventType.ChangeTile: { if (!(mapEvent is ChangeTileEvent changeTileEvent)) diff --git a/Ambermoon.Core/UI/Global.cs b/Ambermoon.Core/UI/Global.cs index 44f48176..661cac78 100644 --- a/Ambermoon.Core/UI/Global.cs +++ b/Ambermoon.Core/UI/Global.cs @@ -29,6 +29,6 @@ public partial class Global public static readonly Rect[] ExtendedPartyMemberPortraitAreas = Enumerable.Range(0, 6).Select(index => new Rect(15 + index * 48, 0, 48, 36)).ToArray(); public const int GlyphWidth = 6; - public const int GlyphLineHeight = 6; + public const int GlyphLineHeight = 7; } } diff --git a/Ambermoon.Core/UI/ItemGrid.cs b/Ambermoon.Core/UI/ItemGrid.cs index f8d86ef6..49a0ab71 100644 --- a/Ambermoon.Core/UI/ItemGrid.cs +++ b/Ambermoon.Core/UI/ItemGrid.cs @@ -409,7 +409,7 @@ public bool Hover(Position position) hoveredItemName.Text = itemNameText; hoveredItemName.DisplayLayer = 2; hoveredItemName.X = Util.Limit(0, position.X - textWidth / 2, Global.VirtualScreenWidth - textWidth); - hoveredItemName.Y = position.Y - Global.GlyphLineHeight - 2; + hoveredItemName.Y = position.Y - Global.GlyphLineHeight - 1; hoveredItemName.Visible = true; return true; diff --git a/Ambermoon.Core/UI/Layout.cs b/Ambermoon.Core/UI/Layout.cs index 008495c2..b51210e0 100644 --- a/Ambermoon.Core/UI/Layout.cs +++ b/Ambermoon.Core/UI/Layout.cs @@ -319,6 +319,7 @@ public static DraggedItem FromExternal(ItemGrid itemGrid, int slotIndex, UIItem Popup activePopup = null; public bool PopupActive => activePopup != null; public bool PopupDisableButtons => activePopup?.DisableButtons == true; + public bool PopupClickToClose => activePopup?.CloseOnClick == true; int buttonGridPage = 0; uint? ticksPerMovement = null; internal IRenderView RenderView { get; } @@ -482,17 +483,45 @@ void CloseOptionMenu() game.InputEnable = true; } - void OpenPopup(Position position, int columns, int rows, bool disableButtons = false, bool closeOnClick = true) + internal Popup OpenPopup(Position position, int columns, int rows, bool disableButtons = false, bool closeOnClick = true) { activePopup = new Popup(game, RenderView, position, columns, rows) { DisableButtons = disableButtons, CloseOnClick = closeOnClick }; + return activePopup; + } + + internal Popup OpenTextPopup(IText text, Action closeAction, bool disableButtons = false, bool closeOnClick = true) + { + // min text position = 32, 68 + // max text size = 256, 112 + // max 16 text rows + const int maxTextWidth = 256; + const int maxTextHeight = 112; + var processedText = RenderView.TextProcessor.WrapText(text, + new Rect((Global.VirtualScreenWidth - maxTextWidth) / 2, 0, maxTextWidth, int.MaxValue), + new Size(Global.GlyphWidth, Global.GlyphLineHeight)); + var textBounds = new Rect(32, 69, maxTextWidth, Math.Min(processedText.LineCount * Global.GlyphLineHeight, maxTextHeight)); + var renderText = RenderView.RenderTextFactory.Create(RenderView.GetLayer(Layer.Text), + processedText, TextColor.Gray, true, textBounds); + int popupColumns = 2 + (textBounds.Width + 15) / 16; + int popupRows = 2 + (textBounds.Height + 15) / 16; + var popupArea = Rect.Create(textBounds.Center, new Size(popupColumns * 16, popupRows * 16)); + activePopup = new Popup(game, RenderView, popupArea.Position, popupColumns, popupRows) + { + DisableButtons = disableButtons, + CloseOnClick = closeOnClick + }; + activePopup.AddText(renderText); + activePopup.Closed += closeAction; + return activePopup; } internal void ClosePopup() { + activePopup?.OnClosed(); activePopup?.Destroy(); activePopup = null; } diff --git a/Ambermoon.Core/UI/Popup.cs b/Ambermoon.Core/UI/Popup.cs index 23d6d3e3..901ae78b 100644 --- a/Ambermoon.Core/UI/Popup.cs +++ b/Ambermoon.Core/UI/Popup.cs @@ -4,7 +4,7 @@ namespace Ambermoon.UI { - class Popup + internal class Popup { const byte BaseDisplayLayer = 20; readonly Game game; @@ -69,6 +69,12 @@ void AddBorder(PopupFrame frame, int column, int row) public bool CloseOnClick { get; set; } = true; public bool DisableButtons { get; set; } = false; + public event Action Closed; + + public void OnClosed() + { + Closed?.Invoke(); + } public void Destroy() { @@ -110,6 +116,14 @@ public IRenderText AddText(Position position, string text, TextColor textColor, return renderText; } + public IRenderText AddText(IRenderText renderText, byte displayLayer = 1) + { + renderText.DisplayLayer = (byte)Util.Min(255, BaseDisplayLayer + displayLayer); + renderText.Visible = true; + texts.Add(renderText); + return renderText; + } + public IColoredRect FillArea(Rect area, Color color, byte displayLayer = 1) { var filledArea = renderView.ColoredRectFactory.Create(area.Width, area.Height, color, diff --git a/Ambermoon.Data.Common/IText.cs b/Ambermoon.Data.Common/IText.cs index ce74bcfa..77a607a8 100644 --- a/Ambermoon.Data.Common/IText.cs +++ b/Ambermoon.Data.Common/IText.cs @@ -46,6 +46,14 @@ public interface ITextProcessor { IText ProcessText(string text, ITextNameProvider nameProvider, List dictionary); IText CreateText(string text); + /// + /// Wraps a given text so it fits into the given bounds. + /// + /// Note that the height still can exceed the bound height. + /// In this case the text must be scrolled to view all of it. + /// This only wraps text lines to keep them inside the bound width. + /// + IText WrapText(IText text, Rect bounds, Size glyphSize); } public interface IText diff --git a/Ambermoon.Data.Common/MapEvent.cs b/Ambermoon.Data.Common/MapEvent.cs index 81cd47c4..e0b71345 100644 --- a/Ambermoon.Data.Common/MapEvent.cs +++ b/Ambermoon.Data.Common/MapEvent.cs @@ -123,12 +123,14 @@ public class PopupTextEvent : MapEvent /// From event_pix (0-based). 0xff -> no image. /// public uint EventImageIndex { get; set; } - public byte[] Unknown1 { get; set; } + public bool HasImage => EventImageIndex != 0xff; + public byte Unknown1 { get; set; } public byte[] Unknown2 { get; set; } + public byte[] Unknown3 { get; set; } public override string ToString() { - return $"{Type}: Text {TextIndex}, Image {(EventImageIndex == 0xff ? "None" : EventImageIndex.ToString())}, Unknown1 {string.Join(" ", Unknown1.Select(u => u.ToString("x2")))}, Unknown2 {string.Join(" ", Unknown2.Select(u => u.ToString("x2")))}"; + return $"{Type}: Text {TextIndex}, Image {(EventImageIndex == 0xff ? "None" : EventImageIndex.ToString())}, Unknown1 {Unknown1}, Unknown2 {string.Join(" ", Unknown2.Select(u => u.ToString("x2")))}, Unknown3 {string.Join(" ", Unknown3.Select(u => u.ToString("x2")))}"; } } diff --git a/Ambermoon.Data.Legacy/Serialization/MapReader.cs b/Ambermoon.Data.Legacy/Serialization/MapReader.cs index 59b68bb6..024cd79f 100644 --- a/Ambermoon.Data.Legacy/Serialization/MapReader.cs +++ b/Ambermoon.Data.Legacy/Serialization/MapReader.cs @@ -248,15 +248,17 @@ static MapEvent ParseEvent(IDataReader dataReader) // 5. byte is the map text index // 4 unknown bytes var eventImageIndex = dataReader.ReadByte(); - var unknown1 = dataReader.ReadBytes(3); + var unknown1 = dataReader.ReadByte(); // TODO: seen 1 and 3 so far + var unknown2 = dataReader.ReadBytes(2); var textIndex = dataReader.ReadByte(); - var unknown2 = dataReader.ReadBytes(4); + var unknown3 = dataReader.ReadBytes(4); mapEvent = new PopupTextEvent { EventImageIndex = eventImageIndex, - TextIndex = textIndex, Unknown1 = unknown1, - Unknown2 = unknown2 + TextIndex = textIndex, + Unknown2 = unknown2, + Unknown3 = unknown3 }; break; } diff --git a/Ambermoon.Data.Legacy/Text.cs b/Ambermoon.Data.Legacy/Text.cs index c7584f14..991a0147 100644 --- a/Ambermoon.Data.Legacy/Text.cs +++ b/Ambermoon.Data.Legacy/Text.cs @@ -3,7 +3,7 @@ namespace Ambermoon.Data.Legacy { - public class Text : IText + internal class Text : IText { public Text(byte[] glyphIndices) { @@ -35,6 +35,13 @@ public Text(byte[] glyphIndices) MaxLineSize = currentLineSize; } + public Text(byte[] glyphIndices, int lineCount, int maxLineSize) + { + GlyphIndices = glyphIndices; + LineCount = lineCount; + MaxLineSize = maxLineSize; + } + public byte[] GlyphIndices { get; } public int LineCount { get; } public int MaxLineSize { get; } @@ -121,6 +128,76 @@ public IText CreateText(string text) return new Text(text.Select(ch => CharToGlyph(ch, false)).ToArray()); } + public IText WrapText(IText text, Rect bounds, Size glyphSize) + { + int x = bounds.Left; + int y = bounds.Top; + int lastSpaceIndex = -1; + int maxLineWidth = 0; + int height = 0; + var wrappedGlyphs = new List(text.GlyphIndices.Length); + + void NewLine(int newX = 0) + { + if (x > maxLineWidth) + maxLineWidth = x; + + lastSpaceIndex = -1; + x = bounds.Left + newX; + y += glyphSize.Height; + height = y; + } + + foreach (var glyph in text.GlyphIndices) + { + switch (glyph) + { + case (byte)SpecialGlyph.SoftSpace: + if (wrappedGlyphs.Last() == (byte)SpecialGlyph.NewLine) + continue; + x += glyphSize.Width; + if (x > bounds.Right) + { + wrappedGlyphs.Add((byte)SpecialGlyph.NewLine); + NewLine(); + } + else + { + lastSpaceIndex = wrappedGlyphs.Count; + wrappedGlyphs.Add(glyph); + } + break; + case (byte)SpecialGlyph.NewLine: + wrappedGlyphs.Add(glyph); + NewLine(); + break; + case (byte)SpecialGlyph.FirstColor: + wrappedGlyphs.Add(glyph); + break; + default: + { + wrappedGlyphs.Add(glyph); + x += glyphSize.Width; + if (x > bounds.Right) + { + if (lastSpaceIndex == -1) + throw new AmbermoonException(ExceptionScope.Data, "Text can not be wrapped inside the given bounds."); + + wrappedGlyphs[lastSpaceIndex] = (byte)SpecialGlyph.NewLine; + NewLine((wrappedGlyphs.Count - lastSpaceIndex - 1) * glyphSize.Width); + } + break; + } + } + } + + if (wrappedGlyphs.Last() == (byte)SpecialGlyph.NewLine) + wrappedGlyphs.RemoveAt(wrappedGlyphs.Count - 1); + + // Note: The added 1 is used as after the last new line character there are always other characters. + return new Text(wrappedGlyphs.ToArray(), 1 + height / glyphSize.Height, maxLineWidth / glyphSize.Width); + } + public IText ProcessText(string text, ITextNameProvider nameProvider, List dictionary) { List glyphIndices = new List(); diff --git a/Ambermoon.Renderer.OpenGL/RenderText.cs b/Ambermoon.Renderer.OpenGL/RenderText.cs index 162ccee0..d4015bfd 100644 --- a/Ambermoon.Renderer.OpenGL/RenderText.cs +++ b/Ambermoon.Renderer.OpenGL/RenderText.cs @@ -30,7 +30,7 @@ internal class RenderText : RenderNode, IRenderText { const int CharacterWidth = 6; const int CharacterHeight = 6; - const int LineHeight = 8; + const int LineHeight = 7; const byte ShadowColorIndex = 1; protected int drawIndex = -1; byte displayLayer = 0; @@ -179,7 +179,7 @@ bool NewLine() y += LineHeight; numEmptyCharacterInLine = 0; - return y + CharacterHeight - 1 < bounds.Bottom; + return y + CharacterHeight - 1 <= bounds.Bottom; } void AdjustLineAlign(int lineEndGlyphIndex)