Files
Emberwild/scripts/ui/title_screen.gd

679 lines
25 KiB
GDScript

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 _rot_pick: OptionButton
## [setting, what the New World page calls it]
const ROT_CHOICES := [["normal", "Normally (raw meat lasts 2 days)"], ["slow", "Slowly (twice as long)"], ["off", "Never"]]
var _list: VBoxContainer
var _armed_delete := ""
const Identity = preload("res://scripts/net/identity.gd")
var _after_profile := "main" ## page to return to after saving the profile
var _prof_name: LineEdit
var _prof_msg: Label
var _prof_tag: Label
var _prof_beard: Button
var _prof_tunic: Array = []
var _prof_hair: Array = []
var _prof_fig: Dictionary = {}
var _prof_app: Dictionary = {}
var _join_addr: LineEdit
var _join_pw: LineEdit
var _join_msg: Label
var _join_btn: Button
var _lobby_title: Label
var _lobby_motd: Label
var _lobby_rules: Label
var _lobby_count: Label
var _lobby_list: VBoxContainer
var _lobby_note: Label
func _ready() -> void:
if Net.dedicated:
return # a headless server has no menu (and doesn't need the backdrop world)
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("EMBERWILD", 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()
_pages["profile"] = _profile_page()
_pages["join"] = _join_page()
_pages["lobby"] = _lobby_page()
_pages["host"] = _host_page()
for p in _pages.values():
col.add_child(p)
Net.host_status.connect(_on_host_status)
Net.joined.connect(_enter_server_world)
Net.join_failed.connect(_on_join_failed)
Net.left.connect(_on_left)
Net.players_changed.connect(func():
if _pages["lobby"].visible:
_fill_lobby())
_show("main")
# Back from a server (left, kicked, or it went away): say why.
if Net.state == "offline" and not Net.last_reason.is_empty():
_show("join")
_join_msg.text = Net.last_reason
Net.last_reason = ""
_maybe_capture()
## The server let us in: show who's there for a moment, then load the island.
func _enter_server_world() -> void:
_show("lobby")
_lobby_note.text = "Loading the island..."
for _i in range(3):
await get_tree().process_frame
if Net.state != "connected":
return
Saves.current_id = "" # a server's world isn't saved locally
get_tree().change_scene_to_file(WORLD_SCENE)
## 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])
var me: RefCounted = Net.get_identity()
if me.name.is_empty():
me.name = "Paul" # in memory only, for the pictures
for page in ["main", "new", "load", "profile", "join", "host", "lobby"]:
if page == "lobby":
# Sample data: the lobby normally needs a live server.
Net.server_info = {"server_name": "Paul's island", "motd": "Welcome! Be nice to the wolves.",
"max_players": 8, "pvp": false, "raiding": false, "difficulty": "normal"}
Net.players = {1: {"name": "Paul", "admin": true, "appearance": {"tunic": 1}},
2: {"name": "Freya", "admin": false, "appearance": {"tunic": 2}},
3: {"name": "Old Bjorn", "admin": false, "appearance": {"tunic": 3}}}
Net.local_peer = 1
_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:
if _pages["lobby"].visible:
Net.leave()
elif Net.state == "connecting" or Net.hosting:
Net.stop_hosting()
Net.stop()
_join_msg.text = ""
_show("main")
else:
_show("main")
func _show(page: String) -> void:
# Joining (or hosting) needs a name: send people to their profile first.
if page in ["join", "host"] and Identity.name_problem(Net.get_identity().name) != "":
_after_profile = page
page = "profile"
if page == "host":
_fill_host()
for k in _pages:
_pages[k].visible = k == page
_armed_delete = ""
if page == "profile":
_fill_profile()
elif page == "join":
_join_shown_at = Time.get_ticks_msec()
_join_addr.text = Net.get_identity().last_server
_join_btn.disabled = false
_join_addr.grab_focus()
elif page == "lobby":
_fill_lobby()
if page == "load":
_fill_list()
elif page == "new":
_name_edit.text = _unique_name()
_seed_edit.text = ""
_rot_pick.select(0)
_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("Host Game", _show.bind("host")))
v.add_child(_button("Join Game", _show.bind("join")))
var n: String = Net.get_identity().name
v.add_child(_button("Profile: %s" % n if not n.is_empty() else "Set Up Your Profile",
func(): _after_profile = "main"; _show("profile")))
v.add_child(_button("Quit", get_tree().quit))
return v
# --- multiplayer: profile, join, lobby ----------------------------------------
func _profile_page() -> Control:
var p := UITheme.panel()
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", 22)
p.add_child(h)
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 8)
h.add_child(v)
v.add_child(UITheme.label("Your profile", 24))
v.add_child(UITheme.label("Name (shown over your character to other players)", 15, UITheme.TEXT_DIM))
_prof_name = LineEdit.new()
_prof_name.custom_minimum_size = Vector2(300, 40)
_prof_name.max_length = 16
_prof_name.text_changed.connect(func(_t): _update_profile_preview())
_prof_name.text_submitted.connect(func(_t): _save_profile())
v.add_child(_prof_name)
v.add_child(UITheme.label("Tunic", 15, UITheme.TEXT_DIM))
v.add_child(_swatch_row(Identity.TUNICS, "tunic", _prof_tunic))
v.add_child(UITheme.label("Hair", 15, UITheme.TEXT_DIM))
v.add_child(_swatch_row(Identity.HAIR, "hair", _prof_hair))
_prof_beard = Button.new()
_prof_beard.toggle_mode = true
_prof_beard.focus_mode = Control.FOCUS_NONE
_prof_beard.custom_minimum_size = Vector2(140, 34)
_prof_beard.add_theme_font_size_override("font_size", 16)
_prof_beard.toggled.connect(func(on): _prof_app.beard = on; _update_profile_preview())
v.add_child(_prof_beard)
_prof_msg = UITheme.label("", 14, UITheme.TEXT_DIM)
v.add_child(_prof_msg)
var b := HBoxContainer.new()
b.add_theme_constant_override("separation", 10)
b.add_child(_button("Save", _save_profile, 140))
b.add_child(_button("Back", func(): _show("main"), 120))
v.add_child(b)
# A little figure: hair, face, beard, tunic.
var fig := VBoxContainer.new()
fig.add_theme_constant_override("separation", 0)
fig.alignment = BoxContainer.ALIGNMENT_CENTER
_prof_tag = UITheme.label("", 15)
_prof_tag.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
fig.add_child(_prof_tag)
_prof_fig = {}
for part in [["hair", Vector2(44, 14)], ["face", Vector2(40, 30)], ["beard", Vector2(34, 12)], ["tunic", Vector2(64, 60)]]:
var c := CenterContainer.new()
var r := ColorRect.new()
r.custom_minimum_size = part[1]
c.add_child(r)
fig.add_child(c)
_prof_fig[part[0]] = r
_prof_fig.face.color = Color(0.86, 0.68, 0.55)
h.add_child(fig)
return p
func _swatch_row(colors: Array, key: String, store: Array) -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 6)
for i in range(colors.size()):
var s := Button.new()
s.custom_minimum_size = Vector2(36, 30)
s.focus_mode = Control.FOCUS_NONE
s.pressed.connect(func(): _prof_app[key] = i; _update_profile_preview())
row.add_child(s)
store.append(s)
return row
func _fill_profile() -> void:
var me: RefCounted = Net.get_identity()
_prof_name.text = me.name
_prof_app = me.appearance.duplicate()
_prof_beard.set_pressed_no_signal(bool(_prof_app.beard))
_prof_msg.text = "Pick a name to join servers." if me.name.is_empty() else ""
_update_profile_preview()
_prof_name.grab_focus()
func _update_profile_preview() -> void:
for pair in [[_prof_tunic, Identity.TUNICS, "tunic"], [_prof_hair, Identity.HAIR, "hair"]]:
for i in range(pair[0].size()):
var sel: bool = int(_prof_app[pair[2]]) == i
var st := UITheme.box(pair[1][i], UITheme.SELECTED if sel else UITheme.SLOT_BORDER, 3 if sel else 1, 4, 0)
for state in ["normal", "hover", "pressed"]:
pair[0][i].add_theme_stylebox_override(state, st)
_prof_beard.text = "Beard: %s" % ("yes" if _prof_app.beard else "no")
var bst := UITheme.box(UITheme.SLOT_BG, UITheme.SELECTED if _prof_app.beard else UITheme.SLOT_BORDER, 2 if _prof_app.beard else 1, 4, 6)
for state in ["normal", "hover", "pressed", "hover_pressed"]:
_prof_beard.add_theme_stylebox_override(state, bst)
var hair: Color = Identity.HAIR[int(_prof_app.hair)]
_prof_fig.hair.color = hair
_prof_fig.beard.color = hair if _prof_app.beard else Color(0, 0, 0, 0)
_prof_fig.tunic.color = Identity.TUNICS[int(_prof_app.tunic)]
var n := _prof_name.text
_prof_tag.text = n if not n.is_empty() else "your name"
var bad := Identity.name_problem(n)
_prof_msg.text = bad if not n.is_empty() else "Pick a name to join servers."
_prof_msg.add_theme_color_override("font_color", UITheme.BAD if not bad.is_empty() and not n.is_empty() else UITheme.TEXT_DIM)
func _save_profile() -> void:
var bad := Identity.name_problem(_prof_name.text)
if not bad.is_empty():
_update_profile_preview()
return
var me: RefCounted = Net.get_identity()
me.name = _prof_name.text
me.appearance = Identity.sanitize_appearance(_prof_app)
me.save_profile()
_rebuild_main()
_show(_after_profile)
var _join_shown_at := -100000
# --- hosting (M2.7) -------------------------------------------------------------
var _host_name: LineEdit
var _host_pw: LineEdit
var _host_port: LineEdit
var _host_seed: LineEdit
var _host_seed_label: Label
var _host_preset: OptionButton
var _host_msg: Label
var _host_btn: Button
const PRESET_IDS := ["co-op", "rivals", "adversarial"]
const PRESET_NAMES := ["Co-op: no PvP, bases safe, friends share", "Friendly rivals: PvP on, bases safe",
"Adversarial: PvP, raiding, locks, drop everything on death"]
func _host_page() -> Control:
var p := UITheme.panel()
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 7)
p.add_child(v)
v.add_child(UITheme.label("Host a game", 24))
v.add_child(UITheme.label("Runs a server on this computer and puts you in it. Friends join with your address.", 13, UITheme.TEXT_DIM))
v.add_child(UITheme.label("Server name", 15, UITheme.TEXT_DIM))
_host_name = LineEdit.new()
_host_name.custom_minimum_size = Vector2(420, 38)
_host_name.max_length = 40
v.add_child(_host_name)
v.add_child(UITheme.label("Rules", 15, UITheme.TEXT_DIM))
_host_preset = OptionButton.new()
for n in PRESET_NAMES:
_host_preset.add_item(n)
_host_preset.custom_minimum_size = Vector2(420, 36)
v.add_child(_host_preset)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var c1 := VBoxContainer.new()
c1.add_child(UITheme.label("Password (optional)", 15, UITheme.TEXT_DIM))
_host_pw = LineEdit.new()
_host_pw.secret = true
_host_pw.custom_minimum_size = Vector2(200, 38)
c1.add_child(_host_pw)
row.add_child(c1)
var c2 := VBoxContainer.new()
c2.add_child(UITheme.label("Port (UDP)", 15, UITheme.TEXT_DIM))
_host_port = LineEdit.new()
_host_port.custom_minimum_size = Vector2(100, 38)
c2.add_child(_host_port)
row.add_child(c2)
var c3 := VBoxContainer.new()
_host_seed_label = UITheme.label("Seed", 15, UITheme.TEXT_DIM)
c3.add_child(_host_seed_label)
_host_seed = LineEdit.new()
_host_seed.placeholder_text = "random"
_host_seed.custom_minimum_size = Vector2(100, 38)
c3.add_child(_host_seed)
row.add_child(c3)
v.add_child(row)
_host_msg = UITheme.label("", 14, UITheme.TEXT_DIM)
_host_msg.autowrap_mode = TextServer.AUTOWRAP_WORD
_host_msg.custom_minimum_size = Vector2(420, 0)
v.add_child(_host_msg)
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", 10)
_host_btn = _button("Start", _start_host, 140)
h.add_child(_host_btn)
h.add_child(_button("Back", func():
Net.stop_hosting()
Net.stop()
_show("main"), 120))
v.add_child(h)
return p
func _fill_host() -> void:
var c: RefCounted = load("res://scripts/net/server_config.gd").open(Net.host_config)
var n: String = c.data.server_name
_host_name.text = "%s's island" % Net.get_identity().name if n == "Emberwild server" else n
_host_port.text = str(int(c.data.port))
_host_pw.text = String(c.data.password)
var preset := "co-op"
if bool(c.data.pvp):
preset = "adversarial" if bool(c.data.raiding) else "rivals"
_host_preset.select(PRESET_IDS.find(preset))
var meta_path: String = Net.host_config.get_base_dir().path_join("worlds/server-world/meta.json")
var existing: bool = FileAccess.file_exists(meta_path)
_host_seed.editable = not existing
_host_seed_label.text = "Seed" if not existing else "Seed (kept)"
_host_seed.text = "" if not existing else str(int(c.data.seed))
_host_msg.text = "Your hosted island carries on where you left it." if existing else "A new island will be made."
_host_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
_host_btn.disabled = false
func _start_host() -> void:
var port_t := _host_port.text.strip_edges()
if not port_t.is_valid_int() or int(port_t) < 1024 or int(port_t) > 65535:
_on_host_status("The port is a number from 1024 to 65535 (24242 is the usual one)", true)
return
var seed_t := _host_seed.text.strip_edges()
var seed := int(seed_t) if seed_t.is_valid_int() else (seed_t.hash() if not seed_t.is_empty() else randi() % 1000000)
var name := _host_name.text.strip_edges()
if name.is_empty():
name = "%s's island" % Net.get_identity().name
_host_btn.disabled = true
Net.host(name, _host_pw.text, int(port_t), PRESET_IDS[maxi(_host_preset.selected, 0)], seed)
func _on_host_status(text: String, failed: bool) -> void:
if not _pages["host"].visible:
return
_host_msg.text = text
_host_msg.add_theme_color_override("font_color", UITheme.BAD if failed else UITheme.TEXT_DIM)
if failed:
_host_btn.disabled = false
func _join_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("Join a server", 24))
v.add_child(UITheme.label("Address (host, or host:port; the default port is %d)" % Net.DEFAULT_PORT, 15, UITheme.TEXT_DIM))
_join_addr = LineEdit.new()
_join_addr.placeholder_text = "e.g. 100.101.102.103 or homeserver:24242"
_join_addr.custom_minimum_size = Vector2(400, 40)
_join_addr.text_submitted.connect(func(_t): _connect())
v.add_child(_join_addr)
v.add_child(UITheme.label("Password (if the server has one)", 15, UITheme.TEXT_DIM))
_join_pw = LineEdit.new()
_join_pw.secret = true
_join_pw.custom_minimum_size = Vector2(400, 40)
_join_pw.text_submitted.connect(func(_t): _connect())
v.add_child(_join_pw)
v.add_child(UITheme.label("On Tailscale, use the server's 100.x address or its machine name.", 13, UITheme.TEXT_DIM))
_join_msg = UITheme.label("", 15, UITheme.TEXT_DIM)
_join_msg.autowrap_mode = TextServer.AUTOWRAP_WORD
_join_msg.custom_minimum_size = Vector2(400, 0)
v.add_child(_join_msg)
var h := HBoxContainer.new()
h.add_theme_constant_override("separation", 10)
_join_btn = _button("Connect", _connect, 160)
h.add_child(_join_btn)
h.add_child(_button("Back", func():
Net.stop()
_join_msg.text = ""
_show("main"), 120))
v.add_child(h)
return p
func _connect() -> void:
# (A click or Enter carried over from the page before can't connect.)
if Time.get_ticks_msec() - _join_shown_at < 400:
return
if _join_addr.text.strip_edges().is_empty():
_join_msg.text = "Enter the server's address"
_join_msg.add_theme_color_override("font_color", UITheme.BAD)
return
_join_msg.text = "Connecting to %s..." % _join_addr.text.strip_edges()
_join_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
_join_btn.disabled = true
Net.join(_join_addr.text, _join_pw.text)
func _on_join_failed(reason: String) -> void:
if _pages["host"].visible:
_on_host_status(reason, true)
return
_join_btn.disabled = false
_join_msg.text = reason
_join_msg.add_theme_color_override("font_color", UITheme.BAD)
func _on_left(reason: String) -> void:
_show("join")
_join_msg.text = reason
_join_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
func _lobby_page() -> Control:
var p := UITheme.panel()
var v := VBoxContainer.new()
v.add_theme_constant_override("separation", 8)
p.add_child(v)
_lobby_title = UITheme.label("", 24)
v.add_child(_lobby_title)
_lobby_motd = UITheme.label("", 15, UITheme.TEXT_DIM)
_lobby_motd.autowrap_mode = TextServer.AUTOWRAP_WORD
_lobby_motd.custom_minimum_size = Vector2(420, 0)
v.add_child(_lobby_motd)
_lobby_rules = UITheme.label("", 14, UITheme.TEXT_DIM)
v.add_child(_lobby_rules)
_lobby_count = UITheme.label("", 17)
v.add_child(_lobby_count)
_lobby_list = VBoxContainer.new()
_lobby_list.add_theme_constant_override("separation", 4)
v.add_child(_lobby_list)
_lobby_note = UITheme.label("", 13, UITheme.TEXT_DIM)
_lobby_note.autowrap_mode = TextServer.AUTOWRAP_WORD
_lobby_note.custom_minimum_size = Vector2(420, 0)
v.add_child(_lobby_note)
v.add_child(_button("Disconnect", Net.leave, 180))
return p
func _fill_lobby() -> void:
var s: Dictionary = Net.server_info
_lobby_title.text = String(s.get("server_name", "Server"))
_lobby_motd.text = String(s.get("motd", ""))
var mode := "Co-op"
if s.get("pvp", false):
mode = "Adversarial" if s.get("raiding", false) else "Friendly rivals"
_lobby_rules.text = "%s · PvP %s · raiding %s · %s difficulty" % [mode, "on" if s.get("pvp", false) else "off",
"on" if s.get("raiding", false) else "off", s.get("difficulty", "normal")]
_lobby_count.text = "Players (%d/%d)" % [Net.players.size(), int(s.get("max_players", 0))]
for c in _lobby_list.get_children():
c.queue_free()
var peers := Net.players.keys()
peers.sort_custom(func(a, b): return String(Net.players[a].name).to_lower() < String(Net.players[b].name).to_lower())
for peer in peers:
var pl: Dictionary = Net.players[peer]
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
var sw := ColorRect.new()
sw.custom_minimum_size = Vector2(16, 16)
sw.color = Identity.TUNICS[int(pl.get("appearance", {}).get("tunic", 0))]
var c := CenterContainer.new()
c.add_child(sw)
row.add_child(c)
var tags := []
if peer == Net.local_peer:
tags.append("you")
if pl.get("admin", false):
tags.append("admin")
row.add_child(UITheme.label(String(pl.name) + (" (%s)" % ", ".join(tags) if not tags.is_empty() else ""), 16,
UITheme.SELECTED if peer == Net.local_peer else UITheme.TEXT))
_lobby_list.add_child(row)
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)
v.add_child(UITheme.label("Food spoils", 15, UITheme.TEXT_DIM))
_rot_pick = OptionButton.new()
for t in ROT_CHOICES:
_rot_pick.add_item(t[1])
_rot_pick.custom_minimum_size = Vector2(360, 36)
v.add_child(_rot_pick)
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())
var rot: String = ROT_CHOICES[maxi(_rot_pick.selected, 0)][0]
_play(Saves.create_world(world_name, seed, {"rot": rot}))
func _play(id: String) -> void:
Saves.current_id = id
get_tree().change_scene_to_file(WORLD_SCENE)