Files
TheriapolisV3/Theriapolis.Godot/Scenes/Steps/StepClade.cs
T

154 lines
5.2 KiB
C#
Raw Normal View History

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;
}
}