Files
Christopher Wiebe 83c6343783 M6.21: Dark theme parity + card shadow polish + hover/selection fixes
--dark command-line flag swaps CodexTheme.DefaultPalette to Dark
before any UI mounts; both TitleScreen and Wizard pick it up via
the no-arg Build() overload.

Stepper colours track the active palette. ApplyStateColors and the
Active step's gild underline previously read from a stub that
hardcoded parchment values, so the Active label rendered as
brown-black ink against the dark bg (invisible). Both sites now
read CodexTheme.DefaultPalette directly.

Card hover stays applied while the cursor is over an inner Button.
PanelContainer.MouseExited fires when the cursor crosses onto a
child that captures input (Sire/Dam toggles, Sheep/Goat toggles,
trait pickers); the recheck defers and uses GetGlobalRect.HasPoint
on the cursor position so hover only drops when the cursor truly
leaves the card area.

Selection stylebox lands on first refresh. SetSelected was
previously called inside BuildCard before AddChild, so
HasThemeStylebox returned false (theme cascade unreachable) and
the override silently dropped — it only re-attached when
MouseEntered later re-ran Apply. Refactored SetSelected/SetHover
through a new ApplyOrDefer helper that uses CallDeferred when the
card isn't in tree yet, so the seal border + drop shadow appear
immediately on selection rather than only after the first hover.

Selection drop shadow refined. Was a 14px shadow at offset (0,14)
which overlapped the next card by 16px in the v_separation:12
grid. Now offset (4,4) + size 6 — diagonal "light from upper-left"
direction, total reach 10px, leaves a 2px clearance before the
next card so the shadow reads as a shadow on the surface below.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:55:40 -07:00

165 lines
5.5 KiB
C#

using Godot;
using Theriapolis.GodotHost.Platform;
using Theriapolis.GodotHost.Rendering;
using Theriapolis.GodotHost.Scenes;
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;
// --dark is independent of the entry-point flags: it sets the codex
// palette default before any UI mounts so both TitleScreen and the
// wizard pick it up via CodexTheme.Build()'s no-arg overload.
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "--dark")
{
CodexTheme.DefaultPalette = CodexPalette.Dark;
break;
}
}
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;
}
// Default entry point — TitleScreen. M0's hello-world Label is no
// longer the boot UI; the title swaps itself for the wizard when
// "New Character" is clicked, or shuts the engine down on Quit.
foreach (Node child in GetChildren())
child.QueueFree();
AddChild(new TitleScreen());
}
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);
}
}
}