using FontStashSharp; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Widgets; public enum ChipKind { Trait, // gild edge, italic serif, default for clade/species/feature traits TraitDetriment, SkillFromBg, // gild edge — sealed by background SkillFromClass, // seal-red edge — picked from class options Language, // mono-tag pill, ink border BgFeature, // seal-red trait variant for the background card's feature row } /// /// Pill-shaped chip used for traits, skills, languages, and background feature /// names. Hovering surfaces a popover with the full description; clicking /// fires for the few cases the screen needs (skill toggle). /// public sealed class CodexChip : CodexWidget { public string Text { get; set; } public string PopoverTitle { get; set; } public string PopoverBody { get; set; } public string? PopoverTag { get; set; } public ChipKind Kind { get; set; } public System.Action? OnClick { get; set; } private readonly CodexAtlas _atlas; private readonly SpriteFontBase _font; /// The screen sets this when the user hovers over us; the screen handles popover layout. public bool IsHovered { get; private set; } public CodexChip(string text, ChipKind kind, CodexAtlas atlas, string popoverTitle = "", string popoverBody = "", string? popoverTag = null) { Text = text; Kind = kind; _atlas = atlas; PopoverTitle = popoverTitle; PopoverBody = popoverBody; PopoverTag = popoverTag; _font = kind == ChipKind.Language ? CodexFonts.MonoTagSmall : CodexFonts.SerifItalic; } protected override Point MeasureCore(Point available) { var s = _font.MeasureString(Text); return new Point((int)s.X + CodexDensity.ChipPad * 3, (int)System.MathF.Ceiling(_font.LineHeight) + CodexDensity.ChipPad * 2); } protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) { IsHovered = ContainsPoint(input.MousePosition); if (IsHovered && input.LeftJustReleased) OnClick?.Invoke(); } public override void Draw(SpriteBatch sb, GameTime gt) { var (fill, border, text) = GetColors(); if (IsHovered) fill = HoverShift(fill); sb.Draw(_atlas.Pixel, Bounds, fill); // 1-px outline. 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(Text); float tx = Bounds.X + (Bounds.Width - s.X) / 2f; float ty = Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f; _font.DrawText(sb, Text, new Vector2(tx, ty), text); } private (Color fill, Color border, Color text) GetColors() => Kind switch { ChipKind.Trait => (Mix(CodexColors.Gild, CodexColors.Bg, 0.07f), Mix(CodexColors.Gild, CodexColors.Rule, 0.55f), CodexColors.Ink), ChipKind.TraitDetriment => (Mix(CodexColors.Seal, CodexColors.Bg, 0.08f), Mix(CodexColors.Seal, CodexColors.Rule, 0.55f), CodexColors.Ink), ChipKind.SkillFromBg => (Mix(CodexColors.Gild, CodexColors.Bg, 0.06f), Mix(CodexColors.Gild, CodexColors.Rule, 0.60f), CodexColors.Gild), ChipKind.SkillFromClass => (Mix(CodexColors.Seal, CodexColors.Bg, 0.06f), Mix(CodexColors.Seal, CodexColors.Rule, 0.55f), CodexColors.Seal), ChipKind.Language => (CodexColors.Bg, CodexColors.Rule, CodexColors.InkSoft), ChipKind.BgFeature => (Mix(CodexColors.Seal, CodexColors.Bg, 0.08f), Mix(CodexColors.Seal, CodexColors.Rule, 0.55f), CodexColors.Seal), _ => (CodexColors.Bg, CodexColors.Rule, CodexColors.Ink), }; private static Color Mix(Color a, Color b, float t) => new( (byte)(a.R * t + b.R * (1 - t)), (byte)(a.G * t + b.G * (1 - t)), (byte)(a.B * t + b.B * (1 - t)), (byte)0xFF); private static Color HoverShift(Color c) => new((byte)System.Math.Min(255, c.R + 14), (byte)System.Math.Min(255, c.G + 14), (byte)System.Math.Min(255, c.B + 14), c.A); } /// /// Small +N / −N pill that sits next to ability names. Visually a chip with /// monospace text and seal-red (positive) or ink-mute (negative) chrome. /// Hover surfaces a popover listing the contributing sources (clade, species). /// public sealed class CodexBonusPill : CodexWidget { public int Total { get; } public string PopoverBody { get; set; } = ""; private readonly CodexAtlas _atlas; private readonly SpriteFontBase _font; public bool IsHovered { get; private set; } public CodexBonusPill(int total, CodexAtlas atlas, string popoverBody = "") { Total = total; _atlas = atlas; PopoverBody = popoverBody; _font = CodexFonts.MonoTag; } protected override Point MeasureCore(Point available) { string label = (Total >= 0 ? "+" : "") + Total.ToString(); var s = _font.MeasureString(label); return new Point((int)s.X + 12, (int)System.MathF.Ceiling(_font.LineHeight) + 6); } protected override void ArrangeCore(Rectangle bounds) { } public override void Update(GameTime gt, CodexInput input) => IsHovered = ContainsPoint(input.MousePosition); public override void Draw(SpriteBatch sb, GameTime gt) { var border = Total >= 0 ? CodexColors.Seal : CodexColors.InkMute; var fill = Total >= 0 ? new Color(CodexColors.Seal.R, CodexColors.Seal.G, CodexColors.Seal.B, (byte)(IsHovered ? 36 : 18)) : new Color(CodexColors.InkMute.R, CodexColors.InkMute.G, CodexColors.InkMute.B, (byte)(IsHovered ? 28 : 14)); 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); string label = (Total >= 0 ? "+" : "") + Total.ToString(); var s = _font.MeasureString(label); float tx = Bounds.X + (Bounds.Width - s.X) / 2f; float ty = Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f; _font.DrawText(sb, label, new Vector2(tx, ty), border); } }