M6.16: Unified hybrid species grid + codex-styled hover popover
StepSpecies hybrid mode now uses one grid combining sire-clade species (under a "SIRE — <Clade>" eyebrow) and dam-clade species (under "DAM — <Clade>"). Cards are click-to-select like the purebred path — since clades are guaranteed disjoint by StepClade's parent-conflict rule, the lineage is implicit from the species' clade and no per-card toggles are needed. Hover popover now picks up the codex theme: parchment Bg2 panel with a gild border, rounded 14px corners, and soft drop shadow; H3 display serif title, mono Eyebrow tag, CardBody description. Detriment popovers swap to a 3px seal-red border via the panel_detriment stylebox override (replaces the old red Modulate hack). Theme propagation fix: CanvasLayer breaks Godot's Control theme inheritance, so the popup was rendering on Godot defaults. _Ready defers a lookup of the parent Control's theme and assigns it directly to the popup so the codex parchment + Cormorant/CrimsonPro fonts actually resolve. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,11 @@ namespace Theriapolis.GodotHost.Scenes.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Step II — Species. Direct port of <c>StepSpecies</c> in
|
||||
/// <c>src/steps.jsx</c> plus the Phase 6.5 hybrid extension: when
|
||||
/// <see cref="CharacterDraft.IsHybrid"/> is true the step shows two
|
||||
/// stacked grids, one filtered by SireCladeId and one by DamCladeId.
|
||||
/// <c>src/steps.jsx</c> plus the Phase 6.5 hybrid extension. Hybrid mode
|
||||
/// uses a single unified grid (M6.16) — sire and dam species lists are
|
||||
/// merged, and each card carries either a Sire or Dam toggle in its
|
||||
/// header matching the clade it belongs to (sire/dam clades are
|
||||
/// guaranteed disjoint by StepClade's parent-conflict rule).
|
||||
/// </summary>
|
||||
public partial class StepSpecies : VBoxContainer, IStep
|
||||
{
|
||||
@@ -17,8 +19,7 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
private VBoxContainer _purebredSection = null!;
|
||||
private VBoxContainer _hybridSection = null!;
|
||||
private GridContainer _purebredGrid = null!;
|
||||
private GridContainer _sireGrid = null!;
|
||||
private GridContainer _damGrid = null!;
|
||||
private GridContainer _hybridGrid = null!;
|
||||
|
||||
// Phase B species trait + detriment pickers — single-pick per lineage.
|
||||
private VBoxContainer _pickSection = null!;
|
||||
@@ -77,13 +78,14 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
_hybridSection.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(_hybridSection);
|
||||
|
||||
_hybridSection.AddChild(new Label { Text = "SIRE — Paternal Lineage", ThemeTypeVariation = "Eyebrow" });
|
||||
_sireGrid = MakeGrid();
|
||||
_hybridSection.AddChild(_sireGrid);
|
||||
|
||||
_hybridSection.AddChild(new Label { Text = "DAM — Maternal Lineage", ThemeTypeVariation = "Eyebrow" });
|
||||
_damGrid = MakeGrid();
|
||||
_hybridSection.AddChild(_damGrid);
|
||||
_hybridSection.AddChild(new Label
|
||||
{
|
||||
Text = "Pick one species per parent lineage. Sire's clade species "
|
||||
+ "are listed first, then Dam's — click any card to pick it.",
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
});
|
||||
_hybridGrid = MakeGrid();
|
||||
_hybridSection.AddChild(_hybridGrid);
|
||||
|
||||
// Phase B species pickers: one trait + one detriment per parent.
|
||||
_pickSection = new VBoxContainer();
|
||||
@@ -130,10 +132,7 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
|
||||
if (hybrid)
|
||||
{
|
||||
RefreshGrid(_sireGrid, _draft.SireCladeId, _draft.SireSpeciesId,
|
||||
spId => OnLineageSpeciesPicked("sire", spId));
|
||||
RefreshGrid(_damGrid, _draft.DamCladeId, _draft.DamSpeciesId,
|
||||
spId => OnLineageSpeciesPicked("dam", spId));
|
||||
RefreshHybridGrid();
|
||||
|
||||
SyncSpeciesPicks(_sirePickCol, ref _sirePicksBuiltFor, "sire",
|
||||
_draft.SireSpeciesId, _draft.SireChosenSpeciesTrait, _draft.SireChosenSpeciesDetriment);
|
||||
@@ -398,6 +397,35 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
grid.AddChild(BuildCard(sp, sp.Id == selectedSpecies, onClick));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hybrid mode: rebuild the unified grid with sire-clade species
|
||||
/// followed by dam-clade species. Sire and dam clades are
|
||||
/// guaranteed distinct by StepClade's parent-conflict rule, so the
|
||||
/// species lists are disjoint — each card unambiguously belongs to
|
||||
/// one lineage and a click on the card commits the pick. Full
|
||||
/// rebuild on every Refresh is safe because Bind installs Refresh
|
||||
/// as a deferred callback.
|
||||
/// </summary>
|
||||
private void RefreshHybridGrid()
|
||||
{
|
||||
foreach (var c in _hybridGrid.GetChildren()) c.Free();
|
||||
AddHybridLineageBlock("sire", _draft.SireCladeId, _draft.SireSpeciesId);
|
||||
AddHybridLineageBlock("dam", _draft.DamCladeId, _draft.DamSpeciesId);
|
||||
}
|
||||
|
||||
private void AddHybridLineageBlock(string lineage, string cladeId, string selectedSpeciesId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cladeId)) return;
|
||||
var clade = CodexContent.Clade(cladeId);
|
||||
string headerLabel = (lineage == "sire" ? "SIRE" : "DAM")
|
||||
+ (clade is null ? "" : " — " + clade.Name.ToUpperInvariant());
|
||||
_hybridGrid.AddChild(new Label { Text = headerLabel, ThemeTypeVariation = "Eyebrow" });
|
||||
|
||||
foreach (var sp in CodexContent.SpeciesOfClade(cladeId))
|
||||
_hybridGrid.AddChild(BuildCard(sp, sp.Id == selectedSpeciesId,
|
||||
spId => OnLineageSpeciesPicked(lineage, spId)));
|
||||
}
|
||||
|
||||
private static Control BuildCard(SpeciesDef sp, bool selected, System.Action<string> onClick)
|
||||
{
|
||||
var card = CodexCard.Make();
|
||||
|
||||
@@ -46,18 +46,33 @@ public partial class PopoverLayer : CanvasLayer
|
||||
{
|
||||
Layer = 100;
|
||||
BuildPopover();
|
||||
// Theme inheritance walks Control descendants only — CanvasLayer is
|
||||
// a plain Node, so it breaks the propagation chain from the Wizard
|
||||
// Control above. _Ready is bottom-up, so the parent Wizard hasn't
|
||||
// assigned its codex theme yet — defer the lookup until parent's
|
||||
// _Ready has run, then pull its theme onto the popup directly.
|
||||
CallDeferred(MethodName.InheritParentTheme);
|
||||
}
|
||||
|
||||
private void InheritParentTheme()
|
||||
{
|
||||
if (GetParent() is Control parentControl && parentControl.Theme is not null)
|
||||
_popup.Theme = parentControl.Theme;
|
||||
}
|
||||
|
||||
private void BuildPopover()
|
||||
{
|
||||
// Ignore so clicks/scroll/hover all pass through to whatever's
|
||||
// beneath. The popover is purely a visual readout; the chip
|
||||
// owns the lifecycle entirely.
|
||||
// owns the lifecycle entirely. ThemeTypeVariation pulls the
|
||||
// CodexPopover stylebox (parchment bg2 + gild border + rounded
|
||||
// corners + soft shadow) defined in CodexTheme.
|
||||
_popup = new PanelContainer
|
||||
{
|
||||
Visible = false,
|
||||
MouseFilter = Control.MouseFilterEnum.Ignore,
|
||||
ZIndex = 100,
|
||||
ThemeTypeVariation = "CodexPopover",
|
||||
};
|
||||
AddChild(_popup);
|
||||
|
||||
@@ -66,19 +81,29 @@ public partial class PopoverLayer : CanvasLayer
|
||||
_popup.AddChild(v);
|
||||
|
||||
var nameRow = new HBoxContainer();
|
||||
nameRow.AddThemeConstantOverride("separation", 8);
|
||||
nameRow.AddThemeConstantOverride("separation", 10);
|
||||
v.AddChild(nameRow);
|
||||
|
||||
_titleLabel = new Label();
|
||||
// Display-serif title at H3 size (20px) — pulls the trait name
|
||||
// out of the body copy below.
|
||||
_titleLabel = new Label { ThemeTypeVariation = "H3" };
|
||||
nameRow.AddChild(_titleLabel);
|
||||
|
||||
_tagLabel = new Label { Visible = false };
|
||||
// Mono uppercase tag label, vertically centred against the title.
|
||||
_tagLabel = new Label
|
||||
{
|
||||
Visible = false,
|
||||
ThemeTypeVariation = "Eyebrow",
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
|
||||
};
|
||||
nameRow.AddChild(_tagLabel);
|
||||
|
||||
_descLabel = new Label
|
||||
{
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
CustomMinimumSize = new Vector2(220, 0),
|
||||
ThemeTypeVariation = "CardBody",
|
||||
};
|
||||
v.AddChild(_descLabel);
|
||||
}
|
||||
@@ -91,10 +116,18 @@ public partial class PopoverLayer : CanvasLayer
|
||||
_tagLabel.Text = !string.IsNullOrEmpty(tag) ? tag.ToUpperInvariant()
|
||||
: (detriment ? "DETRIMENT" : "");
|
||||
|
||||
// M6.3 default-theme tint: detriment popover gets a red modulate so
|
||||
// it reads visually distinct from a regular trait. The proper
|
||||
// codex StyleBox swap lands in the theming pass.
|
||||
_popup.Modulate = detriment ? new Color(1f, 0.78f, 0.78f) : Colors.White;
|
||||
// Detriment popover swaps to the seal-bordered stylebox via
|
||||
// override; non-detriment clears the override so the default
|
||||
// CodexPopover panel takes effect again.
|
||||
if (detriment && _popup.HasThemeStylebox("panel_detriment", "CodexPopover"))
|
||||
{
|
||||
var box = _popup.GetThemeStylebox("panel_detriment", "CodexPopover");
|
||||
_popup.AddThemeStyleboxOverride("panel", box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.RemoveThemeStyleboxOverride("panel");
|
||||
}
|
||||
|
||||
_popup.Visible = true;
|
||||
_popup.ResetSize();
|
||||
|
||||
@@ -106,12 +106,19 @@ public static class CodexTheme
|
||||
cardSelected.ShadowOffset = new Vector2(0, 14);
|
||||
theme.SetStylebox("panel_selected", "Card", cardSelected);
|
||||
|
||||
// Popover frame — gild border + soft shadow. Matches .trait-hint.
|
||||
// Popover frame — gild border + soft shadow + rounded corners.
|
||||
// Matches .trait-hint, with a softer corner radius than the rest of
|
||||
// the codex (cards/buttons use 2px sharp) so the floating reveal
|
||||
// reads as a friendlier secondary surface.
|
||||
theme.SetTypeVariation("CodexPopover", "PanelContainer");
|
||||
var popover = new StyleBoxFlat
|
||||
{
|
||||
BgColor = p.Bg2,
|
||||
BorderColor = p.Gild,
|
||||
CornerRadiusTopLeft = 14,
|
||||
CornerRadiusTopRight = 14,
|
||||
CornerRadiusBottomLeft = 14,
|
||||
CornerRadiusBottomRight = 14,
|
||||
ContentMarginLeft = 16,
|
||||
ContentMarginRight = 16,
|
||||
ContentMarginTop = 14,
|
||||
@@ -120,11 +127,14 @@ public static class CodexTheme
|
||||
ShadowSize = 18,
|
||||
ShadowOffset = new Vector2(0, 12),
|
||||
};
|
||||
popover.SetBorderWidthAll(1);
|
||||
popover.SetBorderWidthAll(2);
|
||||
theme.SetStylebox("panel", "CodexPopover", popover);
|
||||
|
||||
// Detriment swap — seal-red border drawn at 3px so the warning reads
|
||||
// unambiguously against the parchment bg even at a glance.
|
||||
var popoverDetriment = (StyleBoxFlat)popover.Duplicate();
|
||||
popoverDetriment.BorderColor = p.Seal;
|
||||
popoverDetriment.SetBorderWidthAll(3);
|
||||
theme.SetStylebox("panel_detriment", "CodexPopover", popoverDetriment);
|
||||
|
||||
// Pill — small trait/skill chip. Mirrors .trait-chips .t-name from
|
||||
|
||||
Reference in New Issue
Block a user