156 lines
6.8 KiB
C#
156 lines
6.8 KiB
C#
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Pill-shaped chip used for traits, skills, languages, and background feature
|
|||
|
|
/// names. Hovering surfaces a popover with the full description; clicking
|
|||
|
|
/// fires <see cref="OnClick"/> for the few cases the screen needs (skill toggle).
|
|||
|
|
/// </summary>
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
/// <summary>The screen sets this when the user hovers over us; the screen handles popover layout.</summary>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 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).
|
|||
|
|
/// </summary>
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|