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>
146 lines
4.7 KiB
C#
146 lines
4.7 KiB
C#
using Godot;
|
|
using Theriapolis.GodotHost.Platform;
|
|
using Theriapolis.GodotHost.Rendering;
|
|
using Theriapolis.GodotHost.UI;
|
|
|
|
namespace Theriapolis.GodotHost;
|
|
|
|
// Control (not Node) so child Control scenes (Wizard, KitchenSink, etc.)
|
|
// can anchor to a real rect that fills the viewport. With a plain Node
|
|
// parent, anchors are ignored and Controls sit at (0,0) at intrinsic min
|
|
// size, which causes wide content to overflow off the right edge.
|
|
public partial class Main : Control
|
|
{
|
|
public override void _Ready()
|
|
{
|
|
// GetCmdlineArgs returns every arg (Godot's own flags + ours);
|
|
// GetCmdlineUserArgs only returns args after a "--" separator.
|
|
// Use the union so users don't have to remember the separator.
|
|
var userArgs = OS.GetCmdlineUserArgs();
|
|
var allArgs = OS.GetCmdlineArgs();
|
|
var args = new string[userArgs.Length + allArgs.Length];
|
|
userArgs.CopyTo(args, 0);
|
|
allArgs.CopyTo(args, userArgs.Length);
|
|
|
|
ulong? smokeTestSeed = null;
|
|
ulong? worldMapSeed = null;
|
|
bool runAssetTest = false;
|
|
bool runCodexTest = false;
|
|
bool runWizard = false;
|
|
(ulong seed, int tx, int ty)? tacticalArgs = null;
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
if (args[i] == "--codex-test")
|
|
{
|
|
runCodexTest = true;
|
|
break;
|
|
}
|
|
if (args[i] == "--wizard")
|
|
{
|
|
runWizard = true;
|
|
break;
|
|
}
|
|
if (args[i] == "--smoke-test")
|
|
{
|
|
ulong seed = 12345UL;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var parsed))
|
|
seed = parsed;
|
|
smokeTestSeed = seed;
|
|
break;
|
|
}
|
|
if (args[i] == "--asset-test")
|
|
{
|
|
runAssetTest = true;
|
|
break;
|
|
}
|
|
if (args[i] == "--world-map")
|
|
{
|
|
ulong seed = 12345UL;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var parsed))
|
|
seed = parsed;
|
|
worldMapSeed = seed;
|
|
break;
|
|
}
|
|
if (args[i] == "--tactical")
|
|
{
|
|
ulong seed = 12345UL;
|
|
int tx = 128, ty = 128;
|
|
if (i + 1 < args.Length && ulong.TryParse(args[i + 1], out var s))
|
|
seed = s;
|
|
if (i + 2 < args.Length && int.TryParse(args[i + 2], out var x))
|
|
tx = x;
|
|
if (i + 3 < args.Length && int.TryParse(args[i + 3], out var y))
|
|
ty = y;
|
|
tacticalArgs = (seed, tx, ty);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (smokeTestSeed.HasValue)
|
|
{
|
|
int code = SmokeTest.Run(smokeTestSeed.Value);
|
|
GetTree().Quit(code);
|
|
return;
|
|
}
|
|
|
|
if (runAssetTest)
|
|
{
|
|
int code = AssetTest.Run();
|
|
GetTree().Quit(code);
|
|
return;
|
|
}
|
|
|
|
if (worldMapSeed.HasValue)
|
|
{
|
|
// M4: unified seamless-zoom view. --world-map starts zoomed out
|
|
// (fit-to-viewport, initialZoom=0 = compute fit), --tactical
|
|
// starts at native sprite zoom 32 with the player at the given
|
|
// tile. Wheel between them seamlessly.
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
AddChild(new WorldView(worldMapSeed.Value));
|
|
return;
|
|
}
|
|
|
|
if (tacticalArgs.HasValue)
|
|
{
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
var (seed, tx, ty) = tacticalArgs.Value;
|
|
AddChild(new WorldView(seed, tx, ty, initialZoom: 32f));
|
|
return;
|
|
}
|
|
|
|
if (runCodexTest)
|
|
{
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
AddChild(new KitchenSink());
|
|
return;
|
|
}
|
|
|
|
if (runWizard)
|
|
{
|
|
foreach (Node child in GetChildren())
|
|
child.QueueFree();
|
|
var packed = ResourceLoader.Load<PackedScene>("res://Scenes/Wizard.tscn");
|
|
AddChild(packed.Instantiate());
|
|
return;
|
|
}
|
|
|
|
GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
|
|
}
|
|
|
|
public override void _UnhandledInput(InputEvent @event)
|
|
{
|
|
if (@event.IsActionPressed("ui_toggle_fullscreen"))
|
|
{
|
|
var mode = DisplayServer.WindowGetMode();
|
|
DisplayServer.WindowSetMode(
|
|
mode == DisplayServer.WindowMode.Fullscreen
|
|
? DisplayServer.WindowMode.Windowed
|
|
: DisplayServer.WindowMode.Fullscreen);
|
|
}
|
|
}
|
|
}
|