diff --git a/.gitignore b/.gitignore index 22b6020..57fd54c 100644 --- a/.gitignore +++ b/.gitignore @@ -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/) # ───────────────────────────────────────────── diff --git a/client/assets/theme/game_theme.tres b/client/assets/theme/game_theme.tres index 2f94a07..889c6fe 100644 --- a/client/assets/theme/game_theme.tres +++ b/client/assets/theme/game_theme.tres @@ -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") diff --git a/client/project.godot b/client/project.godot index 0ea126e..29567d0 100644 --- a/client/project.godot +++ b/client/project.godot @@ -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" diff --git a/client/scenes/creation/CharacterCreation.tscn b/client/scenes/creation/CharacterCreation.tscn index 0636818..403f675 100644 --- a/client/scenes/creation/CharacterCreation.tscn +++ b/client/scenes/creation/CharacterCreation.tscn @@ -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" diff --git a/client/scripts/theme/build_game_theme.gd b/client/scripts/theme/build_game_theme.gd index 60e027e..9dd5664 100644 --- a/client/scripts/theme/build_game_theme.gd +++ b/client/scripts/theme/build_game_theme.gd @@ -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: diff --git a/client/scripts/theme/theme_keys.gd b/client/scripts/theme/theme_keys.gd index 8bffbf6..5358793 100644 --- a/client/scripts/theme/theme_keys.gd +++ b/client/scripts/theme/theme_keys.gd @@ -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", } diff --git a/client/scripts/ui/creation/character_creation.gd b/client/scripts/ui/creation/character_creation.gd index e25c363..29c0b53 100644 --- a/client/scripts/ui/creation/character_creation.gd +++ b/client/scripts/ui/creation/character_creation.gd @@ -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: diff --git a/client/tests/unit/test_character_creation_screen.gd b/client/tests/unit/test_character_creation_screen.gd index f5eb251..c18266a 100644 --- a/client/tests/unit/test_character_creation_screen.gd +++ b/client/tests/unit/test_character_creation_screen.gd @@ -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") diff --git a/content/world/callings/cutpurse.json b/content/world/callings/cutpurse.json index 296da5e..23013d3 100644 --- a/content/world/callings/cutpurse.json +++ b/content/world/callings/cutpurse.json @@ -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." } diff --git a/docs/traps.md b/docs/traps.md index 835835f..6c1f6e5 100644 --- a/docs/traps.md +++ b/docs/traps.md @@ -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.