From 1c861e25147a2012527b8d85fb590b5971b78618 Mon Sep 17 00:00:00 2001 From: Acipenser Sturio Date: Mon, 4 Mar 2024 21:54:07 +0300 Subject: [PATCH] zzre: add DialogChestPuzzle (#331) * ChestPuzzle: dialog template * Fix cancel button * Temporary succeed button * Complete layout * Correctly handle size variable * Ugly opacity api * Add board state functionality * style: IDE0300 * Add win condition * Minor fixes * Tile buttons seamlessly * Make buttons silent * Load and save min tries * Update Label instead of recreating entity * Use static * Use ButtonTiles Active component --- zzre/game/Game.cs | 1 + .../components/dialog/DialogChestPuzzle.cs | 15 ++ zzre/game/components/dialog/DialogState.cs | 1 + zzre/game/components/ui/Fade.cs | 4 +- zzre/game/components/ui/Silent.cs | 3 + .../game/messages/dialog/DialogChestPuzzle.cs | 8 + zzre/game/systems/dialog/DialogChestPuzzle.cs | 198 ++++++++++++++++++ zzre/game/systems/dialog/DialogGambling.cs | 2 +- zzre/game/systems/dialog/DialogScript.cs | 11 +- zzre/game/systems/ui/Cursor.cs | 3 +- zzre/game/systems/ui/UIPreloader.cs | 20 +- 11 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 zzre/game/components/dialog/DialogChestPuzzle.cs create mode 100644 zzre/game/components/ui/Silent.cs create mode 100644 zzre/game/messages/dialog/DialogChestPuzzle.cs create mode 100644 zzre/game/systems/dialog/DialogChestPuzzle.cs diff --git a/zzre/game/Game.cs b/zzre/game/Game.cs index 12ece781..0d6cf4a0 100644 --- a/zzre/game/Game.cs +++ b/zzre/game/Game.cs @@ -156,6 +156,7 @@ public Game(ITagContainer diContainer, Savegame savegame) new systems.DialogChoice(this), new systems.DialogTrading(this), new systems.DialogGambling(this), + new systems.DialogChestPuzzle(this), new systems.NonFairyAnimation(this), new systems.AmbientSounds(this), diff --git a/zzre/game/components/dialog/DialogChestPuzzle.cs b/zzre/game/components/dialog/DialogChestPuzzle.cs new file mode 100644 index 00000000..47eefe40 --- /dev/null +++ b/zzre/game/components/dialog/DialogChestPuzzle.cs @@ -0,0 +1,15 @@ + +namespace zzre.game.components; + +public struct DialogChestPuzzle +{ + public DefaultEcs.Entity DialogEntity; + public int Size; + public int LabelExit; + public uint NumAttempts; + public bool LockBoard; + public Rect BgRect; + public DefaultEcs.Entity[] Cells; + public DefaultEcs.Entity Attempts; + public DefaultEcs.Entity Action; +} diff --git a/zzre/game/components/dialog/DialogState.cs b/zzre/game/components/dialog/DialogState.cs index 2f8f7e48..736b871b 100644 --- a/zzre/game/components/dialog/DialogState.cs +++ b/zzre/game/components/dialog/DialogState.cs @@ -9,6 +9,7 @@ public enum DialogState Choice, Trading, Gambling, + ChestPuzzle, PreFightWild, PreFightNpc, NpcWalking, diff --git a/zzre/game/components/ui/Fade.cs b/zzre/game/components/ui/Fade.cs index ec166bc2..66eda83c 100644 --- a/zzre/game/components/ui/Fade.cs +++ b/zzre/game/components/ui/Fade.cs @@ -8,9 +8,9 @@ public record struct Fade( float OutDuration, float CurrentTime = 0f) { - public static Fade SingleIn(float duration) => new( + public static Fade SingleIn(float duration, float to = 0.8f) => new( From: 0f, - To: 0.8f, + To: to, StartDelay: 0f, InDuration: duration, SustainDelay: float.PositiveInfinity, diff --git a/zzre/game/components/ui/Silent.cs b/zzre/game/components/ui/Silent.cs new file mode 100644 index 00000000..b1673748 --- /dev/null +++ b/zzre/game/components/ui/Silent.cs @@ -0,0 +1,3 @@ +namespace zzre.game.components.ui; + +public readonly record struct Silent(); diff --git a/zzre/game/messages/dialog/DialogChestPuzzle.cs b/zzre/game/messages/dialog/DialogChestPuzzle.cs new file mode 100644 index 00000000..033b9bbb --- /dev/null +++ b/zzre/game/messages/dialog/DialogChestPuzzle.cs @@ -0,0 +1,8 @@ + +namespace zzre.game.messages; + +public record struct DialogChestPuzzle( + DefaultEcs.Entity DialogEntity, + int Size, + int LabelExit +); diff --git a/zzre/game/systems/dialog/DialogChestPuzzle.cs b/zzre/game/systems/dialog/DialogChestPuzzle.cs new file mode 100644 index 00000000..a0934e5a --- /dev/null +++ b/zzre/game/systems/dialog/DialogChestPuzzle.cs @@ -0,0 +1,198 @@ +using System; +using System.Linq; +using System.Numerics; +using zzio; +using zzio.db; + +namespace zzre.game.systems; + +public partial class DialogChestPuzzle : ui.BaseScreen +{ + private static readonly components.ui.ElementId IDCancel = new(1000); + private static readonly components.ui.ElementId IDNext = new(1001); + + private static readonly UID UIDCancel = new(0xD45B15B1); + private static readonly UID UIDNext = new(0xCABAD411); + + private static readonly UID UIDAttempts = new(0x7B48CC11); + private static readonly UID UIDMinTries = new(0xEE63D011); + private static readonly UID UIDBoxOfTricks = new(0x6588C491); + private static readonly UID UIDChestOpened = new(0xF798C91); + + private readonly MappedDB db; + private readonly zzio.Savegame savegame; + private readonly IDisposable resetUISubscription; + + public DialogChestPuzzle(ITagContainer diContainer) : base(diContainer, BlockFlags.None) + { + db = diContainer.GetTag(); + savegame = diContainer.GetTag(); + + resetUISubscription = World.Subscribe(HandleResetUI); + OnElementDown += HandleElementDown; + } + + public override void Dispose() + { + base.Dispose(); + resetUISubscription.Dispose(); + } + + private void HandleResetUI(in messages.DialogResetUI message) + { + foreach (var entity in Set.GetEntities()) + entity.Dispose(); + } + + protected override void HandleOpen(in messages.DialogChestPuzzle message) + { + message.DialogEntity.Set(components.DialogState.ChestPuzzle); + + World.Publish(new messages.DialogResetUI(message.DialogEntity)); + var uiEntity = World.CreateEntity(); + uiEntity.Set(new components.Parent(message.DialogEntity)); + + preload.CreateDialogBackground(uiEntity, animateOverlay: true, out var bgRect, opacity: 1f); + + uiEntity.Set(new components.DialogChestPuzzle{ + DialogEntity = message.DialogEntity, + Size = message.Size, + LabelExit = message.LabelExit, + NumAttempts = 0, + Cells = new DefaultEcs.Entity[message.Size*message.Size], + BgRect = bgRect + }); + ref var puzzle = ref uiEntity.Get(); + + preload.CreateLabel(uiEntity) + .With(puzzle.BgRect.Min + new Vector2(20, 20)) + .With(preload.Fnt001) + .WithText(db.GetText(UIDBoxOfTricks).Text) + .Build(); + + CreateBoard(uiEntity, ref puzzle); + puzzle.Attempts = preload.CreateLabel(uiEntity) + .With(puzzle.BgRect.Min + new Vector2(25, 120)) + .With(preload.Fnt000) + .WithText(FormatAttempts(ref puzzle)) + .WithLineHeight(15) + .Build(); + puzzle.Action = preload.CreateSingleDialogButton(uiEntity, UIDCancel, IDCancel, puzzle.BgRect, buttonOffsetY: -45f); + } + + private string FormatAttempts(ref components.DialogChestPuzzle puzzle) => + $"{db.GetText(UIDAttempts).Text}: {puzzle.NumAttempts}\n{db.GetText(UIDMinTries).Text}: {savegame.switchGameMinMoves}"; + + private DefaultEcs.Entity CreateBoard(DefaultEcs.Entity parent, ref components.DialogChestPuzzle puzzle) + { + var entity = World.CreateEntity(); + entity.Set(new components.Parent(parent)); + + var offset = new Vector2(-1, -1) + new Vector2(-23, -23) * puzzle.Size; + + for (int row = 0; row < puzzle.Size; row++) + { + for (int col = 0; col < puzzle.Size; col++) + { + var cell = row * puzzle.Size + col; + var IDCell = new components.ui.ElementId(cell); + var button = preload.CreateButton(entity) + .With(IDCell) + .With(offset + new Vector2(46 * col, 46 * row)) + .With(new components.ui.ButtonTiles(1, Active: 2)) + .With(preload.Swt000) + .Build(); + button.Set(button.Get().GrownBy(new Vector2(1, 1))); // No gaps + button.Set(new components.ui.Silent()); + if (cell % 2 == 0) button.Set(new components.ui.Active()); + puzzle.Cells[cell] = button; + } + } + + return entity; + } + + private static readonly (int row, int col)[] flipped = [ + (0, 0), + (-1, 0), + (0, -1), + (1, 0), + (0, 1) + ]; + + private void UpdateBoard(DefaultEcs.Entity parent, ref components.DialogChestPuzzle puzzle, int cellId) + { + puzzle.NumAttempts += 1; + World.Publish(new messages.SpawnSample("resources/audio/sfx/gui/_g000.wav")); + + int row = cellId / puzzle.Size; + int col = cellId % puzzle.Size; + + foreach (var coord in flipped) + { + if (coord.row + row < puzzle.Size && coord.row + row >= 0 && + coord.col + col < puzzle.Size && coord.col + col >= 0) + { + var cell = (coord.row + row) * puzzle.Size + (coord.col + col); + if (puzzle.Cells[cell].TryGet(out var _)) + puzzle.Cells[cell].Remove(); + else + puzzle.Cells[cell].Set(new components.ui.Active()); + } + } + + if (puzzle.Cells.All(x => x.Has()) || puzzle.Cells.All(x => !x.Has())) + Succeed(parent, ref puzzle); + + puzzle.Attempts.Set(new components.ui.Label(FormatAttempts(ref puzzle))); + } + + private void Succeed(DefaultEcs.Entity parent, ref components.DialogChestPuzzle puzzle) + { + World.Publish(new messages.SpawnSample($"resources/audio/sfx/specials/_s022.wav")); + + preload.CreateLabel(parent) + .With(new Vector2(0, -126)) + .With(components.ui.FullAlignment.Center) + .With(preload.Fnt001) + .WithText(db.GetText(UIDChestOpened).Text) + .Build(); + + if (savegame.switchGameMinMoves > puzzle.NumAttempts) + savegame.switchGameMinMoves = puzzle.NumAttempts; + + puzzle.LockBoard = true; + + puzzle.Action.Dispose(); + puzzle.Action = preload.CreateSingleDialogButton(parent, UIDNext, IDNext, puzzle.BgRect, buttonOffsetY: -45f); + } + + private static readonly components.ui.ElementId FirstCellId = new(0); + + private void HandleElementDown(DefaultEcs.Entity entity, components.ui.ElementId clickedId) + { + var uiEntity = Set.GetEntities()[0]; + ref var puzzle = ref uiEntity.Get(); + ref var script = ref puzzle.DialogEntity.Get(); + + if (!puzzle.LockBoard && clickedId.InRange(FirstCellId, FirstCellId + puzzle.Size*puzzle.Size, out var cellId)) { + UpdateBoard(uiEntity, ref puzzle, cellId); + } + else if (clickedId == IDCancel) + { + World.Publish(new messages.SpawnSample($"resources/audio/sfx/gui/_g003.wav")); + puzzle.DialogEntity.Set(components.DialogState.NextScriptOp); + uiEntity.Dispose(); + } + else if (clickedId == IDNext) + { + script.CurrentI = script.LabelTargets[puzzle.LabelExit]; + puzzle.DialogEntity.Set(components.DialogState.NextScriptOp); + uiEntity.Dispose(); + } + } + + protected override void Update(float timeElapsed, in DefaultEcs.Entity entity, ref components.DialogChestPuzzle component) + { + } +} diff --git a/zzre/game/systems/dialog/DialogGambling.cs b/zzre/game/systems/dialog/DialogGambling.cs index c6cdd5bc..2822b01c 100644 --- a/zzre/game/systems/dialog/DialogGambling.cs +++ b/zzre/game/systems/dialog/DialogGambling.cs @@ -132,7 +132,7 @@ private void RebuildPrimary(DefaultEcs.Entity parent, ref components.DialogGambl private void AddBottomButtons(DefaultEcs.Entity parent, ref components.DialogGambling gambling) { if (CanAfford(ref gambling)) - preload.CreateSingleDialogButton(parent, UIDRepeat, IDRepeat, gambling.BgRect, offset: 1); + preload.CreateSingleDialogButton(parent, UIDRepeat, IDRepeat, gambling.BgRect, buttonOffsetY: -90f); preload.CreateSingleDialogButton(parent, UIDExit, IDExit, gambling.BgRect); } diff --git a/zzre/game/systems/dialog/DialogScript.cs b/zzre/game/systems/dialog/DialogScript.cs index aec04ab8..f2f932f2 100644 --- a/zzre/game/systems/dialog/DialogScript.cs +++ b/zzre/game/systems/dialog/DialogScript.cs @@ -509,7 +509,16 @@ private void EndGame(DefaultEcs.Entity entity) private void SubGame(DefaultEcs.Entity entity, SubGameType subGameType, int size, int labelExit) { - LogUnimplementedInstructionWarning(); + switch (subGameType) { + case SubGameType.ChestPuzzle: + World.Publish(new messages.DialogChestPuzzle(entity, size + 2, labelExit)); + break; + case SubGameType.ElfGame: + LogUnimplementedInstructionWarning(); + break; + default: + throw new NotSupportedException($"Unsupported SubGameType: {subGameType}"); + }; } private void PlayAnimation(DefaultEcs.Entity entity, AnimationType animation) diff --git a/zzre/game/systems/ui/Cursor.cs b/zzre/game/systems/ui/Cursor.cs index 5d473f2d..e47b37e9 100644 --- a/zzre/game/systems/ui/Cursor.cs +++ b/zzre/game/systems/ui/Cursor.cs @@ -55,7 +55,8 @@ private void Update( if (!entity.Has()) { entity.Set(); - World.Publish(new messages.SpawnSample("resources/audio/sfx/gui/_g000.wav")); + if (!entity.Has()) + World.Publish(new messages.SpawnSample("resources/audio/sfx/gui/_g000.wav")); } hoveredElement = new(entity, elementId); } diff --git a/zzre/game/systems/ui/UIPreloader.cs b/zzre/game/systems/ui/UIPreloader.cs index f196aee6..51b9c4df 100644 --- a/zzre/game/systems/ui/UIPreloader.cs +++ b/zzre/game/systems/ui/UIPreloader.cs @@ -38,7 +38,8 @@ public class UIPreloader Log000, Cls000, Cls001, - Map000; + Map000, + Swt000; private static readonly UID UIDYouHave = new(0x070EE421); @@ -70,6 +71,7 @@ public UIPreloader(ITagContainer diContainer) Cls000 = Preload(out var tsCls000, "cls000", isFont: false); Cls001 = Preload(out var tsCls001, "cls001", isFont: false); Map000 = Preload(out var tsMap000, "map000", isFont: false); + Swt000 = Preload(out var tsSwt000, "swt000", isFont: false); tsFnt000.Alternatives.Add(tsFnt001); tsFnt000.Alternatives.Add(tsFnt002); @@ -153,7 +155,8 @@ public UIPreloader(ITagContainer diContainer) public void CreateDialogBackground( DefaultEcs.Entity parent, bool animateOverlay, - out Rect backgroundRect) + out Rect backgroundRect, + float opacity = 0.8f) { var image = CreateImage(parent) .WithBitmap("std000") @@ -162,20 +165,21 @@ public void CreateDialogBackground( .Build(); backgroundRect = image.Get(); - CreateBackOverlay(parent, animateOverlay, backgroundRect); + CreateBackOverlay(parent, animateOverlay, opacity, backgroundRect); } public void CreateBackOverlay( DefaultEcs.Entity parent, bool animateOverlay, + float opacity, Rect backgroundRect) { var overlay = CreateImage(parent) - .With(DefaultOverlayColor with { a = animateOverlay ? 0f : 0.8f }) + .With(DefaultOverlayColor with { a = animateOverlay ? 0f : opacity }) .With(backgroundRect) .WithRenderOrder(2); if (animateOverlay) - overlay.With(components.ui.Fade.SingleIn(0.8f)); + overlay.With(components.ui.Fade.SingleIn(0.8f, opacity)); overlay.Build(); } @@ -214,13 +218,11 @@ public DefaultEcs.Entity CreateCurrencyLabel(DefaultEcs.Entity parent, ItemRow c .WithText($"{GetDBText(UIDYouHave)} {{{3000 + currency.CardId.EntityId}}}x{inventory.CountCards(currency.CardId)}") .Build(); - private const float ButtonOffsetY = -50f; - private const float RepeatButtonOffsetY = -40f; - public DefaultEcs.Entity CreateSingleDialogButton(DefaultEcs.Entity entity, UID textUID, components.ui.ElementId elementId, Rect bgRect, int offset = 0) + public DefaultEcs.Entity CreateSingleDialogButton(DefaultEcs.Entity entity, UID textUID, components.ui.ElementId elementId, Rect bgRect, float buttonOffsetY = -50f) { var button = CreateButton(entity) .With(elementId) - .With(new Vector2(bgRect.Center.X, bgRect.Max.Y + ButtonOffsetY + RepeatButtonOffsetY * offset)) + .With(new Vector2(bgRect.Center.X, bgRect.Max.Y + buttonOffsetY)) .With(new components.ui.ButtonTiles(0, 1)) .With(components.ui.FullAlignment.TopCenter) .With(Btn000)