using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Core.Rules.Stats; using Theriapolis.Game.CodexUI.Core; using Theriapolis.Game.CodexUI.Steps; using Theriapolis.Game.CodexUI.Widgets; using Theriapolis.Game.UI; namespace Theriapolis.Game.CodexUI.Screens; /// /// Right-column live summary. Reads from 's /// state and renders five blocks (Name, Lineage, Calling+History, Abilities, /// Skills) plus a stat strip. Mirrors the React <Aside /> /// component in app.jsx. /// public sealed class CodexAside { private readonly CodexCharacterCreationScreen _s; private readonly CodexAtlas _atlas; public CodexAside(CodexCharacterCreationScreen s, CodexAtlas atlas) { _s = s; _atlas = atlas; } public CodexWidget Build() { // Aside lives next to the page main and shares the screen's // popover layer — hovering a chip here pops the same parchment- // and-gilt popover the cards on the left side use. var popover = _s.AsidePopover; var col = new Column { Spacing = 14, HAlignChildren = HAlign.Stretch }; col.Add(new CodexLabel("THE SUBJECT", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(NameBlock()); col.Add(LineageBlock(popover)); col.Add(CallingBlock(popover)); col.Add(HistoryBlock(popover)); col.Add(BuildStatStrip()); col.Add(SkillsBlock(popover)); return col; } private CodexWidget NameBlock() { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("NAME", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new CodexLabel(string.IsNullOrWhiteSpace(_s.Name) ? "(unnamed)" : _s.Name, CodexFonts.DisplayMedium, CodexColors.Ink)); return col; } private CodexWidget LineageBlock(CodexHoverPopover popover) { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("LINEAGE", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new CodexLabel(_s.Species?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink)); if (_s.Clade is not null || _s.Species is not null) { string sub = (_s.Clade?.Name ?? "—").ToUpperInvariant() + (_s.Clade is not null ? " · " + _s.Clade.Kind.ToUpperInvariant() : "") + (_s.Species is not null ? " · " + CodexCopy.SizeLabel(_s.Species.Size).ToUpperInvariant() : ""); col.Add(new CodexLabel(sub, CodexFonts.MonoTagSmall, CodexColors.InkMute)); } // Trait chips — clade traits + species traits (each hover-popover'd). var chips = new WrapRow(); if (_s.Clade is not null) { foreach (var t in _s.Clade.Traits) chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait)); foreach (var t in _s.Clade.Detriments) chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment)); } if (_s.Species is not null) { foreach (var t in _s.Species.Traits) chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait)); foreach (var t in _s.Species.Detriments) chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment)); } if (chips.Children.Count > 0) col.Add(chips); return col; } private CodexWidget CallingBlock(CodexHoverPopover popover) { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("CALLING", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new CodexLabel(_s.Class?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink)); if (_s.Class is not null) col.Add(new CodexLabel($"D{_s.Class.HitDie} · {string.Join("/", _s.Class.PrimaryAbility)}", CodexFonts.MonoTagSmall, CodexColors.InkMute)); // Level-1 feature chips for the chosen class (hover surfaces description). if (_s.Class is not null) { var lvl1 = System.Array.Find(_s.Class.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 (!_s.Class.FeatureDefinitions.TryGetValue(k, out var fd)) continue; chips.Add(new HoverableChip(_atlas, popover, fd.Name, fd.Name, fd.Description, fd.Kind?.ToUpperInvariant(), ChipKind.Trait)); } if (chips.Children.Count > 0) col.Add(chips); } } return col; } private CodexWidget HistoryBlock(CodexHoverPopover popover) { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("HISTORY", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new CodexLabel(_s.Background?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink)); if (_s.Background is not null && !string.IsNullOrEmpty(_s.Background.FeatureName)) { var chips = new WrapRow(); chips.Add(new HoverableChip(_atlas, popover, _s.Background.FeatureName, _s.Background.FeatureName, _s.Background.FeatureDescription, "FEATURE", ChipKind.BgFeature)); col.Add(chips); } return col; } private CodexWidget SkillsBlock(CodexHoverPopover popover) { int total = _s.ChosenSkills.Count + (_s.Background?.SkillProficiencies.Length ?? 0); var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("SKILLS · " + total, CodexFonts.MonoTag, CodexColors.InkMute)); var chips = new WrapRow(); if (_s.Background is not null) foreach (var s in _s.Background.SkillProficiencies) chips.Add(new HoverableChip(_atlas, popover, CodexCopy.SkillName(s), CodexCopy.SkillName(s), CodexCopy.SkillDescription(s), "BACKGROUND", ChipKind.SkillFromBg)); foreach (var s in _s.ChosenSkills.OrderBy(x => x.ToString())) { // Map enum → snake_case JSON id; otherwise SleightOfHand / // AnimalHandling lose their hover descriptions because the // CodexCopy switch is keyed on the JSON form. string id = CodexCopy.SkillIdToJson(s); chips.Add(new HoverableChip(_atlas, popover, CodexCopy.SkillName(id), CodexCopy.SkillName(id), CodexCopy.SkillDescription(id), "CLASS", ChipKind.SkillFromClass)); } col.Add(chips); return col; } private CodexWidget BuildStatStrip() { var col = new Column { Spacing = 4 }; col.Add(new CodexLabel("ABILITIES", CodexFonts.MonoTag, CodexColors.InkMute)); col.Add(new StatStrip(_s, _atlas)); return col; } } /// /// Six-cell strip (one per ability) with name, final score, and signed /// modifier. Used in the aside panel and on the review step. /// internal sealed class StatStrip : CodexWidget { private readonly CodexCharacterCreationScreen _s; private readonly CodexAtlas _atlas; public StatStrip(CodexCharacterCreationScreen s, CodexAtlas atlas) { _s = s; _atlas = atlas; } protected override Point MeasureCore(Point available) => new(available.X, 50); protected override void ArrangeCore(Rectangle bounds) { } public override void Draw(SpriteBatch sb, GameTime gt) { var abFont = CodexFonts.MonoTagSmall; var scoreFont = CodexFonts.DisplayMedium; var modFont = CodexFonts.MonoTagSmall; int cellW = (Bounds.Width - 5 * 4) / 6; for (int i = 0; i < CodexCopy.AbilityOrder.Length; i++) { var ab = CodexCopy.AbilityOrder[i]; int x = Bounds.X + i * (cellW + 4); var rect = new Rectangle(x, Bounds.Y, cellW, Bounds.Height); sb.Draw(_atlas.Pixel, rect, CodexColors.Bg); DrawBorder(sb, rect, CodexColors.Rule, 1); string ablab = ab.ToString(); var s1 = abFont.MeasureString(ablab); abFont.DrawText(sb, ablab, new Vector2(rect.X + (rect.Width - s1.X) / 2f, rect.Y + 4), CodexColors.InkMute); int? base_ = _s.StatAssign.TryGetValue(ab, out var v) ? v : (int?)null; int total = (base_ ?? 0) + _s.TotalBonus(ab); int mod = AbilityScores.Mod(total); string scoreText = base_ is null ? "—" : total.ToString(); string modText = base_ is null ? "" : ((mod >= 0 ? "+" : "") + mod); var s2 = scoreFont.MeasureString(scoreText); scoreFont.DrawText(sb, scoreText, new Vector2(rect.X + (rect.Width - s2.X) / 2f, rect.Y + 16), CodexColors.Ink); if (modText.Length > 0) { var s3 = modFont.MeasureString(modText); modFont.DrawText(sb, modText, new Vector2(rect.X + (rect.Width - s3.X) / 2f, rect.Bottom - modFont.LineHeight - 4), mod >= 0 ? CodexColors.Seal : CodexColors.InkMute); } } } private void DrawBorder(SpriteBatch sb, Rectangle r, Color c, int t) { sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, r.Width, t), c); sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Bottom - t, r.Width, t), c); sb.Draw(_atlas.Pixel, new Rectangle(r.X, r.Y, t, r.Height), c); sb.Draw(_atlas.Pixel, new Rectangle(r.Right - t, r.Y, t, r.Height), c); } }