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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user