From 26e6dd641361cd0ad348ea78b78415bc3f674369 Mon Sep 17 00:00:00 2001 From: Gig Line Dev Date: Mon, 2 Nov 2026 16:10:50 +0100 Subject: [PATCH] Close every top modal through one Esc and right-click path - 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. --- CHANGELOG.md | 3 +++ MEMORY.md | 6 +++++ TODO.md | 7 ++++-- docs/development-status.md | 1 + docs/smoke-checklist.md | 1 + docs/ui-copy.md | 26 +++++++++++++++++++- src/game/game_manager.gd | 42 ++++++++++++++++++++++++++++++--- src/test/smoke_session_shell.gd | 31 ++++++++++++++++++++++++ 8 files changed, 111 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f5dce0..b531135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/MEMORY.md b/MEMORY.md index 0a9c15b..729c257 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -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 diff --git a/TODO.md b/TODO.md index d56ebb0..bd01c2c 100644 --- a/TODO.md +++ b/TODO.md @@ -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, diff --git a/docs/development-status.md b/docs/development-status.md index be53327..ea84a87 100644 --- a/docs/development-status.md +++ b/docs/development-status.md @@ -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`. diff --git a/docs/smoke-checklist.md b/docs/smoke-checklist.md index ae56575..997c4e4 100644 --- a/docs/smoke-checklist.md +++ b/docs/smoke-checklist.md @@ -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. diff --git a/docs/ui-copy.md b/docs/ui-copy.md index 8bb1cb5..7281910 100644 --- a/docs/ui-copy.md +++ b/docs/ui-copy.md @@ -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 diff --git a/src/game/game_manager.gd b/src/game/game_manager.gd index 5c5432a..a42811a 100644 --- a/src/game/game_manager.gd +++ b/src/game/game_manager.gd @@ -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: diff --git a/src/test/smoke_session_shell.gd b/src/test/smoke_session_shell.gd index 48c3d97..86b8b30 100644 --- a/src/test/smoke_session_shell.gd +++ b/src/test/smoke_session_shell.gd @@ -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: