using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Core.Data; 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 I — Clade. Two grouped grids (Predators / Prey) of clade cards, /// each card showing the clade name + kind + ability mods + language chips /// + trait chips. Selection swaps the species default to the first species /// belonging to that clade. /// public static class StepClade { public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover) { var col = new Column { Spacing = 14 }; col.Add(StepCommon.PageIntro("Folio I — Of Bloodlines", "Choose your Clade", "The seven great families of Theriapolis. Your clade is the body you were born to — the broad shape of your gait, the fall of your shadow, the words your scent carries before you speak.")); col.Add(new CodexLabel("PREDATORS", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(BuildGrid(s, atlas, popover, "predator")); col.Add(new CodexLabel("PREY", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(BuildGrid(s, atlas, popover, "prey")); return col; } private static CodexWidget BuildGrid(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, string kind) { var grid = new Grid { Columns = 3 }; foreach (var c in s.Clades) { if (c.Kind != kind) continue; grid.Add(BuildCard(s, atlas, popover, c)); } return grid; } private static CodexWidget BuildCard(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, CladeDef c) { var content = new Column { Spacing = 8 }; // Header: sigil + name/kind var headerRow = new Row { Spacing = 12, VAlignChildren = VAlign.Top }; headerRow.Add(new SigilWidget(atlas, c.Id)); var titleCol = new Column { Spacing = 2 }; titleCol.Add(new CodexLabel(c.Name, CodexFonts.DisplayMedium, CodexColors.Ink)); titleCol.Add(new CodexLabel(c.Kind.ToUpperInvariant(), CodexFonts.MonoTagSmall, CodexColors.InkMute)); headerRow.Add(titleCol); content.Add(headerRow); // Mods row if (c.AbilityMods.Count > 0) { var mods = new WrapRow(); foreach (var kv in c.AbilityMods) mods.Add(new ModChipMini(atlas, kv.Key, kv.Value)); content.Add(mods); } // Languages content.Add(new CodexLabel("LANGUAGES", CodexFonts.MonoTagSmall, CodexColors.InkMute)); var langs = new WrapRow(); foreach (var l in c.Languages) langs.Add(new HoverableChip(atlas, popover, CodexCopy.LanguageName(l), CodexCopy.LanguageName(l), CodexCopy.LanguageDescription(l), null, ChipKind.Language)); content.Add(langs); // Traits content.Add(new CodexLabel("TRAITS", CodexFonts.MonoTagSmall, CodexColors.InkMute)); var traits = new WrapRow(); foreach (var t in c.Traits) traits.Add(new HoverableChip(atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait)); foreach (var t in c.Detriments) traits.Add(new HoverableChip(atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment)); content.Add(traits); bool isSelected = s.Clade == c; var card = new CodexCard(atlas, content, isSelected, onClick: () => { s.Clade = c; // If the previously-picked species belongs to a different // clade, drop it — but never auto-pick a new species. The // user must visit the Species folio explicitly so the // Calling step stays locked behind that decision. if (s.Species is not null && s.Species.CladeId != c.Id) s.Species = null; s.InvalidateLayout(); }); card.CornerSigil = atlas.SigilFor(c.Id); return card; } } /// Renders the clade sigil placeholder + a centred initial letter. internal sealed class SigilWidget : CodexWidget { private readonly CodexAtlas _atlas; private readonly string _cladeId; public SigilWidget(CodexAtlas atlas, string cladeId) { _atlas = atlas; _cladeId = cladeId; } protected override Point MeasureCore(Point available) => new(56, 56); protected override void ArrangeCore(Rectangle bounds) { } public override void Draw(SpriteBatch sb, GameTime gt) { sb.Draw(_atlas.SigilFor(_cladeId), Bounds, Color.White); // Letter overlay so the placeholder is identifiable. char ch = char.ToUpper(_cladeId.Length > 0 ? _cladeId[0] : '?'); var font = CodexFonts.DisplayMedium; var s = font.MeasureString(ch.ToString()); font.DrawText(sb, ch.ToString(), new Vector2(Bounds.X + (Bounds.Width - s.X) / 2f, Bounds.Y + (Bounds.Height - font.LineHeight) / 2f), CodexColors.Ink); } } /// Inline mod pill drawn as `STR +1` / `DEX -1` etc. internal sealed class ModChipMini : CodexWidget { private readonly CodexAtlas _atlas; private readonly string _label; private readonly bool _positive; private readonly SpriteFontBase _font = CodexFonts.MonoTagSmall; public ModChipMini(CodexAtlas atlas, string ab, int v) { _atlas = atlas; _label = $"{ab} {(v >= 0 ? "+" : "")}{v}"; _positive = v >= 0; } protected override Point MeasureCore(Point available) { var s = _font.MeasureString(_label); return new Point((int)s.X + 14, (int)System.MathF.Ceiling(_font.LineHeight) + 6); } protected override void ArrangeCore(Rectangle bounds) { } public override void Draw(SpriteBatch sb, GameTime gt) { var border = _positive ? CodexColors.Seal : CodexColors.InkMute; var fill = CodexColors.Bg; sb.Draw(_atlas.Pixel, Bounds, fill); 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); var s = _font.MeasureString(_label); _font.DrawText(sb, _label, new Vector2(Bounds.X + (Bounds.Width - s.X) / 2f, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f), _positive ? CodexColors.Seal : CodexColors.InkSoft); } } /// /// Chip variant that drives the screen's popover when hovered. Acts as a /// thin wrapper around plus a popover-show side /// effect during update. /// internal sealed class HoverableChip : CodexWidget { private readonly CodexChip _chip; private readonly CodexHoverPopover _popover; private readonly string _title, _body; private readonly string? _tag; private readonly bool _detriment; public HoverableChip(CodexAtlas atlas, CodexHoverPopover popover, string text, string popTitle, string popBody, string? popTag, ChipKind kind) { _chip = new CodexChip(text, kind, atlas, popTitle, popBody, popTag); _popover = popover; _title = popTitle; _body = popBody; _tag = popTag; _detriment = kind == ChipKind.TraitDetriment; } public System.Action? OnClick { get => _chip.OnClick; set => _chip.OnClick = value; } protected override Point MeasureCore(Point available) => _chip.Measure(available); protected override void ArrangeCore(Rectangle bounds) => _chip.Arrange(bounds); public override void Update(GameTime gt, CodexInput input) { _chip.Update(gt, input); if (_chip.IsHovered && !string.IsNullOrEmpty(_body)) _popover.Show(_chip.Bounds, _title, _body, _tag, _detriment); } public override void Draw(SpriteBatch sb, GameTime gt) => _chip.Draw(sb, gt); } /// Reusable page-intro block: small mono eyebrow, large display title, body paragraph. public static class StepCommon { public static CodexWidget PageIntro(string eyebrow, string title, string body) { var col = new Column { Spacing = 6 }; col.Add(new CodexLabel(eyebrow.ToUpperInvariant(), CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new CodexLabel(title, CodexFonts.DisplayLarge, CodexColors.Ink)); col.Add(new CodexLabel(body, CodexFonts.SerifBody, CodexColors.InkSoft)); return new Padding(col, new Thickness(0, 0, 0, 14)); } }