Compare commits
2 Commits
f7cadaeb68
...
83c6343783
| Author | SHA1 | Date | |
|---|---|---|---|
| 83c6343783 | |||
| 2db442be7e |
@@ -1,6 +1,7 @@
|
||||
using Godot;
|
||||
using Theriapolis.GodotHost.Platform;
|
||||
using Theriapolis.GodotHost.Rendering;
|
||||
using Theriapolis.GodotHost.Scenes;
|
||||
using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost;
|
||||
@@ -28,6 +29,19 @@ public partial class Main : Control
|
||||
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")
|
||||
@@ -128,7 +142,12 @@ public partial class Main : Control
|
||||
return;
|
||||
}
|
||||
|
||||
GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
|
||||
// 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)
|
||||
|
||||
@@ -7,17 +7,3 @@ anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -200.0
|
||||
offset_top = -16.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 16.0
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
text = "Theriapolis · Godot port · M0 · F11 toggles fullscreen"
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using Godot;
|
||||
using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost.Scenes;
|
||||
|
||||
/// <summary>
|
||||
/// Entry-point screen — vertical button stack on a parchment field with the
|
||||
/// codex title and a version label. Per port-plan §M6, exists primarily to
|
||||
/// validate the design system in a non-trivial composition before the player
|
||||
/// reaches character creation.
|
||||
///
|
||||
/// Button actions:
|
||||
/// New Character — swap self for the Wizard scene under the Main parent
|
||||
/// (siblings cleared so the wizard fills the viewport).
|
||||
/// Continue — disabled until <see cref="CharacterAssembler.PersistedStatePath"/>
|
||||
/// exists; full pickup lands with the M7 play loop.
|
||||
/// Quit — shut down the engine.
|
||||
/// </summary>
|
||||
public partial class TitleScreen : Control
|
||||
{
|
||||
private const string VersionLabel = "PORT / GODOT · M6.20";
|
||||
private const string WizardScenePath = "res://Scenes/Wizard.tscn";
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
Theme = CodexTheme.Build();
|
||||
|
||||
// Backing panel so the parchment Bg fills the viewport (the Control
|
||||
// itself paints nothing). Same pattern as Wizard.cs.
|
||||
// Note: SetAnchorsAndOffsetsPreset is required (not just AnchorRight =
|
||||
// 1) because Godot's anchor setters preserve visual position by
|
||||
// adjusting offsets — manual anchor edits leave the control at 0×0.
|
||||
var bg = new Panel { MouseFilter = MouseFilterEnum.Ignore };
|
||||
AddChild(bg);
|
||||
bg.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
MoveChild(bg, 0);
|
||||
|
||||
// Centered title + button stack column.
|
||||
var center = new CenterContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||
AddChild(center);
|
||||
center.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||
|
||||
var col = new VBoxContainer { CustomMinimumSize = new Vector2(360, 0) };
|
||||
col.AddThemeConstantOverride("separation", 28);
|
||||
center.AddChild(col);
|
||||
|
||||
var titleBlock = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
|
||||
titleBlock.AddThemeConstantOverride("separation", 4);
|
||||
col.AddChild(titleBlock);
|
||||
titleBlock.AddChild(new Label
|
||||
{
|
||||
Text = "THERIAPOLIS",
|
||||
ThemeTypeVariation = "CodexTitle",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
titleBlock.AddChild(new Label
|
||||
{
|
||||
Text = "CODEX OF BECOMING",
|
||||
ThemeTypeVariation = "Eyebrow",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
|
||||
var buttonStack = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
buttonStack.AddThemeConstantOverride("separation", 12);
|
||||
col.AddChild(buttonStack);
|
||||
|
||||
var newBtn = MakeMenuButton("New Character", primary: true);
|
||||
newBtn.Pressed += OnNewCharacter;
|
||||
buttonStack.AddChild(newBtn);
|
||||
|
||||
var continueBtn = MakeMenuButton("Continue", primary: false);
|
||||
continueBtn.Disabled = !FileAccess.FileExists(CharacterAssembler.PersistedStatePath);
|
||||
continueBtn.Pressed += OnContinue;
|
||||
buttonStack.AddChild(continueBtn);
|
||||
|
||||
var quitBtn = MakeMenuButton("Quit", primary: false);
|
||||
quitBtn.Pressed += OnQuit;
|
||||
buttonStack.AddChild(quitBtn);
|
||||
|
||||
// Version chip in the bottom-right corner — small mono Eyebrow tag,
|
||||
// sits over the parchment field at a comfortable margin.
|
||||
var versionLabel = new Label
|
||||
{
|
||||
Text = VersionLabel,
|
||||
ThemeTypeVariation = "Eyebrow",
|
||||
AnchorLeft = 1, AnchorRight = 1,
|
||||
AnchorTop = 1, AnchorBottom = 1,
|
||||
OffsetLeft = -180, OffsetTop = -28,
|
||||
OffsetRight = -16, OffsetBottom = -10,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
AddChild(versionLabel);
|
||||
}
|
||||
|
||||
private static Button MakeMenuButton(string text, bool primary)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Text = text,
|
||||
FocusMode = FocusModeEnum.None,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(0, 44),
|
||||
};
|
||||
if (primary) btn.ThemeTypeVariation = "PrimaryButton";
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void OnNewCharacter()
|
||||
{
|
||||
var packed = ResourceLoader.Load<PackedScene>(WizardScenePath);
|
||||
if (packed is null)
|
||||
{
|
||||
GD.PushError($"[title] Failed to load {WizardScenePath}");
|
||||
return;
|
||||
}
|
||||
var parent = GetParent();
|
||||
if (parent is null) return;
|
||||
// Clear siblings so the wizard fills the viewport, then swap in.
|
||||
foreach (Node sibling in parent.GetChildren())
|
||||
if (sibling != this) sibling.QueueFree();
|
||||
var wizardNode = packed.Instantiate();
|
||||
parent.AddChild(wizardNode);
|
||||
// The wizard's "← Title" back-button (visible on step 0) emits
|
||||
// BackToTitle; reinstate this title screen when that fires.
|
||||
if (wizardNode is Wizard wizard)
|
||||
wizard.BackToTitle += () => SwapBackToTitle(parent);
|
||||
QueueFree();
|
||||
}
|
||||
|
||||
private static void SwapBackToTitle(Node parent)
|
||||
{
|
||||
foreach (Node child in parent.GetChildren()) child.QueueFree();
|
||||
parent.AddChild(new TitleScreen());
|
||||
}
|
||||
|
||||
private void OnContinue()
|
||||
{
|
||||
// M7 territory — the play-loop screens that consume the persisted
|
||||
// character don't exist yet. For now, surface a print so the click
|
||||
// does something visible and the button isn't dead UI.
|
||||
GD.Print($"[title] Continue: {CharacterAssembler.PersistedStatePath} exists. "
|
||||
+ "Play-loop pickup lands with M7.");
|
||||
}
|
||||
|
||||
private void OnQuit() => GetTree().Quit();
|
||||
}
|
||||
@@ -21,8 +21,11 @@ public static class CodexCard
|
||||
|
||||
/// <summary>
|
||||
/// Creates a PanelContainer with ThemeTypeVariation = "Card" plus
|
||||
/// hover signal wiring. The MouseEntered/MouseExited handlers update
|
||||
/// the hover meta and re-apply the right stylebox.
|
||||
/// hover signal wiring. MouseEntered marks hover; MouseExited defers
|
||||
/// a recheck against the card's global rect so moving the cursor
|
||||
/// from the card body onto an inner Button (which captures the
|
||||
/// parent's MouseExited via mouse-filter Stop) does not clear the
|
||||
/// hover state — the cursor is still visually within the card.
|
||||
/// </summary>
|
||||
public static PanelContainer Make()
|
||||
{
|
||||
@@ -32,20 +35,52 @@ public static class CodexCard
|
||||
MouseFilter = Control.MouseFilterEnum.Stop,
|
||||
};
|
||||
card.MouseEntered += () => SetHover(card, true);
|
||||
card.MouseExited += () => SetHover(card, false);
|
||||
card.MouseExited += () =>
|
||||
Callable.From(() => RecheckHover(card)).CallDeferred();
|
||||
return card;
|
||||
}
|
||||
|
||||
private static void RecheckHover(PanelContainer card)
|
||||
{
|
||||
if (!GodotObject.IsInstanceValid(card)) return;
|
||||
// Hover stays true as long as the cursor is anywhere within the
|
||||
// card's rect (including over any child control). Drop only when
|
||||
// the cursor has truly left the card area.
|
||||
bool stillOver = card.GetGlobalRect().HasPoint(card.GetGlobalMousePosition());
|
||||
SetHover(card, stillOver);
|
||||
}
|
||||
|
||||
public static void SetSelected(PanelContainer card, bool selected)
|
||||
{
|
||||
card.SetMeta(SelectedMeta, selected);
|
||||
Apply(card);
|
||||
ApplyOrDefer(card);
|
||||
}
|
||||
|
||||
private static void SetHover(PanelContainer card, bool hover)
|
||||
{
|
||||
card.SetMeta(HoverMeta, hover);
|
||||
ApplyOrDefer(card);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply now if the card is already in the scene tree, otherwise defer
|
||||
/// until end-of-frame so the parent theme cascade is reachable. Step
|
||||
/// builders call SetSelected on a freshly-created card before
|
||||
/// AddChild — the theme isn't visible at that point and HasThemeStylebox
|
||||
/// returns false, which previously meant the override silently dropped
|
||||
/// and only re-attached when MouseEntered later re-ran Apply.
|
||||
/// </summary>
|
||||
private static void ApplyOrDefer(PanelContainer card)
|
||||
{
|
||||
if (card.IsInsideTree())
|
||||
{
|
||||
Apply(card);
|
||||
return;
|
||||
}
|
||||
Callable.From(() =>
|
||||
{
|
||||
if (GodotObject.IsInstanceValid(card)) Apply(card);
|
||||
}).CallDeferred();
|
||||
}
|
||||
|
||||
private static void Apply(PanelContainer card)
|
||||
|
||||
@@ -29,7 +29,15 @@ public static class CodexTheme
|
||||
private static FontFile? _mono;
|
||||
private static bool _fontsLoaded;
|
||||
|
||||
public static Theme Build() => Build(CodexPalette.Parchment);
|
||||
/// <summary>
|
||||
/// Palette used by the no-arg <see cref="Build()"/>. Set this before any
|
||||
/// UI mounts to swap the active codex palette globally — e.g. Main reads
|
||||
/// the <c>--dark</c> command-line flag and assigns <see cref="CodexPalette.Dark"/>
|
||||
/// here. Defaults to <see cref="CodexPalette.Parchment"/>.
|
||||
/// </summary>
|
||||
public static CodexPalette DefaultPalette { get; set; } = CodexPalette.Parchment;
|
||||
|
||||
public static Theme Build() => Build(DefaultPalette);
|
||||
|
||||
public static Theme Build(CodexPalette palette)
|
||||
{
|
||||
@@ -101,9 +109,15 @@ public static class CodexTheme
|
||||
var cardSelected = (StyleBoxFlat)card.Duplicate();
|
||||
cardSelected.BorderColor = p.Seal;
|
||||
cardSelected.SetBorderWidthAll(3);
|
||||
cardSelected.ShadowColor = WithAlpha(p.Seal, 0.6f);
|
||||
cardSelected.ShadowSize = 14;
|
||||
cardSelected.ShadowOffset = new Vector2(0, 14);
|
||||
// Drop shadow: directional (light from upper-left) and sized so the
|
||||
// shadow's bottom edge stays clear of the next card. Card grids
|
||||
// separate cards by 12px (v_separation in StepClade / StepSpecies /
|
||||
// StepClass / etc.) — offset.y + size ≤ 11 keeps a 1px-minimum gap
|
||||
// before the next card so the shadow reads as a shadow on the
|
||||
// surface below, not as a smudge between cards.
|
||||
cardSelected.ShadowColor = WithAlpha(p.Seal, 0.55f);
|
||||
cardSelected.ShadowSize = 6;
|
||||
cardSelected.ShadowOffset = new Vector2(4, 4);
|
||||
theme.SetStylebox("panel_selected", "Card", cardSelected);
|
||||
|
||||
// Popover frame — gild border + soft shadow + rounded corners.
|
||||
|
||||
@@ -104,7 +104,7 @@ public partial class CodexStepper : HBoxContainer
|
||||
{
|
||||
var underline = new ColorRect
|
||||
{
|
||||
Color = TryGetThemeColor("font_color", "Gild") ?? new Color("#b48a3c"),
|
||||
Color = CodexTheme.DefaultPalette.Gild,
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
underline.AnchorTop = 1.0f;
|
||||
@@ -124,15 +124,19 @@ public partial class CodexStepper : HBoxContainer
|
||||
// Default theme colours come from the StepperNum/StepperName variations
|
||||
// (ink-mute). State overrides bring active steps to ink and complete
|
||||
// to seal-red. Locked uses the dim default plus reduced opacity.
|
||||
// Pull the colours from CodexTheme.DefaultPalette so the stepper
|
||||
// tracks the active palette (parchment vs dark) instead of forcing
|
||||
// the parchment values regardless.
|
||||
var palette = CodexTheme.DefaultPalette;
|
||||
Color? numColor = state switch
|
||||
{
|
||||
StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")),
|
||||
StepState.Complete => TryGetGlobalThemeColor("Seal", new Color("#7a1f12")),
|
||||
StepState.Active => palette.Ink,
|
||||
StepState.Complete => palette.Seal,
|
||||
_ => null,
|
||||
};
|
||||
Color? nameColor = state switch
|
||||
{
|
||||
StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")),
|
||||
StepState.Active => palette.Ink,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -149,14 +153,6 @@ public partial class CodexStepper : HBoxContainer
|
||||
}
|
||||
}
|
||||
|
||||
private static Color? TryGetGlobalThemeColor(string name, Color fallback) => fallback;
|
||||
|
||||
private Color? TryGetThemeColor(string property, string variation)
|
||||
{
|
||||
if (HasThemeColor(property, variation)) return GetThemeColor(property, variation);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string Roman(int n) => n switch
|
||||
{
|
||||
1 => "I",
|
||||
|
||||
Reference in New Issue
Block a user