diff --git a/Theriapolis.Godot/Scenes/Aside.cs b/Theriapolis.Godot/Scenes/Aside.cs
index a7553e0..97e8859 100644
--- a/Theriapolis.Godot/Scenes/Aside.cs
+++ b/Theriapolis.Godot/Scenes/Aside.cs
@@ -259,14 +259,21 @@ public partial class Aside : MarginContainer
AddCladeTraits(flow, CodexContent.Clade(_draft.CladeId));
}
- // Species traits.
- var sp = CodexContent.SpeciesById(_draft.EffectiveSpeciesId);
- if (sp is not null)
+ // Species traits — purebred uses the single species; hybrids show
+ // BOTH parent species per theriapolis-rpg-clades.md ("Choose ONE
+ // species trait from each parent").
+ if (_draft.IsHybrid)
{
- foreach (var t in sp.Traits)
- flow.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description });
- foreach (var d in sp.Detriments)
- flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
+ AddSpeciesTraits(flow, CodexContent.SpeciesById(_draft.SireSpeciesId));
+ AddSpeciesTraits(flow, CodexContent.SpeciesById(_draft.DamSpeciesId));
+
+ // Universal hybrid detriments — every hybrid has all four.
+ foreach (var (name, desc) in UniversalHybridDetriments)
+ flow.AddChild(new TraitChip { TraitName = name, Description = desc, Detriment = true });
+ }
+ else
+ {
+ AddSpeciesTraits(flow, CodexContent.SpeciesById(_draft.SpeciesId));
}
// Class level-1 features.
@@ -321,6 +328,43 @@ public partial class Aside : MarginContainer
flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
}
+ private static void AddSpeciesTraits(HFlowContainer flow, Theriapolis.Core.Data.SpeciesDef? species)
+ {
+ if (species is null) return;
+ foreach (var t in species.Traits)
+ flow.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description });
+ foreach (var d in species.Detriments)
+ flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
+ }
+
+ ///
+ /// Hardcoded from theriapolis-rpg-clades.md "Universal Hybrid Detriments"
+ /// (lines 749-753). Every hybrid character has all four. The schema
+ /// doesn't carry these yet — they live in the clades doc as canonical
+ /// rules text.
+ ///
+ private static readonly (string Name, string Description)[] UniversalHybridDetriments =
+ {
+ ("Scent Dysphoria",
+ "Your scent broadcasts conflicting Clade signals. Creatures with scent ability "
+ + "who detect you must make a WIS save (DC 10) or experience instinctive unease, "
+ + "imposing disadvantage on their first CHA check with you. Scent-masking products "
+ + "can suppress this for 1d4 hours but fail under stress."),
+ ("Illegible Body Language",
+ "Your tail and ear movements blend two Clade grammars. Disadvantage on nonverbal "
+ + "communication checks (CHA checks relying on body language) with purebred creatures. "
+ + "Other hybrids read you normally."),
+ ("Social Stigma",
+ "In non-progressive settlements, disadvantage on CHA checks with strangers and on "
+ + "checks to secure housing, employment, or official services. Even in progressive "
+ + "areas, the first CHA check with any new NPC is made at -2 until you've established "
+ + "rapport."),
+ ("Medical Incompatibility",
+ "Healing potions and magical healing function at 75% effectiveness (round down). "
+ + "Medical treatment calibrated for purebred physiologies works imperfectly on your "
+ + "blended body. Hybrid-specific medicine exists but is expensive and rare."),
+ };
+
private static void AddSkillChip(HFlowContainer flow, string skillId, string tag)
{
var s = SkillsCatalog.Get(skillId);
diff --git a/Theriapolis.Godot/Scenes/Steps/StepClade.cs b/Theriapolis.Godot/Scenes/Steps/StepClade.cs
index 779df89..e0c5458 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepClade.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepClade.cs
@@ -26,10 +26,25 @@ public partial class StepClade : VBoxContainer, IStep
private VBoxContainer _hybridSection = null!;
private OptionButton _dominantToggle = null!;
+ // Lineage-bonus pickers (per theriapolis-rpg-clades.md hybrid rules,
+ // simplified to allow stacking on the same ability).
+ private VBoxContainer _bonusSection = null!;
+ private HBoxContainer _sireBonusRow = null!;
+ private HBoxContainer _damBonusRow = null!;
+
private readonly Dictionary _purebredCards = new();
private readonly Dictionary _sireCards = new();
private readonly Dictionary _damCards = new();
+ // Bonus rows are mutated in place too: when the clade hasn't changed
+ // since the last build, we only flip ButtonPressed states. Rebuilding
+ // would Free() the very button whose Pressed callback we're inside,
+ // which Godot defers — leaving stale buttons next to the new ones.
+ private readonly Dictionary _sireBonusButtons = new();
+ private readonly Dictionary _damBonusButtons = new();
+ private string _sireBonusBuiltFor = "";
+ private string _damBonusBuiltFor = "";
+
public void Bind(CharacterDraft draft)
{
_draft = draft;
@@ -86,6 +101,21 @@ public partial class StepClade : VBoxContainer, IStep
_hybridSection.AddChild(damGrid);
PopulateGrid(damGrid, _damCards, id => OnLineageCladePicked("dam", id));
+ // Lineage bonus pickers — hybrids pick ONE ability mod from each
+ // parent clade. Stacking on the same ability is allowed; mods sum.
+ _bonusSection = new VBoxContainer();
+ _bonusSection.AddThemeConstantOverride("separation", 8);
+ _hybridSection.AddChild(_bonusSection);
+ _bonusSection.AddChild(new Label { Text = "LINEAGE BONUSES" });
+
+ _sireBonusRow = new HBoxContainer();
+ _sireBonusRow.AddThemeConstantOverride("separation", 8);
+ _bonusSection.AddChild(_sireBonusRow);
+
+ _damBonusRow = new HBoxContainer();
+ _damBonusRow.AddThemeConstantOverride("separation", 8);
+ _bonusSection.AddChild(_damBonusRow);
+
var dominantRow = new HBoxContainer();
dominantRow.AddThemeConstantOverride("separation", 8);
_hybridSection.AddChild(dominantRow);
@@ -131,6 +161,8 @@ public partial class StepClade : VBoxContainer, IStep
patch["sire_species_id"] = "";
patch["dam_clade_id"] = "";
patch["dam_species_id"] = "";
+ patch["sire_chosen_ability"] = "";
+ patch["dam_chosen_ability"] = "";
}
ClearBackgroundIfInvalidated(patch);
_draft.Patch(patch);
@@ -159,6 +191,63 @@ public partial class StepClade : VBoxContainer, IStep
int dominantIdx = _draft.DominantParent == "dam" ? 1 : 0;
if (_dominantToggle.Selected != dominantIdx) _dominantToggle.Select(dominantIdx);
+
+ if (hybrid) RebuildBonusPickers();
+ }
+
+ ///
+ /// Syncs the lineage-bonus button rows with the current sire/dam
+ /// clade picks. When the clade hasn't changed since the last build
+ /// we only update each button's ButtonPressed state — see field
+ /// comment above for why a full rebuild here is unsafe.
+ ///
+ private void RebuildBonusPickers()
+ {
+ SyncLineageBonusRow(_sireBonusRow, _sireBonusButtons, ref _sireBonusBuiltFor, "sire",
+ _draft.SireCladeId, _draft.SireChosenAbility);
+ SyncLineageBonusRow(_damBonusRow, _damBonusButtons, ref _damBonusBuiltFor, "dam",
+ _draft.DamCladeId, _draft.DamChosenAbility);
+ }
+
+ private void SyncLineageBonusRow(HBoxContainer row, Dictionary cache,
+ ref string builtFor, string lineage,
+ string cladeId, string chosen)
+ {
+ if (cladeId == builtFor)
+ {
+ foreach (var (ab, btn) in cache)
+ {
+ bool want = ab == chosen;
+ if (btn.ButtonPressed != want) btn.SetPressedNoSignal(want);
+ }
+ return;
+ }
+
+ foreach (var c in row.GetChildren()) c.Free();
+ cache.Clear();
+ builtFor = cladeId;
+
+ var clade = CodexContent.Clade(cladeId);
+ if (clade is null)
+ {
+ row.AddChild(new Label { Text = "(pick a clade above)" });
+ return;
+ }
+
+ foreach (var (ab, value) in clade.AbilityMods)
+ {
+ string captured = ab;
+ var btn = new Button
+ {
+ Text = $"{ab} {(value >= 0 ? "+" : "")}{value}",
+ ToggleMode = true,
+ ButtonPressed = ab == chosen,
+ FocusMode = Control.FocusModeEnum.None,
+ };
+ btn.Pressed += () => OnLineageBonusPicked(lineage, captured);
+ row.AddChild(btn);
+ cache[ab] = btn;
+ }
}
private static void UpdateSelection(Dictionary cards, string selectedId)
@@ -192,10 +281,23 @@ public partial class StepClade : VBoxContainer, IStep
var sp = CodexContent.SpeciesById(currentSpecies);
if (sp is null || !string.Equals(sp.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase))
patch[lineage + "_species_id"] = "";
+
+ // Clade swap invalidates the previously-picked lineage bonus
+ // (different clade has different mod options).
+ patch[lineage + "_chosen_ability"] = "";
+
ClearBackgroundIfInvalidated(patch);
_draft.Patch(patch);
}
+ private void OnLineageBonusPicked(string lineage, string ability)
+ {
+ _draft.Patch(new Godot.Collections.Dictionary
+ {
+ { lineage + "_chosen_ability", ability },
+ });
+ }
+
///
/// Clade changes can make a previously-valid background unavailable
/// (e.g. picking Felidae while Pack-Raised is selected). Build the
diff --git a/Theriapolis.Godot/Scenes/Steps/StepReview.cs b/Theriapolis.Godot/Scenes/Steps/StepReview.cs
new file mode 100644
index 0000000..83f5077
--- /dev/null
+++ b/Theriapolis.Godot/Scenes/Steps/StepReview.cs
@@ -0,0 +1,160 @@
+using Godot;
+using Theriapolis.GodotHost.UI;
+
+namespace Theriapolis.GodotHost.Scenes.Steps;
+
+///
+/// Step VIII — Sign. Direct port of StepReview in
+/// src/steps.jsx: name entry plus the Confirm button that
+/// composes the final character and emits the handoff signal per
+/// GODOT_PORTING_GUIDE.md §11.
+///
+/// The full per-step review already lives in the Aside summary, so
+/// this step focuses on what's new at this stage: the name input
+/// and the Confirm action. Bottom of the page mirrors the React
+/// prototype — single big primary button.
+///
+public partial class StepReview : VBoxContainer, IStep
+{
+ /// Fired when Confirm is pressed and every step validates.
+ /// The parent Wizard catches this and persists the draft + advances
+ /// the harness; the rest of the game-side handoff lands in a future
+ /// milestone (intro scene, world load, etc.).
+ [Signal] public delegate void CharacterConfirmedEventHandler(CharacterDraft draft);
+
+ private CharacterDraft _draft = null!;
+ private LineEdit _nameField = null!;
+ private Button _confirmBtn = null!;
+ private Label _confirmStatus = null!;
+
+ public void Bind(CharacterDraft draft)
+ {
+ _draft = draft;
+ _draft.Changed += () => Callable.From(Refresh).CallDeferred();
+ Build();
+ }
+
+ public string? Validate() => WizardValidation.Validate(7, _draft);
+
+ private void Build()
+ {
+ AddThemeConstantOverride("separation", 18);
+
+ var intro = new VBoxContainer();
+ intro.AddThemeConstantOverride("separation", 6);
+ AddChild(intro);
+ intro.AddChild(new Label { Text = "FOLIO VIII · SIGN" });
+ intro.AddChild(new Label { Text = "Sign the Codex" });
+ intro.AddChild(new Label
+ {
+ Text = "Review the right-rail summary, then sign your name. "
+ + "The name you sign here is the one the world will speak.",
+ AutowrapMode = TextServer.AutowrapMode.WordSmart,
+ });
+
+ // Name field.
+ var nameBlock = new VBoxContainer();
+ nameBlock.AddThemeConstantOverride("separation", 6);
+ AddChild(nameBlock);
+ nameBlock.AddChild(new Label { Text = "NAME" });
+ _nameField = new LineEdit
+ {
+ PlaceholderText = "Enter your character's name...",
+ Text = _draft.CharacterName,
+ CustomMinimumSize = new Vector2(360, 0),
+ };
+ _nameField.TextChanged += OnNameChanged;
+ nameBlock.AddChild(_nameField);
+
+ // Confirm action — disabled until every step validates.
+ var actionBlock = new VBoxContainer();
+ actionBlock.AddThemeConstantOverride("separation", 8);
+ AddChild(actionBlock);
+
+ _confirmStatus = new Label
+ {
+ Text = "",
+ AutowrapMode = TextServer.AutowrapMode.WordSmart,
+ };
+ actionBlock.AddChild(_confirmStatus);
+
+ _confirmBtn = new Button
+ {
+ Text = "Confirm & Begin",
+ CustomMinimumSize = new Vector2(220, 0),
+ };
+ _confirmBtn.Pressed += OnConfirmPressed;
+ actionBlock.AddChild(_confirmBtn);
+
+ Refresh();
+ }
+
+ private void OnNameChanged(string newText)
+ {
+ // Patch only when the field actually differs from the draft —
+ // otherwise the Changed signal would refresh us in a loop.
+ if (newText == _draft.CharacterName) return;
+ _draft.Patch(new Godot.Collections.Dictionary { { "character_name", newText } });
+ }
+
+ private void Refresh()
+ {
+ if (_nameField is null) return;
+ // Sync the field if external state changed without going through
+ // the LineEdit (e.g. loading a saved draft someday).
+ if (_nameField.Text != _draft.CharacterName)
+ _nameField.Text = _draft.CharacterName;
+
+ int firstUnmet = WizardValidation.FirstIncomplete(_draft);
+ bool allValid = firstUnmet == -1;
+ _confirmBtn.Disabled = !allValid;
+ if (allValid)
+ {
+ _confirmStatus.Text = $"Ready: {_draft.CharacterName} stands at the threshold.";
+ }
+ else if (firstUnmet == 7)
+ {
+ _confirmStatus.Text = "Enter a name to sign.";
+ }
+ else
+ {
+ _confirmStatus.Text = $"Some folios remain — see Folio {Roman(firstUnmet + 1)}.";
+ }
+ }
+
+ private void OnConfirmPressed()
+ {
+ if (WizardValidation.FirstIncomplete(_draft) != -1) return;
+
+ // Persist the draft so a future load path can pick it up.
+ const string SavePath = "user://character.tres";
+ var err = ResourceSaver.Save(_draft, SavePath);
+ if (err != Error.Ok)
+ GD.PushWarning($"[review] ResourceSaver.Save failed: {err}");
+ else
+ GD.Print($"[review] Saved character draft to {SavePath}");
+
+ GD.Print($"[review] Confirmed: {Summarise(_draft)}");
+
+ EmitSignal(SignalName.CharacterConfirmed, _draft);
+
+ _confirmBtn.Disabled = true;
+ _confirmStatus.Text = $"{_draft.CharacterName} steps into Theriapolis.";
+ }
+
+ private static string Summarise(CharacterDraft d)
+ {
+ string lineage = d.IsHybrid
+ ? $"hybrid {d.SireCladeId}/{d.SireSpeciesId} × {d.DamCladeId}/{d.DamSpeciesId} (dom={d.DominantParent})"
+ : $"{d.CladeId}/{d.SpeciesId}";
+ return $"{d.CharacterName} — {lineage}, {d.ClassId}/{d.SubclassId}, bg={d.BackgroundId}, "
+ + $"skills=[{string.Join(",", d.ChosenSkills)}]";
+ }
+
+ private static string Roman(int n) => n switch
+ {
+ 1 => "I", 2 => "II", 3 => "III", 4 => "IV",
+ 5 => "V", 6 => "VI", 7 => "VII", 8 => "VIII",
+ _ => n.ToString(),
+ };
+}
diff --git a/Theriapolis.Godot/Scenes/Steps/StepSkills.cs b/Theriapolis.Godot/Scenes/Steps/StepSkills.cs
index 72873c4..a6e607d 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepSkills.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepSkills.cs
@@ -23,7 +23,6 @@ public partial class StepSkills : VBoxContainer, IStep
{
private CharacterDraft _draft = null!;
private Label _countLabel = null!;
- private Label _classBgLabel = null!;
private GridContainer _groupsGrid = null!;
public void Bind(CharacterDraft draft)
@@ -52,15 +51,8 @@ public partial class StepSkills : VBoxContainer, IStep
AutowrapMode = TextServer.AutowrapMode.WordSmart,
});
- var meta = new HBoxContainer();
- meta.AddThemeConstantOverride("separation", 24);
- AddChild(meta);
_countLabel = new Label { Text = "0 / 0 chosen" };
- meta.AddChild(_countLabel);
- var spacer = new Control { SizeFlagsHorizontal = SizeFlags.ExpandFill };
- meta.AddChild(spacer);
- _classBgLabel = new Label { Text = "" };
- meta.AddChild(_classBgLabel);
+ AddChild(_countLabel);
_groupsGrid = new GridContainer
{
@@ -86,9 +78,9 @@ public partial class StepSkills : VBoxContainer, IStep
var classOptions = new HashSet(cls?.SkillOptions ?? System.Array.Empty());
var chosen = new HashSet(_draft.ChosenSkills);
- _countLabel.Text = $"{chosen.Count} / {required} chosen +{lockedFromBg.Count} sealed by background";
- _classBgLabel.Text = (cls is null ? "Pick a calling first." :
- $"Calling: {cls.Name} · History: {bg?.Name ?? "—"}").ToUpperInvariant();
+ _countLabel.Text = cls is null
+ ? "Pick a calling first."
+ : $"{chosen.Count} / {required} chosen +{lockedFromBg.Count} sealed by background";
foreach (var c in _groupsGrid.GetChildren()) c.QueueFree();
diff --git a/Theriapolis.Godot/Scenes/Wizard.cs b/Theriapolis.Godot/Scenes/Wizard.cs
index c18823f..e818803 100644
--- a/Theriapolis.Godot/Scenes/Wizard.cs
+++ b/Theriapolis.Godot/Scenes/Wizard.cs
@@ -51,7 +51,7 @@ public partial class Wizard : Control
typeof(Steps.StepBackground), // 4 History
typeof(Steps.StepStats), // 5 Abilities
typeof(Steps.StepSkills), // 6 Skills
- null, // 7 Sign — M6.6
+ typeof(Steps.StepReview), // 7 Sign
};
public override void _Ready()
diff --git a/Theriapolis.Godot/UI/AbilityCalc.cs b/Theriapolis.Godot/UI/AbilityCalc.cs
index bf8b79b..393eb0c 100644
--- a/Theriapolis.Godot/UI/AbilityCalc.cs
+++ b/Theriapolis.Godot/UI/AbilityCalc.cs
@@ -24,20 +24,35 @@ public static class AbilityCalc
var list = new List();
if (draft.IsHybrid)
{
- AddCladeSource(list, ability, CodexContent.Clade(draft.SireCladeId), " (sire)");
- AddCladeSource(list, ability, CodexContent.Clade(draft.DamCladeId), " (dam)");
+ // Hybrids: take ONE ability modifier from each parent clade.
+ // Picks stack if they happen to land on the same ability — the
+ // original "no stack, take +1 + free elsewhere" rule was
+ // dropped per project decision. Species mods don't apply.
+ if (draft.SireChosenAbility == ability)
+ AddCladeChoice(list, ability, CodexContent.Clade(draft.SireCladeId), " (sire)");
+ if (draft.DamChosenAbility == ability)
+ AddCladeChoice(list, ability, CodexContent.Clade(draft.DamCladeId), " (dam)");
}
else
{
AddCladeSource(list, ability, CodexContent.Clade(draft.CladeId), "");
+ var sp = CodexContent.SpeciesById(draft.EffectiveSpeciesId);
+ if (sp is not null) AddDictSource(list, ability, sp.Name, sp.AbilityMods);
}
- var sp = CodexContent.SpeciesById(draft.EffectiveSpeciesId);
- if (sp is not null) AddDictSource(list, ability, sp.Name, sp.AbilityMods);
-
return list;
}
+ /// The chosen-mod path: one mod from a parent clade. The value
+ /// is the clade's actual mod for that ability (so picking CON from
+ /// canidae yields +1 while picking CON from ursidae yields +2).
+ private static void AddCladeChoice(List list, string ability, CladeDef? clade, string suffix)
+ {
+ if (clade is null) return;
+ if (clade.AbilityMods.TryGetValue(ability, out int v) && v != 0)
+ list.Add(new ModSource(clade.Name + suffix, v));
+ }
+
public static int TotalBonus(string ability, CharacterDraft draft)
=> Sources(ability, draft).Sum(s => s.Value);
diff --git a/Theriapolis.Godot/UI/CharacterDraft.cs b/Theriapolis.Godot/UI/CharacterDraft.cs
index 3368edc..5d3a6d9 100644
--- a/Theriapolis.Godot/UI/CharacterDraft.cs
+++ b/Theriapolis.Godot/UI/CharacterDraft.cs
@@ -38,6 +38,15 @@ public partial class CharacterDraft : Resource
/// "sire" or "dam" — which parent the PC presents as.
[Export] public string DominantParent { get; set; } = "sire";
+ // Per theriapolis-rpg-clades.md "Building a Hybrid", simplified:
+ // hybrids take ONE ability modifier from each parent Clade. The
+ // original no-stack rule (take +1 + free elsewhere on overlap) was
+ // dropped — overlapping picks just sum.
+ /// Ability key picked from sire's clade mods (e.g. "STR").
+ [Export] public string SireChosenAbility { get; set; } = "";
+ /// Ability key picked from dam's clade mods.
+ [Export] public string DamChosenAbility { get; set; } = "";
+
///
/// Resolves the "active" clade for downstream steps (Class / Subclass
/// / Background). Purebred uses ; hybrids use
@@ -106,12 +115,14 @@ public partial class CharacterDraft : Resource
case "stat_assign": StatAssign = (Godot.Collections.Dictionary)patch[key]; break;
case "chosen_skills": ChosenSkills = (Godot.Collections.Array)patch[key]; break;
case "character_name": CharacterName = (string)patch[key]; break;
- case "is_hybrid": IsHybrid = (bool)patch[key]; break;
- case "sire_clade_id": SireCladeId = (string)patch[key]; break;
- case "sire_species_id": SireSpeciesId = (string)patch[key]; break;
- case "dam_clade_id": DamCladeId = (string)patch[key]; break;
- case "dam_species_id": DamSpeciesId = (string)patch[key]; break;
- case "dominant_parent": DominantParent = (string)patch[key]; break;
+ case "is_hybrid": IsHybrid = (bool)patch[key]; break;
+ case "sire_clade_id": SireCladeId = (string)patch[key]; break;
+ case "sire_species_id": SireSpeciesId = (string)patch[key]; break;
+ case "dam_clade_id": DamCladeId = (string)patch[key]; break;
+ case "dam_species_id": DamSpeciesId = (string)patch[key]; break;
+ case "dominant_parent": DominantParent = (string)patch[key]; break;
+ case "sire_chosen_ability": SireChosenAbility = (string)patch[key]; break;
+ case "dam_chosen_ability": DamChosenAbility = (string)patch[key]; break;
default:
GD.PushWarning($"[CharacterDraft] unknown patch key: {k}");
break;
diff --git a/Theriapolis.Godot/UI/WizardValidation.cs b/Theriapolis.Godot/UI/WizardValidation.cs
index ac48e89..eb85dd0 100644
--- a/Theriapolis.Godot/UI/WizardValidation.cs
+++ b/Theriapolis.Godot/UI/WizardValidation.cs
@@ -39,6 +39,15 @@ public static class WizardValidation
if (string.IsNullOrEmpty(draft.DamCladeId)) return "Pick a dam clade.";
if (draft.SireCladeId == draft.DamCladeId)
return "Sire and dam must be different clades.";
+
+ // Pick one ability mod from each parent clade. Stacking on
+ // the same ability is allowed (the rule was simplified to
+ // permit duplicate picks summing).
+ if (string.IsNullOrEmpty(draft.SireChosenAbility))
+ return "Pick a lineage bonus from the sire clade.";
+ if (string.IsNullOrEmpty(draft.DamChosenAbility))
+ return "Pick a lineage bonus from the dam clade.";
+
return null;
}
return string.IsNullOrEmpty(draft.CladeId) ? "Pick a clade." : null;