using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Core; using Theriapolis.Game.CodexUI.Screens; using Theriapolis.Game.CodexUI.Widgets; using Theriapolis.Game.UI; namespace Theriapolis.Game.CodexUI.Steps; /// /// Step VII — Sign. Name input + summary panels for lineage / calling / /// abilities / skills / starting kit. Each panel has an Edit › link that /// jumps the wizard back to the source step. /// public static class StepReview { public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover) { var col = new Column { Spacing = 14 }; col.Add(StepCommon.PageIntro( "Folio VII — Of Names & Witness", "Sign the Codex", "Review your character. The name you sign here is the one the world will speak.")); // Name input var nameBlock = new Column { Spacing = 4 }; nameBlock.Add(new CodexLabel("NAME", CodexFonts.MonoTag, CodexColors.InkMute)); var nameInput = new CodexTextBox(s.Name, atlas, fixedWidth: 480, onChanged: t => s.Name = t) { Placeholder = "Wanderer", }; nameBlock.Add(nameInput); col.Add(nameBlock); // Lineage / Calling pair var pair = new Grid { Columns = 2 }; pair.Add(BuildBlock(atlas, "Lineage", () => s.Step = 0, s.InvalidateLayout, BuildLineage(s))); pair.Add(BuildBlock(atlas, "Calling & History", () => s.Step = 2, s.InvalidateLayout, BuildCalling(s))); col.Add(pair); // Final abilities col.Add(BuildBlock(atlas, "Final Abilities", () => s.Step = 4, s.InvalidateLayout, new StatStrip(s, atlas))); // Skills — HoverableChip so the player can re-read each skill's // codex flavour text without bouncing back to the Skills folio. var skillsBlock = new WrapRow(); if (s.Background is not null) foreach (var sk in s.Background.SkillProficiencies) skillsBlock.Add(new HoverableChip(atlas, popover, CodexCopy.SkillName(sk), CodexCopy.SkillName(sk), CodexCopy.SkillDescription(sk), "BACKGROUND", ChipKind.SkillFromBg)); foreach (var sk in s.ChosenSkills.OrderBy(x => x.ToString())) { string id = CodexCopy.SkillIdToJson(sk); skillsBlock.Add(new HoverableChip(atlas, popover, CodexCopy.SkillName(id), CodexCopy.SkillName(id), CodexCopy.SkillDescription(id), "CLASS", ChipKind.SkillFromClass)); } col.Add(BuildBlock(atlas, "Skills", () => s.Step = 5, s.InvalidateLayout, skillsBlock)); // Starting kit — each chip resolves the item def for description / // properties / damage etc. and shows them on hover. var kitBlock = new WrapRow(); if (s.Class?.StartingKit is not null) foreach (var entry in s.Class.StartingKit) kitBlock.Add(new KitItemWidget(atlas, popover, entry, s.Content)); col.Add(BuildBlock(atlas, "Starting Kit", () => s.Step = 2, s.InvalidateLayout, kitBlock)); return col; } private static CodexWidget BuildLineage(CodexCharacterCreationScreen s) { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel(s.Clade?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink)); col.Add(new CodexLabel( (s.Species?.Name ?? "—") + (s.Species is not null ? $" · {CodexCopy.SizeLabel(s.Species.Size).ToUpperInvariant()}" : ""), CodexFonts.SerifBody, CodexColors.InkSoft)); return col; } private static CodexWidget BuildCalling(CodexCharacterCreationScreen s) { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel(s.Class?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink)); col.Add(new CodexLabel( (s.Class is not null ? $"D{s.Class.HitDie} · {string.Join("/", s.Class.PrimaryAbility)}" : ""), CodexFonts.MonoTagSmall, CodexColors.InkMute)); col.Add(new CodexLabel(s.Background?.Name ?? "—", CodexFonts.SerifItalic, CodexColors.InkSoft)); return col; } private static CodexWidget BuildBlock(CodexAtlas atlas, string title, System.Action onEdit, System.Action onAfterEdit, CodexWidget body) { var inner = new Column { Spacing = 8 }; var head = new Row { Spacing = 8, VAlignChildren = VAlign.Middle }; head.Add(new CodexLabel(title, CodexFonts.DisplaySmall, CodexColors.Ink)); var spacerL = new HSpacer(); head.Add(spacerL); head.Add(new EditLink(atlas, () => { onEdit(); onAfterEdit(); })); inner.Add(head); inner.Add(body); return new CodexPanel(atlas, inner); } } internal sealed class EditLink : CodexWidget { private readonly CodexAtlas _atlas; private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.MonoTagSmall; private readonly System.Action _onClick; private bool _hovered; private const string Text = "EDIT ›"; public EditLink(CodexAtlas atlas, System.Action onClick) { _atlas = atlas; _onClick = onClick; } protected override Point MeasureCore(Point available) { var s = _font.MeasureString(Text); return new Point((int)s.X + 6, (int)System.MathF.Ceiling(_font.LineHeight) + 4); } protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) { _hovered = ContainsPoint(input.MousePosition); if (_hovered && input.LeftJustReleased) _onClick(); } public override void Draw(SpriteBatch sb, GameTime gt) { var s = _font.MeasureString(Text); var color = _hovered ? CodexColors.GildBright : CodexColors.Gild; _font.DrawText(sb, Text, new Vector2(Bounds.X, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f), color); } } internal sealed class HSpacer : CodexWidget { protected override Point MeasureCore(Point available) => new(System.Math.Max(0, available.X - 80), 1); protected override void ArrangeCore(Rectangle bounds) { } } internal sealed class KitItemWidget : CodexWidget { private readonly CodexAtlas _atlas; private readonly CodexHoverPopover _popover; private readonly Theriapolis.Core.Data.StartingKitItem _entry; private readonly Theriapolis.Core.Data.ItemDef? _itemDef; private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.SerifBody; private readonly FontStashSharp.SpriteFontBase _qtyFont = CodexFonts.MonoTagSmall; private bool _hovered; public KitItemWidget(CodexAtlas atlas, CodexHoverPopover popover, Theriapolis.Core.Data.StartingKitItem entry, Theriapolis.Core.Data.ContentResolver content) { _atlas = atlas; _popover = popover; _entry = entry; // Item id may not resolve (forward-compat starting kits, missing // entries) — gracefully fall back to a name-only popover. content.Items.TryGetValue(entry.ItemId, out _itemDef); } protected override Point MeasureCore(Point available) => new(160, 48); protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) { _hovered = ContainsPoint(input.MousePosition); if (_hovered) ShowPopover(); } private void ShowPopover() { string title = CodexCopy.ItemName(_entry.ItemId); string body = ComposePopoverBody(); string? tag = _itemDef?.Kind?.ToUpperInvariant(); _popover.Show(Bounds, title, body, tag); } /// Compose a multi-line description from the item's stats and /// the auto-equip slot. Falls back to the codex flavor description /// when no kind-specific stats are available. private string ComposePopoverBody() { var sb = new System.Text.StringBuilder(); if (_entry.Qty > 1) sb.Append($"Quantity: ×{_entry.Qty}\n"); if (_entry.AutoEquip) sb.Append($"Equipped to: {_entry.EquipSlot.Replace('_', ' ')}\n"); if (_itemDef is not null) { switch (_itemDef.Kind) { case "weapon": sb.Append($"Damage: {_itemDef.Damage}"); if (!string.IsNullOrEmpty(_itemDef.DamageType)) sb.Append(' ').Append(_itemDef.DamageType); if (!string.IsNullOrEmpty(_itemDef.DamageVersatile)) sb.Append($" ({_itemDef.DamageVersatile} two-handed)"); sb.Append('\n'); break; case "armor": sb.Append($"AC {_itemDef.AcBase}"); if (_itemDef.AcMaxDex >= 0) sb.Append($" + DEX (max +{_itemDef.AcMaxDex})"); if (_itemDef.MinStr > 0) sb.Append($" · Min STR {_itemDef.MinStr}"); sb.Append('\n'); break; case "shield": sb.Append($"+{_itemDef.AcBase} AC\n"); break; case "consumable": if (!string.IsNullOrEmpty(_itemDef.Healing)) sb.Append($"Heals {_itemDef.Healing}\n"); break; } if (_itemDef.Properties.Length > 0) sb.Append("Properties: ").Append(string.Join(", ", _itemDef.Properties)).Append('\n'); if (!string.IsNullOrEmpty(_itemDef.Description)) sb.Append(_itemDef.Description); } if (sb.Length == 0) sb.Append("(no description)"); return sb.ToString().TrimEnd('\n'); } public override void Draw(SpriteBatch sb, GameTime gt) { Color border = _hovered ? CodexColors.Gild : _entry.AutoEquip ? CodexColors.Gild : CodexColors.Rule; sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), border); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), border); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, 1, Bounds.Height), border); sb.Draw(_atlas.Pixel, new Rectangle(Bounds.Right - 1, Bounds.Y, 1, Bounds.Height), border); _font.DrawText(sb, CodexCopy.ItemName(_entry.ItemId), new Vector2(Bounds.X + 8, Bounds.Y + 6), CodexColors.Ink); _qtyFont.DrawText(sb, "×" + _entry.Qty, new Vector2(Bounds.X + 8, Bounds.Y + 22), CodexColors.InkMute); if (_entry.AutoEquip) _qtyFont.DrawText(sb, _entry.EquipSlot.ToUpperInvariant(), new Vector2(Bounds.X + 8, Bounds.Bottom - _qtyFont.LineHeight - 4), CodexColors.Gild); } }