diff --git a/client/scripts/ui/creation/creation_copy.gd b/client/scripts/ui/creation/creation_copy.gd new file mode 100644 index 0000000..6a7fea6 --- /dev/null +++ b/client/scripts/ui/creation/creation_copy.gd @@ -0,0 +1,76 @@ +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("_", " ") diff --git a/client/scripts/ui/creation/creation_copy.gd.uid b/client/scripts/ui/creation/creation_copy.gd.uid new file mode 100644 index 0000000..75e02e7 --- /dev/null +++ b/client/scripts/ui/creation/creation_copy.gd.uid @@ -0,0 +1 @@ +uid://rm6bkgstenf1 diff --git a/client/tests/unit/test_creation_copy.gd b/client/tests/unit/test_creation_copy.gd new file mode 100644 index 0000000..79b7809 --- /dev/null +++ b/client/tests/unit/test_creation_copy.gd @@ -0,0 +1,54 @@ +extends "res://addons/gut/test.gd" + + +func test_calling_detail_reads_the_table_rather_than_repeating_it(): + # The guard that fails if anyone types "d8" into a string literal. If someone + # retunes the Cutpurse's hit die, this test keeps passing and the CARD FOLLOWS — + # which is the whole point. A hardcoded line would go red here. + for id in Callings.IDS: + var line := CreationCopy.calling_detail(id) + assert_string_contains(line, "d%d" % Callings.hit_die(id)) + assert_string_contains(line, CreationCopy.armor_word(id)) + assert_string_contains(line, Callings.talent(id)) + assert_string_contains(line, str(Callings.skill_count(id))) + for s in Callings.saves(id): + assert_string_contains(line, s.to_upper()) + + +func test_casters_show_a_pool_and_martials_show_cooldowns(): + assert_string_contains(CreationCopy.calling_detail("hedge_mage"), "MP (MAG)") + assert_string_contains(CreationCopy.calling_detail("bonesetter"), "MP (FTH)") + assert_string_contains(CreationCopy.calling_detail("sellsword"), "cooldowns") + assert_false(CreationCopy.calling_detail("sellsword").contains("MP")) + + +func test_race_trait_lines_are_derived_from_the_race_table(): + assert_string_contains(CreationCopy.race_trait("human"), "every save") + assert_string_contains(CreationCopy.race_trait("human"), "skill of choice") + assert_string_contains(CreationCopy.race_trait("elf"), "Perception") + assert_string_contains(CreationCopy.race_trait("elf"), "Nightsight") + assert_string_contains(CreationCopy.race_trait("dwarf"), "Poison") + assert_string_contains(CreationCopy.race_trait("dwarf"), "Nightsight") + assert_string_contains(CreationCopy.race_trait("beastfolk"), "Claws") + assert_string_contains(CreationCopy.race_trait("beastfolk"), "Keen scent") + + +func test_every_race_has_a_non_empty_trait_line(): + for id in Races.IDS: + assert_ne(CreationCopy.race_trait(id).strip_edges(), "", "%s has no trait line" % id) + + +func test_skill_labels_are_human_readable(): + assert_eq(CreationCopy.skill_label("sleight_of_hand"), "sleight of hand") + assert_eq(CreationCopy.skill_label("faith_lore"), "faith lore") + assert_eq(CreationCopy.skill_label("stealth"), "stealth") + + +func test_nothing_in_the_copy_mentions_luck(): + # §7 — the player must never be able to CALCULATE that he is cursed. + for id in Callings.IDS: + assert_false(CreationCopy.calling_detail(id).to_lower().contains("luck")) + assert_false(CreationCopy.calling_detail(id).to_lower().contains("lck")) + for id in Races.IDS: + assert_false(CreationCopy.race_trait(id).to_lower().contains("luck")) + assert_false(CreationCopy.race_trait(id).to_lower().contains("lck")) diff --git a/client/tests/unit/test_creation_copy.gd.uid b/client/tests/unit/test_creation_copy.gd.uid new file mode 100644 index 0000000..280de2d --- /dev/null +++ b/client/tests/unit/test_creation_copy.gd.uid @@ -0,0 +1 @@ +uid://bvb87ime573x4