Files
Emberwild/scripts/ui/pause_menu.gd

255 lines
8.9 KiB
GDScript

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 (the bag row you're on)"], ["K / M", "Hotbar shows the bag row above / below"],
["Q (shift+Q)", "Drop what you're holding (just one)"], ["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)
# Volumes (sfx.gd keeps them in user://audio.json).
for row in [["Music", "Music"], ["Sounds", "SFX"], ["Ambience", "Ambience"], ["Old Tam's voice", "Voice"]]:
var vr := HBoxContainer.new()
var lab := UITheme.label(row[0], 16)
lab.custom_minimum_size = Vector2(150, 0)
vr.add_child(lab)
var vs := HSlider.new()
vs.min_value = 0.0
vs.max_value = 1.0
vs.step = 0.05
vs.value = float(Sfx.volumes.get(row[1], 0.8))
vs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
vs.focus_mode = Control.FOCUS_NONE
vs.accessibility_name = "%s volume" % row[0]
var bus: String = row[1]
vs.value_changed.connect(func(v: float): Sfx.set_volume(bus, v))
vr.add_child(vs)
_main.add_child(vr)
# Old Tam's tips in normal worlds (T4): on or off, or all of them again.
if not Guide.active:
var tips_row := HBoxContainer.new()
var tips := CheckButton.new()
tips.text = "Old Tam's tips"
tips.focus_mode = Control.FOCUS_NONE
tips.button_pressed = Hints.enabled
tips.add_theme_font_size_override("font_size", 18)
tips.size_flags_horizontal = Control.SIZE_EXPAND_FILL
tips.toggled.connect(Hints.set_enabled)
tips_row.add_child(tips)
var again := Button.new()
again.text = "Show again"
again.focus_mode = Control.FOCUS_NONE
again.add_theme_font_size_override("font_size", 15)
again.tooltip_text = "Every tip shows once more, when it next fits"
again.pressed.connect(func(): Hints.reset_seen(); again.text = "Done")
tips_row.add_child(again)
_main.add_child(tips_row)
var upd := CheckButton.new()
upd.text = "Check for updates at start (Emberwild %s)" % Updates.version
upd.focus_mode = Control.FOCUS_NONE
upd.button_pressed = Updates.enabled
upd.add_theme_font_size_override("font_size", 16)
upd.toggled.connect(Updates.set_enabled)
_main.add_child(upd)
# The tutorial: skip a step, or leave it (saved, to carry on later).
if Guide.active:
_main.add_child(_button("Skip this tutorial step", func(): Guide.skip(); close()))
_main.add_child(_button("Leave the tutorial (carry on later)", _on_quit_to_title))
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"