Files
code_of_conquest_dnd/client/scripts/rules/callings.gd
Phillip Tarrant 78e18107b2 feat(rules): the race, calling, skill and attribute tables
Mechanics are rules and live in code, beside Luck and Currency. The skill pools
are new — the races/classes spec said only 'the stat-appropriate slice' and never
enumerated them; each pool is larger than its pick count so the choice is real.

The dwarf's +2 vs poison is exposed separately from save() rather than folded
into it: baking a conditional into a flat number is how a sheet starts lying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:14:44 -05:00

94 lines
3.3 KiB
GDScript

class_name Callings
extends RefCounted
## The 7 callings (races-and-classes spec §4). A calling is what a VILLAGE calls you,
## not what a rulebook calls you — Cutpurse, not Assassin; Bonesetter, not Priest.
##
## The skill POOLS are this spec's addition: the races/classes spec said only "drawn
## from the class's stat-appropriate slice" and never enumerated them. Each pool is the
## calling's primary-stat skills plus a small adjacent slice, always larger than the
## pick count so the choice is real.
##
## L1 talents are ID STUBS. M5 implements them. armor is a proficiency CATEGORY; real
## armor is an item (M7).
const IDS := ["sellsword", "reaver", "cutpurse", "trapper", "hedge_mage", "bonesetter", "bloodsworn"]
const TABLE := {
"sellsword": {
"primary": "str", "hit_die": 10, "saves": ["str", "con"],
"skill_pool": ["athletics", "intimidation", "endurance", "perception"], "skill_count": 2,
"casting_stat": "", "armor": "heavy", "talent": "second_wind",
},
"reaver": {
"primary": "str", "hit_die": 12, "saves": ["str", "con"],
"skill_pool": ["athletics", "intimidation", "endurance", "acrobatics"], "skill_count": 2,
"casting_stat": "", "armor": "medium", "talent": "blood_fury",
},
"cutpurse": {
"primary": "dex", "hit_die": 8, "saves": ["dex", "mag"],
"skill_pool": ["stealth", "sleight_of_hand", "acrobatics", "perception", "athletics", "intimidation"],
"skill_count": 4,
"casting_stat": "", "armor": "light", "talent": "backstab",
},
"trapper": {
"primary": "dex", "hit_die": 10, "saves": ["dex", "con"],
"skill_pool": ["stealth", "acrobatics", "perception", "endurance", "athletics"], "skill_count": 3,
"casting_stat": "", "armor": "light", "talent": "set_snare",
},
"hedge_mage": {
"primary": "mag", "hit_die": 6, "saves": ["mag", "fth"],
"skill_pool": ["sorcery", "perception", "sleight_of_hand", "faith_lore"], "skill_count": 2,
"casting_stat": "mag", "armor": "none", "talent": "hexbolt",
},
"bonesetter": {
"primary": "fth", "hit_die": 8, "saves": ["fth", "con"],
"skill_pool": ["faith_lore", "perception", "endurance", "intimidation"], "skill_count": 2,
"casting_stat": "fth", "armor": "medium", "talent": "mend",
},
"bloodsworn": {
"primary": "fth", "hit_die": 8, "saves": ["fth", "mag"],
"skill_pool": ["faith_lore", "sorcery", "intimidation", "perception"], "skill_count": 2,
"casting_stat": "fth", "armor": "light", "talent": "pact_mark",
},
}
static func exists(id: String) -> bool:
return TABLE.has(id)
static func primary(id: String) -> String:
return str(TABLE.get(id, {}).get("primary", ""))
static func hit_die(id: String) -> int:
return int(TABLE.get(id, {}).get("hit_die", 0))
static func saves(id: String) -> Array:
return TABLE.get(id, {}).get("saves", [])
static func skill_pool(id: String) -> Array:
return TABLE.get(id, {}).get("skill_pool", [])
static func skill_count(id: String) -> int:
return int(TABLE.get(id, {}).get("skill_count", 0))
static func casting_stat(id: String) -> String:
return str(TABLE.get(id, {}).get("casting_stat", ""))
static func is_caster(id: String) -> bool:
return casting_stat(id) != ""
static func armor(id: String) -> String:
return str(TABLE.get(id, {}).get("armor", ""))
static func talent(id: String) -> String:
return str(TABLE.get(id, {}).get("talent", ""))