Files
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

232 lines
9.9 KiB
C#

using FontStashSharp;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Theriapolis.Core.Rules.Stats;
using Theriapolis.Game.CodexUI.Core;
using Theriapolis.Game.CodexUI.Steps;
using Theriapolis.Game.CodexUI.Widgets;
using Theriapolis.Game.UI;
namespace Theriapolis.Game.CodexUI.Screens;
/// <summary>
/// Right-column live summary. Reads from <see cref="CodexCharacterCreationScreen"/>'s
/// state and renders five blocks (Name, Lineage, Calling+History, Abilities,
/// Skills) plus a stat strip. Mirrors the React <c>&lt;Aside /&gt;</c>
/// component in <c>app.jsx</c>.
/// </summary>
public sealed class CodexAside
{
private readonly CodexCharacterCreationScreen _s;
private readonly CodexAtlas _atlas;
public CodexAside(CodexCharacterCreationScreen s, CodexAtlas atlas)
{
_s = s;
_atlas = atlas;
}
public CodexWidget Build()
{
// Aside lives next to the page main and shares the screen's
// popover layer — hovering a chip here pops the same parchment-
// and-gilt popover the cards on the left side use.
var popover = _s.AsidePopover;
var col = new Column { Spacing = 14, HAlignChildren = HAlign.Stretch };
col.Add(new CodexLabel("THE SUBJECT", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(NameBlock());
col.Add(LineageBlock(popover));
col.Add(CallingBlock(popover));
col.Add(HistoryBlock(popover));
col.Add(BuildStatStrip());
col.Add(SkillsBlock(popover));
return col;
}
private CodexWidget NameBlock()
{
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("NAME", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new CodexLabel(string.IsNullOrWhiteSpace(_s.Name) ? "(unnamed)" : _s.Name,
CodexFonts.DisplayMedium, CodexColors.Ink));
return col;
}
private CodexWidget LineageBlock(CodexHoverPopover popover)
{
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("LINEAGE", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new CodexLabel(_s.Species?.Name ?? "—",
CodexFonts.DisplayMedium, CodexColors.Ink));
if (_s.Clade is not null || _s.Species is not null)
{
string sub = (_s.Clade?.Name ?? "—").ToUpperInvariant()
+ (_s.Clade is not null ? " · " + _s.Clade.Kind.ToUpperInvariant() : "")
+ (_s.Species is not null ? " · " + CodexCopy.SizeLabel(_s.Species.Size).ToUpperInvariant() : "");
col.Add(new CodexLabel(sub, CodexFonts.MonoTagSmall, CodexColors.InkMute));
}
// Trait chips — clade traits + species traits (each hover-popover'd).
var chips = new WrapRow();
if (_s.Clade is not null)
{
foreach (var t in _s.Clade.Traits)
chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait));
foreach (var t in _s.Clade.Detriments)
chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment));
}
if (_s.Species is not null)
{
foreach (var t in _s.Species.Traits)
chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, null, ChipKind.Trait));
foreach (var t in _s.Species.Detriments)
chips.Add(new HoverableChip(_atlas, popover, t.Name, t.Name, t.Description, "DETRIMENT", ChipKind.TraitDetriment));
}
if (chips.Children.Count > 0) col.Add(chips);
return col;
}
private CodexWidget CallingBlock(CodexHoverPopover popover)
{
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("CALLING", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new CodexLabel(_s.Class?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink));
if (_s.Class is not null)
col.Add(new CodexLabel($"D{_s.Class.HitDie} · {string.Join("/", _s.Class.PrimaryAbility)}",
CodexFonts.MonoTagSmall, CodexColors.InkMute));
// Level-1 feature chips for the chosen class (hover surfaces description).
if (_s.Class is not null)
{
var lvl1 = System.Array.Find(_s.Class.LevelTable, e => e.Level == 1);
if (lvl1 is not null)
{
var chips = new WrapRow();
foreach (var k in lvl1.Features)
{
if (k == "asi" || k == "subclass_select" || k == "subclass_feature") continue;
if (!_s.Class.FeatureDefinitions.TryGetValue(k, out var fd)) continue;
chips.Add(new HoverableChip(_atlas, popover, fd.Name, fd.Name, fd.Description, fd.Kind?.ToUpperInvariant(), ChipKind.Trait));
}
if (chips.Children.Count > 0) col.Add(chips);
}
}
return col;
}
private CodexWidget HistoryBlock(CodexHoverPopover popover)
{
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("HISTORY", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new CodexLabel(_s.Background?.Name ?? "—", CodexFonts.DisplayMedium, CodexColors.Ink));
if (_s.Background is not null && !string.IsNullOrEmpty(_s.Background.FeatureName))
{
var chips = new WrapRow();
chips.Add(new HoverableChip(_atlas, popover,
_s.Background.FeatureName, _s.Background.FeatureName, _s.Background.FeatureDescription,
"FEATURE", ChipKind.BgFeature));
col.Add(chips);
}
return col;
}
private CodexWidget SkillsBlock(CodexHoverPopover popover)
{
int total = _s.ChosenSkills.Count + (_s.Background?.SkillProficiencies.Length ?? 0);
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("SKILLS · " + total, CodexFonts.MonoTag, CodexColors.InkMute));
var chips = new WrapRow();
if (_s.Background is not null)
foreach (var s in _s.Background.SkillProficiencies)
chips.Add(new HoverableChip(_atlas, popover,
CodexCopy.SkillName(s),
CodexCopy.SkillName(s),
CodexCopy.SkillDescription(s),
"BACKGROUND",
ChipKind.SkillFromBg));
foreach (var s in _s.ChosenSkills.OrderBy(x => x.ToString()))
{
// Map enum → snake_case JSON id; otherwise SleightOfHand /
// AnimalHandling lose their hover descriptions because the
// CodexCopy switch is keyed on the JSON form.
string id = CodexCopy.SkillIdToJson(s);
chips.Add(new HoverableChip(_atlas, popover,
CodexCopy.SkillName(id),
CodexCopy.SkillName(id),
CodexCopy.SkillDescription(id),
"CLASS",
ChipKind.SkillFromClass));
}
col.Add(chips);
return col;
}
private CodexWidget BuildStatStrip()
{
var col = new Column { Spacing = 4 };
col.Add(new CodexLabel("ABILITIES", CodexFonts.MonoTag, CodexColors.InkMute));
col.Add(new StatStrip(_s, _atlas));
return col;
}
}
/// <summary>
/// Six-cell strip (one per ability) with name, final score, and signed
/// modifier. Used in the aside panel and on the review step.
/// </summary>
internal sealed class StatStrip : CodexWidget
{
private readonly CodexCharacterCreationScreen _s;
private readonly CodexAtlas _atlas;
public StatStrip(CodexCharacterCreationScreen s, CodexAtlas atlas) { _s = s; _atlas = atlas; }
protected override Point MeasureCore(Point available) => new(available.X, 50);
protected override void ArrangeCore(Rectangle bounds) { }
public override void Draw(SpriteBatch sb, GameTime gt)
{
var abFont = CodexFonts.MonoTagSmall;
var scoreFont = CodexFonts.DisplayMedium;
var modFont = CodexFonts.MonoTagSmall;
int cellW = (Bounds.Width - 5 * 4) / 6;
for (int i = 0; i < CodexCopy.AbilityOrder.Length; i++)
{
var ab = CodexCopy.AbilityOrder[i];
int x = Bounds.X + i * (cellW + 4);
var rect = new Rectangle(x, Bounds.Y, cellW, Bounds.Height);
sb.Draw(_atlas.Pixel, rect, CodexColors.Bg);
DrawBorder(sb, rect, CodexColors.Rule, 1);
string ablab = ab.ToString();
var s1 = abFont.MeasureString(ablab);
abFont.DrawText(sb, ablab, new Vector2(rect.X + (rect.Width - s1.X) / 2f, rect.Y + 4), CodexColors.InkMute);
int? base_ = _s.StatAssign.TryGetValue(ab, out var v) ? v : (int?)null;
int total = (base_ ?? 0) + _s.TotalBonus(ab);
int mod = AbilityScores.Mod(total);
string scoreText = base_ is null ? "—" : total.ToString();
string modText = base_ is null ? "" : ((mod >= 0 ? "+" : "") + mod);
var s2 = scoreFont.MeasureString(scoreText);
scoreFont.DrawText(sb, scoreText, new Vector2(rect.X + (rect.Width - s2.X) / 2f, rect.Y + 16), CodexColors.Ink);
if (modText.Length > 0)
{
var s3 = modFont.MeasureString(modText);
modFont.DrawText(sb, modText, new Vector2(rect.X + (rect.Width - s3.X) / 2f, rect.Bottom - modFont.LineHeight - 4),
mod >= 0 ? CodexColors.Seal : CodexColors.InkMute);
}
}
}
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);
}
}