From 0ab4715aee0ecffc19e396ceec141ec34affc95e Mon Sep 17 00:00:00 2001 From: Christopher Wiebe Date: Wed, 6 May 2026 21:26:38 -0700 Subject: [PATCH] M6.16: Unified hybrid species grid + codex-styled hover popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StepSpecies hybrid mode now uses one grid combining sire-clade species (under a "SIRE — " eyebrow) and dam-clade species (under "DAM — "). 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 --- Theriapolis.Godot/Scenes/Steps/StepSpecies.cs | 60 ++++++++++++++----- .../Scenes/Widgets/PopoverLayer.cs | 49 ++++++++++++--- Theriapolis.Godot/UI/CodexTheme.cs | 14 ++++- 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs index afaaffd..865d2d9 100644 --- a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs +++ b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs @@ -7,9 +7,11 @@ namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step II — Species. Direct port of StepSpecies in -/// src/steps.jsx plus the Phase 6.5 hybrid extension: when -/// is true the step shows two -/// stacked grids, one filtered by SireCladeId and one by DamCladeId. +/// src/steps.jsx 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). /// 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)); } + /// + /// 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. + /// + 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 onClick) { var card = CodexCard.Make(); diff --git a/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs b/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs index 8fc0f21..04f6587 100644 --- a/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs +++ b/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs @@ -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(); diff --git a/Theriapolis.Godot/UI/CodexTheme.cs b/Theriapolis.Godot/UI/CodexTheme.cs index 382bc90..d7b4b61 100644 --- a/Theriapolis.Godot/UI/CodexTheme.cs +++ b/Theriapolis.Godot/UI/CodexTheme.cs @@ -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