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:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user