diff --git a/Content/Data/classes.json b/Content/Data/classes.json
index 02ef4d4..6797f4e 100644
--- a/Content/Data/classes.json
+++ b/Content/Data/classes.json
@@ -2,6 +2,7 @@
{
"id": "fangsworn",
"name": "Fangsworn",
+ "description": "\"I swore my fangs to something bigger than myself. Whether that was smart is a separate question.\"\n\nThe professional warrior. Trained combatants — soldiers, mercenaries, duelists, bodyguards, career fighters who turned violence into discipline. Fangsworn don't rely on rage or instinct; they rely on repetition, conditioning, and the brutal arithmetic of who hits harder and more often. Every army needs a backbone, and it's always been teeth.",
"hit_die": 10,
"primary_ability": ["STR", "DEX"],
"saves": ["STR", "CON"],
@@ -59,6 +60,7 @@
{
"id": "bulwark",
"name": "Bulwark",
+ "description": "\"Running is smart. Standing is stupid. I do it anyway, because someone behind me can't run.\"\n\nThe wall. The shield. The one who stays when everyone else runs. Bulwarks are defenders first — born from the herd-fighting traditions of prey Clades, where bovid wall-tactics, cervid herd-coordination, and leporid community-shielding fused into a single protective doctrine. They hold the line, they take the hit, and they make sure the people behind them get to live.",
"hit_die": 12,
"primary_ability": ["CON"],
"saves": ["CON", "CHA"],
@@ -118,6 +120,7 @@
{
"id": "feral",
"name": "Feral",
+ "description": "\"You think civilization fixed us? It just taught us to hold our breath.\"\n\nThe old brain. The one before words. Every sentient creature carries the Feral Age inside them — the pre-sapient animal, pure instinct. Most people keep that door locked. Ferals open it. The implications disturb everyone, especially prey-Clade Ferals whose ancestors survived by suppressing those exact urges; the rage of a hunted thing that has decided, today, to stop running is its own kind of terrible.",
"hit_die": 12,
"primary_ability": ["STR", "CON"],
"saves": ["STR", "CON"],
@@ -180,6 +183,7 @@
{
"id": "shadow_pelt",
"name": "Shadow-Pelt",
+ "description": "\"Everyone watches the fangs. Nobody watches the shadow behind the fangs.\"\n\nThieves, assassins, spies, scouts. Shadow-Pelts specialize in precision over force, infiltration over confrontation. The calling is cross-Clade by design — felid stealth, mustelid sinuousness, the Leporid art of being small enough to overlook. Any species that can move quietly and think two steps ahead can take this path. Most cities pretend they don't exist. Most cities are wrong.",
"hit_die": 8,
"primary_ability": ["DEX"],
"saves": ["DEX", "INT"],
@@ -243,6 +247,7 @@
{
"id": "scent_broker",
"name": "Scent-Broker",
+ "description": "\"You think information is what you hear? What you read? Amateur. Information is what you smell when someone walks into the room. Fear. Lies. Lust. Disease. Lineage. Every creature broadcasts their secrets with every breath. I just learned to listen.\"\n\nA calling unique to Theriapolis. Scent-Brokers are intelligence operatives, diplomats, perfume-mages — readers of pheromones and emotional scent. They weaponize the gap between what people say and what their bodies tell on them. Half merchant, half spy, all nose.",
"hit_die": 8,
"primary_ability": ["WIS"],
"saves": ["WIS", "CHA"],
@@ -302,6 +307,7 @@
},
{
"id": "covenant_keeper",
+ "description": "\"The Covenant says we don't eat the speaking ones. I'm here to make sure everyone remembers.\"\n\nThe oath made flesh. The law with teeth. Covenant-Keepers are part judge, part priest, part enforcer — their authority comes from the Covenant of Claws, the sacred-legal compact that defines sentience, prohibits cannibalism between Clades, and binds civilization together. Where the Covenant is honored, they are diplomats and arbiters. Where it is broken, they are the answer.",
"name": "Covenant-Keeper",
"hit_die": 10,
"primary_ability": ["CHA"],
@@ -362,6 +368,7 @@
},
{
"id": "muzzle_speaker",
+ "description": "\"The first howl that meant something other than hunger — that was the beginning of everything. I'm what happens when you take that gift seriously.\"\n\nVoice is power, always has been. Muzzle-Speakers are bards evolved for a world where vocalization varies wildly by Clade and subsonic communication is ancestral. They use voice, cadence, rhythm, and inter-species emotive frequencies to inspire, manipulate, heal, and harm. Songs that calm a stampeding herd, war-howls that rally a fractured pack, lullabies sung in registers older than language.",
"name": "Muzzle-Speaker",
"hit_die": 8,
"primary_ability": ["CHA"],
@@ -424,6 +431,7 @@
},
{
"id": "claw_wright",
+ "description": "\"Your body doesn't fit the chair? I'll build you a better chair. Your paws can't hold the scalpel? I'll build you a better scalpel. The world wasn't designed for you? I'll redesign the world.\"\n\nThe paw that builds. Engineers, inventors, adaptive-technology specialists. In a world of body diversity — paws, hooves, talons, beaks, dewclaws, prehensile tails — generalist designers are invaluable. Half mechanical, half medical, all problem-solver. They work in armories, workshops, prosthetics labs, and the back rooms of underground hybrid clinics.",
"name": "Claw-Wright",
"hit_die": 8,
"primary_ability": ["INT"],
diff --git a/Content/Data/species.json b/Content/Data/species.json
index 0a08fed..431830e 100644
--- a/Content/Data/species.json
+++ b/Content/Data/species.json
@@ -63,12 +63,30 @@
"base_speed_ft": 30,
"traits": [
{ "id": "commanding_presence", "name": "Commanding Presence", "description": "Proficiency in Intimidation (expertise if already proficient). When intimidating, may roar — creatures within 15 ft. who hear it make a WIS save (DC = 8 + prof + CHA) or are frightened until end of next turn. Once per short rest." },
- { "id": "pride_fighter", "name": "Pride Fighter", "description": "Lion-folk can both grant and benefit from flanking. When you and an ally are adjacent to the same enemy, both gain +2 to attack rolls against that enemy." },
- { "id": "mane_guard", "name": "Mane Guard", "description": "+1 AC against attacks targeting the neck or throat." }
+ { "id": "pride_fighter", "name": "Pride Fighter", "description": "Lion-folk can both grant and benefit from flanking. When you and an ally are adjacent to the same enemy, both gain +2 to attack rolls against that enemy." }
],
"detriments": [
{ "id": "territorial_ego", "name": "Territorial Ego", "description": "Disadvantage on CHA (Persuasion) when negotiating shared resources, territory, or leadership positions." },
{ "id": "heat_lethargy", "name": "Heat Lethargy", "description": "In temperatures above 90°F, CON save (DC 10) every hour of strenuous activity or gain a level of exhaustion." }
+ ],
+ "variant_axis": "sex",
+ "variants": [
+ {
+ "id": "male",
+ "name": "Maned",
+ "traits": [
+ { "id": "mane_guard", "name": "Mane Guard", "description": "+1 AC against attacks targeting the neck or throat. The mane is armor that grew there on its own." }
+ ],
+ "detriments": []
+ },
+ {
+ "id": "female",
+ "name": "Maneless",
+ "traits": [
+ { "id": "huntress_reflexes", "name": "Huntress Reflexes", "description": "Base speed +5 ft. Advantage on initiative rolls. Lionesses do the hunting in the pride for a reason." }
+ ],
+ "detriments": []
+ }
]
},
{
@@ -204,12 +222,34 @@
"description": "\"Herd-builders, wall-builders, civilization-builders. The hooves that stamped order into chaos.\"\n\nTall and long-legged, powerful through the haunches and chest. Tawny brown body, darker neck, pale rump patch. Males grow impressive branching antlers — broad, branching, shed and regrown annually — used in display, defense, and cultural adornment.",
"size": "medium_large",
"ability_mods": { "STR": 1 },
- "base_speed_ft": 30,
+ "base_speed_ft": 40,
"traits": [
- { "id": "majestic_antlers", "name": "Majestic Antlers", "description": "Antler attack deals 1d8 + STR piercing. Charging attack: if you move at least 20 ft. straight before attacking, deal +1d6 damage and target makes a STR save (DC = 8 + prof + STR) or is knocked back 5 ft." }
+ { "id": "herd_coordination", "name": "Herd Coordination", "description": "When you take the Help action to assist an ally's check or attack, the ally gains a +3 bonus instead of advantage. Cervidae herd instinct made tactical." },
+ { "id": "endurance_runner", "name": "Endurance Runner", "description": "Base speed 40 ft. Advantage on CON saves against forced march, exhaustion from prolonged movement, and effects that would slow your pace on open ground." }
],
"detriments": [
- { "id": "antler_drag", "name": "Antler Drag", "description": "During antler-shed season (1 month per year), antlers fall off — antler attack damage reduced by 1 die step until they regrow." }
+ { "id": "herd_instinct", "name": "Herd Instinct", "description": "When an allied creature within 30 ft. takes the Dash action to flee combat, WIS save (DC 12) or use your reaction to Dash in the same direction. The herd moves together — even when only some of it should." }
+ ],
+ "variant_axis": "sex",
+ "variants": [
+ {
+ "id": "male",
+ "name": "Bull",
+ "traits": [
+ { "id": "antler_combat", "name": "Antler Combat", "description": "Antler attack deals 1d8 + STR piercing. On a hit, you may forgo damage to shove the target 5 ft. While the full rack is grown (outside antler-shed season), antler attacks have reach 10 ft." }
+ ],
+ "detriments": [
+ { "id": "antler_drag", "name": "Antler Drag", "description": "During antler-shed season (1 month per year), antlers fall off — antler attack damage reduced by 1 die step and the 10 ft. reach is lost until they regrow." }
+ ]
+ },
+ {
+ "id": "female",
+ "name": "Cow",
+ "traits": [
+ { "id": "kick", "name": "Kick", "description": "Hooved kick attack deals 1d8 + STR bludgeoning. On a critical hit, the target is knocked prone. The herd's other answer to threats." }
+ ],
+ "detriments": []
+ }
]
},
{
@@ -300,17 +340,35 @@
]
},
{
- "id": "ram",
+ "id": "sheep",
"clade_id": "bovidae",
- "name": "Ram-Folk",
- "description": "\"Climbers, thinkers, and the reason every mountain fortress in the world was built by someone with horns.\"\n\nStocky and compact, lower center of gravity than bull-folk — powerful especially through the hindquarters. Wool (sheep lineage) or coarse hair (goat). Spiral or sweeping horns, cloven hooves, horizontal-slit pupils — distinctive and unsettling to other Clades.",
+ "name": "Sheep-Folk",
+ "description": "\"We climb because the mountain asked us to. We grow wool because the wind asked us to. We come back, again and again, because that's what the herd is for.\"\n\nStocky and compact, lower center of gravity than bull-folk — powerful especially through the hindquarters. Heavy fleece, tightly curled and lanolin-rich, naturally weather-shedding. Spiral or sweeping horns, cloven hooves, horizontal-slit pupils. Sheep-folk built the high pastures and the wool trade that supports half the world's textile economy.",
"size": "medium",
"ability_mods": { "WIS": 1 },
"base_speed_ft": 30,
"traits": [
{ "id": "mountain_born", "name": "Mountain Born", "description": "Climb speed equal to walking speed. Immune to altitude sickness. Advantage on DEX checks and saves on narrow, unstable, or steep surfaces." },
{ "id": "headbutt", "name": "Headbutt", "description": "Horn attack deals 1d10 + STR when using Charge (20-ft. run-up). Target hit must make a CON save (DC = 8 + prof + STR) or be dazed (disadvantage on next attack roll)." },
- { "id": "wool_insulation", "name": "Wool Insulation", "description": "Resistance to cold damage. Advantage on saves against cold environments." }
+ { "id": "wool_insulation", "name": "Wool Insulation", "description": "Resistance to cold damage. Advantage on saves against cold environments. The fleece does what the fleece is for." }
+ ],
+ "detriments": [
+ { "id": "horizontal_pupils", "name": "Horizontal Pupils", "description": "Disadvantage on Perception checks requiring depth perception at distances greater than 60 ft." },
+ { "id": "herd_mentality", "name": "Herd Mentality", "description": "When 3+ visible allies are moving in a direction, WIS save (DC 10) or feel compelled to move with them." }
+ ]
+ },
+ {
+ "id": "goat",
+ "clade_id": "bovidae",
+ "name": "Goat-Folk",
+ "description": "\"Yes, I can stand on that. Yes, I can eat that. Yes, I am going to.\"\n\nLeaner and more angular than sheep-folk, athletic through the shoulder, with the same low center of gravity. Coarse hair instead of wool. Curving horns swept back. Horizontal-slit pupils. Goat-folk thrive where nothing else can — desert mesas, cliff edges, salt marshes, the upper slopes where the air thins and the rocks don't hold.",
+ "size": "medium",
+ "ability_mods": { "WIS": 1 },
+ "base_speed_ft": 30,
+ "traits": [
+ { "id": "mountain_born", "name": "Mountain Born", "description": "Climb speed equal to walking speed. Immune to altitude sickness. Advantage on DEX checks and saves on narrow, unstable, or steep surfaces." },
+ { "id": "headbutt", "name": "Headbutt", "description": "Horn attack deals 1d10 + STR when using Charge (20-ft. run-up). Target hit must make a CON save (DC = 8 + prof + STR) or be dazed (disadvantage on next attack roll)." },
+ { "id": "stubborn_metabolism", "name": "Stubborn Metabolism", "description": "Subsist on half normal rations. Advantage on CON saves against ingested poisons, spoiled food, and unusual environmental contaminants. Goat-line digestive systems are infamous for a reason." }
],
"detriments": [
{ "id": "horizontal_pupils", "name": "Horizontal Pupils", "description": "Disadvantage on Perception checks requiring depth perception at distances greater than 60 ft." },
diff --git a/Theriapolis.Core/Data/ClassDef.cs b/Theriapolis.Core/Data/ClassDef.cs
index f0ec95c..27caa88 100644
--- a/Theriapolis.Core/Data/ClassDef.cs
+++ b/Theriapolis.Core/Data/ClassDef.cs
@@ -16,6 +16,12 @@ public sealed record ClassDef
[JsonPropertyName("name")]
public string Name { get; init; } = "";
+ /// Codex-voice calling description: in-character quote followed
+ /// by a one-paragraph profile (mirrors CladeDef / SpeciesDef). Surfaced
+ /// on the Step III calling card.
+ [JsonPropertyName("description")]
+ public string Description { get; init; } = "";
+
/// Hit die size: 6 / 8 / 10 / 12.
[JsonPropertyName("hit_die")]
public int HitDie { get; init; } = 8;
diff --git a/Theriapolis.Core/Rules/Stats/Size.cs b/Theriapolis.Core/Rules/Stats/Size.cs
index 7444cfa..468b2e7 100644
--- a/Theriapolis.Core/Rules/Stats/Size.cs
+++ b/Theriapolis.Core/Rules/Stats/Size.cs
@@ -8,7 +8,7 @@ public enum SizeCategory : byte
{
Tiny = 0, // reserved; no Phase 5 species uses this
Small = 1, // rabbit-folk, housecat-folk, ferret-folk
- Medium = 2, // fox-folk, deer-folk, ram-folk, leopard-folk, badger-folk, hare-folk
+ Medium = 2, // fox-folk, deer-folk, sheep-folk, goat-folk, leopard-folk, badger-folk, hare-folk
MediumLarge = 3, // wolf-folk, elk-folk, lion-folk, wolverine-folk, coyote-folk
Large = 4, // brown bear-folk, polar bear-folk, moose-folk, bull-folk, bison-folk
Huge = 5, // reserved; no Phase 5 species uses this
diff --git a/Theriapolis.Godot/Scenes/Steps/StepBackground.cs b/Theriapolis.Godot/Scenes/Steps/StepBackground.cs
index 569ef04..3c54a97 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepBackground.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepBackground.cs
@@ -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) =>
diff --git a/Theriapolis.Godot/Scenes/Steps/StepClass.cs b/Theriapolis.Godot/Scenes/Steps/StepClass.cs
index cc52c27..84c63d4 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepClass.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepClass.cs
@@ -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();
diff --git a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
index 865d2d9..52a3d9e 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
@@ -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", "" },
});
- }
-
- /// Sync the purebred lineage picker row. Visible iff the
- /// picked species declares a lineage-axis variant.
- 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();
- 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);
- }
///
/// 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).
///
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
}
}
- ///
- /// 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.
- ///
- 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 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 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)));
}
///
@@ -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.
///
private void RefreshHybridGrid()
{
diff --git a/Theriapolis.Godot/Scenes/Steps/StepSubclass.cs b/Theriapolis.Godot/Scenes/Steps/StepSubclass.cs
index b24a945..bc05a34 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepSubclass.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepSubclass.cs
@@ -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) =>
diff --git a/Theriapolis.Tests/Data/ContentLoadTests.cs b/Theriapolis.Tests/Data/ContentLoadTests.cs
index fa15877..89bd19d 100644
--- a/Theriapolis.Tests/Data/ContentLoadTests.cs
+++ b/Theriapolis.Tests/Data/ContentLoadTests.cs
@@ -113,7 +113,8 @@ public sealed class ContentLoadTests
[InlineData("rabbit", -1, 2, 0, 0, 1, 0)]
[InlineData("hare", -1, 2, 1, 0, 0, 0)]
[InlineData("bull", 2, 0, 1, 0, 0, 0)]
- [InlineData("ram", 1, 0, 1, 0, 1, 0)]
+ [InlineData("sheep", 1, 0, 1, 0, 1, 0)]
+ [InlineData("goat", 1, 0, 1, 0, 1, 0)]
[InlineData("bison", 1, 0, 2, 0, 0, 0)]
public void Clade_Plus_Species_AbilityMods_MatchQuickRefTable(
string speciesId, int str, int dex, int con, int @int, int wis, int cha)
diff --git a/Theriapolis.Tests/Rules/HybridCharacterTests.cs b/Theriapolis.Tests/Rules/HybridCharacterTests.cs
index 43b7c02..81d3494 100644
--- a/Theriapolis.Tests/Rules/HybridCharacterTests.cs
+++ b/Theriapolis.Tests/Rules/HybridCharacterTests.cs
@@ -124,7 +124,7 @@ public sealed class HybridCharacterTests
[InlineData("ursidae", "brown_bear", "bovidae", "bull")]
[InlineData("felidae", "leopard", "cervidae", "deer")]
[InlineData("mustelidae","badger", "leporidae", "rabbit")]
- [InlineData("bovidae", "ram", "cervidae", "elk")]
+ [InlineData("bovidae", "sheep", "cervidae", "elk")]
[InlineData("leporidae", "rabbit", "felidae", "housecat")]
public void TryBuildHybrid_AllCrossCladeCombinationsValid(
string sireClade, string sireSpecies, string damClade, string damSpecies)