M6.17: Variant content + Sheep/Goat split + calling lore + uniform card layout
Species variants populated against the M6.13 schema: - Lion-Folk sex axis: Mane Guard (male) / Huntress Reflexes (female, +5 ft speed + advantage on initiative). - Elk-Folk sex axis: Antler Combat with 10 ft reach when full rack (male, retains seasonal Antler Drag) / Kick (female, prone on crit). Base traits restored to doc canon: Herd Coordination (Help → +3) + Endurance Runner (40 ft + advantage CON vs forced march); base speed bumped 30 → 40; new base detriment Herd Instinct. Ram-Folk replaced with separate Sheep-Folk + Goat-Folk species rather than a lineage-axis variant on a single Ram entry. Bovidae now has 4 species. The lineage-axis toggle UI in StepSpecies BuildCard rolled back; the schema stays for sex-axis (Lion/Elk) which auto-resolves. ContentLoadTests + HybridCharacterTests updated; Size.cs comment too. Calling lore: ClassDef gains Description; classes.json populated for all 8 callings with the doc's italic blockquote + paragraph profile. StepClass surfaces the description on the card. Card layout uniformity: StepClass / StepSubclass / StepBackground all switched to single-column ExpandFill grids (matching StepClade / StepSpecies). Each card now spans the wizard's content width so the description and feature chips have room to breathe. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,8 @@ public partial class StepBackground : VBoxContainer, IStep
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
});
|
||||
|
||||
_grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
|
||||
_grid.AddThemeConstantOverride("h_separation", 16);
|
||||
_grid.AddThemeConstantOverride("v_separation", 16);
|
||||
_grid = new GridContainer { Columns = 1, SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_grid.AddThemeConstantOverride("v_separation", 12);
|
||||
AddChild(_grid);
|
||||
|
||||
Refresh();
|
||||
@@ -67,7 +66,7 @@ public partial class StepBackground : VBoxContainer, IStep
|
||||
bool selected = _draft.BackgroundId == bg.Id;
|
||||
|
||||
var card = CodexCard.Make();
|
||||
card.CustomMinimumSize = new Vector2(200, 0);
|
||||
card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
CodexCard.SetSelected(card, selected);
|
||||
|
||||
card.GuiInput += (InputEvent e) =>
|
||||
|
||||
@@ -47,9 +47,11 @@ public partial class StepClass : VBoxContainer, IStep
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
});
|
||||
|
||||
_grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
|
||||
_grid.AddThemeConstantOverride("h_separation", 16);
|
||||
_grid.AddThemeConstantOverride("v_separation", 16);
|
||||
// Single-column layout matches StepClade / StepSpecies — each card
|
||||
// spans the wizard's content width so the description text fits
|
||||
// comfortably and the calling's tone lands before the mechanics.
|
||||
_grid = new GridContainer { Columns = 1, SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_grid.AddThemeConstantOverride("v_separation", 12);
|
||||
AddChild(_grid);
|
||||
|
||||
Refresh();
|
||||
@@ -68,7 +70,7 @@ public partial class StepClass : VBoxContainer, IStep
|
||||
bool selected = _draft.ClassId == cls.Id;
|
||||
|
||||
var card = CodexCard.Make();
|
||||
card.CustomMinimumSize = new Vector2(200, 0);
|
||||
card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
CodexCard.SetSelected(card, selected);
|
||||
|
||||
card.GuiInput += (InputEvent e) =>
|
||||
@@ -97,6 +99,16 @@ public partial class StepClass : VBoxContainer, IStep
|
||||
ThemeTypeVariation = "CardMeta",
|
||||
});
|
||||
|
||||
if (!string.IsNullOrEmpty(cls.Description))
|
||||
{
|
||||
box.AddChild(new Label
|
||||
{
|
||||
Text = cls.Description,
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
MouseFilter = Control.MouseFilterEnum.Pass,
|
||||
});
|
||||
}
|
||||
|
||||
if (cls.Saves.Length > 0)
|
||||
{
|
||||
var savesRow = new HBoxContainer();
|
||||
|
||||
@@ -28,11 +28,6 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
private string _sirePicksBuiltFor = "";
|
||||
private string _damPicksBuiltFor = "";
|
||||
|
||||
// Lineage-axis variant picker for purebred path (Ram-Folk sheep/goat etc.).
|
||||
// Hybrid path embeds its own lineage picker into the per-parent col.
|
||||
private VBoxContainer _purebredVariantSection = null!;
|
||||
private string _purebredVariantBuiltFor = "";
|
||||
|
||||
public void Bind(CharacterDraft draft)
|
||||
{
|
||||
_draft = draft;
|
||||
@@ -68,12 +63,6 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
_purebredGrid = MakeGrid();
|
||||
_purebredSection.AddChild(_purebredGrid);
|
||||
|
||||
// Lineage picker (Ram-Folk sheep/goat). Visible only when the
|
||||
// selected species has VariantAxis == "lineage".
|
||||
_purebredVariantSection = new VBoxContainer { Visible = false };
|
||||
_purebredVariantSection.AddThemeConstantOverride("separation", 6);
|
||||
_purebredSection.AddChild(_purebredVariantSection);
|
||||
|
||||
_hybridSection = new VBoxContainer();
|
||||
_hybridSection.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(_hybridSection);
|
||||
@@ -141,111 +130,25 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshGrid(_purebredGrid, _draft.CladeId, _draft.SpeciesId,
|
||||
spId => OnPurebredSpeciesPicked(spId));
|
||||
SyncPurebredVariant();
|
||||
RefreshPurebredGrid();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPurebredSpeciesPicked(string speciesId)
|
||||
{
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ "species_id", speciesId },
|
||||
// Species swap invalidates lineage variant.
|
||||
{ "species_variant", "" },
|
||||
});
|
||||
}
|
||||
private void OnPurebredSpeciesPicked(string speciesId) =>
|
||||
_draft.Patch(new Godot.Collections.Dictionary { { "species_id", speciesId } });
|
||||
|
||||
private void OnLineageSpeciesPicked(string lineage, string speciesId)
|
||||
{
|
||||
private void OnLineageSpeciesPicked(string lineage, string speciesId) =>
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ lineage + "_species_id", speciesId },
|
||||
// Species swap invalidates the previously-picked species trait/detriment + variant.
|
||||
// Species swap invalidates the previously-picked trait/detriment.
|
||||
{ lineage + "_chosen_species_trait", "" },
|
||||
{ lineage + "_chosen_species_detriment", "" },
|
||||
{ lineage + "_species_variant", "" },
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Sync the purebred lineage picker row. Visible iff the
|
||||
/// picked species declares a lineage-axis variant.</summary>
|
||||
private void SyncPurebredVariant()
|
||||
{
|
||||
var sp = CodexContent.SpeciesById(_draft.SpeciesId);
|
||||
bool show = sp is not null && sp.VariantAxis == "lineage" && sp.Variants.Length > 0;
|
||||
_purebredVariantSection.Visible = show;
|
||||
if (!show)
|
||||
{
|
||||
_purebredVariantBuiltFor = "";
|
||||
foreach (var c in _purebredVariantSection.GetChildren()) c.Free();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_purebredVariantBuiltFor == _draft.SpeciesId)
|
||||
{
|
||||
// Same species — just update which lineage button is pressed.
|
||||
foreach (var child in _purebredVariantSection.GetChildren())
|
||||
{
|
||||
if (child is Button btn)
|
||||
{
|
||||
bool want = btn.Name == _draft.SpeciesVariant;
|
||||
if (btn.ButtonPressed != want) btn.SetPressedNoSignal(want);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var c in _purebredVariantSection.GetChildren()) c.Free();
|
||||
_purebredVariantBuiltFor = _draft.SpeciesId;
|
||||
|
||||
_purebredVariantSection.AddChild(new Label { Text = "LINEAGE", ThemeTypeVariation = "Eyebrow" });
|
||||
var row = new HBoxContainer();
|
||||
row.AddThemeConstantOverride("separation", 8);
|
||||
_purebredVariantSection.AddChild(row);
|
||||
foreach (var v in sp!.Variants)
|
||||
{
|
||||
string captured = v.Id;
|
||||
var btn = new Button
|
||||
{
|
||||
Text = v.Name,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = v.Id == _draft.SpeciesVariant,
|
||||
FocusMode = Control.FocusModeEnum.None,
|
||||
Name = v.Id,
|
||||
};
|
||||
var btnRef = btn;
|
||||
btn.Pressed += () =>
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ "species_variant", btnRef.ButtonPressed ? captured : "" },
|
||||
});
|
||||
// Hover popover summarizes the variant's traits + detriments.
|
||||
string capturedName = v.Name;
|
||||
string capturedDesc = SummarizeVariant(v);
|
||||
btn.MouseEntered += () =>
|
||||
PopoverLayer.Instance?.ShowFor(btnRef, capturedName, capturedDesc, "lineage", false);
|
||||
btn.MouseExited += () =>
|
||||
PopoverLayer.Instance?.ScheduleClose();
|
||||
row.AddChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
private static string SummarizeVariant(Theriapolis.Core.Data.SpeciesVariantDef v)
|
||||
{
|
||||
var parts = new System.Collections.Generic.List<string>();
|
||||
foreach (var t in v.Traits) parts.Add($"• {t.Name}: {t.Description}");
|
||||
foreach (var d in v.Detriments) parts.Add($"• {d.Name} (detriment): {d.Description}");
|
||||
return parts.Count == 0 ? "(no extra traits)" : string.Join("\n", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mutate-in-place sync for the species-pick column (one trait button
|
||||
/// group + one detriment button group, radio-style; plus a lineage
|
||||
/// picker when the species declares a lineage-axis variant). Same
|
||||
/// Free()-defer hazard as the bonus rows in StepClade — only rebuild
|
||||
/// when the species id changes.
|
||||
/// group + one detriment button group, radio-style).
|
||||
/// </summary>
|
||||
private void SyncSpeciesPicks(VBoxContainer col, ref string builtFor,
|
||||
string lineage, string speciesId,
|
||||
@@ -255,8 +158,6 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
{
|
||||
UpdateRadioGroup(col, "trait", chosenTrait);
|
||||
UpdateRadioGroup(col, "detriment", chosenDetriment);
|
||||
UpdateRadioGroup(col, "lineage",
|
||||
lineage == "sire" ? _draft.SireSpeciesVariant : _draft.DamSpeciesVariant);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -275,22 +176,6 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
return;
|
||||
}
|
||||
|
||||
// Lineage picker first when applicable, so the player picks
|
||||
// lineage before reading the trait/detriment list (variant
|
||||
// content layers on top).
|
||||
if (sp.VariantAxis == "lineage" && sp.Variants.Length > 0)
|
||||
{
|
||||
col.AddChild(new Label { Text = "Lineage", ThemeTypeVariation = "Eyebrow" });
|
||||
string currentVariant = lineage == "sire" ? _draft.SireSpeciesVariant : _draft.DamSpeciesVariant;
|
||||
BuildRadioGroup(col, "lineage", lineage, VariantsAsTraits(sp.Variants),
|
||||
currentVariant,
|
||||
(lin, id) => _draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ lin + "_species_variant", id },
|
||||
}),
|
||||
isDetriment: false);
|
||||
}
|
||||
|
||||
col.AddChild(new Label { Text = "Trait", ThemeTypeVariation = "Eyebrow" });
|
||||
BuildRadioGroup(col, "trait", lineage, sp.Traits, chosenTrait,
|
||||
(lin, id) => OnSpeciesPickToggled(lin, "trait", id), isDetriment: false);
|
||||
@@ -307,27 +192,6 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapter — BuildRadioGroup operates on TraitDef[]; project the variant
|
||||
/// list into TraitDef-shape so it can drive the same radio renderer.
|
||||
/// Description summarises the variant's contents for the hover popover.
|
||||
/// </summary>
|
||||
private static Theriapolis.Core.Data.TraitDef[] VariantsAsTraits(
|
||||
Theriapolis.Core.Data.SpeciesVariantDef[] variants)
|
||||
{
|
||||
var arr = new Theriapolis.Core.Data.TraitDef[variants.Length];
|
||||
for (int i = 0; i < variants.Length; i++)
|
||||
{
|
||||
arr[i] = new Theriapolis.Core.Data.TraitDef
|
||||
{
|
||||
Id = variants[i].Id,
|
||||
Name = variants[i].Name,
|
||||
Description = SummarizeVariant(variants[i]),
|
||||
};
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
private static void BuildRadioGroup(VBoxContainer parent, string kind, string lineage,
|
||||
Theriapolis.Core.Data.TraitDef[] options, string selected,
|
||||
System.Action<string, string> onPicked, bool isDetriment)
|
||||
@@ -389,12 +253,13 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
_draft.Patch(new Godot.Collections.Dictionary { { field, traitId } });
|
||||
}
|
||||
|
||||
private static void RefreshGrid(GridContainer grid, string cladeId, string selectedSpecies, System.Action<string> onClick)
|
||||
private void RefreshPurebredGrid()
|
||||
{
|
||||
foreach (var c in grid.GetChildren()) c.Free();
|
||||
if (string.IsNullOrEmpty(cladeId)) return;
|
||||
foreach (var sp in CodexContent.SpeciesOfClade(cladeId))
|
||||
grid.AddChild(BuildCard(sp, sp.Id == selectedSpecies, onClick));
|
||||
foreach (var c in _purebredGrid.GetChildren()) c.Free();
|
||||
if (string.IsNullOrEmpty(_draft.CladeId)) return;
|
||||
foreach (var sp in CodexContent.SpeciesOfClade(_draft.CladeId))
|
||||
_purebredGrid.AddChild(BuildCard(sp, sp.Id == _draft.SpeciesId,
|
||||
spId => OnPurebredSpeciesPicked(spId)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -402,9 +267,8 @@ public partial class StepSpecies : VBoxContainer, IStep
|
||||
/// 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.
|
||||
/// one lineage. Full rebuild on every Refresh is safe because Bind
|
||||
/// installs Refresh as a deferred callback.
|
||||
/// </summary>
|
||||
private void RefreshHybridGrid()
|
||||
{
|
||||
|
||||
@@ -50,9 +50,8 @@ public partial class StepSubclass : VBoxContainer, IStep
|
||||
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
||||
});
|
||||
|
||||
_grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
|
||||
_grid.AddThemeConstantOverride("h_separation", 16);
|
||||
_grid.AddThemeConstantOverride("v_separation", 16);
|
||||
_grid = new GridContainer { Columns = 1, SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_grid.AddThemeConstantOverride("v_separation", 12);
|
||||
AddChild(_grid);
|
||||
|
||||
Refresh();
|
||||
@@ -78,7 +77,7 @@ public partial class StepSubclass : VBoxContainer, IStep
|
||||
bool selected = _draft.SubclassId == sub.Id;
|
||||
|
||||
var card = CodexCard.Make();
|
||||
card.CustomMinimumSize = new Vector2(200, 0);
|
||||
card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
|
||||
CodexCard.SetSelected(card, selected);
|
||||
|
||||
card.GuiInput += (InputEvent e) =>
|
||||
|
||||
Reference in New Issue
Block a user