extends CanvasLayer ## Character roster: pick one, or make one. ## ## Shown when the player has no character in the world -- at first login, and ## again when their last one dies. Permadeath makes this a screen players will ## see repeatedly, so dead characters stay listed rather than vanishing: seeing ## the run you lost is the point of keeping the record. signal select_requested(character_id: String) signal create_requested(character_name: String) signal closed var _list: VBoxContainer var _name_field: LineEdit var _create_button: Button var _status: Label var _title: Label var _close_button: Button var _known: Array[Dictionary] = [] var _rng := RandomNumberGenerator.new() var _selected: String = "" func _ready() -> void: layer = 30 _rng.randomize() var root := UiTheme.themed_root() add_child(root) var scrim := ColorRect.new() scrim.color = Color(0.03, 0.03, 0.06, 0.92) scrim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(scrim) var centre := CenterContainer.new() centre.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(centre) # A panel, so a dialog reads as a thing laid over the world rather # than as text floating on a dimmed one. var frame := UiTheme.dialog_panel() centre.add_child(frame) var panel := VBoxContainer.new() panel.custom_minimum_size = Vector2(460.0, 0.0) panel.add_theme_constant_override("separation", 8) frame.add_child(panel) _title = Label.new() _title.text = "CHARACTERS" _title.add_theme_font_size_override("font_size", 26) panel.add_child(_title) _list = VBoxContainer.new() _list.add_theme_constant_override("separation", 4) panel.add_child(_list) var spacer := Control.new() spacer.custom_minimum_size = Vector2(0.0, 10.0) panel.add_child(spacer) var row := HBoxContainer.new() panel.add_child(row) _name_field = LineEdit.new() _name_field.placeholder_text = "new character name" _name_field.text = Character.random_name(_rng) _name_field.max_length = Character.MAX_NAME _name_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(_name_field) _create_button = Button.new() _create_button.text = "Create" _create_button.pressed.connect(_on_create) row.add_child(_create_button) _close_button = Button.new() _close_button.text = "Back to the hub" _close_button.pressed.connect(func() -> void: closed.emit()) panel.add_child(_close_button) _status = Label.new() _status.add_theme_font_size_override("font_size", 12) _status.add_theme_color_override("font_color", Color(1.0, 0.6, 0.5)) panel.add_child(_status) visible = false func _on_create() -> void: var wanted := _name_field.text.strip_edges() if wanted.is_empty(): set_status("give the character a name") return create_requested.emit(wanted) # Offer the next suggestion straight away rather than clearing to blank. _name_field.text = Character.random_name(_rng) func set_status(text: String) -> void: _status.text = text ## Rebuild from the server's roster. Called on every change rather than diffed: ## the list is at most a handful of rows and correctness matters more than ## avoiding a few Control allocations. func refresh(characters: Array[Dictionary], selected: String) -> void: _known = characters _selected = selected for child in _list.get_children(): child.queue_free() # Everything the server sends is alive: a dead character is gone as far as # the player is concerned, and retirement is the server's own bookkeeping. var living := characters.size() for c in characters: _list.add_child(_row_for(c)) if characters.is_empty(): var empty := Label.new() empty.text = "No characters yet. Name one below to begin." empty.add_theme_color_override("font_color", Color(0.7, 0.75, 0.85)) _list.add_child(empty) var room := living < CharacterStore.MAX_ACTIVE _create_button.disabled = not room _name_field.editable = room _title.text = "CHARACTERS (%d / %d living)" % [living, CharacterStore.MAX_ACTIVE] if not room: set_status("%d living characters is the limit -- one must fall first" % CharacterStore.MAX_ACTIVE) func _row_for(c: Dictionary) -> Control: var row := HBoxContainer.new() # The character's colour, which is its only identity until cosmetics exist. var swatch := ColorRect.new() swatch.color = c["colour"] swatch.custom_minimum_size = Vector2(14.0, 14.0) row.add_child(swatch) var playing := String(c["id"]) == _selected var label := Label.new() label.text = " %s level %d %d hp%s" % [ c["name"], int(c["level"]), int(c["max_hp"]), " <- playing" if playing else ""] label.add_theme_color_override("font_color", Color(0.85, 0.9, 1.0)) label.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(label) var play := Button.new() play.text = "Play" play.disabled = playing play.pressed.connect(func() -> void: select_requested.emit(String(c["id"]))) row.add_child(play) return row ## Whether the player may dismiss this screen. False when they have nothing to ## play, in which case there is nowhere to dismiss it to. func set_dismissible(can_close: bool) -> void: _close_button.visible = can_close