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>
232 lines
9.9 KiB
C#
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><Aside /></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);
|
|
}
|
|
}
|