b451f83174
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>
190 lines
8.0 KiB
C#
190 lines
8.0 KiB
C#
using FontStashSharp;
|
|
using Microsoft.Xna.Framework;
|
|
using Microsoft.Xna.Framework.Graphics;
|
|
using Theriapolis.Game.CodexUI.Core;
|
|
|
|
namespace Theriapolis.Game.CodexUI.Widgets;
|
|
|
|
/// <summary>
|
|
/// Selectable card. The clade / species / class / background pickers each
|
|
/// render a grid of these. Visual states track the React design:
|
|
/// - Default: rule-coloured 1-px border, parchment fill.
|
|
/// - Hover: gilded border, subtle gild halo overlay.
|
|
/// - Selected: seal-red border + inner glow + corner wax-seal accent.
|
|
///
|
|
/// Click toggles selection by calling <see cref="OnClick"/>; the parent step
|
|
/// owns the "what is selected" state and rebuilds. The card's content tree
|
|
/// is whatever <see cref="Content"/> child is supplied — typically a small
|
|
/// Column with name + meta + chips.
|
|
/// </summary>
|
|
public sealed class CodexCard : CodexWidget
|
|
{
|
|
public CodexWidget? Content { get; set; }
|
|
public bool IsSelected { get; set; }
|
|
public System.Action? OnClick { get; set; }
|
|
public Texture2D? CornerSigil { get; set; }
|
|
public string? CornerLetter { get; set; } // overlay glyph drawn on the sigil placeholder
|
|
|
|
private readonly CodexAtlas _atlas;
|
|
private bool _hovered;
|
|
private const int Pad = CodexDensity.CardPad;
|
|
|
|
public CodexCard(CodexAtlas atlas, CodexWidget? content = null, bool selected = false, System.Action? onClick = null)
|
|
{
|
|
_atlas = atlas;
|
|
Content = content;
|
|
if (content is not null) content.Parent = this;
|
|
IsSelected = selected;
|
|
OnClick = onClick;
|
|
}
|
|
|
|
protected override Point MeasureCore(Point available)
|
|
{
|
|
if (Content is null) return new Point(System.Math.Min(CodexDensity.CardWidth, available.X), 80);
|
|
var inner = new Point(available.X - Pad * 2, available.Y - Pad * 2);
|
|
var s = Content.Measure(inner);
|
|
return new Point(s.X + Pad * 2, s.Y + Pad * 2);
|
|
}
|
|
|
|
protected override void ArrangeCore(Rectangle bounds)
|
|
{
|
|
Content?.Arrange(new Rectangle(
|
|
bounds.X + Pad, bounds.Y + Pad,
|
|
bounds.Width - Pad * 2,
|
|
bounds.Height - Pad * 2));
|
|
}
|
|
|
|
public override void Update(GameTime gt, CodexInput input)
|
|
{
|
|
_hovered = ContainsPoint(input.MousePosition);
|
|
if (_hovered && input.LeftJustReleased) OnClick?.Invoke();
|
|
Content?.Update(gt, input);
|
|
}
|
|
|
|
public override void Draw(SpriteBatch sb, GameTime gt)
|
|
{
|
|
// Body fill — parchment shade
|
|
sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg2);
|
|
// Subtle top-down lift overlay (2-px gradient strip) to match `linear-gradient(180deg, rgba(255,250,235,0.05), transparent 30%)`.
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 2), new Color(255, 250, 235, 18));
|
|
|
|
// Border colour by state.
|
|
Color border = IsSelected ? CodexColors.Seal : (_hovered ? CodexColors.Gild : CodexColors.Rule);
|
|
int thickness = IsSelected ? 2 : 1;
|
|
DrawBorder(sb, Bounds, border, thickness);
|
|
|
|
if (IsSelected)
|
|
{
|
|
// Inner glow strip — 1px inside the border.
|
|
DrawBorder(sb, new Rectangle(Bounds.X + thickness, Bounds.Y + thickness, Bounds.Width - thickness * 2, Bounds.Height - thickness * 2),
|
|
new Color(border.R, border.G, border.B, (byte)40), 1);
|
|
// Corner wax-seal accent
|
|
int sealSize = 28;
|
|
sb.Draw(_atlas.WaxSeal, new Rectangle(Bounds.Right - sealSize / 2 - 4, Bounds.Y - sealSize / 2 + 4, sealSize, sealSize), Color.White);
|
|
}
|
|
else if (_hovered)
|
|
{
|
|
sb.Draw(_atlas.Pixel, Bounds, new Color(CodexColors.Gild.R, CodexColors.Gild.G, CodexColors.Gild.B, (byte)10));
|
|
}
|
|
|
|
Content?.Draw(sb, gt);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 7-step horizontal stepper at the top of the wizard. Each step exposes a
|
|
/// locked / active / complete state with the matching marker (✕ / Roman /
|
|
/// ✓). Clicking a non-locked step navigates there.
|
|
/// </summary>
|
|
public sealed class CodexStepper : CodexWidget
|
|
{
|
|
public string[] Names { get; }
|
|
public int Current { get; set; }
|
|
public bool[] Complete { get; set; }
|
|
public bool[] Locked { get; set; }
|
|
public System.Action<int>? OnPick { get; set; }
|
|
|
|
private readonly CodexAtlas _atlas;
|
|
private readonly SpriteFontBase _romanFont = CodexFonts.DisplayMedium;
|
|
private readonly SpriteFontBase _labelFont = CodexFonts.MonoTagSmall;
|
|
|
|
public CodexStepper(string[] names, CodexAtlas atlas)
|
|
{
|
|
Names = names;
|
|
Complete = new bool[names.Length];
|
|
Locked = new bool[names.Length];
|
|
_atlas = atlas;
|
|
}
|
|
|
|
protected override Point MeasureCore(Point available) => new(available.X, 64);
|
|
protected override void ArrangeCore(Rectangle bounds) { }
|
|
|
|
private static readonly string[] Roman = new[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" };
|
|
|
|
public override void Update(GameTime gt, CodexInput input)
|
|
{
|
|
if (!input.LeftJustReleased) return;
|
|
int colW = Bounds.Width / Names.Length;
|
|
for (int i = 0; i < Names.Length; i++)
|
|
{
|
|
var cell = new Rectangle(Bounds.X + i * colW, Bounds.Y, colW, Bounds.Height);
|
|
if (cell.Contains(input.MousePosition) && !Locked[i] && i != Current) { OnPick?.Invoke(i); return; }
|
|
}
|
|
}
|
|
|
|
public override void Draw(SpriteBatch sb, GameTime gt)
|
|
{
|
|
// Opaque parchment background so the stepper masks any body
|
|
// scroll-overflow that drew under it.
|
|
sb.Draw(_atlas.Pixel, Bounds, CodexColors.Bg);
|
|
// Top + bottom rule
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), CodexColors.Rule);
|
|
sb.Draw(_atlas.Pixel, new Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), CodexColors.Rule);
|
|
sb.Draw(_atlas.Pixel, Bounds, new Color(0, 0, 0, 6));
|
|
|
|
int colW = Bounds.Width / Names.Length;
|
|
for (int i = 0; i < Names.Length; i++)
|
|
{
|
|
var cell = new Rectangle(Bounds.X + i * colW, Bounds.Y, colW, Bounds.Height);
|
|
// Vertical separator between cells.
|
|
if (i < Names.Length - 1)
|
|
sb.Draw(_atlas.Pixel, new Rectangle(cell.Right - 1, cell.Y + 4, 1, cell.Height - 8),
|
|
new Color(CodexColors.Rule.R, CodexColors.Rule.G, CodexColors.Rule.B, (byte)128));
|
|
|
|
bool isCurrent = i == Current;
|
|
bool isComplete = Complete[i] && !isCurrent;
|
|
bool isLocked = Locked[i];
|
|
// Numeral / mark
|
|
string mark = isLocked ? "✕" : (isComplete ? "✓" : Roman[i]);
|
|
Color numColor = isLocked ? CodexColors.InkMute
|
|
: isComplete ? CodexColors.Seal
|
|
: isCurrent ? CodexColors.Ink
|
|
: CodexColors.InkMute;
|
|
|
|
var ms = _romanFont.MeasureString(mark);
|
|
float numX = cell.X + (cell.Width - ms.X) / 2f;
|
|
float numY = cell.Y + 12;
|
|
_romanFont.DrawText(sb, mark, new Vector2(numX, numY), numColor);
|
|
|
|
// Step name
|
|
var ls = _labelFont.MeasureString(Names[i]);
|
|
float lx = cell.X + (cell.Width - ls.X) / 2f;
|
|
float ly = cell.Y + cell.Height - _labelFont.LineHeight - 8;
|
|
Color labelColor = isLocked ? new Color(CodexColors.InkMute.R, CodexColors.InkMute.G, CodexColors.InkMute.B, (byte)115)
|
|
: isCurrent ? CodexColors.Ink : CodexColors.InkMute;
|
|
_labelFont.DrawText(sb, Names[i].ToUpperInvariant(), new Vector2(lx, ly), labelColor);
|
|
|
|
if (isCurrent)
|
|
sb.Draw(_atlas.Pixel, new Rectangle((int)(cell.X + cell.Width * 0.14f), cell.Bottom - 2, (int)(cell.Width * 0.72f), 2), CodexColors.Gild);
|
|
}
|
|
}
|
|
}
|