2026-05-02 19:35:03 -07:00
|
|
|
using Godot;
|
|
|
|
|
using Theriapolis.Core.Data;
|
2026-05-02 20:57:02 -07:00
|
|
|
using Theriapolis.GodotHost.Scenes.Widgets;
|
2026-05-02 19:35:03 -07:00
|
|
|
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;
|
|
|
|
|
|
2026-05-02 20:57:02 -07:00
|
|
|
// PanelContainer (a Container subclass) so the card height is
|
|
|
|
|
// driven by its inner VBoxContainer's content. Switching from
|
|
|
|
|
// Button avoids the issue where Button's intrinsic min size
|
|
|
|
|
// doesn't aggregate from non-Button children, causing chips to
|
|
|
|
|
// overflow into the cards below at high trait counts.
|
|
|
|
|
var card = new PanelContainer
|
2026-05-02 19:35:03 -07:00
|
|
|
{
|
2026-05-02 20:57:02 -07:00
|
|
|
CustomMinimumSize = new Vector2(200, 0),
|
|
|
|
|
MouseFilter = MouseFilterEnum.Stop,
|
2026-05-02 19:35:03 -07:00
|
|
|
};
|
2026-05-02 20:57:02 -07:00
|
|
|
if (selected) card.Modulate = new Color(1f, 0.95f, 0.85f); // gild tint placeholder until theming
|
|
|
|
|
|
|
|
|
|
card.GuiInput += (InputEvent e) =>
|
2026-05-02 19:35:03 -07:00
|
|
|
{
|
2026-05-02 20:57:02 -07:00
|
|
|
if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
|
2026-05-02 19:35:03 -07:00
|
|
|
{
|
2026-05-02 20:57:02 -07:00
|
|
|
// 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 },
|
|
|
|
|
});
|
2026-05-02 19:35:03 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-02 20:57:02 -07:00
|
|
|
var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass };
|
2026-05-02 19:35:03 -07:00
|
|
|
box.AddThemeConstantOverride("separation", 6);
|
2026-05-02 20:57:02 -07:00
|
|
|
card.AddChild(box);
|
2026-05-02 19:35:03 -07:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 20:57:02 -07:00
|
|
|
if (clade.Traits.Length > 0 || clade.Detriments.Length > 0)
|
2026-05-02 19:35:03 -07:00
|
|
|
{
|
2026-05-02 20:57:02 -07:00
|
|
|
var chips = new HFlowContainer { MouseFilter = MouseFilterEnum.Pass };
|
|
|
|
|
chips.AddThemeConstantOverride("h_separation", 6);
|
|
|
|
|
chips.AddThemeConstantOverride("v_separation", 4);
|
|
|
|
|
box.AddChild(chips);
|
|
|
|
|
|
|
|
|
|
foreach (var t in clade.Traits)
|
2026-05-02 19:35:03 -07:00
|
|
|
{
|
2026-05-02 20:57:02 -07:00
|
|
|
var chip = new TraitChip
|
|
|
|
|
{
|
|
|
|
|
TraitName = t.Name,
|
|
|
|
|
Description = t.Description,
|
|
|
|
|
};
|
|
|
|
|
chips.AddChild(chip);
|
|
|
|
|
}
|
|
|
|
|
foreach (var d in clade.Detriments)
|
|
|
|
|
{
|
|
|
|
|
var chip = new TraitChip
|
|
|
|
|
{
|
|
|
|
|
TraitName = d.Name,
|
|
|
|
|
Description = d.Description,
|
|
|
|
|
Detriment = true,
|
|
|
|
|
};
|
|
|
|
|
chips.AddChild(chip);
|
|
|
|
|
}
|
2026-05-02 19:35:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-05-02 20:57:02 -07:00
|
|
|
return card;
|
2026-05-02 19:35:03 -07:00
|
|
|
}
|
|
|
|
|
}
|