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

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
# ─────────────────────────────────────────────
# Local scratch — never versioned
# ─────────────────────────────────────────────
# Ad-hoc F6 / smoke-test captures. Kept for the working session, not the repo.
/screenshots/
# ─────────────────────────────────────────────
# Godot 4.7 (client/)
# ─────────────────────────────────────────────

View File

@@ -1,13 +1,29 @@
[gd_resource type="Theme" format=3]
[ext_resource type="FontFile" path="res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf" id="1_605w4"]
[ext_resource type="FontFile" path="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf" id="2_4lwr2"]
[ext_resource type="FontFile" path="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf" id="3_eckwq"]
[ext_resource type="FontFile" path="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf" id="2_4lwr2"]
[ext_resource type="FontFile" path="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf" id="3_eckwq"]
[ext_resource type="FontFile" path="res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf" id="4_i0b23"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_cqbvn"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cqbvn"]
content_margin_left = 10.0
content_margin_top = 16.0
content_margin_right = 10.0
content_margin_bottom = 14.0
bg_color = Color(0.94509804, 0.9098039, 0.8235294, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5gpk"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_q5gpk"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bbpmc"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -23,7 +39,7 @@ corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bbpmc"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73lme"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -39,7 +55,7 @@ corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73lme"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -55,23 +71,19 @@ corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
content_margin_left = 8.0
content_margin_top = 3.0
content_margin_right = 8.0
content_margin_bottom = 3.0
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2tooq"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -87,9 +99,9 @@ corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2tooq"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_4kten"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4kten"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p6wtm"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -105,7 +117,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p6wtm"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0fjdt"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -121,7 +133,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0fjdt"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ns3kg"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -137,7 +149,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ns3kg"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jly1r"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -153,7 +165,7 @@ corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jly1r"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lw8lk"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -170,7 +182,7 @@ corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lw8lk"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nh0e7"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -186,9 +198,9 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_nh0e7"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_dtr07"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dtr07"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nmowx"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -204,7 +216,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nmowx"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oi067"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -220,7 +232,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oi067"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukblj"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -236,7 +248,7 @@ corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukblj"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utt73"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -252,7 +264,7 @@ corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utt73"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5u8fh"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -268,7 +280,7 @@ corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_5u8fh"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p0qbh"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -284,28 +296,12 @@ corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p0qbh"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6y6ak"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
bg_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
@@ -321,7 +317,7 @@ content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.47843137, 0.18431373, 0.16078432, 1)
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
@@ -337,6 +333,22 @@ content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
bg_color = Color(0.47843137, 0.18431373, 0.16078432, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_najyk"]
content_margin_left = 10.0
content_margin_top = 16.0
content_margin_right = 10.0
content_margin_bottom = 14.0
bg_color = Color(0.94509804, 0.9098039, 0.8235294, 1)
border_width_left = 2
border_width_top = 2
@@ -348,23 +360,19 @@ corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_najyk"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
content_margin_bottom = 6.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jtb1k"]
content_margin_left = 9.0
content_margin_top = 2.0
content_margin_right = 9.0
content_margin_bottom = 2.0
bg_color = Color(0.56078434, 0.41568628, 0.16470589, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
border_color = Color(0.56078434, 0.41568628, 0.16470589, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jtb1k"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wmbj4"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -380,9 +388,9 @@ corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wmbj4"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xdylb"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xdylb"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pjnll"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -398,7 +406,7 @@ corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pjnll"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_klelq"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -414,7 +422,7 @@ corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_klelq"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i2sl5"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -430,7 +438,7 @@ corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i2sl5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_onsig"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -446,9 +454,9 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_onsig"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_gw0t7"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gw0t7"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_88l5g"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -464,7 +472,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_88l5g"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f8qac"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -480,7 +488,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f8qac"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u6gcs"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -496,9 +504,9 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_u6gcs"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_nufy3"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nufy3"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0d7vs"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -514,7 +522,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0d7vs"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3fnfk"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -530,7 +538,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_3fnfk"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2txdw"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -546,9 +554,9 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2txdw"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_w7qkp"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_w7qkp"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rthnr"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -564,7 +572,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rthnr"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ta3oh"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -580,7 +588,7 @@ corner_radius_top_right = 14
corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ta3oh"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ohwwq"]
content_margin_left = 12.0
content_margin_top = 6.0
content_margin_right = 12.0
@@ -597,116 +605,126 @@ corner_radius_bottom_right = 14
corner_radius_bottom_left = 14
[resource]
default_font = ExtResource("3_eckwq")
default_font = ExtResource("2_4lwr2")
default_font_size = 18
AbilityCard/base_type = &"PanelContainer"
AbilityCard/styles/panel = SubResource("StyleBoxFlat_cqbvn")
Accent/base_type = &"Label"
Accent/colors/font_color = Color(0.56078434, 0.41568628, 0.16470589, 1)
Accent/font_sizes/font_size = 24
Accent/fonts/font = ExtResource("1_605w4")
CardBody/base_type = &"Label"
CardBody/colors/font_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
CardBody/font_sizes/font_size = 13
CardBody/fonts/font = ExtResource("2_4lwr2")
CardTitle/base_type = &"Label"
CardTitle/colors/font_color = Color(0.22745098, 0.18431373, 0.10980392, 1)
CardTitle/font_sizes/font_size = 19
CardTitle/fonts/font = ExtResource("2_4lwr2")
Chip/base_type = &"Button"
Chip/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
Chip/font_sizes/font_size = 11
Chip/fonts/font = ExtResource("2_4lwr2")
Chip/styles/focus = SubResource("StyleBoxEmpty_cqbvn")
Chip/styles/hover = SubResource("StyleBoxFlat_q5gpk")
Chip/styles/normal = SubResource("StyleBoxFlat_bbpmc")
Chip/styles/pressed = SubResource("StyleBoxFlat_73lme")
Chip/fonts/font = ExtResource("3_eckwq")
Chip/styles/focus = SubResource("StyleBoxEmpty_q5gpk")
Chip/styles/hover = SubResource("StyleBoxFlat_bbpmc")
Chip/styles/normal = SubResource("StyleBoxFlat_73lme")
Chip/styles/pressed = SubResource("StyleBoxFlat_js3tr")
ChosenTag/base_type = &"Label"
ChosenTag/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
ChosenTag/font_sizes/font_size = 10
ChosenTag/fonts/font = ExtResource("2_4lwr2")
ChosenTag/styles/normal = SubResource("StyleBoxFlat_js3tr")
ChosenTag/fonts/font = ExtResource("3_eckwq")
ChosenTag/styles/normal = SubResource("StyleBoxFlat_ube3r")
DarkPanel/base_type = &"PanelContainer"
DarkPanel/styles/panel = SubResource("StyleBoxFlat_ube3r")
DarkPanel/styles/panel = SubResource("StyleBoxFlat_2tooq")
DockButton/base_type = &"Button"
DockButton/colors/font_color = Color(0.7882353, 0.7411765, 0.6392157, 1)
DockButton/styles/focus = SubResource("StyleBoxEmpty_2tooq")
DockButton/styles/hover = SubResource("StyleBoxFlat_4kten")
DockButton/styles/normal = SubResource("StyleBoxFlat_p6wtm")
DockButton/styles/pressed = SubResource("StyleBoxFlat_0fjdt")
DockButton/styles/focus = SubResource("StyleBoxEmpty_4kten")
DockButton/styles/hover = SubResource("StyleBoxFlat_p6wtm")
DockButton/styles/normal = SubResource("StyleBoxFlat_0fjdt")
DockButton/styles/pressed = SubResource("StyleBoxFlat_ns3kg")
Heading/base_type = &"Label"
Heading/colors/font_color = Color(0.22745098, 0.18431373, 0.10980392, 1)
Heading/font_sizes/font_size = 34
Heading/fonts/font = ExtResource("3_eckwq")
Heading/fonts/font = ExtResource("2_4lwr2")
ItemTile/base_type = &"PanelContainer"
ItemTile/styles/panel = SubResource("StyleBoxFlat_ns3kg")
ItemTile/styles/panel = SubResource("StyleBoxFlat_jly1r")
ItemTileEmpty/base_type = &"PanelContainer"
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_jly1r")
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_lw8lk")
Mono/base_type = &"Label"
Mono/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
Mono/font_sizes/font_size = 13
Mono/fonts/font = ExtResource("2_4lwr2")
Mono/fonts/font = ExtResource("3_eckwq")
ParchmentButton/base_type = &"Button"
ParchmentButton/colors/font_color = Color(0.2901961, 0.24705882, 0.17254902, 1)
ParchmentButton/colors/font_disabled_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
ParchmentButton/styles/disabled = SubResource("StyleBoxFlat_lw8lk")
ParchmentButton/styles/focus = SubResource("StyleBoxEmpty_nh0e7")
ParchmentButton/styles/hover = SubResource("StyleBoxFlat_dtr07")
ParchmentButton/styles/normal = SubResource("StyleBoxFlat_nmowx")
ParchmentButton/styles/pressed = SubResource("StyleBoxFlat_oi067")
ParchmentButton/styles/disabled = SubResource("StyleBoxFlat_nh0e7")
ParchmentButton/styles/focus = SubResource("StyleBoxEmpty_dtr07")
ParchmentButton/styles/hover = SubResource("StyleBoxFlat_nmowx")
ParchmentButton/styles/normal = SubResource("StyleBoxFlat_oi067")
ParchmentButton/styles/pressed = SubResource("StyleBoxFlat_ukblj")
ParchmentCard/base_type = &"PanelContainer"
ParchmentCard/styles/panel = SubResource("StyleBoxFlat_ukblj")
ParchmentCard/styles/panel = SubResource("StyleBoxFlat_utt73")
ParchmentInset/base_type = &"PanelContainer"
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_utt73")
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_5u8fh")
PrimaryCTA/base_type = &"Button"
PrimaryCTA/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
PrimaryCTA/colors/font_disabled_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
PrimaryCTA/styles/disabled = SubResource("StyleBoxFlat_5u8fh")
PrimaryCTA/styles/hover = SubResource("StyleBoxFlat_p0qbh")
PrimaryCTA/styles/normal = SubResource("StyleBoxFlat_6y6ak")
PrimaryCTA/styles/pressed = SubResource("StyleBoxFlat_txxuc")
PrimaryCTA/styles/disabled = SubResource("StyleBoxFlat_p0qbh")
PrimaryCTA/styles/hover = SubResource("StyleBoxFlat_6y6ak")
PrimaryCTA/styles/normal = SubResource("StyleBoxFlat_txxuc")
PrimaryCTA/styles/pressed = SubResource("StyleBoxFlat_7d6ug")
PrimaryCard/base_type = &"PanelContainer"
PrimaryCard/styles/panel = SubResource("StyleBoxFlat_7d6ug")
PrimaryCard/styles/panel = SubResource("StyleBoxFlat_najyk")
PrimaryTag/base_type = &"Label"
PrimaryTag/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
PrimaryTag/font_sizes/font_size = 9
PrimaryTag/fonts/font = ExtResource("2_4lwr2")
PrimaryTag/styles/normal = SubResource("StyleBoxFlat_najyk")
PrimaryTag/fonts/font = ExtResource("3_eckwq")
PrimaryTag/styles/normal = SubResource("StyleBoxFlat_jtb1k")
RichTextLabel/colors/default_color = Color(0.2901961, 0.24705882, 0.17254902, 1)
RichTextLabel/font_sizes/normal_font_size = 20
RichTextLabel/fonts/italics_font = ExtResource("4_i0b23")
RichTextLabel/fonts/normal_font = ExtResource("3_eckwq")
RichTextLabel/fonts/normal_font = ExtResource("2_4lwr2")
SectionLabel/base_type = &"Label"
SectionLabel/colors/font_color = Color(0.5411765, 0.46666667, 0.28235295, 1)
SectionLabel/font_sizes/font_size = 13
SectionLabel/fonts/font = ExtResource("2_4lwr2")
SectionLabel/fonts/font = ExtResource("3_eckwq")
SelectCard/base_type = &"Button"
SelectCard/colors/font_color = Color(0.22745098, 0.18431373, 0.10980392, 1)
SelectCard/colors/font_disabled_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
SelectCard/styles/disabled = SubResource("StyleBoxFlat_jtb1k")
SelectCard/styles/focus = SubResource("StyleBoxEmpty_wmbj4")
SelectCard/styles/hover = SubResource("StyleBoxFlat_xdylb")
SelectCard/styles/normal = SubResource("StyleBoxFlat_pjnll")
SelectCard/styles/pressed = SubResource("StyleBoxFlat_klelq")
SelectCard/styles/disabled = SubResource("StyleBoxFlat_wmbj4")
SelectCard/styles/focus = SubResource("StyleBoxEmpty_xdylb")
SelectCard/styles/hover = SubResource("StyleBoxFlat_pjnll")
SelectCard/styles/normal = SubResource("StyleBoxFlat_klelq")
SelectCard/styles/pressed = SubResource("StyleBoxFlat_i2sl5")
SkillChip/base_type = &"Button"
SkillChip/colors/font_color = Color(0.3529412, 0.2627451, 0.14901961, 1)
SkillChip/colors/font_disabled_color = Color(0.6627451, 0.5803922, 0.39215687, 1)
SkillChip/colors/font_hover_color = Color(0.3529412, 0.2627451, 0.14901961, 1)
SkillChip/colors/font_pressed_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
SkillChip/font_sizes/font_size = 12
SkillChip/fonts/font = ExtResource("2_4lwr2")
SkillChip/styles/disabled = SubResource("StyleBoxFlat_i2sl5")
SkillChip/styles/focus = SubResource("StyleBoxEmpty_onsig")
SkillChip/styles/hover = SubResource("StyleBoxFlat_gw0t7")
SkillChip/styles/normal = SubResource("StyleBoxFlat_88l5g")
SkillChip/styles/pressed = SubResource("StyleBoxFlat_f8qac")
SkillChip/fonts/font = ExtResource("3_eckwq")
SkillChip/styles/disabled = SubResource("StyleBoxFlat_onsig")
SkillChip/styles/focus = SubResource("StyleBoxEmpty_gw0t7")
SkillChip/styles/hover = SubResource("StyleBoxFlat_88l5g")
SkillChip/styles/normal = SubResource("StyleBoxFlat_f8qac")
SkillChip/styles/pressed = SubResource("StyleBoxFlat_u6gcs")
Tab/base_type = &"Button"
Tab/colors/font_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
Tab/styles/focus = SubResource("StyleBoxEmpty_u6gcs")
Tab/styles/hover = SubResource("StyleBoxFlat_nufy3")
Tab/styles/normal = SubResource("StyleBoxFlat_0d7vs")
Tab/styles/pressed = SubResource("StyleBoxFlat_3fnfk")
Tab/styles/focus = SubResource("StyleBoxEmpty_nufy3")
Tab/styles/hover = SubResource("StyleBoxFlat_0d7vs")
Tab/styles/normal = SubResource("StyleBoxFlat_3fnfk")
Tab/styles/pressed = SubResource("StyleBoxFlat_2txdw")
TabActive/base_type = &"Button"
TabActive/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
TabActive/styles/focus = SubResource("StyleBoxEmpty_2txdw")
TabActive/styles/hover = SubResource("StyleBoxFlat_w7qkp")
TabActive/styles/normal = SubResource("StyleBoxFlat_rthnr")
TabActive/styles/pressed = SubResource("StyleBoxFlat_ta3oh")
TabActive/styles/focus = SubResource("StyleBoxEmpty_w7qkp")
TabActive/styles/hover = SubResource("StyleBoxFlat_rthnr")
TabActive/styles/normal = SubResource("StyleBoxFlat_ta3oh")
TabActive/styles/pressed = SubResource("StyleBoxFlat_ohwwq")
TitleKicker/base_type = &"Label"
TitleKicker/colors/font_color = Color(0.6627451, 0.5176471, 0.24705882, 1)
TitleKicker/font_sizes/font_size = 13
TitleKicker/fonts/font = ExtResource("2_4lwr2")
TitleKicker/fonts/font = ExtResource("3_eckwq")
TitleLogo/base_type = &"Label"
TitleLogo/colors/font_color = Color(0.9098039, 0.8666667, 0.78431374, 1)
TitleLogo/font_sizes/font_size = 116
TitleLogo/fonts/font = ExtResource("3_eckwq")
TitleLogo/fonts/font = ExtResource("2_4lwr2")

View File

@@ -11,19 +11,28 @@ config_version=5
[application]
config/name="coc-rpg"
config/version="0.01-alpha"
config/description="AI-driven single-player party RPG. Code owns state. AI owns text."
config/version="0.01-alpha"
run/main_scene="res://scenes/title/TitleScreen.tscn"
config/features=PackedStringArray("4.7")
[display]
; Design/logical canvas stays 1920x1080 (charter §16, mockups authored here).
; The DESIGN CANVAS stays 1920x1080 — the mockups (§16) are authored there, and every
; .tscn's offsets are in those coordinates. It is a logical canvas, not a demand on the
; monitor: canvas_items + keep scales it uniformly into whatever window it is given, so
; the UI never reflows, it only gets smaller. Nothing below changes a layout number.
window/size/viewport_width=1920
window/size/viewport_height=1080
; Open a smaller 1600x900 window that scales the 1920x1080 canvas down, so it
; doesn't fill the whole screen. canvas_items + keep aspect does the scaling.
window/size/window_width_override=1600
window/size/window_height_override=900
; The WINDOW is what the laptop has to fit, and it is a different question. 1600x900 was
; larger than a 1600x900 laptop's *usable* area once the WM took its panel and titlebar,
; so the window manager clamped it to 1589x752 and the run came up pillarboxed. 1280x720
; is 16:9 and fits any laptop this ships to; the window is resizable (Godot's default),
; and canvas_items rescales the canvas live on every resize.
window/size/window_width_override=1280
window/size/window_height_override=720
window/size/resizable=true
window/stretch/mode="canvas_items"
; keep = letterbox rather than distort or reflow. This IS Godot 4's default, so the editor
; prunes the line whenever it rewrites this file — that is cosmetic, not a behaviour change.
window/stretch/aspect="keep"

View File

@@ -145,7 +145,7 @@ layout_mode = 2
theme_override_constants/separation = 14
[node name="Race0" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"]
custom_minimum_size = Vector2(0, 108)
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
size_flags_horizontal = 3
toggle_mode = true
@@ -166,6 +166,7 @@ theme_override_constants/separation = 5
[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"]
layout_mode = 2
mouse_filter = 2
theme_type_variation = &"CardTitle"
text = "Human"
[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"]
@@ -173,7 +174,7 @@ layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
autowrap_mode = 3
theme_override_font_sizes/font_size = 14
theme_type_variation = &"CardBody"
text = "…"
[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"]
@@ -199,7 +200,7 @@ theme_type_variation = &"ChosenTag"
text = "CHOSEN"
[node name="Race1" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"]
custom_minimum_size = Vector2(0, 108)
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
size_flags_horizontal = 3
toggle_mode = true
@@ -220,6 +221,7 @@ theme_override_constants/separation = 5
[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"]
layout_mode = 2
mouse_filter = 2
theme_type_variation = &"CardTitle"
text = "Elf"
[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"]
@@ -227,7 +229,7 @@ layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
autowrap_mode = 3
theme_override_font_sizes/font_size = 14
theme_type_variation = &"CardBody"
text = "…"
[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"]
@@ -253,7 +255,7 @@ theme_type_variation = &"ChosenTag"
text = "CHOSEN"
[node name="Race2" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"]
custom_minimum_size = Vector2(0, 108)
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
size_flags_horizontal = 3
toggle_mode = true
@@ -274,6 +276,7 @@ theme_override_constants/separation = 5
[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"]
layout_mode = 2
mouse_filter = 2
theme_type_variation = &"CardTitle"
text = "Dwarf"
[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"]
@@ -281,7 +284,7 @@ layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
autowrap_mode = 3
theme_override_font_sizes/font_size = 14
theme_type_variation = &"CardBody"
text = "…"
[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"]
@@ -307,7 +310,7 @@ theme_type_variation = &"ChosenTag"
text = "CHOSEN"
[node name="Race3" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"]
custom_minimum_size = Vector2(0, 108)
custom_minimum_size = Vector2(0, 140)
layout_mode = 2
size_flags_horizontal = 3
toggle_mode = true
@@ -328,6 +331,7 @@ theme_override_constants/separation = 5
[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"]
layout_mode = 2
mouse_filter = 2
theme_type_variation = &"CardTitle"
text = "Beastfolk"
[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"]
@@ -335,7 +339,7 @@ layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
autowrap_mode = 3
theme_override_font_sizes/font_size = 14
theme_type_variation = &"CardBody"
text = "…"
[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"]
@@ -393,7 +397,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Sellsword"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling0/Box"]
@@ -407,13 +412,13 @@ text = "STR"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -438,7 +443,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Reaver"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling1/Box"]
@@ -452,13 +458,13 @@ text = "STR"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -483,7 +489,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Cutpurse"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling2/Box"]
@@ -497,13 +504,13 @@ text = "DEX"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -528,7 +535,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Trapper"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling3/Box"]
@@ -542,13 +550,13 @@ text = "DEX"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -573,7 +581,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Hedge-Mage"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling4/Box"]
@@ -587,13 +596,13 @@ text = "MAG"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -618,7 +627,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Bonesetter"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling5/Box"]
@@ -632,13 +642,13 @@ text = "FTH"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -663,7 +673,8 @@ theme_override_constants/separation = 5
layout_mode = 2
mouse_filter = 2
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_type_variation = &"CardTitle"
theme_override_font_sizes/font_size = 17
text = "Bloodsworn"
[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling6/Box"]
@@ -677,13 +688,13 @@ text = "FTH"
visible = false
layout_mode = 1
anchors_preset = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -33.0
anchor_left = 1.0
anchor_right = 1.0
offset_left = -74.0
offset_top = -11.0
offset_right = 33.0
offset_right = -10.0
offset_bottom = 9.0
grow_horizontal = 2
grow_horizontal = 0
mouse_filter = 2
theme_type_variation = &"ChosenTag"
text = "CHOSEN"
@@ -853,7 +864,7 @@ text = "⟳ re-roll"
layout_mode = 2
theme_type_variation = &"Accent"
theme_override_font_sizes/font_size = 15
text = "rolled 3d6 — that roll is your floor, spend up only"
text = "rolled 3d6, never below 8 — that roll is your floor, spend up only"
[node name="Cards" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection"]
layout_mode = 2
@@ -863,7 +874,7 @@ theme_override_constants/separation = 12
custom_minimum_size = Vector2(0, 132)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"ParchmentCard"
theme_type_variation = &"AbilityCard"
[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0"]
layout_mode = 2
@@ -906,18 +917,24 @@ layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "+"
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0"]
[node name="Overlay" type="Control" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0"]
layout_mode = 2
mouse_filter = 2
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Overlay"]
visible = false
layout_mode = 1
anchors_preset = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -32.0
offset_top = -10.0
offset_right = 32.0
offset_bottom = 8.0
offset_left = -42.0
offset_top = -26.0
offset_right = 42.0
offset_bottom = -6.0
grow_horizontal = 2
mouse_filter = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"PrimaryTag"
text = "PRIMARY"
@@ -925,7 +942,7 @@ text = "PRIMARY"
custom_minimum_size = Vector2(0, 132)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"ParchmentCard"
theme_type_variation = &"AbilityCard"
[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1"]
layout_mode = 2
@@ -968,18 +985,24 @@ layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "+"
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1"]
[node name="Overlay" type="Control" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1"]
layout_mode = 2
mouse_filter = 2
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Overlay"]
visible = false
layout_mode = 1
anchors_preset = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -32.0
offset_top = -10.0
offset_right = 32.0
offset_bottom = 8.0
offset_left = -42.0
offset_top = -26.0
offset_right = 42.0
offset_bottom = -6.0
grow_horizontal = 2
mouse_filter = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"PrimaryTag"
text = "PRIMARY"
@@ -987,7 +1010,7 @@ text = "PRIMARY"
custom_minimum_size = Vector2(0, 132)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"ParchmentCard"
theme_type_variation = &"AbilityCard"
[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2"]
layout_mode = 2
@@ -1030,18 +1053,24 @@ layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "+"
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2"]
[node name="Overlay" type="Control" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2"]
layout_mode = 2
mouse_filter = 2
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Overlay"]
visible = false
layout_mode = 1
anchors_preset = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -32.0
offset_top = -10.0
offset_right = 32.0
offset_bottom = 8.0
offset_left = -42.0
offset_top = -26.0
offset_right = 42.0
offset_bottom = -6.0
grow_horizontal = 2
mouse_filter = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"PrimaryTag"
text = "PRIMARY"
@@ -1049,7 +1078,7 @@ text = "PRIMARY"
custom_minimum_size = Vector2(0, 132)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"ParchmentCard"
theme_type_variation = &"AbilityCard"
[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3"]
layout_mode = 2
@@ -1092,18 +1121,24 @@ layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "+"
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3"]
[node name="Overlay" type="Control" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3"]
layout_mode = 2
mouse_filter = 2
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Overlay"]
visible = false
layout_mode = 1
anchors_preset = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -32.0
offset_top = -10.0
offset_right = 32.0
offset_bottom = 8.0
offset_left = -42.0
offset_top = -26.0
offset_right = 42.0
offset_bottom = -6.0
grow_horizontal = 2
mouse_filter = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"PrimaryTag"
text = "PRIMARY"
@@ -1111,7 +1146,7 @@ text = "PRIMARY"
custom_minimum_size = Vector2(0, 132)
layout_mode = 2
size_flags_horizontal = 3
theme_type_variation = &"ParchmentCard"
theme_type_variation = &"AbilityCard"
[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4"]
layout_mode = 2
@@ -1154,18 +1189,24 @@ layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "+"
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4"]
[node name="Overlay" type="Control" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4"]
layout_mode = 2
mouse_filter = 2
[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Overlay"]
visible = false
layout_mode = 1
anchors_preset = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -32.0
offset_top = -10.0
offset_right = 32.0
offset_bottom = 8.0
offset_left = -42.0
offset_top = -26.0
offset_right = 42.0
offset_bottom = -6.0
grow_horizontal = 2
mouse_filter = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"PrimaryTag"
text = "PRIMARY"

View File

@@ -50,6 +50,23 @@ static func build_theme() -> Theme:
return theme
static func _pill(bg: Color, radius: int, margin_x: int, margin_y: int) -> StyleBoxFlat:
# A BADGE, not a card. _flat() bakes in the 12/6 content margin every panel and
# button on the sheet wants — on a 9px "PRIMARY" flag that padding is most of the
# control, which is what made the badge swallow the ability card's rolled value.
# The mock's flags are padding:2px 9px (PRIMARY) and 3px 8px (CHOSEN).
var s := StyleBoxFlat.new()
s.bg_color = bg
s.border_color = bg
s.set_border_width_all(0)
s.set_corner_radius_all(radius)
s.content_margin_left = margin_x
s.content_margin_right = margin_x
s.content_margin_top = margin_y
s.content_margin_bottom = margin_y
return s
static func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
var s := StyleBoxFlat.new()
s.bg_color = bg
@@ -83,6 +100,20 @@ static func _fonts(theme: Theme) -> void:
theme.set_font(&"font", ThemeKeys.MONO, load(MONO))
theme.set_font_size(&"font_size", ThemeKeys.MONO, 13)
theme.set_color(&"font_color", ThemeKeys.MONO, Palette.MUTED_MONO)
# Card type — the name + blurb inside a select-card. These sit ON PARCHMENT, so
# they take INK, not CREAM. Godot's built-in default Label colour is WHITE, and a
# Label left bare inherits it: the race and calling names shipped invisible on the
# parchment sheet for exactly that reason. There is no such thing as an unstyled
# Label on this sheet — every one of them names a role. (mock: 19px/17px #3a2f1c
# title, 13px italic-serif #6a5a3a blurb.)
theme.set_type_variation(ThemeKeys.CARD_TITLE, "Label")
theme.set_font(&"font", ThemeKeys.CARD_TITLE, serif)
theme.set_font_size(&"font_size", ThemeKeys.CARD_TITLE, 19)
theme.set_color(&"font_color", ThemeKeys.CARD_TITLE, Palette.INK_HEADING)
theme.set_type_variation(ThemeKeys.CARD_BODY, "Label")
theme.set_font(&"font", ThemeKeys.CARD_BODY, serif)
theme.set_font_size(&"font_size", ThemeKeys.CARD_BODY, 13)
theme.set_color(&"font_color", ThemeKeys.CARD_BODY, Palette.INK_MUTED)
static func _title_type(theme: Theme) -> void:
@@ -231,21 +262,38 @@ static func _tags_and_primary(theme: Theme) -> void:
# calling's primary attribute wears. Label takes a `normal` stylebox in Godot 4.
var chosen := ThemeKeys.CHOSEN_TAG
theme.set_type_variation(chosen, "Label")
theme.set_stylebox(&"normal", chosen, _flat(Palette.BLOOD, Palette.BLOOD, 1, 5))
theme.set_stylebox(&"normal", chosen, _pill(Palette.BLOOD, 5, 8, 3))
theme.set_font(&"font", chosen, load(MONO))
theme.set_font_size(&"font_size", chosen, 10)
theme.set_color(&"font_color", chosen, Palette.CREAM_BRIGHT)
var primary_tag := ThemeKeys.PRIMARY_TAG
theme.set_type_variation(primary_tag, "Label")
theme.set_stylebox(&"normal", primary_tag, _flat(Palette.INK_GOLD, Palette.INK_GOLD, 1, 5))
theme.set_stylebox(&"normal", primary_tag, _pill(Palette.INK_GOLD, 5, 9, 2))
theme.set_font(&"font", primary_tag, load(MONO))
theme.set_font_size(&"font_size", primary_tag, 9)
theme.set_color(&"font_color", primary_tag, Palette.CREAM_BRIGHT)
# The ability card in both states. It is NOT a plain ParchmentCard: it wears the PRIMARY
# flag on its top edge, and _flat()'s 6px top margin puts the "STR" key label directly
# under the badge — they collided. The mock's card is padding:16px 10px 14px, and that
# 16px IS the headroom the badge needs. ParchmentCard cannot simply be retuned: the
# shell shares it, and the shell has no badge.
theme.set_stylebox(&"panel", ThemeKeys.ABILITY_CARD,
_ability_card(Palette.PARCHMENT_BORDER, 1))
theme.set_type_variation(ThemeKeys.ABILITY_CARD, "PanelContainer")
theme.set_type_variation(ThemeKeys.PRIMARY_CARD, "PanelContainer")
theme.set_stylebox(&"panel", ThemeKeys.PRIMARY_CARD,
_flat(Palette.PARCHMENT_CARD, Palette.INK_GOLD, 2, 12))
_ability_card(Palette.INK_GOLD, 2))
static func _ability_card(border: Color, width: int) -> StyleBoxFlat:
var s := _flat(Palette.PARCHMENT_CARD, border, width, 12)
s.content_margin_top = 16
s.content_margin_bottom = 14
s.content_margin_left = 10
s.content_margin_right = 10
return s
static func _tiles(theme: Theme) -> void:

View File

@@ -21,6 +21,11 @@ const SKILL_CHIP := &"SkillChip" # toggle: unpicked / picked / granted-so-
const CHOSEN_TAG := &"ChosenTag" # the little crimson "CHOSEN" flag
const PRIMARY_TAG := &"PrimaryTag" # the little gold "PRIMARY" flag
const PRIMARY_CARD := &"PrimaryCard" # the ability card ringed as the calling's primary
const ABILITY_CARD := &"AbilityCard" # ...and its UNringed state. Not ParchmentCard: the
# ability card carries a badge on its top edge and
# needs the headroom for it (mock padding-top 16 vs
# the generic card's 6), and ParchmentCard is shared
# with the shell.
## Font roles (Label type-variations carrying a font, not a stylebox). Kept
## separate from ALL below, which is stylebox variations + their base type only.
@@ -30,6 +35,8 @@ const MONO := &"Mono"
const TITLE_LOGO := &"TitleLogo"
const TITLE_KICKER := &"TitleKicker"
const SECTION_LABEL := &"SectionLabel"
const CARD_TITLE := &"CardTitle" # a select-card's name, dark ink ON PARCHMENT
const CARD_BODY := &"CardBody" # a select-card's blurb, muted ink ON PARCHMENT
## Every FONT role + the base Control type it decorates. Deliberately NOT folded
## into ALL — ALL's "stylebox variations only" contract is relied on elsewhere
@@ -45,6 +52,8 @@ const FONT_ROLES := {
TITLE_LOGO: "Label",
TITLE_KICKER: "Label",
SECTION_LABEL: "Label",
CARD_TITLE: "Label",
CARD_BODY: "Label",
}
## Base Control types the builder styles DIRECTLY — no set_type_variation is
@@ -79,4 +88,5 @@ const ALL := {
CHOSEN_TAG: "Label",
PRIMARY_TAG: "Label",
PRIMARY_CARD: "PanelContainer",
ABILITY_CARD: "PanelContainer",
}

View File

@@ -258,8 +258,12 @@ func _bind_abilities() -> void:
int(rolled[stat]), (" +%d" % spent) if spent > 0 else ""]
card.get_node("Box/Buttons/Plus").disabled = not draft.can_increment(stat)
card.get_node("Box/Buttons/Minus").disabled = not draft.can_decrement(stat)
card.get_node("Tag").visible = (stat == primary)
card.theme_type_variation = ThemeKeys.PRIMARY_CARD if stat == primary else ThemeKeys.PARCHMENT_CARD
# Overlay/Tag, not Tag: the ability card is a PanelContainer, and a Container
# force-fits every child it has — which stretched the badge across the card and
# parked it on top of the rolled value. The Control overlay is not a Container,
# so the badge's anchors survive and it rides the card's top edge (ADR 0001).
card.get_node("Overlay/Tag").visible = (stat == primary)
card.theme_type_variation = ThemeKeys.PRIMARY_CARD if stat == primary else ThemeKeys.ABILITY_CARD
func _bind_bay() -> void:

View File

@@ -2,6 +2,7 @@ extends "res://addons/gut/test.gd"
const SCENE := "res://scenes/creation/CharacterCreation.tscn"
const ContentDB = preload("res://scripts/content/content_db.gd")
const NewGame = preload("res://scripts/newgame/new_game.gd")
func _world() -> ContentDB:
@@ -592,9 +593,9 @@ func test_ability_card_surfaces_the_calling_s_primary_stat():
var stat: String = Attributes.IDS[i]
var card: PanelContainer = s._ability_cards[i]
var is_primary := (stat == primary)
assert_eq(card.get_node("Tag").visible, is_primary,
assert_eq(card.get_node("Overlay/Tag").visible, is_primary,
"%s's Tag must show only when it is the calling's primary stat" % stat)
var expected_variation: StringName = ThemeKeys.PRIMARY_CARD if is_primary else ThemeKeys.PARCHMENT_CARD
var expected_variation: StringName = ThemeKeys.PRIMARY_CARD if is_primary else ThemeKeys.ABILITY_CARD
assert_eq(card.theme_type_variation, expected_variation,
"%s's card style must reflect whether it is the primary stat" % stat)
@@ -610,3 +611,151 @@ func test_pressing_enter_on_an_illegal_draft_emits_nothing():
s._enter.pressed.emit()
assert_signal_not_emitted(s, "creation_confirmed",
"an illegal draft must never reach creation_confirmed, even when Enter fires anyway")
# ------------------------------------------- the screen a human actually has to read
## Luminance gap a Label must clear against the surface it is drawn on. Every styled
## Label on the sheet measures >= 0.419 (the dimmest is SectionLabel's ink); an UNSTYLED
## Label — Godot's built-in Label colour is WHITE — measures 0.112 on parchment. The
## threshold sits in that gap, nearer the failure so it cannot be squeaked past.
const MIN_CONTRAST := 0.30
func _labels_under(n: Node, out: Array = []) -> Array:
for c in n.get_children():
if c is Label:
out.append(c)
_labels_under(c, out)
return out
func _laid_out() -> void:
## Geometry assertions are meaningless until the container tree has actually sorted.
## On the frame add_child() runs, a race card reports 102px wide and its autowrapped
## blurb reports a 2508px minimum height (one unwrapped line) — numbers that would
## make a size guard fail for the wrong reason, or pass for one.
await get_tree().process_frame
await get_tree().process_frame
func test_no_label_on_the_parchment_sheet_is_unreadable():
# The race and calling names, and the race blurbs, SHIPPED INVISIBLE: 15 Labels were
# left with no type-variation, so they inherited Godot's default Label colour — white
# — and rendered white-on-parchment. The trait line beneath them was the only readable
# string on the card. Nothing in the suite could see it, because every assertion was
# about text CONTENT, and the content was correct. It was the colour that was wrong.
#
# This is TOTAL: no Label is skipped. A Label that paints its own background (the
# CHOSEN / PRIMARY pills) is judged against THAT background rather than the sheet, so
# it is checked, not exempted — a whitelist here would be the same hole traps.md #16
# describes, where the guard's own `continue` covered the gap it was hunting.
var s := _screen()
var sheet: Control = s.get_node("Split/Sheet")
var parchment := Palette.SHEET_TOP.get_luminance()
var checked := 0
for lab in _labels_under(sheet):
var col: Color = lab.get_theme_color(&"font_color")
var bg := parchment
var surface := "the parchment sheet"
var sb: StyleBox = lab.get_theme_stylebox(&"normal")
if sb is StyleBoxFlat and (sb as StyleBoxFlat).bg_color.a > 0.0:
bg = (sb as StyleBoxFlat).bg_color.get_luminance()
surface = "its own pill"
checked += 1
assert_gt(absf(col.get_luminance() - bg), MIN_CONTRAST,
"'%s' (variation '%s') is unreadable on %s: text luminance %.3f vs surface %.3f" % [
lab.name, str(lab.theme_type_variation), surface, col.get_luminance(), bg])
# 15 Labels carried the bug. If this sweep ever walks fewer than that it has stopped
# reaching the cards, and a green result would mean nothing.
assert_gt(checked, 15, "the sweep must actually reach the sheet's labels, not an empty subtree")
func test_the_primary_badge_rides_the_card_edge_instead_of_covering_the_roll():
# The ability card is a PanelContainer. A Container FORCE-FITS its children, so the
# badge's authored anchors were dead letters: it stretched to the card's full content
# width (200 of 224px) and centred itself vertically, straight across the rolled value
# — measured, not guessed. Nesting it under a plain Control (NOT a Container) restores
# absolute positioning. These three assertions are the three ways that regressed.
var s := _screen()
_complete_a_valid_draft(s)
await _laid_out()
var primary: String = Callings.primary("sellsword")
var i: int = Attributes.IDS.find(primary)
assert_gt(i, -1, "the sellsword's primary must be a real attribute, or this test proves nothing")
var card: PanelContainer = s._ability_cards[i]
var tag: Label = card.get_node("Overlay/Tag")
var value: Label = card.get_node("Box/Value")
assert_true(tag.visible, "the primary card must show its badge, or the geometry below is untested")
var tag_r := tag.get_global_rect()
var card_r := card.get_global_rect()
var value_r := value.get_global_rect()
assert_lt(tag_r.position.y, card_r.position.y,
"the badge must RIDE the card's top edge, not sit inside the card")
assert_lt(tag_r.size.x, card_r.size.x * 0.6,
"the badge is a flag, not a banner — it stretched to the card's full width (%.0f of %.0f)" % [
tag_r.size.x, card_r.size.x])
assert_false(tag_r.intersects(value_r),
"the badge is covering the rolled value: badge %s vs value %s" % [tag_r, value_r])
func test_no_card_overflows_its_own_border():
# "+1 skill of choice" and "Claws · Keen scent" rendered BELOW the race card's bottom
# border: the card's height is a fixed custom_minimum_size, and its Box is anchored
# inside it rather than being a container child — so content CANNOT push the card
# taller, it just spills out. Every card must be authored tall enough for its own text.
var s := _screen()
await _laid_out()
var cards: Array = []
cards.append_array(s._race_cards)
cards.append_array(s._calling_cards)
assert_gt(cards.size(), 4, "no cards reached the sweep")
# Measure against the CARD, never against the Box's own size. Godot clamps a Control's
# size UP to its combined minimum, so box.size.y is forced equal to the very number it
# would be compared against — `needed <= box.size.y` is a tautology that stays green
# with the card at its original, overflowing 108px. It was written that way first and
# it passed against the bug it names (traps.md: a green suite is evidence only if the
# test can fail). The card's height is the thing the text actually spills out of.
for card in cards:
var box: VBoxContainer = card.get_node("Box")
var needed := box.get_combined_minimum_size().y
# The Box is anchored inside the card; offset_bottom is negative, so this is the
# card's height less its top and bottom insets.
var available: float = card.size.y - box.offset_top + box.offset_bottom
assert_lte(needed, available,
"'%s' overflows its card: content needs %.0fpx, the card leaves %.0fpx" % [
box.get_node("Name").text, needed, available])
func test_the_hint_label_advertises_the_floor_the_roll_actually_enforces():
# The label used to say "rolled 3d6" and stop there, but the roll is maxi(8, 3d6)
# (new_game.gd) — so a player who rolled low read a label that looked like it was
# lying to him. The label now names the floor, and this pins it TO THE CODE.
#
# The floor is OBSERVED, never hardcoded: roll a deterministic spread of seeds and
# take the minimum the player can actually be dealt. Retune maxi(8, ..) to 7 and the
# observed floor becomes 7, the label still claims 8, and this goes red. Delete the
# clamp and the floor falls to 3 — red. Reword the label past the claim — red.
#
# It costs the phrase "never below N": reword that and update this string, do not
# delete the guard. The number must keep coming from the roll.
var observed_floor := 99
for seed_value in range(1, 201): # deterministic — no RNG in the test itself
for stat in Attributes.IDS:
observed_floor = mini(observed_floor, int(NewGame.roll_attributes(seed_value)[stat]))
# ~16% of 3d6 lands at or under 7, so across 1000 rolls the minimum is the CLAMP,
# not the luck of the draw. Assert the clamp is actually binding rather than trusting
# that: an unclamped 3d6 reaches 3, and a floor at or below 3 would make the pin vacuous.
assert_true(observed_floor > 3,
"the roll's minimum is %d — the clamp is not binding, so the label's floor claim is a lie" % observed_floor)
var hint: Label = _screen().get_node("Split/Sheet/Body/AbilitySection/Head/Hint")
assert_string_contains(hint.text, "never below %d" % observed_floor)
assert_string_contains(hint.text, "3d6")

View File

@@ -1 +1 @@
{ "id": "cutpurse", "name": "Cutpurse", "blurb": "Purses, locks, confidences — you have taken all three. The trick was never the hands. It was knowing which pocket was worth it.", "fragment": "quick fingers, quicker exits, and a knife for the rest." }
{ "id": "cutpurse", "name": "Cutpurse", "blurb": "Purses, locks, confidences — you have taken all three. The trick was never the hands. It was knowing which pocket was worth it.", "fragment": "you pick your man and take what he will not miss until morning, and the towns where you picked wrong still have your description at the gate." }

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.