Files
TheriapolisV3/Theriapolis.Game/CodexUI/Steps/StepClass.cs
T
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

118 lines
5.4 KiB
C#

using Microsoft.Xna.Framework;
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 III — Calling. Two-column grid of class cards. Cards recommended
/// for the chosen clade get a small "★ Suits Clade" badge. Level-1
/// features render as inline trait chips with hover popovers.
/// </summary>
public static class StepClass
{
public static CodexWidget Build(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover)
{
var col = new Column { Spacing = 14 };
col.Add(StepCommon.PageIntro(
"Folio III — Of Vocations",
"Choose your Calling",
"Eight callings exist within the Covenant. Each shapes how you fight, treat, parley, or unmake the world."));
var grid = new Grid { Columns = 2 };
foreach (var c in s.Classes) grid.Add(BuildCard(s, atlas, popover, c));
col.Add(grid);
return col;
}
private static CodexWidget BuildCard(CodexCharacterCreationScreen s, CodexAtlas atlas, CodexHoverPopover popover, ClassDef c)
{
var content = new Column { Spacing = 8 };
var titleRow = new Row { Spacing = 8, VAlignChildren = VAlign.Middle };
titleRow.Add(new CodexLabel(c.Name, CodexFonts.DisplayMedium, CodexColors.Ink));
bool suits = s.Clade is not null && CodexCopy.IsSuited(c.Id, s.Clade.Id);
if (suits) titleRow.Add(new RecBadge(atlas));
content.Add(titleRow);
content.Add(new CodexLabel(
$"D{c.HitDie} · PRIMARY {string.Join("/", c.PrimaryAbility)} · SAVES {string.Join("/", c.Saves)}",
CodexFonts.MonoTagSmall, CodexColors.InkMute));
// Level-1 features as trait chips
var lvl1 = System.Array.Find(c.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 (!c.FeatureDefinitions.TryGetValue(k, out var fd)) continue;
chips.Add(new HoverableChip(atlas, popover, fd.Name, fd.Name, fd.Description, fd.Kind?.ToUpperInvariant(), ChipKind.Trait));
}
content.Add(chips);
}
content.Add(new CodexLabel(
$"PICKS {c.SkillsChoose} SKILL{(c.SkillsChoose > 1 ? "S" : "")} · ARMOR: {string.Join(", ", c.ArmorProficiencies)}",
CodexFonts.MonoTagSmall, CodexColors.InkMute));
return new CodexCard(atlas, content, s.Class == c,
onClick: () =>
{
// If switching class, drop any previously-picked skills
// that aren't on the new class's option list — but never
// auto-pick. The Sign step must stay locked until the
// user explicitly visits the Skills folio.
if (s.Class != c)
{
s.Class = c;
var allowed = new System.Collections.Generic.HashSet<string>(c.SkillOptions, System.StringComparer.OrdinalIgnoreCase);
var bgLocked = new System.Collections.Generic.HashSet<string>(
s.Background?.SkillProficiencies ?? System.Array.Empty<string>(),
System.StringComparer.OrdinalIgnoreCase);
s.ChosenSkills.RemoveWhere(sk =>
{
string raw = sk.ToString().ToLowerInvariant();
return !allowed.Contains(raw) || bgLocked.Contains(raw);
});
}
s.InvalidateLayout();
});
}
}
internal sealed class RecBadge : CodexWidget
{
private readonly CodexAtlas _atlas;
private readonly FontStashSharp.SpriteFontBase _font = CodexFonts.MonoTagSmall;
private const string Text = "★ SUITS CLADE";
public RecBadge(CodexAtlas atlas) { _atlas = atlas; }
protected override Point MeasureCore(Point available)
{
var s = _font.MeasureString(Text);
return new Point((int)s.X + 12, (int)System.MathF.Ceiling(_font.LineHeight) + 6);
}
protected override void ArrangeCore(Microsoft.Xna.Framework.Rectangle bounds) { }
public override void Draw(Microsoft.Xna.Framework.Graphics.SpriteBatch sb, Microsoft.Xna.Framework.GameTime gt)
{
var fill = new Microsoft.Xna.Framework.Color(CodexColors.Gild.R, CodexColors.Gild.G, CodexColors.Gild.B, (byte)20);
sb.Draw(_atlas.Pixel, Bounds, fill);
sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Y, Bounds.Width, 1), CodexColors.Gild);
sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Bottom - 1, Bounds.Width, 1), CodexColors.Gild);
sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.X, Bounds.Y, 1, Bounds.Height), CodexColors.Gild);
sb.Draw(_atlas.Pixel, new Microsoft.Xna.Framework.Rectangle(Bounds.Right - 1, Bounds.Y, 1, Bounds.Height), CodexColors.Gild);
var s = _font.MeasureString(Text);
_font.DrawText(sb, Text,
new Microsoft.Xna.Framework.Vector2(Bounds.X + (Bounds.Width - s.X) / 2f,
Bounds.Y + (Bounds.Height - _font.LineHeight) / 2f),
CodexColors.Gild);
}
}