ee5439285c
Pivots from M5's code-built UI to the editor-authorable .tscn pattern
recommended in GODOT_PORTING_GUIDE.md, after a session of fighting
Godot idioms with code-only layout. Default theme only; the parchment
Theme lands last per the guide's §12 build order so layout bugs surface
as layout bugs, not theming bugs.
GODOT_PORTING_GUIDE.md:
Authored by Claude Design as the canonical port reference. Maps the
React prototype's structure onto Godot 4.6 with concrete code sketches
and a build-order recommendation. Drove the M6 architecture.
Fonts/:
Cormorant Garamond (Medium + MediumItalic) and Crimson Pro (Regular +
Italic + SemiBold) under OFL — the React prototype's serif-display
and serif-body families. Not yet wired through CodexTheme.Build()
because theming is deferred; CodexTheme.LoadFontFromFonts already
picks them up automatically when the Theme pass lands.
Scenes/Wizard.tscn + Wizard.cs:
Wizard shell per guide §4: codex-header (title + folio counter) +
Stepper + Page (StepHost + Aside) + NavBar (Back / validation / Next).
All node lookups via unique-name (%) syntax; layout authored as a
scene file you can open in the editor. Step lifecycle drives the
Aside via signal binding. Stepper logic mirrors app.jsx — locked
iff some EARLIER step is unsatisfied; "type not yet implemented"
doesn't lock.
Scenes/Aside.tscn + Aside.cs:
Right-rail summary per guide §10. Single Refresh() rebuild on
CharacterDraft.Changed; cheap enough not to bother with partial
updates. Width 320 (was 380 before the layout overflow fix).
Scenes/Steps/IStep.cs + StepClade.cs:
Per-step Bind(draft) + Validate() contract. StepClade renders the
3-column clade card grid; click commits via CharacterDraft.Patch
which triggers the Resource.Changed signal that Aside and Wizard
both subscribe to.
UI/CharacterDraft.cs:
Resource (not Node) per guide §2.1. Mirrors app.jsx's `state` shape
exactly. Patch(dictionary) emits the inherited Resource.Changed
signal — listeners use `draft.Changed += handler` regardless of
which field changed. CodexContent provides lazy-loaded immutable
content tables (Clades, Species, Classes, Subclasses, Backgrounds).
Main.{cs,tscn}: Node → Control
When Main was a Node, Control children couldn't anchor to a real
parent rect — they sat at (0,0) at intrinsic min size. With wide
step content (3-column 200-px-card grid), the Wizard's min size
pushed the navbar beyond the viewport's right edge, hiding the Next
button on smaller windowed viewports. Making Main a full-rect-
anchored Control gives child scenes a proper rect to lay out in.
UI/Widgets/CodexStepper.cs:
Anchored the inner vbox to fill the button rect. Without this, the
vbox sat at the button's top-left at intrinsic size and labels
rendered in the corner — visible as the active-step label being
off-center from the highlight bar.
Verified at 1152x720 windowed and (separately) at fullscreen:
- 3-column card grid fits inside Wrap margins + Aside without
horizontal overflow
- Stepper labels centered under their highlight bars
- Next button visible after clade selection; future steps switch
to "coming soon" placeholder when clicked
- Aside summary fills in CLADE block on selection
Closes M6.1. Next per guide §12 build order: M6.2 — StepStats with
drag-drop (highest-risk piece, de-risk before easy steps).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
107 lines
4.8 KiB
C#
107 lines
4.8 KiB
C#
using Godot;
|
|
using System.Collections.Generic;
|
|
using Theriapolis.Core.Data;
|
|
using Theriapolis.GodotHost.Platform;
|
|
|
|
namespace Theriapolis.GodotHost.UI;
|
|
|
|
/// <summary>
|
|
/// In-progress character record. Resource (not Node) per
|
|
/// GODOT_PORTING_GUIDE.md §2.1: serializable via ResourceSaver.save,
|
|
/// inspectable in the editor, doesn't need tree presence.
|
|
///
|
|
/// Mirrors <c>app.jsx</c>'s <c>state</c> shape — clade/species/class/etc.,
|
|
/// stat assignment, chosen skills, name. Mutate via <see cref="Patch"/> so
|
|
/// every change emits <see cref="Changed"/>; the Aside and Wizard listen
|
|
/// and re-render on that single signal.
|
|
/// </summary>
|
|
[GlobalClass]
|
|
public partial class CharacterDraft : Resource
|
|
{
|
|
// NB: Resource defines its own `Changed` signal (emitted via EmitChanged()).
|
|
// We piggyback on it instead of declaring our own to avoid a name clash —
|
|
// listeners do `draft.Changed += ...` exactly the same way either way.
|
|
|
|
[Export] public string CladeId { get; set; } = "";
|
|
[Export] public string SpeciesId { get; set; } = "";
|
|
[Export] public string ClassId { get; set; } = "";
|
|
[Export] public string SubclassId { get; set; } = "";
|
|
[Export] public string BackgroundId { get; set; } = "";
|
|
|
|
/// <summary>"array" or "roll" — assignment method for ability scores.</summary>
|
|
[Export] public string StatMethod { get; set; } = "array";
|
|
[Export] public Godot.Collections.Array<int> StatPool { get; set; }
|
|
= new() { 15, 14, 13, 12, 10, 8 };
|
|
[Export] public Godot.Collections.Dictionary StatAssign { get; set; } = new();
|
|
|
|
[Export] public Godot.Collections.Array<string> ChosenSkills { get; set; } = new();
|
|
[Export] public string CharacterName { get; set; } = "";
|
|
|
|
/// <summary>
|
|
/// Mutate one or more fields, then emit Changed once. Mirrors
|
|
/// the JS prototype's <c>set(patch)</c> from app.jsx.
|
|
/// </summary>
|
|
public void Patch(Godot.Collections.Dictionary patch)
|
|
{
|
|
foreach (var key in patch.Keys)
|
|
{
|
|
string k = (string)key;
|
|
switch (k)
|
|
{
|
|
case "clade_id": CladeId = (string)patch[key]; break;
|
|
case "species_id": SpeciesId = (string)patch[key]; break;
|
|
case "class_id": ClassId = (string)patch[key]; break;
|
|
case "subclass_id": SubclassId = (string)patch[key]; break;
|
|
case "background_id": BackgroundId = (string)patch[key]; break;
|
|
case "stat_method": StatMethod = (string)patch[key]; break;
|
|
case "stat_pool": StatPool = (Godot.Collections.Array<int>)patch[key]; break;
|
|
case "stat_assign": StatAssign = (Godot.Collections.Dictionary)patch[key]; break;
|
|
case "chosen_skills": ChosenSkills = (Godot.Collections.Array<string>)patch[key]; break;
|
|
case "character_name":CharacterName = (string)patch[key]; break;
|
|
default:
|
|
GD.PushWarning($"[CharacterDraft] unknown patch key: {k}");
|
|
break;
|
|
}
|
|
}
|
|
EmitChanged();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lazy-loaded immutable content tables. Replaces the React prototype's
|
|
/// data.jsx + autoload Content per GODOT_PORTING_GUIDE.md §2.2 — same
|
|
/// purpose, idiomatic C#.
|
|
/// </summary>
|
|
public static class CodexContent
|
|
{
|
|
private static CladeDef[]? _clades;
|
|
private static SpeciesDef[]? _species;
|
|
private static ClassDef[]? _classes;
|
|
private static SubclassDef[]? _subclasses;
|
|
private static BackgroundDef[]? _backgrounds;
|
|
|
|
public static CladeDef[] Clades => _clades ??= Load(l => l.LoadClades());
|
|
public static SpeciesDef[] Species => _species ??= Load(l => l.LoadSpecies(Clades));
|
|
public static ClassDef[] Classes => _classes ??= Load(l => l.LoadClasses());
|
|
public static SubclassDef[] Subclasses => _subclasses ??= Load(l => l.LoadSubclasses(Classes));
|
|
public static BackgroundDef[] Backgrounds => _backgrounds ??= Load(l => l.LoadBackgrounds());
|
|
|
|
public static CladeDef? Clade(string id) => System.Array.Find(Clades, c => c.Id == id);
|
|
public static SpeciesDef? SpeciesById(string id) => System.Array.Find(Species, s => s.Id == id);
|
|
public static ClassDef? Class(string id) => System.Array.Find(Classes, c => c.Id == id);
|
|
public static BackgroundDef? Background(string id) => System.Array.Find(Backgrounds, b => b.Id == id);
|
|
|
|
public static IEnumerable<SpeciesDef> SpeciesOfClade(string cladeId)
|
|
{
|
|
foreach (var s in Species)
|
|
if (string.Equals(s.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase))
|
|
yield return s;
|
|
}
|
|
|
|
private static T Load<T>(System.Func<Theriapolis.Core.Data.ContentLoader, T> fn)
|
|
{
|
|
var loader = new Theriapolis.Core.Data.ContentLoader(ContentPaths.DataDir);
|
|
return fn(loader);
|
|
}
|
|
}
|