From c6b8a651aa8c741169c5f6231f82820d11684878 Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Mon, 13 Jul 2026 14:11:16 -0500 Subject: [PATCH] fix(creation-content): four fragments were reading the blurb back to the player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The creation screen shows a race's blurb on its card and the chosen calling's blurb in the detail panel directly above the DM panel. Blurb and fragment are on screen at the same moment, inches apart — so a fragment that paraphrases its own blurb makes the DM sound like it is reciting the card. Four did: - bonesetter lifted two clauses verbatim ("you set the bone, you say the words") - reaver re-ran the blurb's door image with the same sentence shape - hedge_mage re-opened on "what you know" and re-landed the dead teacher - beastfolk repeated both the claws and the being-judged beat Each now says the NEXT thing instead: the hour they wake you and which of the two trades you will not claim credit for; the employer who arranges to be elsewhere; where you went to fail safely and what the book's gaps cost your skin; the town that takes your coin and still wants you outside the walls. beastfolk's rewrite also breaks the race-set monotony — it no longer opens "You were born" (now unique to human) and drops the shared "X, and you have…" spine, so clicking the four cards in a row stops reading as one voice. The parity test guarded presence but could not fail on the three properties that actually make all 28 race×calling concatenations grammatical. It now asserts race fragments start uppercase and end with '.', and calling fragments start lowercase and end with '.' — verified by breaking both and watching all four new assertions fire. 255 client tests green; content build --check green. Co-Authored-By: Claude Opus 4.8 (1M context) --- client/tests/unit/test_content_db.gd | 23 +++++++++++++++++++---- content/world/callings/bonesetter.json | 2 +- content/world/callings/hedge_mage.json | 2 +- content/world/callings/reaver.json | 2 +- content/world/races/beastfolk.json | 2 +- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/client/tests/unit/test_content_db.gd b/client/tests/unit/test_content_db.gd index 0c11ea1..dee944a 100644 --- a/client/tests/unit/test_content_db.gd +++ b/client/tests/unit/test_content_db.gd @@ -136,11 +136,26 @@ func test_every_race_and_calling_carries_a_fragment(): # The DM origin panel (M4-b) composes race.fragment + calling.fragment. A # missing one degrades to the blurb at runtime rather than crashing — but it # must never SHIP missing, so the suite is where it fails. + # + # The composition is "{race.fragment} Now you carry a {Calling}'s work — + # {calling.fragment}", so three shape rules make all 28 pairings legal + # sentences: a race fragment opens a sentence (uppercase) and closes it (.); + # a calling fragment continues one after the em dash (lowercase) and closes + # it (.). Guard them here — a future author adding a calling gets a signal, + # not a subtly ungrammatical DM. for id in Races.IDS: assert_true(db.race(id).has("fragment"), "race %s has no fragment" % id) - assert_ne(str(db.race(id).get("fragment", "")).strip_edges(), "", - "race %s has an empty fragment" % id) + var race_fragment := str(db.race(id).get("fragment", "")).strip_edges() + assert_ne(race_fragment, "", "race %s has an empty fragment" % id) + assert_eq(race_fragment.substr(0, 1), race_fragment.substr(0, 1).to_upper(), + "race %s: fragment opens the composition, so it must start uppercase" % id) + assert_true(race_fragment.ends_with("."), + "race %s: fragment must end with '.' — a sentence precedes 'Now you carry…'" % id) for id in Callings.IDS: assert_true(db.calling(id).has("fragment"), "calling %s has no fragment" % id) - assert_ne(str(db.calling(id).get("fragment", "")).strip_edges(), "", - "calling %s has an empty fragment" % id) + var calling_fragment := str(db.calling(id).get("fragment", "")).strip_edges() + assert_ne(calling_fragment, "", "calling %s has an empty fragment" % id) + assert_eq(calling_fragment.substr(0, 1), calling_fragment.substr(0, 1).to_lower(), + "calling %s: fragment follows an em dash, so it must start lowercase" % id) + assert_true(calling_fragment.ends_with("."), + "calling %s: fragment must end with '.' — it closes the composition" % id) diff --git a/content/world/callings/bonesetter.json b/content/world/callings/bonesetter.json index a58459a..c42cf37 100644 --- a/content/world/callings/bonesetter.json +++ b/content/world/callings/bonesetter.json @@ -1 +1 @@ -{ "id": "bonesetter", "name": "Bonesetter", "blurb": "The Warden is distant and does not explain himself. Still — you set the bone, you say the words, and often enough the rot does not take. Often enough.", "fragment": "you set the bone, you say the words over it, and you have stopped claiming to know which one matters." } +{ "id": "bonesetter", "name": "Bonesetter", "blurb": "The Warden is distant and does not explain himself. Still — you set the bone, you say the words, and often enough the rot does not take. Often enough.", "fragment": "you are the one they wake at the third hour, and you have stopped claiming to know whether it is your hands or the Warden that does the saving." } diff --git a/content/world/callings/hedge_mage.json b/content/world/callings/hedge_mage.json index b884056..ad3fde1 100644 --- a/content/world/callings/hedge_mage.json +++ b/content/world/callings/hedge_mage.json @@ -1 +1 @@ -{ "id": "hedge_mage", "name": "Hedge-Mage", "blurb": "No tower took you. What you know, you got from a book you should not have had and a teacher who is not alive to confirm it.", "fragment": "what you know came out of a stolen book, and there is no one left to tell you what you got wrong." } +{ "id": "hedge_mage", "name": "Hedge-Mage", "blurb": "No tower took you. What you know, you got from a book you should not have had and a teacher who is not alive to confirm it.", "fragment": "you taught yourself the rest where nobody would come looking, and the scars are from the parts the book got wrong." } diff --git a/content/world/callings/reaver.json b/content/world/callings/reaver.json index 07c0a10..6a27575 100644 --- a/content/world/callings/reaver.json +++ b/content/world/callings/reaver.json @@ -1 +1 @@ -{ "id": "reaver", "name": "Reaver", "blurb": "They hire you when they want a door opened and do not care about the door. You do not wear plate. You have never needed it.", "fragment": "they point you at a door and pay you not to care what is behind it." } +{ "id": "reaver", "name": "Reaver", "blurb": "They hire you when they want a door opened and do not care about the door. You do not wear plate. You have never needed it.", "fragment": "they send for you when the talking is finished, and they make a point of being somewhere else when you arrive." } diff --git a/content/world/races/beastfolk.json b/content/world/races/beastfolk.json index dde61e4..5f1d6b3 100644 --- a/content/world/races/beastfolk.json +++ b/content/world/races/beastfolk.json @@ -1 +1 @@ -{ "id": "beastfolk", "name": "Beastfolk", "blurb": "Claws you did not ask for and a nose you cannot switch off. Every gate you pass, someone decides what you are before you speak.", "fragment": "You were born with claws the Margreave never learned to ignore, and you have spent your life being looked at a beat too long." } +{ "id": "beastfolk", "name": "Beastfolk", "blurb": "Claws you did not ask for and a nose you cannot switch off. Every gate you pass, someone decides what you are before you speak.", "fragment": "Most towns will take your coin and still want you outside the walls by dark. You stopped taking that personally a long time ago." }