extends Control ## Pause menu (ESC): resume, controls, fullscreen, mouse sensitivity, quit. ## Pauses the scene tree while open; runs with PROCESS_MODE_ALWAYS so it still ## gets input. const UITheme = preload("res://scripts/ui/theme.gd") const CONTROLS := [ ["W A S D", "Move"], ["Shift", "Sprint"], ["Space", "Jump / swim up"], ["Mouse", "Look"], ["E", "Interact / pick up / add fuel"], ["Left click", "Use tool / place"], ["Right click (holding food)", "Eat"], ["Right click (spear / cloak)", "Throw / wear"], ["Hold left click (bow)", "Draw, release to shoot"], ["Left click (fishing rod)", "Cast / reel in when the float dips"], ["R", "Rotate piece (building pieces: quarter turns)"], ["X", "Dismantle the piece you're looking at"], ["E on a bed", "Set spawn point / sleep (night, under a roof)"], ["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"], ["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"], ["Esc", "This menu (game autosaves every 2 min)"], ] var player: CharacterBody3D var _main: VBoxContainer var _controls: VBoxContainer var _quit_btn: Button var _quit_armed := false var _online := false ## on a server: no pausing, no saving here var _players_label: Label func bind(p: CharacterBody3D) -> void: player = p process_mode = Node.PROCESS_MODE_ALWAYS set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) mouse_filter = Control.MOUSE_FILTER_STOP visible = false var dim := ColorRect.new() dim.color = Color(0, 0, 0, 0.55) dim.set_anchors_preset(Control.PRESET_FULL_RECT) dim.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(dim) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) center.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(center) var panel := UITheme.panel() panel.custom_minimum_size = Vector2(380, 0) center.add_child(panel) var stack := VBoxContainer.new() panel.add_child(stack) # Main page. _main = VBoxContainer.new() _main.add_theme_constant_override("separation", 10) stack.add_child(_main) var title := UITheme.label("Paused", 30) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _main.add_child(title) _main.add_child(_button("Resume", close)) _main.add_child(_button("Controls", _show_controls.bind(true))) var fs := CheckButton.new() fs.text = "Fullscreen" fs.focus_mode = Control.FOCUS_NONE fs.button_pressed = DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN fs.add_theme_font_size_override("font_size", 18) fs.toggled.connect(_on_fullscreen) _main.add_child(fs) var sens_row := HBoxContainer.new() sens_row.add_child(UITheme.label("Mouse sensitivity", 16)) var sens := HSlider.new() sens.min_value = 0.0005 sens.max_value = 0.006 sens.step = 0.0001 sens.value = player.mouse_sensitivity sens.size_flags_horizontal = Control.SIZE_EXPAND_FILL sens.focus_mode = Control.FOCUS_NONE sens.value_changed.connect(func(v: float): player.mouse_sensitivity = v) sens_row.add_child(sens) _main.add_child(sens_row) var saved: bool = Saves.has_world() _online = Net.state == "connected" if _online: _players_label = UITheme.label("", 15) _players_label.autowrap_mode = TextServer.AUTOWRAP_WORD _players_label.custom_minimum_size = Vector2(340, 0) _main.add_child(_players_label) _main.add_child(_button("Disconnect", _on_disconnect)) _quit_btn = _button("Disconnect & Quit to Desktop", _on_quit) elif saved: _main.add_child(_button("Save & Quit to Title", _on_quit_to_title)) _quit_btn = _button("Save & Quit to Desktop", _on_quit) else: _quit_btn = _button("Quit to Desktop", _on_quit) _main.add_child(_quit_btn) var note_text := "Autosaves every 2 minutes. World: %s" % Saves.read_meta(Saves.current_id).get("name", "?") if saved \ else "Not playing a saved world: progress won't be kept." if _online: note_text = "The game keeps running while this menu is open." var note := UITheme.label(note_text, 13, UITheme.TEXT_DIM) note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _main.add_child(note) # Controls page. _controls = VBoxContainer.new() _controls.add_theme_constant_override("separation", 6) _controls.visible = false stack.add_child(_controls) var ct := UITheme.label("Controls", 26) ct.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _controls.add_child(ct) var grid := GridContainer.new() grid.columns = 2 grid.add_theme_constant_override("h_separation", 24) for row in CONTROLS: grid.add_child(UITheme.label(row[0], 16, UITheme.SELECTED)) grid.add_child(UITheme.label(row[1], 16)) _controls.add_child(grid) _controls.add_child(_button("Back", _show_controls.bind(false))) func _button(text: String, cb: Callable) -> Button: var b := Button.new() b.text = text b.focus_mode = Control.FOCUS_NONE b.custom_minimum_size = Vector2(0, 42) b.add_theme_font_size_override("font_size", 18) b.pressed.connect(cb) return b func is_open() -> bool: return visible func open() -> void: visible = true _show_controls(false) _disarm_quit() player.ui_open = true get_tree().paused = not _online # a shared world can't be paused by one player Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if _online: var names: Array[String] = [] for p in Net.players.values(): names.append(String(p.name) + (" (admin)" if p.get("admin", false) else "")) names.sort() _players_label.text = "%s ยท %d online\n%s" % [Net.server_info.get("server_name", "Server"), names.size(), ", ".join(names)] func close() -> void: visible = false player.ui_open = false get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_CAPTURED ## ESC on the controls page goes back; on the main page it resumes. func back() -> void: if _controls.visible: _show_controls(false) else: close() func _show_controls(on: bool) -> void: _controls.visible = on _main.visible = not on func _on_fullscreen(on: bool) -> void: DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN if on else DisplayServer.WINDOW_MODE_WINDOWED) ## Leaves the server (world.gd sees Net.left and goes back to the title). func _on_disconnect() -> void: close() Net.leave() func _on_quit() -> void: if _online: Net.leave() get_tree().quit() return if Saves.has_world(): player.get_parent().save_now() get_tree().quit() return # Unsaved session: ask twice so nobody loses a run by accident. if not _quit_armed: _quit_armed = true _quit_btn.text = "Click again to quit" return get_tree().quit() func _on_quit_to_title() -> void: player.get_parent().save_now() get_tree().paused = false Saves.current_id = "" get_tree().auto_accept_quit = true get_tree().change_scene_to_file("res://scenes/title.tscn") func _disarm_quit() -> void: _quit_armed = false if _online: return _quit_btn.text = "Save & Quit to Desktop" if Saves.has_world() else "Quit to Desktop"