extends Node ## Title screen: Continue, New World, Load World (with delete), Quit. ## The island itself, in menu mode, slowly flies past in the background. const UITheme = preload("res://scripts/ui/theme.gd") const WORLD_SCENE := "res://scenes/world.tscn" var _pages: Dictionary = {} ## name -> Control var _name_edit: LineEdit var _seed_edit: LineEdit var _list: VBoxContainer var _armed_delete := "" func _ready() -> void: Saves.current_id = "" get_tree().auto_accept_quit = true get_tree().paused = false Input.mouse_mode = Input.MOUSE_MODE_VISIBLE var backdrop: Node3D = load(WORLD_SCENE).instantiate() backdrop.menu_mode = true add_child(backdrop) # The backdrop's (disabled) player captures the mouse in its _ready; take # it back, or the menu can't be clicked. Input.mouse_mode = Input.MOUSE_MODE_VISIBLE var ui := CanvasLayer.new() ui.layer = 10 add_child(ui) var root := Control.new() root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) ui.add_child(root) # Soft vignette on the left so the text reads over the scenery. var shade := TextureRect.new() var g := Gradient.new() g.offsets = PackedFloat32Array([0.0, 1.0]) g.colors = PackedColorArray([Color(0, 0, 0, 0.6), Color(0, 0, 0, 0)]) var gt := GradientTexture2D.new() gt.gradient = g gt.fill_to = Vector2(1, 0) shade.texture = gt shade.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE) shade.offset_right = 720 shade.mouse_filter = Control.MOUSE_FILTER_IGNORE root.add_child(shade) var col := VBoxContainer.new() col.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE) col.offset_left = 70 col.offset_top = 90 col.offset_right = 520 col.add_theme_constant_override("separation", 14) root.add_child(col) col.add_child(UITheme.label("VALHEIMLIKE", 54)) col.add_child(UITheme.label("Survive the island. Build a home.", 18, UITheme.TEXT_DIM)) var spacer := Control.new() spacer.custom_minimum_size = Vector2(0, 24) col.add_child(spacer) _pages["main"] = _main_page() _pages["new"] = _new_page() _pages["load"] = _load_page() for p in _pages.values(): col.add_child(p) _show("main") _maybe_capture() ## Dev hook, same as world.gd: `-- --shot=/abs/prefix` saves the title pages. func _maybe_capture() -> void: var prefix := "" for a in OS.get_cmdline_user_args(): if a.begins_with("--shot="): prefix = a.trim_prefix("--shot=") if prefix.is_empty(): return await get_tree().create_timer(1.0).timeout print("[title] mouse mode: %s" % ["visible", "hidden", "captured", "confined"][Input.mouse_mode]) for page in ["main", "new", "load"]: _show(page) await get_tree().create_timer(4.0 if page == "main" else 0.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_title_%s.png" % [prefix, page]) print("[title] saved %s_title_%s.png" % [prefix, page]) get_tree().quit() func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not _pages["main"].visible: _show("main") func _show(page: String) -> void: for k in _pages: _pages[k].visible = k == page _armed_delete = "" if page == "load": _fill_list() elif page == "new": _name_edit.text = _unique_name() _seed_edit.text = "" _name_edit.grab_focus() _name_edit.select_all() func _button(text: String, cb: Callable, min_w: int = 320) -> Button: var b := Button.new() b.text = text b.focus_mode = Control.FOCUS_NONE b.custom_minimum_size = Vector2(min_w, 46) b.add_theme_font_size_override("font_size", 19) b.alignment = HORIZONTAL_ALIGNMENT_LEFT b.pressed.connect(cb) return b func _main_page() -> Control: var v := VBoxContainer.new() v.add_theme_constant_override("separation", 10) var worlds := Saves.list_worlds() if not worlds.is_empty(): var w: Dictionary = worlds[0] v.add_child(_button("Continue: %s" % w.name, _play.bind(w.id))) v.add_child(_button("New World", _show.bind("new"))) var lb := _button("Load World", _show.bind("load")) lb.disabled = worlds.is_empty() v.add_child(lb) v.add_child(_button("Quit", get_tree().quit)) return v func _new_page() -> Control: var p := UITheme.panel() var v := VBoxContainer.new() v.add_theme_constant_override("separation", 8) p.add_child(v) v.add_child(UITheme.label("New World", 24)) v.add_child(UITheme.label("Name", 15, UITheme.TEXT_DIM)) _name_edit = LineEdit.new() _name_edit.custom_minimum_size = Vector2(360, 40) _name_edit.max_length = 32 _name_edit.text_submitted.connect(func(_t): _create()) v.add_child(_name_edit) v.add_child(UITheme.label("Seed (blank = random island, 1337 = the original)", 15, UITheme.TEXT_DIM)) _seed_edit = LineEdit.new() _seed_edit.placeholder_text = "random" _seed_edit.custom_minimum_size = Vector2(360, 40) _seed_edit.text_submitted.connect(func(_t): _create()) v.add_child(_seed_edit) var h := HBoxContainer.new() h.add_theme_constant_override("separation", 10) h.add_child(_button("Create & Play", _create, 170)) h.add_child(_button("Back", _show.bind("main"), 120)) v.add_child(h) return p func _load_page() -> Control: var p := UITheme.panel() var v := VBoxContainer.new() v.add_theme_constant_override("separation", 8) p.add_child(v) v.add_child(UITheme.label("Load World", 24)) var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(460, 320) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED _list = VBoxContainer.new() _list.size_flags_horizontal = Control.SIZE_EXPAND_FILL _list.add_theme_constant_override("separation", 6) scroll.add_child(_list) v.add_child(scroll) v.add_child(_button("Back", _show.bind("main"), 120)) return p func _fill_list() -> void: for c in _list.get_children(): c.queue_free() var worlds := Saves.list_worlds() if worlds.is_empty(): _list.add_child(UITheme.label("No worlds yet.", 16, UITheme.TEXT_DIM)) return for w in worlds: var row := PanelContainer.new() row.add_theme_stylebox_override("panel", UITheme.box(UITheme.SLOT_BG, UITheme.SLOT_BORDER, 1, 5, 8)) var h := HBoxContainer.new() h.add_theme_constant_override("separation", 8) row.add_child(h) var info := VBoxContainer.new() info.size_flags_horizontal = Control.SIZE_EXPAND_FILL info.add_child(UITheme.label(w.name, 18)) var played := int(w.get("play_time", 0.0)) var when := Time.get_datetime_string_from_unix_time(int(w.get("last_played", 0)), true) info.add_child(UITheme.label("seed %d · played %dh %02dm · %s" % [int(w.seed), played / 3600, (played / 60) % 60, when], 13, UITheme.TEXT_DIM)) h.add_child(info) h.add_child(_button("Play", _play.bind(w.id), 80)) var del := Button.new() del.text = "Delete" del.focus_mode = Control.FOCUS_NONE del.custom_minimum_size = Vector2(90, 46) del.add_theme_font_size_override("font_size", 19) del.pressed.connect(_on_delete.bind(w.id, del)) h.add_child(del) _list.add_child(row) func _on_delete(id: String, b: Button) -> void: if _armed_delete != id: _armed_delete = id b.text = "Sure?" return Saves.delete_world(id) _armed_delete = "" _fill_list() if Saves.list_worlds().is_empty(): _rebuild_main() func _rebuild_main() -> void: var old: Control = _pages["main"] var parent := old.get_parent() var idx := old.get_index() old.queue_free() _pages["main"] = _main_page() parent.add_child(_pages["main"]) parent.move_child(_pages["main"], idx) _pages["main"].visible = false func _unique_name() -> String: var names := Saves.list_worlds().map(func(w): return w.name) var n := names.size() + 1 var base := "Island" while ("%s %d" % [base, n]) in names: n += 1 return "%s %d" % [base, n] func _create() -> void: var world_name := _name_edit.text.strip_edges() if world_name.is_empty(): world_name = _unique_name() var seed_text := _seed_edit.text.strip_edges() var seed := int(seed_text) if seed_text.is_valid_int() else (seed_text.hash() if not seed_text.is_empty() else randi()) _play(Saves.create_world(world_name, seed)) func _play(id: String) -> void: Saves.current_id = id get_tree().change_scene_to_file(WORLD_SCENE)