Files
TheriapolisV3/Theriapolis.Game/CodexUI/Widgets/CodexChip.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

156 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}