Add session shell and close world presentation follow-ups
Session shell - Startup venue menu (src/game/ui/start_menu.gd): keyboard + mouse, replaces the live-shift boot and V as the player-facing venue path (V stays a debug bind). - Pause overlay on Esc (src/game/ui/pause_overlay.gd): resume, restart shift, settings, quit to menu. - Settings panel (src/game/ui/settings_panel.gd): mouse look sensitivity and HUD font scale, persisted via Save into user://settings.json and loaded on boot. - Shift review "Run another shift" now returns to the venue menu. - New smoke: src/test/smoke_session_shell.gd (menu -> shift -> pause -> settings -> menu), passes headless and windowed. World and presentation - Skyline backdrop restored with a transparent asset and an alpha-scissor unshaded material; fixes the opaque black slab from the earlier attempt and the unimported texture path. - Steps and landing are a StaticBody3D with per-tread colliders, so the guard and NPCs walk up instead of clipping through. - Marquee Label3D scaled up (72 -> 160) so STEP LINE reads from the sidewalk. - Focus torus and name tag enlarged for 2-3 m readability. - Routine traffic despawns after an idle lifetime (NpcAvatar.ephemeral), so the 16-NPC cap no longer fills with standers; authored events stay for the shift. Docs and tests - PLAN.md test/CI section corrected to the custom headless runner and Gitea Actions. - README controls table, smoke checklist, test count, and status lists refreshed. - TODO/DONE/MEMORY updated; completed sections moved to DONE.md. - Headless suite 77 -> 88 passing (settings round-trip, step colliders, skyline alpha, session-shell UI behavior, traffic lifetime).
This commit is contained in:
1 parent
f11b441c07
commit
be429cab29
20 files changed
+989
-90
No files matched your search
+148
-7
@@ -23,6 +23,9 @@ const Hud := preload("res://src/game/ui/hud.gd")
|
||||
const ActionRadial := preload("res://src/game/ui/action_radial.gd")
|
||||
const RadioPanel := preload("res://src/game/ui/radio_panel.gd")
|
||||
const ShiftReview := preload("res://src/game/ui/shift_review.gd")
|
||||
const StartMenu := preload("res://src/game/ui/start_menu.gd")
|
||||
const PauseOverlay := preload("res://src/game/ui/pause_overlay.gd")
|
||||
const SettingsPanel := preload("res://src/game/ui/settings_panel.gd")
|
||||
|
||||
var world_builder: WorldBuilder = null
|
||||
var door := DoorState.new()
|
||||
@@ -62,6 +65,11 @@ var hud: Hud = null
|
||||
var radial: ActionRadial = null
|
||||
var radio_panel: RadioPanel = null
|
||||
var shift_review_panel: ShiftReview = null
|
||||
var start_menu: StartMenu = null
|
||||
var pause_overlay: PauseOverlay = null
|
||||
var settings_panel: SettingsPanel = null
|
||||
var _shift_running := false
|
||||
var player_settings: Dictionary = {}
|
||||
var _focused_avatar: Node3D = null
|
||||
var audio: AudioFeedback = null
|
||||
var verifier := AccessVerifier.new()
|
||||
@@ -99,6 +107,7 @@ var _nearby_index := -1
|
||||
|
||||
func _ready() -> void:
|
||||
_door_rng.randomize()
|
||||
player_settings = Save.load_settings() if Save else {}
|
||||
scene_root = Node3D.new()
|
||||
scene_root.name = "SceneRoot"
|
||||
add_child(scene_root)
|
||||
@@ -115,6 +124,7 @@ func _ready() -> void:
|
||||
guard.rotation.y = PI
|
||||
guard.picked_target.connect(_on_guard_target)
|
||||
guard.cleared_target.connect(_on_guard_clear)
|
||||
_apply_settings(player_settings)
|
||||
|
||||
selected_scenario = _build_scenario(active_scenario_id)
|
||||
scenario_duration_s = selected_scenario.shift_duration_s
|
||||
@@ -125,9 +135,10 @@ func _ready() -> void:
|
||||
coordinator.radio.responder_arrived.connect(_on_responder_arrived)
|
||||
|
||||
_build_ui()
|
||||
coordinator.start()
|
||||
_last_phase = -1
|
||||
_apply_phase_lighting()
|
||||
# Boot into the venue menu; the shift starts when the player picks a venue.
|
||||
_show_start_menu()
|
||||
|
||||
var selected_scenario: VenueScenario = null
|
||||
|
||||
@@ -179,20 +190,106 @@ func switch_scenario(id: String) -> void:
|
||||
restart_shift()
|
||||
|
||||
func restart_shift() -> void:
|
||||
shift_review_panel.hide()
|
||||
if shift_review_panel:
|
||||
shift_review_panel.hide()
|
||||
_reset_shift_state()
|
||||
_start_shift()
|
||||
|
||||
## Clear all per-shift runtime state (NPCs, incidents, timers).
|
||||
func _reset_shift_state() -> void:
|
||||
for n in npcs:
|
||||
n.queue_free()
|
||||
if is_instance_valid(n):
|
||||
n.queue_free()
|
||||
npcs.clear()
|
||||
npc_counter = 0
|
||||
inspector_current_events = []
|
||||
current_incident_active = false
|
||||
current_incident_npc_id = ""
|
||||
targeted_npc = null
|
||||
_focused_avatar = null
|
||||
_nearby_index = -1
|
||||
inspection_open = false
|
||||
wand_open = false
|
||||
_wand_cooldown = 0.0
|
||||
scenario_duration_s = selected_scenario.shift_duration_s
|
||||
coordinator.setup(selected_scenario, scenario_duration_s, 4242)
|
||||
coordinator.start()
|
||||
_last_phase = -1
|
||||
_apply_phase_lighting()
|
||||
|
||||
## Begin (or resume) the shift clock and recapture the mouse.
|
||||
func _start_shift() -> void:
|
||||
_shift_running = true
|
||||
coordinator.start()
|
||||
if hud:
|
||||
hud.set_shift_state(coordinator, selected_scenario)
|
||||
_refresh_freeze()
|
||||
|
||||
func _show_start_menu() -> void:
|
||||
_shift_running = false
|
||||
if pause_overlay:
|
||||
pause_overlay.close()
|
||||
if shift_review_panel:
|
||||
shift_review_panel.hide()
|
||||
if guard:
|
||||
guard.input_locked = true
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
if start_menu:
|
||||
start_menu.open()
|
||||
|
||||
func _on_venue_selected(scenario_id: String) -> void:
|
||||
if start_menu and start_menu.visible:
|
||||
start_menu.close()
|
||||
active_scenario_id = scenario_id
|
||||
_venue_index = maxi(0, VENUE_ORDER.find(scenario_id))
|
||||
selected_scenario = _build_scenario(scenario_id)
|
||||
if hud:
|
||||
hud.set_wand_icon(false)
|
||||
_reset_shift_state()
|
||||
_start_shift()
|
||||
|
||||
## Apply loaded settings to the guard and HUD.
|
||||
func _apply_settings(settings: Dictionary) -> void:
|
||||
player_settings = settings
|
||||
if guard:
|
||||
guard.look_sensitivity = float(settings.get("look_sensitivity", 0.0022))
|
||||
if hud and hud.has_method("set_font_scale"):
|
||||
hud.set_font_scale(float(settings.get("hud_font_scale", 1.0)))
|
||||
|
||||
func _on_settings_applied(settings: Dictionary) -> void:
|
||||
_apply_settings(settings)
|
||||
if start_menu and start_menu.visible:
|
||||
start_menu.open()
|
||||
elif _shift_running:
|
||||
_open_pause()
|
||||
|
||||
func _open_settings() -> void:
|
||||
if start_menu:
|
||||
start_menu.close()
|
||||
if pause_overlay:
|
||||
pause_overlay.close()
|
||||
if settings_panel:
|
||||
settings_panel.open(player_settings)
|
||||
|
||||
func _open_pause() -> void:
|
||||
if pause_overlay == null or not _shift_running:
|
||||
return
|
||||
if _close_top_modal():
|
||||
pass
|
||||
pause_overlay.open()
|
||||
if guard:
|
||||
guard.input_locked = true
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
func _close_pause() -> void:
|
||||
if pause_overlay:
|
||||
pause_overlay.close()
|
||||
_refresh_freeze()
|
||||
|
||||
func _quit_to_menu() -> void:
|
||||
if pause_overlay:
|
||||
pause_overlay.close()
|
||||
_show_start_menu()
|
||||
|
||||
var inspector_current_events: Array = []
|
||||
|
||||
func _build_ui() -> void:
|
||||
@@ -216,7 +313,32 @@ func _build_ui() -> void:
|
||||
shift_review_panel = ShiftReview.new()
|
||||
add_child(shift_review_panel)
|
||||
shift_review_panel.hide()
|
||||
shift_review_panel.restart_requested.connect(restart_shift)
|
||||
shift_review_panel.restart_requested.connect(_on_review_restart)
|
||||
shift_review_panel.visible = false
|
||||
|
||||
pause_overlay = PauseOverlay.new()
|
||||
add_child(pause_overlay)
|
||||
pause_overlay.hide()
|
||||
pause_overlay.resume_requested.connect(_close_pause)
|
||||
pause_overlay.restart_requested.connect(restart_shift)
|
||||
pause_overlay.settings_requested.connect(_open_settings)
|
||||
pause_overlay.quit_to_menu_requested.connect(_quit_to_menu)
|
||||
|
||||
settings_panel = SettingsPanel.new()
|
||||
add_child(settings_panel)
|
||||
settings_panel.hide()
|
||||
settings_panel.applied.connect(_on_settings_applied)
|
||||
|
||||
start_menu = StartMenu.new()
|
||||
add_child(start_menu)
|
||||
start_menu.hide()
|
||||
start_menu.venue_selected.connect(_on_venue_selected)
|
||||
start_menu.settings_requested.connect(_open_settings)
|
||||
start_menu.quit_requested.connect(func(): get_tree().quit())
|
||||
|
||||
## "Run another shift" on the review screen returns to the venue menu.
|
||||
func _on_review_restart() -> void:
|
||||
_show_start_menu()
|
||||
|
||||
func _add_crosshair() -> void:
|
||||
var layer := CanvasLayer.new()
|
||||
@@ -309,6 +431,9 @@ func _spawn_npc(profile: NpcProfile, intent: String, npc_id: String, event: Dict
|
||||
items.append(str(item))
|
||||
spawn_profile.carried_items = items
|
||||
avatar.setup(spawn_profile, intent, seed_value, spawn_pos, target_pos, door_pos, npc_id, override)
|
||||
# Authored events stay for the whole shift; routine traffic cycles out.
|
||||
avatar.ephemeral = event.get("kind", "routine") == "routine"
|
||||
avatar.lifetime_s = 45.0 + float((npc_counter * 17) % 60)
|
||||
scene_root.add_child(avatar)
|
||||
npcs.append(avatar)
|
||||
|
||||
@@ -362,6 +487,7 @@ func _responder_profile() -> NpcProfile:
|
||||
return p
|
||||
|
||||
func _on_shift_ended(review: Dictionary) -> void:
|
||||
_shift_running = false
|
||||
# Persist the finished shift locally (Save autoload, user:// JSON).
|
||||
if Save:
|
||||
Save.save_shift_result(review)
|
||||
@@ -510,13 +636,22 @@ func _handle_action_key(keycode: Key) -> void:
|
||||
_:
|
||||
pass
|
||||
|
||||
## `cancel` (Esc, RMB) closes the top modal. With nothing open, Esc toggles mouse
|
||||
## capture; RMB does nothing so it never grabs the mouse by accident.
|
||||
## `cancel` (Esc, RMB) closes the top modal. With nothing open, Esc pauses the
|
||||
## shift (or resumes it when paused); RMB does nothing so it never grabs the
|
||||
## mouse by accident.
|
||||
func _handle_cancel(from_mouse: bool) -> void:
|
||||
if _close_top_modal():
|
||||
return
|
||||
if from_mouse:
|
||||
return
|
||||
if settings_panel and settings_panel.visible:
|
||||
return
|
||||
if pause_overlay and pause_overlay.visible:
|
||||
_close_pause()
|
||||
return
|
||||
if _shift_running and not (start_menu and start_menu.visible):
|
||||
_open_pause()
|
||||
return
|
||||
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
else:
|
||||
@@ -610,6 +745,12 @@ func _refresh_freeze() -> void:
|
||||
freeze = true
|
||||
if shift_review_panel and shift_review_panel.visible:
|
||||
freeze = true
|
||||
if pause_overlay and pause_overlay.visible:
|
||||
freeze = true
|
||||
if settings_panel and settings_panel.visible:
|
||||
freeze = true
|
||||
if not _shift_running:
|
||||
freeze = true
|
||||
if guard:
|
||||
guard.input_locked = freeze
|
||||
if freeze:
|
||||
|
||||
Reference in new issue
Block a user