fix(creation-screen): the white text, the badge on top of the roll, and a window too big for the laptop

The human's first F6 run found four defects headless GUT could not see, all
now fixed and break-proven.

1. Fifteen Labels shipped invisible. _fonts() sets a default font and size but
   never a default font_color for the base Label type, so a Label with no
   type-variation inherited Godot's built-in default — WHITE — and rendered
   white-on-parchment: the four race names, four race blurbs, and seven calling
   names, every unreadable string on the sheet. New CardTitle / CardBody roles
   (mock #3a2f1c / #6a5a3a) fix it. NOT a global default Label colour: the Title
   screen has 12 bare Labels on a dark background that rely on white.

2. The PRIMARY badge covered the rolled value. The ability card is a
   PanelContainer, and a Container force-fits its children, so the badge's
   authored anchors were dead letters — it stretched to the card's full width
   and centred over the value. Nesting it under a plain Control (not a
   Container) restores absolute positioning. The badges also carried _flat()'s
   12/6 card padding; a new tight pill stylebox matches the mock's 2px 9px.

3. Race cards overflowed their own border (108px card, 114px of content). Bumped
   to 140. The CHOSEN badge on the calling card moved from centre to the mock's
   top-right, and the ability card got its own AbilityCard variation with 16px
   top headroom for the badge (ParchmentCard is shared with the shell).

4. The default window (1600x900) was larger than a 1600x900 laptop's usable
   area, so the WM clamped it to 1589x752 and the run came up pillarboxed.
   Default is now 1280x720, resizable; the 1920x1080 design canvas is unchanged
   (canvas_items + keep scales it, so no layout number moved).

Also closes M4-b's two open copy items: the cutpurse origin fragment (it
contradicted its own blurb) and the Hint label (it omitted the roll's floor of
8, now pinned to NewGame.roll_attributes).

Guards for 1-4 are new and each was re-broken and watched go red. The overflow
guard in 3 was FIRST WRITTEN AS A TAUTOLOGY — Godot clamps Control.size up to
its combined minimum, so `min <= size` is `x <= x` — and passed against the bug,
29/29, until it was rewritten to measure against the card that clips. That is
traps.md #17 and the thirteenth cannot-fail assertion this branch has caught.

319 client tests green, content build green, theme drift guard satisfied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:56:09 -05:00
parent 7471d2e37f
commit 63cbc8c16e
10 changed files with 565 additions and 229 deletions

View File

@@ -318,6 +318,52 @@ human a wall-clock timeout to even learn something is wrong.
---
## 17. Asserting against a value the engine derives from the thing under test
**M4-b, the F6 pass.** The race cards were authored 108px tall, and their text — the
trait line — visibly rendered *below the card's bottom border* in the running game. The
guard written for it read:
```gdscript
var needed := box.get_combined_minimum_size().y # 114
assert_lte(needed, box.size.y) # box.size.y is ALSO 114
```
It passed. Not against the fix — **against the bug.** The card was put back to its
overflowing 108px and the suite stayed green, 29/29.
The reason is a Godot rule that is easy to forget: **`Control.size` is clamped UP to
`get_combined_minimum_size()`.** The `Box` is anchored inside the card, so a 108px card
leaves it 82px of room — but the Box does not *become* 82px tall. It reports 114, its own
minimum, and quietly overflows the card's border. So the assertion compared 114 against
114: `needed <= box.size.y` is not a size check at all, it is `x <= x`. It could never
have failed, for any card, at any height.
**The guard:** assert against the container that actually *clips* — the card — not
against the child, whose size the engine has already reconciled with the very number you
are testing:
```gdscript
var available: float = card.size.y - box.offset_top + box.offset_bottom # 82
assert_lte(needed, available) # 114 <= 82 -> RED
```
The general shape: **if the engine derives B from A, then `assert(A <= B)` is a
tautology, not a test.** Layout, min-size, and any auto-fitted value are all A-and-B.
Reach for a number the engine computed from a *different* source — the parent, the
authored constant, the viewport — or the assertion is checking that arithmetic works.
Related: this is trap 11's family (an assertion already true before the action ran), but
it is worse — this one is true *by construction*, so no amount of driving state can
redeem it.
**Corollary, same fix:** geometry assertions are meaningless before the container tree has
sorted. On the frame `add_child()` runs, a race card reports **102px** wide and its
autowrapped blurb reports a **2508px** minimum height. `await get_tree().process_frame`
twice, or the guard fails for a reason that has nothing to do with the bug.
---
## The checklist this all reduces to
- Can the new test **fail**? Re-break the code and watch it. If it can't fail, it isn't a test.
@@ -342,3 +388,8 @@ human a wall-clock timeout to even learn something is wrong.
- Does the test **press the node** (emit the real signal), or call the handler directly?
- Did the test **count** move by the expected amount — not just the green banner?
- Can the test **hang** on a broken precondition? Bound every loop.
- Is the assertion's right-hand side **derived by the engine from its left-hand side**
(`Control.size` vs `get_combined_minimum_size()`)? Then it is `x <= x`. Measure against
the parent that clips, not the child that reports.
- Is it a **geometry** assertion? Let the container tree sort first, or it measures an
unlaid-out node.