using Microsoft.Xna.Framework; 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 III — Calling. Two-column grid of class cards. Cards recommended /// for the chosen clade get a small "★ Suits Clade" badge. Level-1 /// features render as inline trait chips with hover popovers. /// public static class StepClass { public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover) { var col = new Column { Spacing = 14 }; col.Add(StepCommon.PageIntro( "Folio III — Of Vocations", "Choose your Calling", "Eight callings exist within the Covenant. Each shapes how you fight, treat, parley, or unmake the world.")); var grid = new Grid { Columns = 2 }; foreach (var c in s.Classes) grid.Add(BuildCard(s, atlas, popover, c)); col.Add(grid); return col; } private static CodexWidget BuildCard(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, ClassDef c) { var content = new Column { Spacing = 8 }; var titleRow = new Row { Spacing = 8, VAlignChildren = VAlign.Middle }; titleRow.Add(new CodexLabel(c.Name, CodexFonts.DisplayMedium, CodexColors.Ink)); bool suits = s.Clade is not null && CodexCopy.IsSuited(c.Id, s.Clade.Id); if (suits) titleRow.Add(new RecBadge(atlas)); content.Add(titleRow); content.Add(new CodexLabel( $"D{c.HitDie} · PRIMARY {string.Join("/", c.PrimaryAbility)} · SAVES {string.Join("/", c.Saves)}", CodexFonts.MonoTagSmall, CodexColors.InkMute)); // Level-1 features as trait chips var lvl1 = System.Array.Find(c.LevelTable, e => e.Level == 1); if (lvl1 is not null) { var chips = new WrapRow(); foreach (var k in lvl1.Features) { if (k == "asi" || k == "subclass_select" || k == "subclass_feature") continue; if (!c.FeatureDefinitions.TryGetValue(k, out var fd)) continue; chips.Add(new HoverableChip(atlas, popover, fd.Name, fd.Name, fd.Description, fd.Kind?.ToUpperInvariant(), ChipKind.Trait)); } content.Add(chips); } content.Add(new CodexLabel( $"PICKS {c.SkillsChoose} SKILL{(c.SkillsChoose > 1 ? "S" : "")} · ARMOR: {string.Join(", ", c.ArmorProficiencies)}", CodexFonts.MonoTagSmall, CodexColors.InkMute)); return new CodexCard(atlas, content, s.Class == c, onClick: () => { // If switching class, drop any previously-picked skills // that aren't on the new class's option list — but never // auto-pick. The Sign step must stay locked until the // user explicitly visits the Skills folio. if (s.Class != c) { s.Class = c; var allowed = new System.Collections.Generic.HashSet(c.SkillOptions, System.StringComparer.OrdinalIgnoreCase); var bgLocked = new System.Collections.Generic.HashSet( s.Background?.SkillProficiencies ?? System.Array.Empty(), System.StringComparer.OrdinalIgnoreCase); s.ChosenSkills.RemoveWhere(sk => { string raw = sk.ToString().ToLowerInvariant(); return !allowed.Contains(raw) || bgLocked.Contains(raw); }); } s.InvalidateLayout(); }); } } internal sealed class RecBadge : CodexWidget { private readonly CodexAtlas _atlas; private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.MonoTagSmall; private const string Text = "★ SUITS CLADE"; public RecBadge(CodexAtlas atlas) { _atlas = atlas; } protected override Point MeasureCore(Point available) { var s = _font.MeasureString(Text); return new Point((int)s.X + 12, (int)System.MathF.Ceiling(_font.LineHeight) + 6); } protected override void ArrangeCore(Microsoft.Xna.Framework.Rectangle bounds) { } public override void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch sb, Microsoft.Xna.Framework.GameTime gt) { var fill = new Microsoft.Xna.Framework.Color(CodexColors.Gild.R, CodexColors.Gild.G, CodexColors.Gild.B, (byte)20); sb.Draw(_atlas.Pixel, Bounds, fill); sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), CodexColors.Gild); sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), CodexColors.Gild); sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Y, 1, Bounds.Height), CodexColors.Gild); sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.Right - 1, Bounds.Y, 1, Bounds.Height), CodexColors.Gild); var s = _font.MeasureString(Text); _font.DrawText(sb, Text, new Microsoft.Xna.Framework.Vector2(Bounds.X + (Bounds.Width - s.X) / 2f, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f), CodexColors.Gild); } }