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