Harden UI layout for font scale and long strings
- Measure and cap the HUD target chip after each font-scale change; clip HUD top labels. - Ellipsize long action-button labels so panels cannot grow past the viewport. - Add accessibility/button layout assertions and refresh the gallery captures. - Record the font/layout pass in TODO, docs, changelog and memory.
No files matched your search
@@ -17,6 +17,9 @@ Semantic Versioning.
|
|||||||
- Esc and right-click now close the top panel consistently, restore the venue
|
- Esc and right-click now close the top panel consistently, restore the venue
|
||||||
menu after menu Settings, and recapture the mouse without leaving the guard
|
menu after menu Settings, and recapture the mouse without leaving the guard
|
||||||
frozen. Help no longer opens Pause behind itself.
|
frozen. Help no longer opens Pause behind itself.
|
||||||
|
- The HUD target chip grows and caps with the selected font scale, top labels
|
||||||
|
clip safely, and long action-button labels ellipsize instead of pushing panels
|
||||||
|
wider than the screen.
|
||||||
- NPC reason answers now use role-specific dialogue contexts, so staff, guests,
|
- NPC reason answers now use role-specific dialogue contexts, so staff, guests,
|
||||||
delivery workers, performers and other archetypes answer instead of silently
|
delivery workers, performers and other archetypes answer instead of silently
|
||||||
deflecting the Reason verb.
|
deflecting the Reason verb.
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
|
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
|
||||||
portrait measured a 0.732 vision gap against the photoreal concept. No
|
portrait measured a 0.732 vision gap against the photoreal concept. No
|
||||||
release build, version bump or tag was created at this gate.
|
release build, version bump or tag was created at this gate.
|
||||||
|
- **Font/layout limits (2026-11-02):** HUD scale is clamped to 0.75–1.75 and only
|
||||||
|
applies to HUD labels. `Hud.set_target_label()` measures the scaled font, widens
|
||||||
|
the chip to 180–420 px and re-measures after a scale change; top labels clip.
|
||||||
|
`UiStyles.apply_button()` sets `clip_text` + ellipsis so long localized labels
|
||||||
|
cannot force a panel past the viewport. Do not replace these with Control.scale.
|
||||||
- **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single
|
- **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single
|
||||||
Esc/right-click path for radial, inspection, wand, radio, timeline, Help,
|
Esc/right-click path for radial, inspection, wand, radio, timeline, Help,
|
||||||
Settings, Pause and review. Settings remembers whether it came from the menu and
|
Settings, Pause and review. Settings remembers whether it came from the menu and
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ screen-design judge before any model/texture changes. No gameplay rules change.
|
|||||||
top-modal close path, menu/settings return handling and pointer recapture are
|
top-modal close path, menu/settings return handling and pointer recapture are
|
||||||
recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help
|
recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help
|
||||||
right-click and pause Settings.)
|
right-click and pause Settings.)
|
||||||
- [ ] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer
|
- [x] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer
|
||||||
strings; fix clipping, overflow and overlapping layouts.
|
strings; fix clipping, overflow and overlapping layouts. (Target chip now
|
||||||
|
measures/caps scaled text, HUD top labels clip, and action buttons ellipsize;
|
||||||
|
regression checks are in `test_accessibility.gd` and `test_ui_buttons.gd`.)
|
||||||
- [ ] Test destructive/irreversible actions (campaign reset, tutorial replay,
|
- [ ] Test destructive/irreversible actions (campaign reset, tutorial replay,
|
||||||
emergency send) require the intended confirmation/hold and cannot double-fire.
|
emergency send) require the intended confirmation/hold and cannot double-fire.
|
||||||
- [ ] Verify radio normal-send vs emergency hold, inspection/wand allow/deny,
|
- [ ] Verify radio normal-send vs emergency hold, inspection/wand allow/deny,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ Implemented so far:
|
|||||||
- Joke Shop / Sunset Strip map (map optimization): the second venue is now a stylized reconstruction of The Comedy Store block at 8433 Sunset Blvd, with a 27 m black hall, signature-covered sloping canopy, circular **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream and white low-rise neighbours, a dry hillside with stucco houses and palms behind the roofline, and a low retail/billboard frontage across Sunset. The same geometry serves day and late lighting through the phase system. Real-site references and limits are in [`joke-shop-map.md`](joke-shop-map.md); the map measures **22,680 triangles across 454 meshes** in the budget test.
|
- Joke Shop / Sunset Strip map (map optimization): the second venue is now a stylized reconstruction of The Comedy Store block at 8433 Sunset Blvd, with a 27 m black hall, signature-covered sloping canopy, circular **THE JOKE SHOP** blade sign, red velvet curtain bays, a west-end entrance, cream and white low-rise neighbours, a dry hillside with stucco houses and palms behind the roofline, and a low retail/billboard frontage across Sunset. The same geometry serves day and late lighting through the phase system. Real-site references and limits are in [`joke-shop-map.md`](joke-shop-map.md); the map measures **22,680 triangles across 454 meshes** in the budget test.
|
||||||
- Venue geometry (v0.7.0, Joke Shop frontage superseded by the map-optimization pass): the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes.
|
- Venue geometry (v0.7.0, Joke Shop frontage superseded by the map-optimization pass): the TV Studio builds a stone facade, bulb canopy, glossy black car and barrier line. `WorldBuilder.VENUE_SPECS` drives facade/street/nav dimensions, and `GameManager` rebuilds only the world container when the venue changes.
|
||||||
- Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report.
|
- Gear fit (post-v0.6.0): Blender gear is measured against the avatar head/skirt silhouette and the checks run in both the Python tooling and the headless suite. Sunglasses, the shoulder bag and the firefighter helmet were fitted to the new faces; the Influencer portrait and gallery were recaptured with a before/after report.
|
||||||
|
- Font/layout pass (0.9.3 work): HUD font scale was exercised at 0.75× and 1.75×; the target chip measures and caps scaled names, top labels clip, and shared action buttons ellipsize long 1.5–2× labels instead of overflowing. Covered by accessibility/button tests and recorded in `docs/ui-copy.md`.
|
||||||
- Modal-close pass (0.9.3 work): one top-modal close path handles Esc/right-click for every overlay, Settings opened from the menu restores the menu, Help no longer opens Pause behind it, and pointer/guard freeze state is recaptured after each close. The session smoke covers these paths.
|
- Modal-close pass (0.9.3 work): one top-modal close path handles Esc/right-click for every overlay, Settings opened from the menu restores the menu, Help no longer opens Pause behind it, and pointer/guard freeze state is recaptured after each close. The session smoke covers these paths.
|
||||||
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
|
- Keyboard/gamepad focus pass (0.9.3 work): `UiFocus` rings wrap through visible/enabled controls, hidden Continue and disabled timeline actions are skipped, each modal has a default focus, and A/B are bound to `ui_accept`/`ui_cancel` for pad-only navigation. Recorded in `docs/ui-copy.md`.
|
||||||
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
|
- Button-state pass (0.9.3 work): a shared `UiStyles` helper now gives every action button explicit normal, hover, pressed, focus and disabled styles; the timeline disables Previous/Next at its boundaries, while hidden continue states and the keyboard-only tutorial skip remain intentional. Recorded in `docs/ui-copy.md`.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 669 KiB After Width: | Height: | Size: 661 KiB |
|
Before Width: | Height: | Size: 393 KiB After Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 760 KiB After Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 414 KiB After Width: | Height: | Size: 405 KiB |
|
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 567 KiB |
|
Before Width: | Height: | Size: 838 KiB After Width: | Height: | Size: 830 KiB |
@@ -45,3 +45,4 @@
|
|||||||
43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock.
|
43. The venue menu offers Hollywood Boulevard — Day and — Night; either starts the same map, and Night starts in the late phase with a full shift clock.
|
||||||
44. The Joke Shop opens on the Sunset Strip club: the black hall, signature canopy, circular **THE JOKE SHOP** sign, red curtains, west entrance, palms, dry hillside with houses, neighbouring frontage and the opposite side of Sunset all read from the door camera. Day lighting and late lighting both look correct; the guard can walk the sidewalk without leaving the nav bounds.
|
44. The Joke Shop opens on the Sunset Strip club: the black hall, signature canopy, circular **THE JOKE SHOP** sign, red curtains, west entrance, palms, dry hillside with houses, neighbouring frontage and the opposite side of Sunset all read from the door camera. Day lighting and late lighting both look correct; the guard can walk the sidewalk without leaving the nav bounds.
|
||||||
45. Esc and right-click close only the top panel (Help, Settings, Pause, Review, Radio, timeline, inspection/wand, action wheel); Settings opened from the venue menu returns to the menu, Help no longer opens Pause behind it, and closing a modal recaptures the mouse without leaving the guard frozen.
|
45. Esc and right-click close only the top panel (Help, Settings, Pause, Review, Radio, timeline, inspection/wand, action wheel); Settings opened from the venue menu returns to the menu, Help no longer opens Pause behind it, and closing a modal recaptures the mouse without leaving the guard frozen.
|
||||||
|
46. At 0.75× and 1.75× HUD font scale, long target names stay inside the target chip, the chip grows/caps correctly, and long button labels ellipsize instead of pushing panels off-screen.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# UI copy and button inventory
|
# UI copy and button inventory
|
||||||
|
|
||||||
Status: dialogue, button-state, focus and modal-close passes complete; layout/end-to-end audit in progress for 0.9.3.
|
Status: dialogue, button-state, focus, modal-close and font/layout passes complete; destructive-action and end-to-end audit in progress for 0.9.3.
|
||||||
|
|
||||||
## Screen inventory
|
## Screen inventory
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ silent profiles can still return an empty context line.
|
|||||||
| Tutorial / Help | The wheel verb is **Ask**, but help and the HUD key strip called it “polite direction”. | Both now say **Ask**; the tutorial tells the player to choose Ask after closing the panel. |
|
| Tutorial / Help | The wheel verb is **Ask**, but help and the HUD key strip called it “polite direction”. | Both now say **Ask**; the tutorial tells the player to choose Ask after closing the panel. |
|
||||||
| Review / campaign | Continue/return/quit labels and campaign-next copy checked. | No ambiguity found; no change. |
|
| Review / campaign | Continue/return/quit labels and campaign-next copy checked. | No ambiguity found; no change. |
|
||||||
| Repetition | Shared tourist/confused/delivery lines recur across venues by design; role-specific reason lines prevent every archetype sounding alike. | Accepted for the shared-archetype model. |
|
| Repetition | Shared tourist/confused/delivery lines recur across venues by design; role-specific reason lines prevent every archetype sounding alike. | Accepted for the shared-archetype model. |
|
||||||
| Localization length | Longest authored NPC/door line is 56 characters; longest UI string is `hud.controls` at 172 characters. | Dialogue fits the HUD log; the 1.5–2× string/font-scale layout test remains a separate 0.9.3 task. |
|
| Localization length | Longest authored NPC/door line is 56 characters; longest UI string is `hud.controls` at 172 characters. | Dialogue fits the HUD log; long-string/font-scale layout is covered below. |
|
||||||
|
|
||||||
## Button state audit
|
## Button state audit
|
||||||
|
|
||||||
@@ -152,10 +152,25 @@ resumes; `_show_start_menu()` keeps the pointer visible at the menu.
|
|||||||
The session smoke now covers menu → Settings → Cancel → menu, Help closing on
|
The session smoke now covers menu → Settings → Cancel → menu, Help closing on
|
||||||
right-click, and Settings cancel from pause leaving the guard unfrozen.
|
right-click, and Settings cancel from pause leaving the guard unfrozen.
|
||||||
|
|
||||||
|
## Font-scale and long-string layout
|
||||||
|
|
||||||
|
The HUD scale is tested at its **0.75×** and **1.75×** bounds. HUD top/time labels
|
||||||
|
clip instead of crossing into each other, and the target chip now measures the
|
||||||
|
scaled target text: normal long names widen the chip, while an extreme synthetic
|
||||||
|
name is capped at 420 px and ellipsized. Action-button labels use `clip_text` and
|
||||||
|
ellipsis in `UiStyles`, so a 1.5–2× pseudo-string cannot push a panel beyond the
|
||||||
|
viewport. The menu shell intentionally keeps its own fixed typography; the HUD
|
||||||
|
scale setting only changes HUD text.
|
||||||
|
|
||||||
|
Regression coverage lives in `test_accessibility.gd` (target-chip width/cap) and
|
||||||
|
`test_ui_buttons.gd` (long action-label minimum width). The pseudo-locale
|
||||||
|
translator workflow remains a 0.10.0 task; this pass uses deterministic long
|
||||||
|
strings and the maximum supported HUD scale.
|
||||||
|
|
||||||
## Remaining audit work
|
## Remaining audit work
|
||||||
|
|
||||||
- Player-facing labels are now routed through `content/strings.en.json`; numeric
|
- Player-facing labels are now routed through `content/strings.en.json`; numeric
|
||||||
values, empty-state clears and debug-only text remain in code by design.
|
values, empty-state clears and debug-only text remain in code by design.
|
||||||
- Focus/hover/pressed/disabled states and keyboard/gamepad rings are recorded above.
|
- Focus/hover/pressed/disabled states and keyboard/gamepad rings are recorded above.
|
||||||
- Test long strings and 1.75x HUD font scale.
|
- Long strings and 0.75×/1.75× HUD scale are covered above.
|
||||||
- Capture every dialog/button state and run `screen_designs.py compare` plus the pinned judge.
|
- Capture every dialog/button state and run `screen_designs.py compare` plus the pinned judge.
|
||||||
@@ -82,9 +82,11 @@ func _init() -> void:
|
|||||||
|
|
||||||
top_label = Label.new()
|
top_label = Label.new()
|
||||||
top_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
top_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
top_label.clip_text = true
|
||||||
top.add_child(top_label)
|
top.add_child(top_label)
|
||||||
|
|
||||||
time_label = Label.new()
|
time_label = Label.new()
|
||||||
|
time_label.clip_text = true
|
||||||
top.add_child(time_label)
|
top.add_child(time_label)
|
||||||
|
|
||||||
var spacer := Control.new()
|
var spacer := Control.new()
|
||||||
@@ -122,6 +124,7 @@ func _init() -> void:
|
|||||||
target_label = Label.new()
|
target_label = Label.new()
|
||||||
target_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
target_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
target_label.clip_text = true
|
||||||
target_label.add_theme_font_size_override("font_size", 17)
|
target_label.add_theme_font_size_override("font_size", 17)
|
||||||
_target_chip.add_child(target_label)
|
_target_chip.add_child(target_label)
|
||||||
|
|
||||||
@@ -394,6 +397,17 @@ func set_target_label(text: String) -> void:
|
|||||||
target_label.text = text
|
target_label.text = text
|
||||||
if _target_chip:
|
if _target_chip:
|
||||||
_target_chip.visible = text.strip_edges() != ""
|
_target_chip.visible = text.strip_edges() != ""
|
||||||
|
# The chip is fixed around the crosshair; widen it for long names and
|
||||||
|
# larger HUD font scales instead of letting the text spill over the world.
|
||||||
|
var font := target_label.get_theme_font("font")
|
||||||
|
var font_size := target_label.get_theme_font_size("font_size")
|
||||||
|
var text_width := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size).x
|
||||||
|
var chip_width := clampf(text_width + 32.0, 180.0, 420.0)
|
||||||
|
_target_chip.offset_left = -chip_width * 0.5
|
||||||
|
_target_chip.offset_right = chip_width * 0.5
|
||||||
|
var chip_height := maxf(40.0, font.get_height(font_size) + 16.0)
|
||||||
|
_target_chip.offset_top = 38.0
|
||||||
|
_target_chip.offset_bottom = 38.0 + chip_height
|
||||||
|
|
||||||
## How many people the aim-free ring (Tab / mouse wheel) can cycle through.
|
## How many people the aim-free ring (Tab / mouse wheel) can cycle through.
|
||||||
func set_nearby_hint(count: int) -> void:
|
func set_nearby_hint(count: int) -> void:
|
||||||
@@ -447,6 +461,9 @@ func set_font_scale(scale: float) -> void:
|
|||||||
_base_font_sizes[label] = label.get_theme_font_size("font_size")
|
_base_font_sizes[label] = label.get_theme_font_size("font_size")
|
||||||
var base: int = int(_base_font_sizes[label])
|
var base: int = int(_base_font_sizes[label])
|
||||||
label.add_theme_font_size_override("font_size", maxi(8, int(round(float(base) * clamped))))
|
label.add_theme_font_size_override("font_size", maxi(8, int(round(float(base) * clamped))))
|
||||||
|
# Recompute the target chip after its font size changed.
|
||||||
|
if target_label:
|
||||||
|
set_target_label(target_label.text)
|
||||||
|
|
||||||
func _make_icon(texture: Texture2D) -> TextureRect:
|
func _make_icon(texture: Texture2D) -> TextureRect:
|
||||||
var tr := TextureRect.new()
|
var tr := TextureRect.new()
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ static func apply_button(button: Button, font_size: int = 0, compact: bool = fal
|
|||||||
return
|
return
|
||||||
if font_size > 0:
|
if font_size > 0:
|
||||||
button.add_theme_font_size_override("font_size", font_size)
|
button.add_theme_font_size_override("font_size", font_size)
|
||||||
|
# Long localized labels must not push a panel past the viewport. The
|
||||||
|
# buttons keep their existing minimum width and ellipsize if needed.
|
||||||
|
button.clip_text = true
|
||||||
|
button.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||||
var margin_h := 12 if compact else 18
|
var margin_h := 12 if compact else 18
|
||||||
var margin_v := 7 if compact else 10
|
var margin_v := 7 if compact else 10
|
||||||
var normal := _box(SURFACE, margin_h, margin_v)
|
var normal := _box(SURFACE, margin_h, margin_v)
|
||||||
|
|||||||
@@ -49,8 +49,18 @@ func test_font_scale_applies_to_core_labels() -> bool:
|
|||||||
hud.set_font_scale(9.0)
|
hud.set_font_scale(9.0)
|
||||||
var capped: int = hud.top_label.get_theme_font_size("font_size")
|
var capped: int = hud.top_label.get_theme_font_size("font_size")
|
||||||
var f := TestUtils.check(capped <= int(round(float(base_top) * 1.75)) + 1, "scale clamps at 1.75")
|
var f := TestUtils.check(capped <= int(round(float(base_top) * 1.75)) + 1, "scale clamps at 1.75")
|
||||||
|
# The target chip must widen with the scaled font instead of spilling over
|
||||||
|
# the world; an extreme name is capped rather than pushing off-screen.
|
||||||
|
hud.set_font_scale(1.75)
|
||||||
|
hud.set_target_label("Intoxicated Pedestrian")
|
||||||
|
var chip = hud.get("_target_chip")
|
||||||
|
var chip_width: float = float(chip.offset_right) - float(chip.offset_left)
|
||||||
|
var g := TestUtils.check(chip_width > 300.0, "target chip widens at max font scale")
|
||||||
|
hud.set_target_label("An Extraordinarily Long Display Name For A Visitor In A Pseudo Locale")
|
||||||
|
var long_width: float = float(chip.offset_right) - float(chip.offset_left)
|
||||||
|
var h := TestUtils.check(long_width <= 420.5, "target chip caps an extreme name")
|
||||||
hud.free()
|
hud.free()
|
||||||
return a and b and c and d and e and f
|
return a and b and c and d and e and f and g and h
|
||||||
|
|
||||||
func test_settings_font_scale_is_applied_after_ui_exists() -> bool:
|
func test_settings_font_scale_is_applied_after_ui_exists() -> bool:
|
||||||
## The HUD is created after settings load, so the manager must re-apply.
|
## The HUD is created after settings load, so the manager must re-apply.
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ func test_shared_button_states_are_complete() -> bool:
|
|||||||
var c := TestUtils.check(disabled != null and normal != null
|
var c := TestUtils.check(disabled != null and normal != null
|
||||||
and disabled.bg_color != normal.bg_color,
|
and disabled.bg_color != normal.bg_color,
|
||||||
"the disabled state is visually distinct from normal")
|
"the disabled state is visually distinct from normal")
|
||||||
|
button.text = "A very long localized action label that must not push the panel beyond the viewport"
|
||||||
|
var d := TestUtils.check(button.clip_text
|
||||||
|
and button.get_combined_minimum_size().x < 100.0,
|
||||||
|
"long action labels ellipsize instead of forcing a wide panel")
|
||||||
button.free()
|
button.free()
|
||||||
return a and b and c
|
return a and b and c and d
|
||||||
|
|
||||||
func test_focus_helper_skips_hidden_and_disabled_controls() -> bool:
|
func test_focus_helper_skips_hidden_and_disabled_controls() -> bool:
|
||||||
var hidden := Button.new()
|
var hidden := Button.new()
|
||||||
|
|||||||