Display strings derived from Callings/Races/Skills at call time. Retune a hit die and the card follows, because the card never knew the number. Prose stays in content; numbers stay in code; nothing is written twice. Deviation from the plan, approved by the human: the plan's test asserted the rendered line contains Callings.armor(id) — the RAW table value — while the plan's own ARMOR_WORD maps "none" to "no armour". "none" is not a substring of "no armour", so the Hedge-Mage failed. The card's copy is what the plan's docstring example shows, so the copy stands and the test was fixed: armor_word(id) is now public and the test asserts against the derived word. The guard is unchanged — hardcode an armour string and retune the table, and it still goes red. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.6 KiB
GDScript
77 lines
2.6 KiB
GDScript
class_name CreationCopy
|
|
extends RefCounted
|
|
## Display strings for the creation screen, DERIVED from the rules tables.
|
|
##
|
|
## Prose lives in content (blurbs, fragments). NUMBERS LIVE IN CODE. The card reads
|
|
## both and repeats neither. A hit die written into a JSON — or typed into a string
|
|
## literal here — is a second source of truth, and it will eventually disagree with
|
|
## the table. Every number below is read from Callings/Races/Skills at call time, so
|
|
## retuning a calling retunes its card.
|
|
##
|
|
## §7: nothing here may mention Luck, in any form. §2: presentation only.
|
|
|
|
const ARMOR_WORD := {
|
|
"none": "no armour",
|
|
"light": "light armour",
|
|
"medium": "medium armour",
|
|
"heavy": "heavy armour",
|
|
}
|
|
|
|
|
|
static func calling_detail(id: String) -> String:
|
|
## e.g. "d8 · light armour · MP (FTH) · saves DEX + MAG · picks 4 skills · talent backstab"
|
|
if not Callings.exists(id):
|
|
return ""
|
|
var saves: Array = []
|
|
for s in Callings.saves(id):
|
|
saves.append(s.to_upper())
|
|
return "d%d · %s · %s · saves %s · picks %d skills · talent %s" % [
|
|
Callings.hit_die(id),
|
|
armor_word(id),
|
|
resource_word(id),
|
|
" + ".join(saves),
|
|
Callings.skill_count(id),
|
|
Callings.talent(id),
|
|
]
|
|
|
|
|
|
static func armor_word(id: String) -> String:
|
|
## The card says "no armour", not "none". Public so the test can assert against
|
|
## the DERIVED word rather than the raw table value — "none" is not a substring
|
|
## of "no armour", and asserting on the raw value only ever passed by accident.
|
|
return ARMOR_WORD.get(Callings.armor(id), Callings.armor(id))
|
|
|
|
|
|
static func resource_word(id: String) -> String:
|
|
## Martials live on cooldowns; casters on a pool off their casting stat.
|
|
if not Callings.is_caster(id):
|
|
return "cooldowns"
|
|
return "MP (%s)" % Callings.casting_stat(id).to_upper()
|
|
|
|
|
|
static func race_trait(id: String) -> String:
|
|
## e.g. "+1 to every save · +1 skill of choice" / "Poison resist · Nightsight"
|
|
if not Races.exists(id):
|
|
return ""
|
|
var parts: Array = []
|
|
if Races.save_bonus(id) > 0:
|
|
parts.append("+%d to every save" % Races.save_bonus(id))
|
|
for s in Races.granted_skills(id):
|
|
parts.append(skill_label(s).capitalize())
|
|
if Races.poison_save_bonus(id) > 0:
|
|
parts.append("Poison resist")
|
|
if Races.flag(id, "nightsight"):
|
|
parts.append("Nightsight")
|
|
if Races.flag(id, "claws"):
|
|
parts.append("Claws")
|
|
if Races.flag(id, "keen_scent"):
|
|
parts.append("Keen scent")
|
|
if Races.wants_bonus_skill(id):
|
|
parts.append("+1 skill of choice")
|
|
return " · ".join(parts)
|
|
|
|
|
|
static func skill_label(skill: String) -> String:
|
|
## sleight_of_hand -> "sleight of hand". Ids are snake_case; humans are not.
|
|
return skill.replace("_", " ")
|