323 lines
12 KiB
GDScript
323 lines
12 KiB
GDScript
extends Control
|
|
## Pause menu (ESC): resume, controls, report a bug, fullscreen, mouse
|
|
## sensitivity, volumes, 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 BugReport = preload("res://scripts/core/bug_report.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)"],
|
|
["N", "Map (fills in as you explore; the compass is along the top)"],
|
|
["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 _report: VBoxContainer
|
|
var _report_note: Label
|
|
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)))
|
|
_main.add_child(_button("Report a bug", _show_report.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 (Cinderwild %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)))
|
|
|
|
# Report a bug page (0.6.0, the playtest: bug_report.gd).
|
|
_report = VBoxContainer.new()
|
|
_report.add_theme_constant_override("separation", 10)
|
|
_report.visible = false
|
|
stack.add_child(_report)
|
|
var rt := UITheme.label("Report a bug", 26)
|
|
rt.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_report.add_child(rt)
|
|
var how := UITheme.label("This opens a new issue on Cinderwild's Gitea page in your browser, filled in with your version, computer and where you are in the game. Say what happened, then press Create Issue. (You'll need to be signed in there.) Nothing is sent from the game itself.", 15)
|
|
how.autowrap_mode = TextServer.AUTOWRAP_WORD
|
|
how.custom_minimum_size = Vector2(380, 0)
|
|
_report.add_child(how)
|
|
_report.add_child(_button("Open the bug report page", _on_report_open))
|
|
_report.add_child(_button("Copy the details", _on_report_copy))
|
|
_report.add_child(_button("Show the log file", _on_report_log))
|
|
var lp := UITheme.label("For a crash or an error, attach the log:\n%s" % BugReport.log_path(), 13, UITheme.TEXT_DIM)
|
|
lp.autowrap_mode = TextServer.AUTOWRAP_ARBITRARY
|
|
lp.custom_minimum_size = Vector2(380, 0)
|
|
_report.add_child(lp)
|
|
_report_note = UITheme.label("", 14, UITheme.SELECTED)
|
|
_report_note.autowrap_mode = TextServer.AUTOWRAP_WORD
|
|
_report_note.custom_minimum_size = Vector2(380, 0)
|
|
_report.add_child(_report_note)
|
|
_report.add_child(_button("Back", _show_report.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)
|
|
_show_report(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:
|
|
# Everyone on the server, in whichever land (J3b), with where they are
|
|
# once anyone's somewhere else.
|
|
var all: Array = Net.everyone()
|
|
var lands := {}
|
|
for p in all:
|
|
lands[String(p.get("land", ""))] = true
|
|
var names: Array[String] = []
|
|
for p in all:
|
|
var where := ""
|
|
if lands.size() > 1 and not String(p.get("land", "")).is_empty():
|
|
where = " · %s" % preload("res://scripts/world/terrain.gd").worldlet_name(String(p.land))
|
|
names.append(String(p.name) + (" (admin)" if p.get("admin", false) else "") + where)
|
|
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 or bug report page goes back; on the main page it resumes.
|
|
func back() -> void:
|
|
if _controls.visible:
|
|
_show_controls(false)
|
|
elif _report.visible:
|
|
_show_report(false)
|
|
else:
|
|
close()
|
|
|
|
func _show_controls(on: bool) -> void:
|
|
_controls.visible = on
|
|
_main.visible = not on
|
|
|
|
func _show_report(on: bool) -> void:
|
|
_report.visible = on
|
|
_main.visible = not on
|
|
_report_note.text = ""
|
|
|
|
func _world() -> Node:
|
|
return player.get_parent() if player != null else null
|
|
|
|
func _on_report_open() -> void:
|
|
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
|
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) # (so the browser isn't hidden behind the game)
|
|
if BugReport.open(_world()):
|
|
_report_note.text = "Opened in your browser. (Can't post there? Copy the details and send them to whoever gave you the game.)"
|
|
else:
|
|
DisplayServer.clipboard_set(BugReport.issue_url(_world()))
|
|
_report_note.text = "Couldn't open a browser: the link's on your clipboard to paste into one."
|
|
|
|
func _on_report_copy() -> void:
|
|
DisplayServer.clipboard_set(BugReport.details_text(_world()))
|
|
_report_note.text = "Copied: paste them into your message."
|
|
|
|
func _on_report_log() -> void:
|
|
OS.shell_open(BugReport.log_dir())
|
|
_report_note.text = "The log is godot.log in that folder."
|
|
|
|
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"
|