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>
118 lines
5.4 KiB
C#
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);
|
|
}
|
|
}
|