Close every top modal through one Esc and right-click path
Full build / Import + headless tests (push) Successful in 37s
Full build / Full desktop build + optional release (push) Skipped

- Add Help, Settings, Pause and review to the top-modal close chain so right-click and Esc behave consistently.
- Restore the venue menu after menu Settings and prevent Help from opening Pause behind itself.
- Reopen pause after in-shift Settings apply and recapture the mouse/unfreeze state on close.
- Extend the session smoke for menu Settings, right-click Help and guard unfreeze.
This commit is contained in:
Gig Line Dev committed 2026-11-02 16:10:50 +01:00
1 parent 8f65751975
commit 26e6dd6413
8 files changed
+111 -6

No files matched your search

+3
View File
@@ -14,6 +14,9 @@ Semantic Versioning.
- Keyboard and gamepad focus rings now wrap through every modal, with explicit
default focus and A/B accept/cancel bindings; hidden Continue and disabled
timeline controls are skipped.
- 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
frozen. Help no longer opens Pause behind itself.
- NPC reason answers now use role-specific dialogue contexts, so staff, guests,
delivery workers, performers and other archetypes answer instead of silently
deflecting the Reason verb.
+6
View File
@@ -61,6 +61,12 @@ 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
portrait measured a 0.732 vision gap against the photoreal concept. No
release build, version bump or tag was created at this gate.
- **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single
Esc/right-click path for radial, inspection, wand, radio, timeline, Help,
Settings, Pause and review. Settings remembers whether it came from the menu and
restores it on apply/cancel; `_refresh_freeze()` recaptures the mouse and
`_show_start_menu()` keeps it visible. Do not add a new modal without adding it
to this close path.
- **UI focus (2026-11-02):** `src/game/ui/ui_focus.gd` links visible/enabled
controls into a wrapping ring; each modal sets default focus on open. Start menu
uses its own focus-target list so Continue, venue rows and footer actions share
+5 -2
View File
@@ -85,8 +85,11 @@ screen-design judge before any model/texture changes. No gameplay rules change.
default focus, confirm/cancel behavior and no focus traps. (Focus rings,
gamepad A/B UI bindings, default focus and per-modal cancel behavior are
recorded in `docs/ui-copy.md`.)
- [ ] Verify every modal closes with Esc/right-click where documented, recaptures
the mouse, and does not leave the guard frozen or the menu visible.
- [x] Verify every modal closes with Esc/right-click where documented, recaptures
the mouse, and does not leave the guard frozen or the menu visible. (Single
top-modal close path, menu/settings return handling and pointer recapture are
recorded in `docs/ui-copy.md`; session smoke covers menu Settings, Help
right-click and pause Settings.)
- [ ] Test buttons at minimum/maximum HUD font scale and with 1.5–2× longer
strings; fix clipping, overflow and overlapping layouts.
- [ ] Test destructive/irreversible actions (campaign reset, tutorial replay,
+1
View File
@@ -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.
- 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.
- 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`.
- 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`.
- Dialogue pass (0.9.3 work): NPC reason answers now use the authored `reason` context, with an in-character answer for every shared archetype; Hollywood and Studio gained door-staff lines; radio cancellation, tutorial Ask terminology, help, start hints and campaign reset copy were clarified. Audit recorded in `docs/ui-copy.md`.
+1
View File
@@ -44,3 +44,4 @@
42. Button states remain readable on hover/focus/pressed; settings Apply/Cancel and radio normal-send/emergency-hold behave as labelled.
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.
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.
+25 -1
View File
@@ -1,6 +1,6 @@
# UI copy and button inventory
Status: dialogue, button-state and keyboard/gamepad focus passes complete; layout/end-to-end audit in progress for 0.9.3.
Status: dialogue, button-state, focus and modal-close passes complete; layout/end-to-end audit in progress for 0.9.3.
## Screen inventory
@@ -128,6 +128,30 @@ events.
| Radio | Send action. | B/Esc cancels; form fields, action and Cancel are linked; A confirms the focused action. |
| Tutorial | None; F1 is the documented skip shortcut. | Keyboard-only by design, no focus trap because the card is mouse-transparent and non-modal. |
## Modal close and pointer recapture
`GameManager._close_top_modal()` is now the single close path for the top
interactive layer. It handles the radial, inspection, wand, radio, timeline,
Help, Settings, Pause and review before Esc/right-click falls back to target
clearing or pause. `_refresh_freeze()` recaptures the mouse when the shift
resumes; `_show_start_menu()` keeps the pointer visible at the menu.
| Surface | Close routes | Result |
| --- | --- | --- |
| Pause | Resume, Esc, right-click | Closes via one path; guard unfreezes and mouse recaptures. |
| Settings | Apply settings, Cancel, Esc, right-click | From the venue menu, both apply and cancel restore the menu. From a shift, cancel returns to the game with the guard unfrozen; apply returns to pause. |
| Help | Close, Esc, right-click | Closes only Help; it no longer opens Pause behind the overlay. Returns to whatever was underneath. |
| Shift review | Return to menu, Esc, right-click | Returns to the venue menu with the pointer visible. |
| Timeline | Close, Esc, right-click | Returns to the review and keeps the pointer visible. |
| Radio | Cancel, Esc, right-click | Closes the form, clears the action hint and recaptures the mouse. |
| Inspection / wand | Esc, right-click | Closes the panel, unfreezes the guard and recaptures the mouse. |
| Action wheel | Esc, right-click | Cancels without a log entry and recaptures the mouse. |
| Start menu | Esc quits the application | Root menu; right-click is intentionally inert. |
| Tutorial | F1 skip | Non-modal card; never blocks the pointer or movement. |
The session smoke now covers menu → Settings → Cancel → menu, Help closing on
right-click, and Settings cancel from pause leaving the guard unfrozen.
## Remaining audit work
- Player-facing labels are now routed through `content/strings.en.json`; numeric
+39 -3
View File
@@ -99,6 +99,9 @@ var shift_review_panel: ShiftReview = null
var start_menu: StartMenu = null
var pause_overlay: PauseOverlay = null
var settings_panel: SettingsPanel = null
## Settings opened from the venue menu must return there on Cancel/Apply;
## settings opened from pause must not leave a menu visible or the guard frozen.
var _settings_from_start_menu := false
var event_timeline: EventTimeline = null
var tutorial_overlay: TutorialOverlay = null
var help_overlay: HelpOverlay = null
@@ -352,8 +355,15 @@ func _phase_start_s(phase: int) -> float:
func _show_start_menu() -> void:
_shift_running = false
_settings_from_start_menu = false
if pause_overlay:
pause_overlay.close()
if settings_panel:
settings_panel.close()
if help_overlay:
help_overlay.close_panel()
if event_timeline:
event_timeline.close()
if shift_review_panel:
shift_review_panel.hide()
if guard:
@@ -411,11 +421,21 @@ func _on_settings_applied(settings: Dictionary) -> void:
Save.save_campaign(campaign.summary())
if coordinator:
coordinator.scheduler.spawn_rate_multiplier = float(_difficulty()["spawn"])
if start_menu and start_menu.visible:
start_menu.open(campaign.summary() if campaign else {})
elif _shift_running:
# A settings panel opened from the venue menu is restored by
# `_on_settings_closed`; an in-shift panel returns to pause. Close it before
# opening pause so the new top-modal pass cannot cancel it twice.
if _shift_running:
if settings_panel:
settings_panel.close()
_open_pause()
func _on_settings_closed() -> void:
if _settings_from_start_menu:
_settings_from_start_menu = false
_show_start_menu()
else:
_refresh_freeze()
## Clear the tutorial completion/skip flags and restart the Hollywood shift so
## the next player can replay the first-shift lesson from Settings.
func _on_reset_campaign() -> void:
@@ -423,6 +443,7 @@ func _on_reset_campaign() -> void:
Save.reset_campaign()
if campaign:
campaign.setup({})
_settings_from_start_menu = false
if settings_panel:
settings_panel.close()
if pause_overlay:
@@ -434,6 +455,7 @@ func _on_replay_tutorial() -> void:
player_settings["tutorial_skipped"] = false
if Save:
Save.save_settings(player_settings)
_settings_from_start_menu = false
if settings_panel:
settings_panel.close()
if pause_overlay:
@@ -445,6 +467,7 @@ func _open_help() -> void:
help_overlay.open(selected_scenario.policy_summary if selected_scenario else "")
func _open_settings() -> void:
_settings_from_start_menu = start_menu != null and start_menu.visible
if start_menu:
start_menu.close()
if pause_overlay:
@@ -517,6 +540,7 @@ func _build_ui() -> void:
settings_panel.applied.connect(_on_settings_applied)
settings_panel.replay_tutorial_requested.connect(_on_replay_tutorial)
settings_panel.reset_campaign_requested.connect(_on_reset_campaign)
settings_panel.closed.connect(_on_settings_closed)
event_timeline = EventTimeline.new()
add_child(event_timeline)
@@ -1367,6 +1391,18 @@ func _close_top_modal() -> bool:
if event_timeline and event_timeline.visible:
event_timeline.close()
return true
if help_overlay and help_overlay.visible:
help_overlay.close_panel()
return true
if settings_panel and settings_panel.visible:
settings_panel._on_cancel()
return true
if pause_overlay and pause_overlay.visible:
_close_pause()
return true
if shift_review_panel and shift_review_panel.visible:
_on_review_restart()
return true
return false
func _open_radial() -> void:
+31
View File
@@ -22,6 +22,33 @@ func _initialize() -> void:
func _process(_delta: float) -> bool:
_frames += 1
if _frames == 5:
_manager = _main.get_node_or_null("GameManager")
if _manager == null or _manager.start_menu == null or not _manager.start_menu.visible:
printerr("start menu not visible before settings check")
quit(1)
return true
_manager._open_settings()
if _frames == 7:
if _manager.settings_panel == null or not _manager.settings_panel.visible:
printerr("settings did not open from the venue menu")
quit(1)
return true
_manager.settings_panel._on_cancel()
if _frames == 8:
if not _manager.start_menu.visible:
printerr("cancelling menu settings did not return to the venue menu")
quit(1)
return true
print("boot: menu settings cancel returns to menu")
_manager._open_help()
_manager._handle_cancel(true)
if _frames == 9:
if _manager.help_overlay.visible or not _manager.start_menu.visible:
printerr("right-click did not close Help and keep the venue menu")
quit(1)
return true
print("boot: help closes on right-click")
if _frames == 10:
_manager = _main.get_node_or_null("GameManager")
if _manager.start_menu == null or not _manager.start_menu.visible:
@@ -65,6 +92,10 @@ func _process(_delta: float) -> bool:
return true
print("boot: settings visible")
_manager.settings_panel._on_cancel()
if _manager.guard.input_locked:
printerr("guard stayed frozen after settings cancel")
quit(1)
return true
_manager._quit_to_menu()
if _frames == 290:
if not _manager.start_menu.visible: