M1a: survival core, pause menu, title screen and save/load
Design: docs/SURVIVAL_DESIGN.md (core loop, systems, architecture, phases). Survival core (M1a): - Items/recipes as data (data/*.json) in the addon SDK's define_item / define_recipe shape; procedural item meshes and offscreen-rendered icons - Inventory class (40 slots, hotbar = row 1) shared by player and chests; click / right-click split / shift-click quick-move - Actions autoload: the intent layer (craft, pickup, harvest_hit, place, transfer) that validates every change; becomes the RPC surface in M2 - Gathering: ground pickups (sticks, stones, flint); trees chopped with an axe fall and leave stumps; boulders mined with a pickaxe shrink per hit - Crafting screen with station checks (workbench); placement mode with green/red ghost, rotation and overlap/slope validation - Pieces: campfire (light, flames, embers), workbench, chest (16 slots) - HUD: hotbar, held tool view model with swing, prompts, toasts Menus and saving: - ESC pause menu: resume, controls, fullscreen, mouse sensitivity, quit - Title screen over a flyover of the island: Continue / New World (name + seed) / Load World (with delete) / Quit - Saves autoload: one folder per world under user://worlds, atomic writes with a .bak; stores only player changes (player, pieces, chest contents, harvested resources, taken pickups) and regenerates the rest from the seed - Autosave every 2 min, Save & Quit to title/desktop, save on window close Fixes: UI screens now fill the viewport (were pinned top-left); title backdrop no longer captures the mouse. Tests: test_survival_core (31 checks), test_save_load (round trip + slots), test_terrain_edges now checks 8 seeds for deep pits / unbanked water.
This commit is contained in:
167
scripts/ui/pause_menu.gd
Normal file
167
scripts/ui/pause_menu.gd
Normal file
@@ -0,0 +1,167 @@
|
||||
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"], ["Left click", "Use tool / place"],
|
||||
["R", "Rotate piece"], ["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting"],
|
||||
["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
|
||||
|
||||
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()
|
||||
if 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."
|
||||
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 = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
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)
|
||||
|
||||
func _on_quit() -> void:
|
||||
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
|
||||
_quit_btn.text = "Save & Quit to Desktop" if Saves.has_world() else "Quit to Desktop"
|
||||
Reference in New Issue
Block a user