M6.1: Character creation wizard foundation (.tscn + Resource-based draft)
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>
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2015 the Cormorant Project Authors (github.com/CatharsisFonts/Cormorant)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,11 @@ using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost;
|
||||
|
||||
public partial class Main : Node
|
||||
// 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()
|
||||
{
|
||||
@@ -22,6 +26,7 @@ public partial class Main : Node
|
||||
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++)
|
||||
{
|
||||
@@ -30,6 +35,11 @@ public partial class Main : Node
|
||||
runCodexTest = true;
|
||||
break;
|
||||
}
|
||||
if (args[i] == "--wizard")
|
||||
{
|
||||
runWizard = true;
|
||||
break;
|
||||
}
|
||||
if (args[i] == "--smoke-test")
|
||||
{
|
||||
ulong seed = 12345UL;
|
||||
@@ -109,6 +119,15 @@ public partial class Main : Node
|
||||
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).");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
[ext_resource type="Script" path="res://Main.cs" id="1_main"]
|
||||
|
||||
[node name="Main" type="Node"]
|
||||
[node name="Main" type="Control"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_main")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Godot;
|
||||
using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost.Scenes;
|
||||
|
||||
/// <summary>
|
||||
/// Right-rail summary of the in-progress character. Single
|
||||
/// <see cref="Refresh"/> rebuilds every section per
|
||||
/// GODOT_PORTING_GUIDE.md §10 — the panel is small enough that full
|
||||
/// rebuild is cheap and partial-update logic isn't worth it. Connect
|
||||
/// the draft via <see cref="SetDraft"/>; the Wizard does this on _Ready.
|
||||
/// </summary>
|
||||
public partial class Aside : MarginContainer
|
||||
{
|
||||
private CharacterDraft? _draft;
|
||||
private VBoxContainer _content = null!;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
AddThemeConstantOverride("margin_left", 18);
|
||||
AddThemeConstantOverride("margin_right", 18);
|
||||
AddThemeConstantOverride("margin_top", 18);
|
||||
AddThemeConstantOverride("margin_bottom", 18);
|
||||
|
||||
_content = new VBoxContainer();
|
||||
_content.AddThemeConstantOverride("separation", 18);
|
||||
AddChild(_content);
|
||||
}
|
||||
|
||||
public void SetDraft(CharacterDraft draft)
|
||||
{
|
||||
_draft = draft;
|
||||
_draft.Changed += Refresh;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (_draft is null || _content is null) return;
|
||||
foreach (var c in _content.GetChildren()) c.QueueFree();
|
||||
|
||||
_content.AddChild(new Label { Text = "SUMMARY" });
|
||||
AddBlock("Clade", CodexContent.Clade(_draft.CladeId)?.Name);
|
||||
AddBlock("Species", CodexContent.SpeciesById(_draft.SpeciesId)?.Name);
|
||||
AddBlock("Calling", CodexContent.Class(_draft.ClassId)?.Name);
|
||||
AddBlock("Background", CodexContent.Background(_draft.BackgroundId)?.Name);
|
||||
AddBlock("Name", string.IsNullOrEmpty(_draft.CharacterName) ? null : _draft.CharacterName);
|
||||
}
|
||||
|
||||
private void AddBlock(string label, string? value)
|
||||
{
|
||||
var v = new VBoxContainer();
|
||||
v.AddThemeConstantOverride("separation", 4);
|
||||
_content.AddChild(v);
|
||||
v.AddChild(new Label { Text = label.ToUpperInvariant() });
|
||||
v.AddChild(new Label { Text = value ?? "—" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://aside6m6v1"]
|
||||
|
||||
[ext_resource type="Script" path="res://Scenes/Aside.cs" id="1_aside"]
|
||||
|
||||
[node name="Aside" type="MarginContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(320, 0)
|
||||
size_flags_horizontal = 0
|
||||
script = ExtResource("1_aside")
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Theriapolis.GodotHost.Scenes.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Common contract for character-creation step scenes. Mirrors
|
||||
/// <c>app.jsx</c>'s per-step <c>validate</c> pattern: each step inspects
|
||||
/// the shared draft and returns null when its requirements are met, or
|
||||
/// a short error string otherwise.
|
||||
///
|
||||
/// Step classes also extend <see cref="Godot.Control"/> so they can be
|
||||
/// parented into the wizard's StepHost. Bind() is called once after
|
||||
/// instantiation; Godot drives the lifecycle as usual.
|
||||
/// </summary>
|
||||
public interface IStep
|
||||
{
|
||||
void Bind(UI.CharacterDraft draft);
|
||||
string? Validate();
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Godot;
|
||||
using Theriapolis.Core.Data;
|
||||
using Theriapolis.GodotHost.UI;
|
||||
|
||||
namespace Theriapolis.GodotHost.Scenes.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step I — Clade. Direct port of <c>StepClade</c> in
|
||||
/// <c>src/steps.jsx</c>: intro paragraph, then a card grid with one
|
||||
/// card per clade. Click selects via <see cref="CharacterDraft.Patch"/>.
|
||||
///
|
||||
/// Default theme only at this layer (per GODOT_PORTING_GUIDE.md §12 build
|
||||
/// order); the parchment look lands in the final theming pass.
|
||||
/// </summary>
|
||||
public partial class StepClade : VBoxContainer, IStep
|
||||
{
|
||||
private CharacterDraft _draft = null!;
|
||||
private GridContainer _grid = null!;
|
||||
|
||||
public void Bind(CharacterDraft draft)
|
||||
{
|
||||
_draft = draft;
|
||||
_draft.Changed += Refresh;
|
||||
Build();
|
||||
}
|
||||
|
||||
public string? Validate() => string.IsNullOrEmpty(_draft?.CladeId) ? "Pick a clade." : null;
|
||||
|
||||
private void Build()
|
||||
{
|
||||
AddThemeConstantOverride("separation", 16);
|
||||
|
||||
var intro = new VBoxContainer();
|
||||
intro.AddThemeConstantOverride("separation", 6);
|
||||
AddChild(intro);
|
||||
intro.AddChild(new Label { Text = "FOLIO I · CLADE" });
|
||||
intro.AddChild(new Label { Text = "Choose a Clade" });
|
||||
intro.AddChild(new Label
|
||||
{
|
||||
Text = "The broad mammalian family of your line. Clade defines the largest "
|
||||
+ "strokes — predator or prey, communal or solitary, scent-driven or "
|
||||
+ "sight-driven. Each clade carries inherited traits and limits that "
|
||||
+ "no character escapes.",
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
CustomMinimumSize = new Vector2(0, 0),
|
||||
});
|
||||
|
||||
_grid = new GridContainer
|
||||
{
|
||||
Columns = 3,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
_grid.AddThemeConstantOverride("h_separation", 16);
|
||||
_grid.AddThemeConstantOverride("v_separation", 16);
|
||||
AddChild(_grid);
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
if (_grid is null) return;
|
||||
foreach (var c in _grid.GetChildren()) c.QueueFree();
|
||||
foreach (var clade in CodexContent.Clades)
|
||||
_grid.AddChild(BuildCard(clade));
|
||||
}
|
||||
|
||||
private Control BuildCard(CladeDef clade)
|
||||
{
|
||||
bool selected = _draft.CladeId == clade.Id;
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Text = "",
|
||||
Flat = false,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = selected,
|
||||
FocusMode = Control.FocusModeEnum.None,
|
||||
// 200 wide so 3 cards + separators (≈ 632) + page margins +
|
||||
// Aside fit inside ≥ 1024-px viewports (the smaller-screen
|
||||
// floor we want to support). Height held at 200 so the inner
|
||||
// labels render at readable size without a content-driven
|
||||
// height collapse (Button isn't a Container, so child vbox
|
||||
// height doesn't bubble up to the button's intrinsic min size).
|
||||
CustomMinimumSize = new Vector2(200, 200),
|
||||
ClipText = false,
|
||||
Alignment = HorizontalAlignment.Left,
|
||||
};
|
||||
btn.Pressed += () =>
|
||||
{
|
||||
// Default species for the new clade — match React app.jsx:
|
||||
// when clade changes, species defaults to first species in clade.
|
||||
string speciesId = "";
|
||||
foreach (var s in CodexContent.SpeciesOfClade(clade.Id))
|
||||
{
|
||||
speciesId = s.Id;
|
||||
break;
|
||||
}
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ "clade_id", clade.Id },
|
||||
{ "species_id", speciesId },
|
||||
});
|
||||
};
|
||||
|
||||
// Label content stacked inside the button via an anchored VBoxContainer
|
||||
// (Button isn't a Container, so we anchor the vbox to fill the button's
|
||||
// rect and let the children flow within it).
|
||||
var box = new VBoxContainer
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
};
|
||||
box.AnchorRight = 1f;
|
||||
box.AnchorBottom = 1f;
|
||||
box.OffsetLeft = 12;
|
||||
box.OffsetTop = 12;
|
||||
box.OffsetRight = -12;
|
||||
box.OffsetBottom = -12;
|
||||
box.AddThemeConstantOverride("separation", 6);
|
||||
btn.AddChild(box);
|
||||
|
||||
box.AddChild(new Label { Text = clade.Name, MouseFilter = MouseFilterEnum.Ignore });
|
||||
box.AddChild(new Label
|
||||
{
|
||||
Text = clade.Kind.ToUpperInvariant(),
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
});
|
||||
|
||||
if (clade.AbilityMods.Count > 0)
|
||||
{
|
||||
var modsRow = new HBoxContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||
modsRow.AddThemeConstantOverride("separation", 8);
|
||||
box.AddChild(modsRow);
|
||||
foreach (var (k, v) in clade.AbilityMods)
|
||||
modsRow.AddChild(new Label
|
||||
{
|
||||
Text = $"{k} {(v >= 0 ? "+" : "")}{v}",
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
});
|
||||
}
|
||||
|
||||
if (clade.Traits.Length > 0)
|
||||
{
|
||||
box.AddChild(new Label
|
||||
{
|
||||
Text = $"{clade.Traits.Length} traits, {clade.Detriments.Length} detriments",
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
});
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using Godot;
|
||||
|
||||
namespace Theriapolis.GodotHost.Scenes;
|
||||
|
||||
/// <summary>
|
||||
/// Character creation wizard shell. Mirrors <c>src/app.jsx</c> per
|
||||
/// GODOT_PORTING_GUIDE.md §4: header + stepper + page (step + aside) +
|
||||
/// nav bar. Owns the <see cref="CharacterDraft"/> resource and dispatches
|
||||
/// each step's content into the StepHost.
|
||||
///
|
||||
/// Default theme only at this layer — per guide §12 (build order),
|
||||
/// the parchment Theme lands as a final pass once structural correctness
|
||||
/// is verified. Until then, font/colour issues are clearly font/colour
|
||||
/// issues, not layout issues.
|
||||
/// </summary>
|
||||
public partial class Wizard : Control
|
||||
{
|
||||
[Signal] public delegate void BackToTitleEventHandler();
|
||||
|
||||
private static readonly string[] StepKeys =
|
||||
{ "clade", "species", "class", "subclass", "background", "stats", "skills", "review" };
|
||||
private static readonly string[] StepNames =
|
||||
{ "Clade", "Species", "Calling", "Subclass", "History", "Abilities", "Skills", "Sign" };
|
||||
|
||||
public UI.CharacterDraft Character { get; private set; } = null!;
|
||||
|
||||
private UI.Widgets.CodexStepper _stepper = null!;
|
||||
private Control _stepHost = null!;
|
||||
private Label _folioLabel = null!;
|
||||
private Label _validation = null!;
|
||||
private Label _navProgress = null!;
|
||||
private Button _backBtn = null!;
|
||||
private Button _nextBtn = null!;
|
||||
|
||||
private int _step;
|
||||
private Steps.IStep? _activeStep;
|
||||
private static readonly System.Type?[] StepTypes =
|
||||
{
|
||||
typeof(Steps.StepClade), // 0 Clade — implemented
|
||||
null, // 1 Species
|
||||
null, // 2 Calling
|
||||
null, // 3 Subclass
|
||||
null, // 4 History
|
||||
null, // 5 Abilities
|
||||
null, // 6 Skills
|
||||
null, // 7 Sign
|
||||
};
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Character = new UI.CharacterDraft();
|
||||
|
||||
_stepper = GetNode<UI.Widgets.CodexStepper>("%Stepper");
|
||||
_stepHost = GetNode<Control>("%StepHost");
|
||||
_folioLabel = GetNode<Label>("%FolioLabel");
|
||||
_validation = GetNode<Label>("%ValidationLabel");
|
||||
_navProgress = GetNode<Label>("%NavProgress");
|
||||
_backBtn = GetNode<Button>("%BackButton");
|
||||
_nextBtn = GetNode<Button>("%NextButton");
|
||||
|
||||
var aside = GetNode<Aside>("%Aside");
|
||||
aside.SetDraft(Character);
|
||||
|
||||
_stepper.StepClicked += OnStepperClicked;
|
||||
_backBtn.Pressed += OnBackPressed;
|
||||
_nextBtn.Pressed += OnNextPressed;
|
||||
Character.Changed += UpdateChrome;
|
||||
|
||||
SwitchToStep(0);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Step lifecycle
|
||||
|
||||
private void SwitchToStep(int index)
|
||||
{
|
||||
if (index < 0 || index >= StepKeys.Length) return;
|
||||
_step = index;
|
||||
|
||||
foreach (var c in _stepHost.GetChildren()) c.QueueFree();
|
||||
_activeStep = null;
|
||||
|
||||
var t = StepTypes[index];
|
||||
if (t is null)
|
||||
{
|
||||
_stepHost.AddChild(new Label
|
||||
{
|
||||
Text = $"{StepNames[index]} step — coming soon.",
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var instance = (Steps.IStep)System.Activator.CreateInstance(t)!;
|
||||
_activeStep = instance;
|
||||
instance.Bind(Character);
|
||||
_stepHost.AddChild((Control)instance);
|
||||
}
|
||||
|
||||
UpdateChrome();
|
||||
}
|
||||
|
||||
private void OnStepperClicked(int index)
|
||||
{
|
||||
if (index <= _step) { SwitchToStep(index); return; }
|
||||
// Forward jump requires current step satisfied. Unimplemented future
|
||||
// steps still accept the jump — SwitchToStep will show a placeholder.
|
||||
if (_activeStep?.Validate() is not null) return;
|
||||
SwitchToStep(index);
|
||||
}
|
||||
|
||||
private void OnBackPressed()
|
||||
{
|
||||
if (_step == 0) { EmitSignal(SignalName.BackToTitle); return; }
|
||||
SwitchToStep(_step - 1);
|
||||
}
|
||||
|
||||
private void OnNextPressed()
|
||||
{
|
||||
if (_step < StepKeys.Length - 1) SwitchToStep(_step + 1);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Chrome (header, stepper, nav-bar) refresh
|
||||
|
||||
private void UpdateChrome()
|
||||
{
|
||||
_folioLabel.Text = $"Folio {Roman(_step + 1)} of VIII — {StepNames[_step]}";
|
||||
|
||||
bool valid = _activeStep?.Validate() is null;
|
||||
string? err = _activeStep?.Validate();
|
||||
_validation.Text = err ?? (_step == StepKeys.Length - 1 ? "Ready to sign" : "Folio complete");
|
||||
_nextBtn.Disabled = !valid;
|
||||
_nextBtn.Visible = _step < StepKeys.Length - 1;
|
||||
_backBtn.Text = _step == 0 ? "← Title" : "← Back";
|
||||
_navProgress.Text = $"{_step + 1} / {StepKeys.Length}";
|
||||
|
||||
RebuildStepperStates(valid);
|
||||
}
|
||||
|
||||
private void RebuildStepperStates(bool currentSatisfied)
|
||||
{
|
||||
// Mirrors app.jsx's lock semantics: a step is Locked iff some EARLIER
|
||||
// step is unsatisfied. "Type not yet implemented" doesn't affect the
|
||||
// lock state — clicking an unimplemented step just shows a placeholder.
|
||||
var states = new UI.Widgets.CodexStepper.StepState[StepNames.Length];
|
||||
for (int i = 0; i < StepNames.Length; i++)
|
||||
{
|
||||
if (i == _step)
|
||||
states[i] = UI.Widgets.CodexStepper.StepState.Active;
|
||||
else if (i < _step)
|
||||
states[i] = UI.Widgets.CodexStepper.StepState.Complete;
|
||||
else
|
||||
states[i] = currentSatisfied
|
||||
? UI.Widgets.CodexStepper.StepState.Pending
|
||||
: UI.Widgets.CodexStepper.StepState.Locked;
|
||||
}
|
||||
_stepper.SetSteps(StepNames, states);
|
||||
}
|
||||
|
||||
private static string Roman(int n) => n switch
|
||||
{
|
||||
1 => "I", 2 => "II", 3 => "III", 4 => "IV",
|
||||
5 => "V", 6 => "VI", 7 => "VII", 8 => "VIII",
|
||||
_ => n.ToString(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://wizard6m6v1"]
|
||||
|
||||
[ext_resource type="Script" path="res://Scenes/Wizard.cs" id="1_wizard"]
|
||||
[ext_resource type="Script" path="res://UI/Widgets/CodexStepper.cs" id="2_stepper"]
|
||||
[ext_resource type="PackedScene" path="res://Scenes/Aside.tscn" id="3_aside"]
|
||||
|
||||
[node name="Wizard" type="Control"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_wizard")
|
||||
|
||||
[node name="Wrap" type="MarginContainer" parent="."]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
theme_override_constants/margin_left = 36
|
||||
theme_override_constants/margin_right = 36
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="Wrap"]
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="Wrap/Layout"]
|
||||
theme_override_constants/separation = 24
|
||||
|
||||
[node name="TitleCol" type="VBoxContainer" parent="Wrap/Layout/Header"]
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Title" type="Label" parent="Wrap/Layout/Header/TitleCol"]
|
||||
text = "THERIAPOLIS · CODEX OF BECOMING"
|
||||
|
||||
[node name="FolioLabel" type="Label" parent="Wrap/Layout/Header/TitleCol"]
|
||||
unique_name_in_owner = true
|
||||
text = "Folio I of VIII — Clade"
|
||||
|
||||
[node name="MetaLabel" type="Label" parent="Wrap/Layout/Header"]
|
||||
text = "PORT/GODOT · M6"
|
||||
|
||||
[node name="Stepper" type="HBoxContainer" parent="Wrap/Layout"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource("2_stepper")
|
||||
|
||||
[node name="Page" type="HBoxContainer" parent="Wrap/Layout"]
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="PageMain" type="MarginContainer" parent="Wrap/Layout/Page"]
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 12
|
||||
theme_override_constants/margin_right = 28
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="Wrap/Layout/Page/PageMain"]
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
|
||||
[node name="StepHost" type="VBoxContainer" parent="Wrap/Layout/Page/PageMain/Scroll"]
|
||||
unique_name_in_owner = true
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Aside" parent="Wrap/Layout/Page" instance=ExtResource("3_aside")]
|
||||
|
||||
[node name="NavBar" type="HBoxContainer" parent="Wrap/Layout"]
|
||||
theme_override_constants/separation = 24
|
||||
|
||||
[node name="BackButton" type="Button" parent="Wrap/Layout/NavBar"]
|
||||
unique_name_in_owner = true
|
||||
text = "← Back"
|
||||
|
||||
[node name="Spacer" type="Control" parent="Wrap/Layout/NavBar"]
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ValidationLabel" type="Label" parent="Wrap/Layout/NavBar"]
|
||||
unique_name_in_owner = true
|
||||
text = ""
|
||||
|
||||
[node name="NavProgress" type="Label" parent="Wrap/Layout/NavBar"]
|
||||
unique_name_in_owner = true
|
||||
text = "1 / 8"
|
||||
|
||||
[node name="NextButton" type="Button" parent="Wrap/Layout/NavBar"]
|
||||
unique_name_in_owner = true
|
||||
text = "Next ›"
|
||||
@@ -0,0 +1,106 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -55,14 +55,22 @@ public partial class CodexStepper : HBoxContainer
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
ToggleMode = false,
|
||||
};
|
||||
// Build a vbox child for two stacked labels.
|
||||
// Build a vbox child for two stacked labels. Button isn't a
|
||||
// Container, so the vbox doesn't get auto-laid-out — anchor it to
|
||||
// fill the button's rect explicitly. Without this, the vbox sits at
|
||||
// (0,0) with intrinsic size and the labels render in the top-left
|
||||
// corner instead of centered.
|
||||
var vbox = new VBoxContainer
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
Alignment = BoxContainer.AlignmentMode.Center,
|
||||
};
|
||||
vbox.AnchorRight = 1f;
|
||||
vbox.AnchorBottom = 1f;
|
||||
vbox.OffsetLeft = 0;
|
||||
vbox.OffsetRight = 0;
|
||||
vbox.OffsetTop = 0;
|
||||
vbox.OffsetBottom = 0;
|
||||
btn.AddChild(vbox);
|
||||
|
||||
var num = new Label
|
||||
|
||||
Reference in New Issue
Block a user