class_name ProfilePage extends Control const CHECK_DEBOUNCE_SECONDS: float = 0.4 const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) var _service: NetworkProfileService var _experience: PlayerExperience var _draft_name: String = "" var _draft_appearance: Dictionary = {} var _persisted_name: String = "" var _persisted_appearance: Dictionary = {} var _category_id: String = "species" var _dirty: bool = false var _allow_duplicate: bool = false var _name_edit: LineEdit var _name_status: Label var _suggestions: HBoxContainer var _category_list: VBoxContainer var _option_list: VBoxContainer var _preview: ProfilePreview var _apply_button: Button var _revert_button: Button var _discard_confirmation: PanelContainer var _confirmation_label: Label var _confirmation_confirm: Button var _keep_editing_button: Button var _confirmation_action: String = "" var _debounce: Timer var _experience_level: Label var _experience_progress: ProgressBar var _experience_value: Label var _feature_preview_cache: Dictionary = {} var _scale_value_label: Label func _ready() -> void: mouse_filter = Control.MOUSE_FILTER_PASS _build_ui() _debounce = Timer.new() _debounce.one_shot = true _debounce.wait_time = CHECK_DEBOUNCE_SECONDS _debounce.timeout.connect(_request_conflict_check) add_child(_debounce) func setup( service: NetworkProfileService, experience: PlayerExperience, world_environment: WorldEnvironment, world_sun: DirectionalLight3D, ) -> void: _service = service _experience = experience if not _service.conflict_result.is_connected(_on_conflict_result): _service.conflict_result.connect(_on_conflict_result) _service.apply_finished.connect(_on_apply_finished) if ( _experience != null and not _experience.experience_changed.is_connected( _on_experience_changed ) ): _experience.experience_changed.connect(_on_experience_changed) _load_persisted() _refresh_experience() _preview.setup_world_lighting(world_environment, world_sun) var identity_value := find_child("IdentityFingerprint", true, false) as Label if identity_value != null: var fingerprint := _service.get_identity_fingerprint() identity_value.text = "Identity • %s • Stored on this device" % ( NetworkIdentityCrypto.compact_suffix(fingerprint) ) identity_value.tooltip_text = ( NetworkIdentityCrypto.format_fingerprint(fingerprint) ) func setup_controller_mapping( mapping_manager: ControllerMappingManagerType, ) -> void: if _preview != null: _preview.setup_controller_mapping(mapping_manager) func set_world_pixel_size(pixel_size: int) -> void: if _preview != null: _preview.set_world_pixel_size(pixel_size) func activate() -> void: visible = true UtilityPageStyle.animate_in(self) if _service != null: _load_persisted() # Keep controller page switches on the Profile navigation bubble. Focusing # the name field here summons the Android keyboard during LB/RB traversal. func deactivate() -> void: visible = false _debounce.stop() _preview.reset_view() func set_interactive(interactive: bool) -> void: mouse_filter = ( Control.MOUSE_FILTER_PASS if interactive else Control.MOUSE_FILTER_IGNORE ) func consume_escape() -> bool: if _discard_confirmation.visible: _discard_confirmation.visible = false _confirmation_action = "" _name_edit.grab_focus() return true if _name_edit.has_focus(): _name_edit.release_focus() return true if _dirty: _show_confirmation("discard") return true return false func has_unsaved_changes() -> bool: return _dirty func request_close_confirmation() -> bool: if not _dirty: return false _show_confirmation("discard") return true func has_modal_confirmation() -> bool: return ( _discard_confirmation != null and _discard_confirmation.visible ) func _build_ui() -> void: UtilityPageStyle.apply_page(self) var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) var layout := VBoxContainer.new() layout.add_theme_constant_override("separation", 8) margin.add_child(layout) var account_row := HBoxContainer.new() account_row.add_theme_constant_override("separation", 14) layout.add_child(account_row) var account_stack := VBoxContainer.new() account_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL account_stack.add_theme_constant_override("separation", 2) account_row.add_child(account_stack) var name_line := HBoxContainer.new() name_line.add_theme_constant_override("separation", 10) account_stack.add_child(name_line) var name_label := Label.new() name_label.text = "player name" name_label.custom_minimum_size.x = 98.0 name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) name_line.add_child(name_label) _name_edit = LineEdit.new() _name_edit.max_length = NetworkProtocol.MAX_DISPLAY_NAME_LENGTH _name_edit.placeholder_text = "Player" _name_edit.custom_minimum_size = Vector2(280, 34) UtilityPageStyle.apply_ocean_line_edit(_name_edit) _name_edit.add_theme_stylebox_override( "normal", UtilityPageStyle.ocean_button_style( UtilityPageStyle.OCEAN_PANEL_MID ) ) _name_edit.text_changed.connect(_on_name_changed) name_line.add_child(_name_edit) var helper := Label.new() helper.text = "Shown to other players in multiplayer." helper.add_theme_font_size_override("font_size", 12) helper.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) account_stack.add_child(helper) _name_status = Label.new() _name_status.add_theme_font_size_override("font_size", 12) _name_status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) account_stack.add_child(_name_status) _suggestions = HBoxContainer.new() _suggestions.add_theme_constant_override("separation", 6) account_stack.add_child(_suggestions) var identity_value := Label.new() identity_value.name = "IdentityFingerprint" identity_value.text = "identity • stored on this device" identity_value.tooltip_text = ( "This identity helps other players recognize you between sessions." ) identity_value.add_theme_font_size_override("font_size", 12) identity_value.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) account_stack.add_child(identity_value) var experience_row := HBoxContainer.new() experience_row.add_theme_constant_override("separation", 9) account_stack.add_child(experience_row) _experience_level = Label.new() _experience_level.custom_minimum_size.x = 64.0 _experience_level.add_theme_font_size_override("font_size", 14) _experience_level.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) experience_row.add_child(_experience_level) _experience_progress = ProgressBar.new() _experience_progress.custom_minimum_size = Vector2(210.0, 14.0) _experience_progress.max_value = 1.0 _experience_progress.show_percentage = false _experience_progress.add_theme_stylebox_override( "background", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_PANEL_MID, 7 ) ) _experience_progress.add_theme_stylebox_override( "fill", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_SELECTED, 7 ) ) experience_row.add_child(_experience_progress) _experience_value = Label.new() _experience_value.add_theme_font_size_override("font_size", 14) _experience_value.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) experience_row.add_child(_experience_value) var actions := HBoxContainer.new() actions.alignment = BoxContainer.ALIGNMENT_END actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN actions.add_theme_constant_override("separation", 7) account_row.add_child(actions) _apply_button = Button.new() _apply_button.text = "apply" _apply_button.custom_minimum_size.x = 72.0 UtilityPageStyle.apply_compact_ocean_button(_apply_button) _apply_button.pressed.connect(_apply) actions.add_child(_apply_button) _revert_button = Button.new() _revert_button.text = "revert" _revert_button.custom_minimum_size.x = 76.0 UtilityPageStyle.apply_compact_ocean_button(_revert_button) _revert_button.pressed.connect(_revert) actions.add_child(_revert_button) var defaults_button := Button.new() defaults_button.text = "defaults" defaults_button.custom_minimum_size.x = 82.0 UtilityPageStyle.apply_compact_ocean_button(defaults_button) defaults_button.pressed.connect(_show_confirmation.bind("defaults")) actions.add_child(defaults_button) var body_panel := PanelContainer.new() body_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL body_panel.add_theme_stylebox_override( "panel", UtilityPageStyle.row_style(false) ) layout.add_child(body_panel) var body_margin := MarginContainer.new() body_margin.add_theme_constant_override("margin_left", 14) body_margin.add_theme_constant_override("margin_top", 10) body_margin.add_theme_constant_override("margin_right", 14) body_margin.add_theme_constant_override("margin_bottom", 10) body_panel.add_child(body_margin) var body := HBoxContainer.new() body.alignment = BoxContainer.ALIGNMENT_BEGIN body.add_theme_constant_override("separation", 16) body_margin.add_child(body) _category_list = VBoxContainer.new() _category_list.custom_minimum_size = Vector2(120, 0) _category_list.add_theme_constant_override("separation", 5) body.add_child(_category_list) _option_list = VBoxContainer.new() _option_list.custom_minimum_size = Vector2(360, 0) _option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL _option_list.size_flags_vertical = Control.SIZE_EXPAND_FILL _option_list.add_theme_constant_override("separation", 5) body.add_child(_option_list) var body_spacer := Control.new() body_spacer.custom_minimum_size.x = 12.0 body_spacer.size_flags_horizontal = Control.SIZE_SHRINK_CENTER body.add_child(body_spacer) var preview_stack := VBoxContainer.new() preview_stack.custom_minimum_size.x = 260.0 preview_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER preview_stack.add_theme_constant_override("separation", 7) body.add_child(preview_stack) var preview_frame := PanelContainer.new() preview_frame.custom_minimum_size = Vector2(260.0, 0.0) preview_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL preview_frame.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_FIELD, 18 ) ) preview_stack.add_child(preview_frame) var preview_layer := Control.new() preview_layer.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview_layer.size_flags_vertical = Control.SIZE_EXPAND_FILL preview_layer.mouse_filter = Control.MOUSE_FILTER_PASS preview_frame.add_child(preview_layer) _preview = preload("res://ui/profile_preview.tscn").instantiate() _preview.custom_minimum_size = Vector2(260.0, 0.0) _preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL _preview.size_flags_vertical = Control.SIZE_EXPAND_FILL preview_layer.add_child(_preview) _preview.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) var reset_view := Button.new() reset_view.text = "↶" reset_view.tooltip_text = "reset view" reset_view.custom_minimum_size = Vector2(36.0, 36.0) reset_view.position = Vector2(-8.0, -8.0) reset_view.z_index = 2 reset_view.pressed.connect(_preview.reset_view) UtilityPageStyle.apply_compact_ocean_button(reset_view) reset_view.add_theme_font_size_override("font_size", 22) preview_layer.add_child(reset_view) _discard_confirmation = PanelContainer.new() _discard_confirmation.visible = false _discard_confirmation.set_anchors_and_offsets_preset(Control.PRESET_CENTER) _discard_confirmation.custom_minimum_size = Vector2(430, 190) _discard_confirmation.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_PANEL_MID, 14 ) ) add_child(_discard_confirmation) var confirm_stack := VBoxContainer.new() confirm_stack.alignment = BoxContainer.ALIGNMENT_CENTER confirm_stack.add_theme_constant_override("separation", 16) _discard_confirmation.add_child(confirm_stack) _confirmation_label = Label.new() _confirmation_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER confirm_stack.add_child(_confirmation_label) var confirm_buttons := HBoxContainer.new() confirm_buttons.alignment = BoxContainer.ALIGNMENT_CENTER confirm_stack.add_child(confirm_buttons) _confirmation_confirm = Button.new() _confirmation_confirm.pressed.connect(_confirm_pending_action) UtilityPageStyle.apply_ocean_button(_confirmation_confirm) confirm_buttons.add_child(_confirmation_confirm) _keep_editing_button = Button.new() _keep_editing_button.text = "keep editing" _keep_editing_button.pressed.connect(func() -> void: _discard_confirmation.visible = false _name_edit.grab_focus() ) UtilityPageStyle.apply_ocean_button(_keep_editing_button) confirm_buttons.add_child(_keep_editing_button) _build_categories() func _build_categories() -> void: for child: Node in _category_list.get_children(): child.queue_free() for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS: var button := Button.new() button.text = CharacterCustomizationCatalog.category_label(category_id) button.toggle_mode = true button.custom_minimum_size.x = 120.0 button.button_pressed = category_id == _category_id button.pressed.connect(_select_category.bind(category_id)) UtilityPageStyle.apply_compact_ocean_button(button) _category_list.add_child(button) _refresh_options() func _select_category(category_id: String) -> void: _category_id = category_id for child: Node in _category_list.get_children(): var button := child as Button if button != null: button.button_pressed = button.text == ( CharacterCustomizationCatalog.category_label(category_id) ) _refresh_options() func _refresh_options() -> void: for child: Node in _option_list.get_children(): child.queue_free() var title := Label.new() title.text = CharacterCustomizationCatalog.category_label(_category_id) title.add_theme_font_size_override("font_size", 18) title.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) _option_list.add_child(title) if _category_id == CharacterCustomizationCatalog.SCALE_CATEGORY_ID: _build_scale_option() return var options: Array = CharacterCustomizationCatalog.options_for(_category_id) if options.is_empty(): var empty := Label.new() empty.text = "More options coming later." empty.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _option_list.add_child(empty) return if _category_id == "fur_pattern": _build_fur_color_options(options) return if _category_id in CharacterCustomizationCatalog.FEATURE_CATEGORIES: _build_feature_preview_options(options) return for option: Dictionary in options: var option_id := str(option["id"]) var button := Button.new() button.text = "×" if option_id == "none" else str(option["label"]) button.tooltip_text = "none" if option_id == "none" else str( option["label"] ) button.toggle_mode = true button.custom_minimum_size.x = 170.0 button.button_pressed = _draft_appearance.get(_category_id) == option_id button.pressed.connect(_select_option.bind(_category_id, option_id)) UtilityPageStyle.apply_compact_ocean_button(button) if option_id == "none": button.add_theme_font_size_override("font_size", 24) _option_list.add_child(button) func _build_scale_option() -> void: var description := Label.new() description.text = "Adjust your character's visual size." description.add_theme_font_size_override("font_size", 14) description.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _option_list.add_child(description) var value_row := HBoxContainer.new() value_row.add_theme_constant_override("separation", 10) value_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL _option_list.add_child(value_row) var small_label := Label.new() small_label.text = "small" small_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER small_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) value_row.add_child(small_label) var slider := HSlider.new() slider.name = "CharacterScaleSlider" slider.min_value = CharacterCustomizationCatalog.MIN_CHARACTER_SCALE slider.max_value = CharacterCustomizationCatalog.MAX_CHARACTER_SCALE slider.step = CharacterCustomizationCatalog.CHARACTER_SCALE_STEP slider.custom_minimum_size = Vector2(220.0, 40.0) slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL slider.tooltip_text = "character size" slider.value = CharacterCustomizationCatalog.character_scale( _draft_appearance.get( CharacterCustomizationCatalog.SCALE_CATEGORY_ID, CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE, ) ) slider.value_changed.connect(_select_scale) value_row.add_child(slider) var large_label := Label.new() large_label.text = "large" large_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER large_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) value_row.add_child(large_label) _scale_value_label = Label.new() _scale_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _scale_value_label.add_theme_font_size_override("font_size", 20) _scale_value_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) _option_list.add_child(_scale_value_label) _update_scale_value_label(float(slider.value)) var gameplay_note := Label.new() gameplay_note.text = "Movement and collision stay the same." gameplay_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER gameplay_note.add_theme_font_size_override("font_size", 12) gameplay_note.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _option_list.add_child(gameplay_note) func _build_feature_preview_options(options: Array) -> void: var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(190.0, 0.0) scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO _option_list.add_child(scroll) var grid := GridContainer.new() grid.columns = 4 grid.add_theme_constant_override("h_separation", 8) grid.add_theme_constant_override("v_separation", 8) grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.add_child(grid) var selected_id := CharacterCustomizationCatalog.canonical_option_id( _category_id, str(_draft_appearance.get(_category_id, "")) ) for option: Dictionary in options: var option_id := str(option.get("id", "")) var button := Button.new() var is_none := option_id == "none" button.text = "×" if is_none else "" button.tooltip_text = "none" if is_none else str( option.get("label", option_id) ) button.toggle_mode = true button.button_pressed = selected_id == option_id button.custom_minimum_size = Vector2(84.0, 64.0) button.alignment = HORIZONTAL_ALIGNMENT_CENTER button.clip_contents = true button.pressed.connect(_select_option.bind(_category_id, option_id)) UtilityPageStyle.apply_compact_ocean_button(button) button.custom_minimum_size = Vector2(84.0, 64.0) if is_none: button.add_theme_font_size_override("font_size", 28) else: var preview := TextureRect.new() preview.texture = _feature_preview_texture( _category_id, option_id ) preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED preview.mouse_filter = Control.MOUSE_FILTER_IGNORE var preview_height := 50.0 var preview_width := clampf( preview_height * PlayerVisualPresenter.feature_uv_aspect( _category_id ), 32.0, 74.0, ) preview.custom_minimum_size = Vector2(preview_width, preview_height) preview.size = Vector2(preview_width, preview_height) preview.position = Vector2((84.0 - preview_width) * 0.5, 7.0) button.add_child(preview) grid.add_child(button) func _feature_preview_texture( category_id: String, option_id: String, ) -> Texture2D: var cache_key := "%s:%s" % [category_id, option_id] if _feature_preview_cache.has(cache_key): return _feature_preview_cache[cache_key] as Texture2D var source := CharacterCustomizationCatalog.texture_for( category_id, option_id ) if source == null: return null var image := source.get_image() var used := image.get_used_rect() if used.size.x <= 0 or used.size.y <= 0: _feature_preview_cache[cache_key] = source return source var padding := 12 used.position -= Vector2i(padding, padding) used.size += Vector2i(padding * 2, padding * 2) used = used.intersection(Rect2i(Vector2i.ZERO, image.get_size())) var cropped := image.get_region(used) var preview := ImageTexture.create_from_image(cropped) _feature_preview_cache[cache_key] = preview return preview func _build_fur_color_options(options: Array) -> void: var grid := GridContainer.new() grid.columns = 4 grid.add_theme_constant_override("h_separation", 10) grid.add_theme_constant_override("v_separation", 10) _option_list.add_child(grid) var selected_id: String = CharacterCustomizationCatalog.canonical_option_id( "fur_pattern", str(_draft_appearance.get("fur_pattern", "white")) ) for option: Dictionary in options: var option_id: String = str(option.get("id", "")) var color: Color = CharacterCustomizationCatalog.option_color( "fur_pattern", option_id ) var button := Button.new() button.text = "" button.tooltip_text = str(option.get("label", option_id)) button.toggle_mode = true button.custom_minimum_size = Vector2(44.0, 44.0) button.button_pressed = selected_id == option_id button.pressed.connect(_select_option.bind("fur_pattern", option_id)) _apply_fur_swatch_style(button, color, button.button_pressed) grid.add_child(button) func _apply_fur_swatch_style( button: Button, color: Color, selected: bool, ) -> void: var normal: StyleBoxFlat = UtilityPageStyle.rounded_style(color, 999) var hover: StyleBoxFlat = UtilityPageStyle.rounded_style( color.lightened(0.12), 999 ) var selected_style: StyleBoxFlat = UtilityPageStyle.rounded_style(color, 999) selected_style.border_color = UtilityPageStyle.OCEAN_TEXT_PRIMARY selected_style.set_border_width_all(3 if selected else 2) button.add_theme_stylebox_override("normal", normal) button.add_theme_stylebox_override("hover", hover) button.add_theme_stylebox_override("pressed", selected_style) button.add_theme_stylebox_override("focus", selected_style) func _select_option(category_id: String, option_id: String) -> void: _draft_appearance[category_id] = option_id _preview.apply_appearance_profile(_draft_appearance) _dirty = _draft_differs() _refresh_options() _refresh_actions() func _select_scale(value: float) -> void: var resolved_scale: float = CharacterCustomizationCatalog.character_scale( value ) _draft_appearance[CharacterCustomizationCatalog.SCALE_CATEGORY_ID] = ( resolved_scale ) _update_scale_value_label(resolved_scale) _preview.apply_appearance_profile(_draft_appearance) _dirty = _draft_differs() _refresh_actions() func _update_scale_value_label(value: float) -> void: if _scale_value_label != null: _scale_value_label.text = "%d%%" % ( CharacterCustomizationCatalog.character_scale_percent(value) ) func _on_name_changed(value: String) -> void: _draft_name = value _allow_duplicate = false _dirty = _draft_differs() _refresh_actions() _clear_suggestions() if NetworkProfilePreferences.is_valid_display_name(value): _name_status.text = "Checking this game…" _debounce.start() else: _name_status.text = "Use 1–24 plain-text characters." func _request_conflict_check() -> void: if _service != null: _service.request_name_check(_draft_name) func _on_conflict_result( _request_id: String, has_conflict: bool, suggestion_names: PackedStringArray, ) -> void: _clear_suggestions() if not has_conflict: _name_status.text = "Name is available in this game." return _name_status.text = "That name is already in use in this game." for suggestion: String in suggestion_names: var button := Button.new() button.text = suggestion button.pressed.connect(_use_suggestion.bind(suggestion)) UtilityPageStyle.apply_compact_ocean_button(button) _suggestions.add_child(button) var anyway := Button.new() anyway.text = "use anyway" anyway.pressed.connect(func() -> void: _allow_duplicate = true _name_status.text = "Duplicate name allowed for this apply." ) UtilityPageStyle.apply_compact_ocean_button(anyway) _suggestions.add_child(anyway) func _on_experience_changed(_total_experience: int, _level: int) -> void: _refresh_experience() func _refresh_experience() -> void: if _experience_level == null: return if _experience == null: _experience_level.text = "level 1" _experience_progress.value = 0.0 _experience_value.text = "0 / 100 xp" return var level: int = _experience.get_level() var current: int = _experience.get_experience_in_level() var required: int = _experience.get_experience_for_next_level() _experience_level.text = "level %d" % level _experience_progress.value = _experience.get_level_progress() _experience_value.text = "%d / %d xp" % [current, required] _experience_value.tooltip_text = "%d total xp" % ( _experience.get_total_experience() ) func _use_suggestion(value: String) -> void: _name_edit.text = value _draft_name = value _allow_duplicate = false _dirty = _draft_differs() _clear_suggestions() _request_conflict_check() func _apply() -> void: if _service == null: return _apply_button.disabled = true _service.apply_profile(_draft_name, _draft_appearance, _allow_duplicate) func _on_apply_finished(accepted: bool, message: String) -> void: _apply_button.disabled = false _name_status.text = message if accepted: _load_persisted() func _load_persisted() -> void: if _service == null: return _persisted_name = _service.get_persisted_name() _persisted_appearance = _service.get_persisted_appearance() _draft_name = _persisted_name _draft_appearance = _persisted_appearance.duplicate(true) _name_edit.text = _draft_name _preview.apply_appearance_profile(_draft_appearance) _dirty = false _allow_duplicate = false _name_status.text = "" _clear_suggestions() _refresh_options() _refresh_actions() func _revert() -> void: _load_persisted() func _confirm_discard() -> void: _discard_confirmation.visible = false _load_persisted() func _show_confirmation(action: String) -> void: _confirmation_action = action _discard_confirmation.visible = true if action == "defaults": _confirmation_label.text = "Reset the profile draft to defaults?" _confirmation_confirm.text = "reset draft" else: _confirmation_label.text = "Discard unsaved profile changes?" _confirmation_confirm.text = "discard changes" _keep_editing_button.grab_focus() func _confirm_pending_action() -> void: _discard_confirmation.visible = false if _confirmation_action == "defaults": _draft_appearance = CharacterCustomizationCatalog.default_snapshot() _preview.apply_appearance_profile(_draft_appearance) _dirty = _draft_differs() _refresh_options() _refresh_actions() else: _confirm_discard() _confirmation_action = "" func _draft_differs() -> bool: return ( _draft_name.strip_edges() != _persisted_name or _draft_appearance != _persisted_appearance ) func _refresh_actions() -> void: _apply_button.disabled = ( not _dirty or not NetworkProfilePreferences.is_valid_display_name(_draft_name) ) _revert_button.disabled = not _dirty func _clear_suggestions() -> void: for child: Node in _suggestions.get_children(): child.queue_free()