Files
Christopher Wiebe 97b49d4145 M6.18: Skill expansion to 5 per ability — 12 new Theriapolis-flavored skills
Skill list grows from 18 → 30 so every ability has 5 skills tied to it.
New skills appended at SkillId byte indices 18–29 to preserve
save-game compat with pre-M6.18 characters.

  STR (+4): Brawl, Build-Read, Force, Haulage
  DEX (+2): Driving, Marksmanship
  CON (+5): Endurance, Fortitude, Hardiness, Lung-Craft, Pain Tolerance
  CHA (+1): Scent-Speak

Endurance covers applied effort over time; Hardiness covers external
condition tolerance (temperature, smoke, altitude); Fortitude is
ingestion-only (poison, spoiled food, ritual draughts). Pain Tolerance
is function-while-wounded. Lung-Craft is breath/voice discipline.

Class skill_options updated across all 8 callings so every new skill
has 3+ class homes (Muzzle-Speaker remains the universal pick of
all 30). Backgrounds left untouched — their skill grants are
doc-canon and would benefit from a separate balancing pass.

CharacterBuilder.SkillToJsonName extended with explicit cases for the
new skills so compound enum names like BuildRead serialize as
"build_read" rather than the fallback "buildread" — caught by
CharacterBuilderTests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:25:56 -07:00

121 lines
4.8 KiB
C#

namespace Theriapolis.Core.Rules.Stats;
/// <summary>
/// Skill list used by Theriapolis — extends the d20 baseline with 12
/// additional skills (M6.18) so each ability has 5 skills tied to it.
/// New skills appended at the end of the byte enum to preserve save-game
/// compatibility with characters created before M6.18. Each skill is
/// backed by a single ability — see <see cref="SkillIdExtensions.Ability"/>.
/// </summary>
public enum SkillId : byte
{
Acrobatics = 0,
AnimalHandling = 1,
Arcana = 2, // Theriapolis: "Advanced Engineering"
Athletics = 3,
Deception = 4,
History = 5,
Insight = 6,
Intimidation = 7,
Investigation = 8,
Medicine = 9,
Nature = 10,
Perception = 11,
Performance = 12,
Persuasion = 13,
Religion = 14, // Theriapolis: Covenant lore
SleightOfHand = 15,
Stealth = 16,
Survival = 17,
// M6.18 — Theriapolis-flavored expansions, 5 per ability.
Brawl = 18, // STR
BuildRead = 19, // STR
Driving = 20, // DEX
Endurance = 21, // CON
Force = 22, // STR
Fortitude = 23, // CON
Hardiness = 24, // CON
Haulage = 25, // STR
LungCraft = 26, // CON
Marksmanship = 27, // DEX
PainTolerance = 28, // CON
ScentSpeak = 29, // CHA
}
public static class SkillIdExtensions
{
public static AbilityId Ability(this SkillId s) => s switch
{
SkillId.Acrobatics => AbilityId.DEX,
SkillId.AnimalHandling => AbilityId.WIS,
SkillId.Arcana => AbilityId.INT,
SkillId.Athletics => AbilityId.STR,
SkillId.Deception => AbilityId.CHA,
SkillId.History => AbilityId.INT,
SkillId.Insight => AbilityId.WIS,
SkillId.Intimidation => AbilityId.CHA,
SkillId.Investigation => AbilityId.INT,
SkillId.Medicine => AbilityId.WIS,
SkillId.Nature => AbilityId.INT,
SkillId.Perception => AbilityId.WIS,
SkillId.Performance => AbilityId.CHA,
SkillId.Persuasion => AbilityId.CHA,
SkillId.Religion => AbilityId.INT,
SkillId.SleightOfHand => AbilityId.DEX,
SkillId.Stealth => AbilityId.DEX,
SkillId.Survival => AbilityId.WIS,
SkillId.Brawl => AbilityId.STR,
SkillId.BuildRead => AbilityId.STR,
SkillId.Driving => AbilityId.DEX,
SkillId.Endurance => AbilityId.CON,
SkillId.Force => AbilityId.STR,
SkillId.Fortitude => AbilityId.CON,
SkillId.Hardiness => AbilityId.CON,
SkillId.Haulage => AbilityId.STR,
SkillId.LungCraft => AbilityId.CON,
SkillId.Marksmanship => AbilityId.DEX,
SkillId.PainTolerance => AbilityId.CON,
SkillId.ScentSpeak => AbilityId.CHA,
_ => throw new ArgumentOutOfRangeException(nameof(s)),
};
/// <summary>Parses a snake_case JSON value (e.g. "animal_handling") into a SkillId.</summary>
public static SkillId FromJson(string raw) => raw.ToLowerInvariant() switch
{
"acrobatics" => SkillId.Acrobatics,
"animal_handling" => SkillId.AnimalHandling,
"arcana" => SkillId.Arcana,
"athletics" => SkillId.Athletics,
"deception" => SkillId.Deception,
"history" => SkillId.History,
"insight" => SkillId.Insight,
"intimidation" => SkillId.Intimidation,
"investigation" => SkillId.Investigation,
"medicine" => SkillId.Medicine,
"nature" => SkillId.Nature,
"perception" => SkillId.Perception,
"performance" => SkillId.Performance,
"persuasion" => SkillId.Persuasion,
"religion" => SkillId.Religion,
"sleight_of_hand" => SkillId.SleightOfHand,
"stealth" => SkillId.Stealth,
"survival" => SkillId.Survival,
"brawl" => SkillId.Brawl,
"build_read" => SkillId.BuildRead,
"driving" => SkillId.Driving,
"endurance" => SkillId.Endurance,
"force" => SkillId.Force,
"fortitude" => SkillId.Fortitude,
"hardiness" => SkillId.Hardiness,
"haulage" => SkillId.Haulage,
"lung_craft" => SkillId.LungCraft,
"marksmanship" => SkillId.Marksmanship,
"pain_tolerance" => SkillId.PainTolerance,
"scent_speak" => SkillId.ScentSpeak,
_ => throw new ArgumentException($"Unknown skill: '{raw}'"),
};
}