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>
This commit is contained in:
Christopher Wiebe
2026-04-30 20:40:51 -07:00
commit b451f83174
525 changed files with 75786 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core.Data;
using Theriapolis.Game.CodexUI.Core;
using Theriapolis.Game.CodexUI.Screens;
using Theriapolis.Game.CodexUI.Widgets;
using Theriapolis.Game.UI;
namespace Theriapolis.Game.CodexUI.Steps;
/// <summary>
/// Step I — Clade. Two grouped grids (Predators / Prey) of clade cards,
/// each card showing the clade name + kind + ability mods + language chips
/// + trait chips. Selection swaps the species default to the first species
/// belonging to that clade.
/// </summary>
public static class StepClade
{
public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover)
{
var col = new Column { Spacing = 14 };
col.Add(StepCommon.PageIntro("Folio I — Of Bloodlines", "Choose your Clade",
"The seven great families of Theriapolis. Your clade is the body you were born to — the broad shape of your gait, the fall of your shadow, the words your scent carries before you speak."));
col.Add(new CodexLabel("PREDATORS", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(BuildGrid(s, atlas, popover, "predator"));
col.Add(new CodexLabel("PREY", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(BuildGrid(s, atlas, popover, "prey"));
return col;
}
private static CodexWidget BuildGrid(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, string kind)
{
var grid = new Grid { Columns = 3 };
foreach (var c in s.Clades)
{
if (c.Kind != kind) continue;
grid.Add(BuildCard(s, atlas, popover, c));
}
return grid;
}
private static CodexWidget BuildCard(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, CladeDef c)
{
var content = new Column { Spacing = 8 };
// Header: sigil + name/kind
var headerRow = new Row { Spacing = 12, VAlignChildren = VAlign.Top };
headerRow.Add(new SigilWidget(atlas, c.Id));
var titleCol = new Column { Spacing = 2 };
titleCol.Add(new CodexLabel(c.Name, CodexFonts.DisplayMedium, CodexColors.Ink));
titleCol.Add(new CodexLabel(c.Kind.ToUpperInvariant(), CodexFonts.MonoTagSmall, CodexColors.InkMute));
headerRow.Add(titleCol);
content.Add(headerRow);
// Mods row
if (c.AbilityMods.Count > 0)
{
var mods = new WrapRow();
foreach (var kv in c.AbilityMods)
mods.Add(new ModChipMini(atlas, kv.Key, kv.Value));
content.Add(mods);
}
// Languages
content.Add(new CodexLabel("LANGUAGES", CodexFonts.MonoTagSmall, CodexColors.InkMute));
var langs = new WrapRow();
foreach (var l in c.Languages)
langs.Add(new HoverableChip(atlas, popover, CodexCopy.LanguageName(l), CodexCopy.LanguageName(l), CodexCopy.LanguageDescription(l), null, ChipKind.Language));
content.Add(langs);
// Traits
content.Add(new CodexLabel("TRAITS", CodexFonts.MonoTagSmall, CodexColors.InkMute));
var traits = new WrapRow();
foreach (var t in c.Traits)
traits.Add(new HoverableChip(atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait));
foreach (var t in c.Detriments)
traits.Add(new HoverableChip(atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment));
content.Add(traits);
bool isSelected = s.Clade == c;
var card = new CodexCard(atlas, content, isSelected,
onClick: () =>
{
s.Clade = c;
// If the previously-picked species belongs to a different
// clade, drop it — but never auto-pick a new species. The
// user must visit the Species folio explicitly so the
// Calling step stays locked behind that decision.
if (s.Species is not null && s.Species.CladeId != c.Id)
s.Species = null;
s.InvalidateLayout();
});
card.CornerSigil = atlas.SigilFor(c.Id);
return card;
}
}
/// <summary>Renders the clade sigil placeholder + a centred initial letter.</summary>
internal sealed class SigilWidget : CodexWidget
{
private readonly CodexAtlas _atlas;
private readonly string _cladeId;
public SigilWidget(CodexAtlas atlas, string cladeId) { _atlas = atlas; _cladeId = cladeId; }
protected override Point MeasureCore(Point available) => new(56, 56);
protected override void ArrangeCore(Rectangle bounds) { }
public override void Draw(SpriteBatch sb, GameTime gt)
{
sb.Draw(_atlas.SigilFor(_cladeId), Bounds, Color.White);
// Letter overlay so the placeholder is identifiable.
char ch = char.ToUpper(_cladeId.Length > 0 ? _cladeId[0] : '?');
var font = CodexFonts.DisplayMedium;
var s = font.MeasureString(ch.ToString());
font.DrawText(sb, ch.ToString(),
new Vector2(Bounds.X + (Bounds.Width - s.X) / 2f, Bounds.Y + (Bounds.Height - font.LineHeight) / 2f),
CodexColors.Ink);
}
}
/// <summary>Inline mod pill drawn as `STR +1` / `DEX -1` etc.</summary>
internal sealed class ModChipMini : CodexWidget
{
private readonly CodexAtlas _atlas;
private readonly string _label;
private readonly bool _positive;
private readonly SpriteFontBase _font = CodexFonts.MonoTagSmall;
public ModChipMini(CodexAtlas atlas, string ab, int v)
{
_atlas = atlas;
_label = $"{ab} {(v >= 0 ? "+" : "")}{v}";
_positive = v >= 0;
}
protected override Point MeasureCore(Point available)
{
var s = _font.MeasureString(_label);
return new Point((int)s.X + 14, (int)System.MathF.Ceiling(_font.LineHeight) + 6);
}
protected override void ArrangeCore(Rectangle bounds) { }
public override void Draw(SpriteBatch sb, GameTime gt)
{
var border = _positive ? CodexColors.Seal : CodexColors.InkMute;
var fill = CodexColors.Bg;
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);
var s = _font.MeasureString(_label);
_font.DrawText(sb, _label,
new Vector2(Bounds.X + (Bounds.Width - s.X) / 2f, Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f),
_positive ? CodexColors.Seal : CodexColors.InkSoft);
}
}
/// <summary>
/// Chip variant that drives the screen's popover when hovered. Acts as a
/// thin wrapper around <see cref="CodexChip"/> plus a popover-show side
/// effect during update.
/// </summary>
internal sealed class HoverableChip : CodexWidget
{
private readonly CodexChip _chip;
private readonly CodexHoverPopover _popover;
private readonly string _title, _body;
private readonly string? _tag;
private readonly bool _detriment;
public HoverableChip(CodexAtlas atlas, CodexHoverPopover popover, string text,
string popTitle, string popBody, string? popTag, ChipKind kind)
{
_chip = new CodexChip(text, kind, atlas, popTitle, popBody, popTag);
_popover = popover;
_title = popTitle;
_body = popBody;
_tag = popTag;
_detriment = kind == ChipKind.TraitDetriment;
}
public System.Action? OnClick { get => _chip.OnClick; set => _chip.OnClick = value; }
protected override Point MeasureCore(Point available) => _chip.Measure(available);
protected override void ArrangeCore(Rectangle bounds) => _chip.Arrange(bounds);
public override void Update(GameTime gt, CodexInput input)
{
_chip.Update(gt, input);
if (_chip.IsHovered && !string.IsNullOrEmpty(_body))
_popover.Show(_chip.Bounds, _title, _body, _tag, _detriment);
}
public override void Draw(SpriteBatch sb, GameTime gt) => _chip.Draw(sb, gt);
}
/// <summary>Reusable page-intro block: small mono eyebrow, large display title, body paragraph.</summary>
public static class StepCommon
{
public static CodexWidget PageIntro(string eyebrow, string title, string body)
{
var col = new Column { Spacing = 6 };
col.Add(new CodexLabel(eyebrow.ToUpperInvariant(), CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new CodexLabel(title, CodexFonts.DisplayLarge, CodexColors.Ink));
col.Add(new CodexLabel(body, CodexFonts.SerifBody, CodexColors.InkSoft));
return new Padding(col, new Thickness(0, 0, 0, 14));
}
}