Load authored pattern masks by body component, add custom fur colors and the themed picker UI, and advance the signed profile protocol to version 6.
1675 lines
58 KiB
GDScript
1675 lines
58 KiB
GDScript
class_name ProfilePage
|
||
extends Control
|
||
|
||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||
const OPTION_GRID_COLUMNS: int = 6
|
||
const FUR_PALETTE_GRID_COLUMNS: int = 10
|
||
const FUR_PATTERN_GRID_COLUMNS: int = 4
|
||
const FUR_PALETTE_SWATCH_SIZE: float = 38.0
|
||
const FUR_CHANNEL_SWATCH_SIZE: float = 22.0
|
||
const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560)
|
||
const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300)
|
||
const FUR_SECTION_PATTERNS: String = "patterns"
|
||
const FUR_SECTION_COLORS: String = "colors"
|
||
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
|
||
const FEATURE_PREVIEW_SCAN_MAX_SIZE: int = 256
|
||
const FEATURE_PREVIEW_TEXTURE_MAX_SIZE: int = 128
|
||
const FEATURE_PREVIEW_PADDING: int = 3
|
||
const ControllerMappingManagerType = preload(
|
||
"res://settings/controller_mapping_manager.gd"
|
||
)
|
||
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
|
||
const GameTheme: Theme = preload("res://ui/game_theme.tres")
|
||
const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
|
||
const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd")
|
||
const VoiceProfilesType = preload(
|
||
"res://player/animalese_voice_profiles.gd"
|
||
)
|
||
const VOICE_CATEGORY_ID: String = "voice"
|
||
|
||
var _service: NetworkProfileService
|
||
var _experience: PlayerExperience
|
||
var _draft_name: String = ""
|
||
var _draft_appearance: Dictionary = {}
|
||
var _persisted_name: String = ""
|
||
var _persisted_appearance: Dictionary = {}
|
||
var _draft_voice_id: String = VoiceProfilesType.DEFAULT_ID
|
||
var _persisted_voice_id: String = VoiceProfilesType.DEFAULT_ID
|
||
var _draft_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
|
||
var _persisted_speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
|
||
var _draft_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID
|
||
var _persisted_call_id: String = VoiceProfilesType.DEFAULT_CALL_ID
|
||
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 _feature_preview_requests: Array[Dictionary] = []
|
||
var _feature_preview_generation: int = 0
|
||
var _feature_preview_worker_active: bool = false
|
||
var _expanded_feature_drawers: Dictionary = {}
|
||
var _feature_drawer_animation_key: String = ""
|
||
var _scale_value_label: Label
|
||
var _voice_preview: AnimaleseVoiceType
|
||
var _voice_preview_tween: Tween
|
||
var _active_fur_section: String = FUR_SECTION_PATTERNS
|
||
var _active_fur_pattern_id: String = CharacterCustomizationCatalog.FUR_STYLE_ID
|
||
var _active_fur_color_id: String = "fur_pattern"
|
||
var _fur_color_channel_buttons: Dictionary[String, Button] = {}
|
||
var _fur_color_channel_swatches: Dictionary[String, Panel] = {}
|
||
var _fur_custom_color_display: Panel
|
||
var _fur_custom_color_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 _exit_tree() -> void:
|
||
_cancel_feature_preview_requests()
|
||
|
||
|
||
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
|
||
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()
|
||
_cancel_feature_preview_requests()
|
||
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
|
||
_voice_preview_tween.kill()
|
||
_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)
|
||
var category_scroll := ScrollContainer.new()
|
||
category_scroll.name = "CategoryScroll"
|
||
category_scroll.custom_minimum_size.x = 120.0
|
||
category_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
category_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
category_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||
body.add_child(category_scroll)
|
||
_category_list = VBoxContainer.new()
|
||
_category_list.custom_minimum_size = Vector2(120, 0)
|
||
_category_list.add_theme_constant_override("separation", 5)
|
||
category_scroll.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()
|
||
var category_ids: Array[String] = []
|
||
for appearance_category: String in CharacterCustomizationCatalog.CATEGORY_IDS:
|
||
category_ids.append(appearance_category)
|
||
category_ids.append(VOICE_CATEGORY_ID)
|
||
for category_id: String in category_ids:
|
||
var button := Button.new()
|
||
button.text = _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 == (
|
||
_category_label(category_id)
|
||
)
|
||
_refresh_options()
|
||
|
||
|
||
func _refresh_options() -> void:
|
||
_cancel_feature_preview_requests()
|
||
_fur_color_channel_buttons.clear()
|
||
_fur_color_channel_swatches.clear()
|
||
_fur_custom_color_display = null
|
||
_fur_custom_color_label = null
|
||
for child: Node in _option_list.get_children():
|
||
child.queue_free()
|
||
if _category_id == VOICE_CATEGORY_ID:
|
||
_build_voice_options()
|
||
return
|
||
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_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 _category_label(category_id: String) -> String:
|
||
return (
|
||
"voice"
|
||
if category_id == VOICE_CATEGORY_ID
|
||
else CharacterCustomizationCatalog.category_label(category_id)
|
||
)
|
||
|
||
|
||
func _build_voice_options() -> void:
|
||
var description := Label.new()
|
||
description.text = "Choose how your character sounds in chat."
|
||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
description.add_theme_font_size_override("font_size", 12)
|
||
description.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||
)
|
||
_option_list.add_child(description)
|
||
var grid := GridContainer.new()
|
||
grid.columns = 3
|
||
grid.add_theme_constant_override("h_separation", 6)
|
||
grid.add_theme_constant_override("v_separation", 4)
|
||
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_option_list.add_child(grid)
|
||
for option: Dictionary in VoiceProfilesType.OPTIONS:
|
||
var option_id := str(option.get("id", ""))
|
||
var button := Button.new()
|
||
button.text = str(option.get("label", option_id))
|
||
button.toggle_mode = true
|
||
button.custom_minimum_size = Vector2(108.0, 32.0)
|
||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
button.button_pressed = _draft_voice_id == option_id
|
||
button.pressed.connect(_select_voice_option.bind(option_id))
|
||
UtilityPageStyle.apply_compact_ocean_button(button)
|
||
grid.add_child(button)
|
||
var divider := HSeparator.new()
|
||
divider.custom_minimum_size.y = 2.0
|
||
_option_list.add_child(divider)
|
||
var speed_title := Label.new()
|
||
speed_title.text = "speech speed"
|
||
speed_title.add_theme_font_size_override("font_size", 13)
|
||
speed_title.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||
)
|
||
_option_list.add_child(speed_title)
|
||
var speed_description := Label.new()
|
||
speed_description.text = "Controls chat speech on this device only."
|
||
speed_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
speed_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
speed_description.add_theme_font_size_override("font_size", 12)
|
||
speed_description.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||
)
|
||
_option_list.add_child(speed_description)
|
||
var speed_grid := GridContainer.new()
|
||
speed_grid.columns = 3
|
||
speed_grid.add_theme_constant_override("h_separation", 6)
|
||
speed_grid.add_theme_constant_override("v_separation", 4)
|
||
speed_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
_option_list.add_child(speed_grid)
|
||
for option: Dictionary in VoiceProfilesType.SPEED_OPTIONS:
|
||
var speed_id := str(option.get("id", ""))
|
||
var speed_button := Button.new()
|
||
speed_button.text = str(option.get("label", speed_id))
|
||
speed_button.tooltip_text = "%d characters per second" % roundi(
|
||
float(option.get("characters_per_second", 28.0))
|
||
)
|
||
speed_button.toggle_mode = true
|
||
speed_button.custom_minimum_size = Vector2(108.0, 32.0)
|
||
speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
speed_button.button_pressed = _draft_speech_speed_id == speed_id
|
||
speed_button.pressed.connect(
|
||
_select_speech_speed_option.bind(speed_id)
|
||
)
|
||
UtilityPageStyle.apply_compact_ocean_button(speed_button)
|
||
speed_grid.add_child(speed_button)
|
||
var call_divider := HSeparator.new()
|
||
call_divider.custom_minimum_size.y = 2.0
|
||
_option_list.add_child(call_divider)
|
||
var call_title := Label.new()
|
||
call_title.text = "call"
|
||
call_title.add_theme_font_size_override("font_size", 13)
|
||
call_title.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||
)
|
||
_option_list.add_child(call_title)
|
||
var call_description := Label.new()
|
||
call_description.text = "Press G to make this sound."
|
||
call_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
call_description.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
call_description.add_theme_font_size_override("font_size", 12)
|
||
call_description.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||
)
|
||
_option_list.add_child(call_description)
|
||
var call_grid := GridContainer.new()
|
||
call_grid.columns = 2
|
||
call_grid.add_theme_constant_override("h_separation", 8)
|
||
call_grid.add_theme_constant_override("v_separation", 8)
|
||
_option_list.add_child(call_grid)
|
||
for option: Dictionary in VoiceProfilesType.CALL_OPTIONS:
|
||
var call_id := str(option.get("id", ""))
|
||
var call_button := Button.new()
|
||
call_button.text = str(option.get("label", call_id))
|
||
call_button.toggle_mode = true
|
||
call_button.custom_minimum_size = Vector2(108.0, 32.0)
|
||
call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
call_button.button_pressed = _draft_call_id == call_id
|
||
call_button.pressed.connect(_select_call_option.bind(call_id))
|
||
UtilityPageStyle.apply_compact_ocean_button(call_button)
|
||
call_grid.add_child(call_button)
|
||
|
||
|
||
func _select_voice_option(voice_id: String) -> void:
|
||
if not VoiceProfilesType.is_valid(voice_id):
|
||
return
|
||
_draft_voice_id = voice_id
|
||
_dirty = _draft_differs()
|
||
_refresh_options()
|
||
_refresh_actions()
|
||
_play_voice_preview()
|
||
|
||
|
||
func _select_speech_speed_option(speed_id: String) -> void:
|
||
if not VoiceProfilesType.is_valid_speed(speed_id):
|
||
return
|
||
_draft_speech_speed_id = speed_id
|
||
_dirty = _draft_differs()
|
||
_refresh_options()
|
||
_refresh_actions()
|
||
_play_voice_preview()
|
||
|
||
|
||
func _select_call_option(call_id: String) -> void:
|
||
if not VoiceProfilesType.is_valid_call(call_id):
|
||
return
|
||
_draft_call_id = call_id
|
||
_dirty = _draft_differs()
|
||
_refresh_options()
|
||
_refresh_actions()
|
||
|
||
|
||
func _play_voice_preview() -> void:
|
||
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
|
||
_voice_preview_tween.kill()
|
||
if not is_instance_valid(_voice_preview):
|
||
_voice_preview = AnimaleseVoiceType.new()
|
||
_voice_preview.name = "ProfileVoicePreview"
|
||
add_child(_voice_preview)
|
||
_voice_preview_tween = _voice_preview.speak_text(
|
||
self,
|
||
"hello there!",
|
||
"profile-preview",
|
||
_draft_voice_id,
|
||
VoiceProfilesType.speed_for(_draft_speech_speed_id),
|
||
)
|
||
|
||
|
||
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))
|
||
|
||
|
||
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 = OPTION_GRID_COLUMNS
|
||
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, ""))
|
||
)
|
||
var groups: Array = CharacterCustomizationCatalog.feature_option_groups(
|
||
_category_id
|
||
)
|
||
var expanded_drawer_id := str(
|
||
_expanded_feature_drawers.get(_category_id, "")
|
||
)
|
||
if expanded_drawer_id.is_empty():
|
||
for group: Dictionary in groups:
|
||
var group_options: Array = group.get("options", []) as Array
|
||
if group_options.size() <= 1:
|
||
continue
|
||
var representative_id := str(
|
||
(group_options[0] as Dictionary).get("id", "")
|
||
)
|
||
for option: Dictionary in group_options:
|
||
if (
|
||
str(option.get("id", "")) == selected_id
|
||
and selected_id != representative_id
|
||
):
|
||
expanded_drawer_id = str(group.get("id", ""))
|
||
_expanded_feature_drawers[_category_id] = (
|
||
expanded_drawer_id
|
||
)
|
||
break
|
||
if not expanded_drawer_id.is_empty():
|
||
break
|
||
|
||
var animation_key := _feature_drawer_animation_key
|
||
for group: Dictionary in groups:
|
||
var group_id := str(group.get("id", ""))
|
||
var group_options: Array = group.get("options", []) as Array
|
||
if group_options.is_empty():
|
||
continue
|
||
var has_variants := group_options.size() > 1
|
||
var is_expanded := has_variants and group_id == expanded_drawer_id
|
||
for option_index: int in range(group_options.size()):
|
||
if option_index > 0 and not is_expanded:
|
||
continue
|
||
var option: Dictionary = group_options[option_index] as Dictionary
|
||
var button := _build_feature_option_button(
|
||
option,
|
||
selected_id,
|
||
group_id,
|
||
option_index == 0,
|
||
has_variants,
|
||
is_expanded,
|
||
)
|
||
grid.add_child(button)
|
||
if (
|
||
option_index > 0
|
||
and animation_key == "%s:%s" % [_category_id, group_id]
|
||
):
|
||
_animate_feature_drawer_button(button)
|
||
_feature_drawer_animation_key = ""
|
||
|
||
|
||
func _build_feature_option_button(
|
||
option: Dictionary,
|
||
selected_id: String,
|
||
drawer_id: String,
|
||
is_representative: bool,
|
||
has_variants: bool,
|
||
is_expanded: bool,
|
||
) -> Button:
|
||
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)
|
||
)
|
||
if is_representative and has_variants:
|
||
button.tooltip_text += (
|
||
" (hide variants)" if is_expanded else " (show variants)"
|
||
)
|
||
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
|
||
if is_representative and has_variants:
|
||
button.pressed.connect(
|
||
_select_feature_drawer.bind(_category_id, drawer_id, option_id)
|
||
)
|
||
else:
|
||
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.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)
|
||
var cached_preview := _cached_feature_preview_texture(
|
||
_category_id, option_id
|
||
)
|
||
if cached_preview != null:
|
||
preview.texture = cached_preview
|
||
else:
|
||
_queue_feature_preview(preview, _category_id, option_id)
|
||
if is_representative and has_variants:
|
||
var indicator := Label.new()
|
||
indicator.text = "<" if is_expanded else ">"
|
||
indicator.position = Vector2(66.0, 1.0)
|
||
indicator.size = Vector2(14.0, 16.0)
|
||
indicator.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
indicator.add_theme_font_size_override("font_size", 13)
|
||
indicator.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||
)
|
||
button.add_child(indicator)
|
||
return button
|
||
|
||
|
||
func _select_feature_drawer(
|
||
category_id: String,
|
||
drawer_id: String,
|
||
option_id: String,
|
||
) -> void:
|
||
if str(_expanded_feature_drawers.get(category_id, "")) == drawer_id:
|
||
_expanded_feature_drawers.erase(category_id)
|
||
else:
|
||
_expanded_feature_drawers[category_id] = drawer_id
|
||
_feature_drawer_animation_key = "%s:%s" % [category_id, drawer_id]
|
||
_select_option(category_id, option_id)
|
||
|
||
|
||
func _animate_feature_drawer_button(button: Button) -> void:
|
||
button.scale = Vector2(0.05, 1.0)
|
||
var faded := button.modulate
|
||
faded.a = 0.0
|
||
button.modulate = faded
|
||
var tween := button.create_tween()
|
||
tween.set_parallel(true)
|
||
tween.tween_property(
|
||
button, "scale", Vector2.ONE, FEATURE_DRAWER_ANIMATION_SECONDS
|
||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||
tween.tween_property(
|
||
button, "modulate:a", 1.0, FEATURE_DRAWER_ANIMATION_SECONDS
|
||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||
|
||
|
||
func _cached_feature_preview_texture(
|
||
category_id: String,
|
||
option_id: String,
|
||
) -> Texture2D:
|
||
var cache_key := "%s:%s" % [category_id, option_id]
|
||
return _feature_preview_cache.get(cache_key) as Texture2D
|
||
|
||
|
||
func _queue_feature_preview(
|
||
target: TextureRect,
|
||
category_id: String,
|
||
option_id: String,
|
||
) -> void:
|
||
_feature_preview_requests.append({
|
||
"generation": _feature_preview_generation,
|
||
"target": weakref(target),
|
||
"category_id": category_id,
|
||
"option_id": option_id,
|
||
})
|
||
if _feature_preview_worker_active:
|
||
return
|
||
_feature_preview_worker_active = true
|
||
_process_feature_preview_requests.call_deferred()
|
||
|
||
|
||
func _cancel_feature_preview_requests() -> void:
|
||
_feature_preview_generation += 1
|
||
_feature_preview_requests.clear()
|
||
|
||
|
||
func _process_feature_preview_requests() -> void:
|
||
while not _feature_preview_requests.is_empty():
|
||
var request: Dictionary = _feature_preview_requests.pop_front()
|
||
await get_tree().process_frame
|
||
if int(request.get("generation", -1)) != _feature_preview_generation:
|
||
continue
|
||
var target_reference := request.get("target") as WeakRef
|
||
if target_reference == null:
|
||
continue
|
||
var target := target_reference.get_ref() as TextureRect
|
||
if target == null or not is_instance_valid(target):
|
||
continue
|
||
var category_id := str(request.get("category_id", ""))
|
||
var option_id := str(request.get("option_id", ""))
|
||
var preview := _cached_feature_preview_texture(category_id, option_id)
|
||
if preview == null:
|
||
preview = _create_feature_preview_texture(category_id, option_id)
|
||
if (
|
||
preview != null
|
||
and int(request.get("generation", -1)) == _feature_preview_generation
|
||
and is_instance_valid(target)
|
||
):
|
||
target.texture = preview
|
||
_feature_preview_worker_active = false
|
||
|
||
|
||
func _create_feature_preview_texture(
|
||
category_id: String,
|
||
option_id: String,
|
||
) -> Texture2D:
|
||
var cache_key := "%s:%s" % [category_id, option_id]
|
||
var resource_path := CharacterCustomizationCatalog.feature_texture_path(
|
||
category_id, option_id
|
||
)
|
||
if resource_path.is_empty():
|
||
return null
|
||
var source := ResourceLoader.load(
|
||
resource_path,
|
||
"Texture2D",
|
||
ResourceLoader.CACHE_MODE_IGNORE,
|
||
) as Texture2D
|
||
if source == null:
|
||
return null
|
||
var image := source.get_image()
|
||
if image == null or image.is_empty():
|
||
return null
|
||
if image.is_compressed() and image.decompress() != OK:
|
||
return null
|
||
_resize_feature_preview_image(image, FEATURE_PREVIEW_SCAN_MAX_SIZE)
|
||
var used := image.get_used_rect()
|
||
if used.size.x <= 0 or used.size.y <= 0:
|
||
return null
|
||
used.position -= Vector2i(FEATURE_PREVIEW_PADDING, FEATURE_PREVIEW_PADDING)
|
||
used.size += Vector2i(
|
||
FEATURE_PREVIEW_PADDING * 2,
|
||
FEATURE_PREVIEW_PADDING * 2,
|
||
)
|
||
used = used.intersection(Rect2i(Vector2i.ZERO, image.get_size()))
|
||
var cropped := image.get_region(used)
|
||
_resize_feature_preview_image(cropped, FEATURE_PREVIEW_TEXTURE_MAX_SIZE)
|
||
var preview := ImageTexture.create_from_image(cropped)
|
||
_feature_preview_cache[cache_key] = preview
|
||
return preview
|
||
|
||
|
||
func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
|
||
var source_size := image.get_size()
|
||
var largest_dimension := maxi(source_size.x, source_size.y)
|
||
if largest_dimension <= max_dimension:
|
||
return
|
||
var scale := float(max_dimension) / float(largest_dimension)
|
||
image.resize(
|
||
maxi(roundi(float(source_size.x) * scale), 1),
|
||
maxi(roundi(float(source_size.y) * scale), 1),
|
||
Image.INTERPOLATE_NEAREST,
|
||
)
|
||
|
||
|
||
func _build_fur_options(options: Array) -> void:
|
||
var section_tabs := HBoxContainer.new()
|
||
section_tabs.add_theme_constant_override("separation", 8)
|
||
_option_list.add_child(section_tabs)
|
||
for section_id: String in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
|
||
var section_button := Button.new()
|
||
section_button.text = section_id
|
||
section_button.toggle_mode = true
|
||
section_button.button_pressed = _active_fur_section == section_id
|
||
section_button.custom_minimum_size = Vector2(132.0, 34.0)
|
||
section_button.pressed.connect(_select_fur_section.bind(section_id))
|
||
UtilityPageStyle.apply_compact_ocean_button(section_button)
|
||
section_tabs.add_child(section_button)
|
||
|
||
if _active_fur_section == FUR_SECTION_COLORS:
|
||
_build_fur_color_channels(options)
|
||
return
|
||
_build_fur_pattern_options()
|
||
|
||
|
||
func _build_fur_pattern_options() -> void:
|
||
if _active_fur_pattern_id not in CharacterCustomizationCatalog.FUR_STYLE_IDS:
|
||
_active_fur_pattern_id = CharacterCustomizationCatalog.FUR_STYLE_ID
|
||
var pattern_stack := VBoxContainer.new()
|
||
pattern_stack.name = "FurPatternStack"
|
||
pattern_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
pattern_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
pattern_stack.add_theme_constant_override("separation", -10)
|
||
_option_list.add_child(pattern_stack)
|
||
var part_tab_margin := MarginContainer.new()
|
||
part_tab_margin.name = "FurPatternPartTabMargin"
|
||
part_tab_margin.custom_minimum_size.y = 38.0
|
||
part_tab_margin.add_theme_constant_override("margin_left", 14)
|
||
pattern_stack.add_child(part_tab_margin)
|
||
var part_tabs := HBoxContainer.new()
|
||
part_tabs.name = "FurPatternPartTabs"
|
||
part_tabs.custom_minimum_size.y = 38.0
|
||
part_tabs.add_theme_constant_override("separation", 4)
|
||
part_tab_margin.add_child(part_tabs)
|
||
for style_index: int in range(
|
||
CharacterCustomizationCatalog.FUR_STYLE_IDS.size()
|
||
):
|
||
var style_field: String = (
|
||
CharacterCustomizationCatalog.FUR_STYLE_IDS[style_index]
|
||
)
|
||
var part_tab: OrganizerTab = OrganizerTabType.new()
|
||
part_tab.text = CharacterCustomizationCatalog.fur_style_label(
|
||
style_field
|
||
)
|
||
part_tab.palette_index = mini(style_index, 2)
|
||
part_tab.custom_minimum_size = Vector2(90.0, 38.0)
|
||
part_tab.focus_mode = Control.FOCUS_ALL
|
||
part_tab.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
part_tab.pressed.connect(_select_fur_pattern_part.bind(style_field))
|
||
part_tabs.add_child(part_tab)
|
||
part_tab.set_selected(
|
||
_active_fur_pattern_id == style_field,
|
||
false,
|
||
)
|
||
|
||
var pattern_panel := PanelContainer.new()
|
||
pattern_panel.name = "FurPatternOptionsPanel"
|
||
pattern_panel.custom_minimum_size.y = 180.0
|
||
pattern_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
pattern_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
pattern_panel.add_theme_stylebox_override(
|
||
"panel",
|
||
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_FIELD, 12),
|
||
)
|
||
pattern_stack.add_child(pattern_panel)
|
||
var pattern_margin := MarginContainer.new()
|
||
pattern_margin.add_theme_constant_override("margin_left", 12)
|
||
pattern_margin.add_theme_constant_override("margin_top", 10)
|
||
pattern_margin.add_theme_constant_override("margin_right", 12)
|
||
pattern_margin.add_theme_constant_override("margin_bottom", 10)
|
||
pattern_panel.add_child(pattern_margin)
|
||
var pattern_scroll := ScrollContainer.new()
|
||
pattern_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
pattern_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
pattern_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
pattern_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||
pattern_margin.add_child(pattern_scroll)
|
||
var pattern_grid := GridContainer.new()
|
||
pattern_grid.columns = FUR_PATTERN_GRID_COLUMNS
|
||
pattern_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
pattern_grid.add_theme_constant_override("h_separation", 8)
|
||
pattern_grid.add_theme_constant_override("v_separation", 8)
|
||
pattern_scroll.add_child(pattern_grid)
|
||
var selected_style_id := str(_draft_appearance.get(
|
||
_active_fur_pattern_id,
|
||
CharacterCustomizationCatalog.DEFAULT_FUR_STYLE,
|
||
))
|
||
var pattern_options := (
|
||
CharacterCustomizationCatalog.fur_pattern_options_for_field(
|
||
_active_fur_pattern_id,
|
||
_draft_appearance,
|
||
)
|
||
)
|
||
var selected_style_available := false
|
||
for pattern_option: Dictionary in pattern_options:
|
||
if str(pattern_option.get("id", "")) == selected_style_id:
|
||
selected_style_available = true
|
||
break
|
||
var displayed_style_id := (
|
||
selected_style_id
|
||
if selected_style_available
|
||
else CharacterCustomizationCatalog.DEFAULT_FUR_STYLE
|
||
)
|
||
for pattern_option: Dictionary in pattern_options:
|
||
var pattern_id := str(pattern_option.get("id", ""))
|
||
var pattern_button := Button.new()
|
||
pattern_button.text = str(pattern_option.get("label", pattern_id))
|
||
pattern_button.toggle_mode = true
|
||
pattern_button.button_pressed = displayed_style_id == pattern_id
|
||
pattern_button.custom_minimum_size = Vector2(118.0, 32.0)
|
||
pattern_button.pressed.connect(_select_option.bind(
|
||
_active_fur_pattern_id,
|
||
pattern_id,
|
||
))
|
||
UtilityPageStyle.apply_compact_ocean_button(pattern_button)
|
||
pattern_grid.add_child(pattern_button)
|
||
|
||
|
||
func _build_fur_color_channels(options: Array) -> void:
|
||
var color_panel := PanelContainer.new()
|
||
color_panel.name = "FurColorOptionsPanel"
|
||
color_panel.custom_minimum_size.y = 250.0
|
||
color_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
color_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
color_panel.add_theme_stylebox_override(
|
||
"panel",
|
||
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_FIELD, 12),
|
||
)
|
||
_option_list.add_child(color_panel)
|
||
var color_margin := MarginContainer.new()
|
||
color_margin.add_theme_constant_override("margin_left", 14)
|
||
color_margin.add_theme_constant_override("margin_top", 12)
|
||
color_margin.add_theme_constant_override("margin_right", 14)
|
||
color_margin.add_theme_constant_override("margin_bottom", 12)
|
||
color_panel.add_child(color_margin)
|
||
var color_stack := VBoxContainer.new()
|
||
color_stack.name = "FurColorStack"
|
||
color_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
color_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
color_stack.add_theme_constant_override("separation", 10)
|
||
color_margin.add_child(color_stack)
|
||
|
||
var channel_grid := GridContainer.new()
|
||
channel_grid.name = "FurColorChannelGrid"
|
||
channel_grid.columns = CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
|
||
channel_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
channel_grid.add_theme_constant_override("h_separation", 10)
|
||
channel_grid.add_theme_constant_override("v_separation", 8)
|
||
color_stack.add_child(channel_grid)
|
||
for color_index: int in range(
|
||
CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
|
||
):
|
||
var color_category_id: String = (
|
||
CharacterCustomizationCatalog.FUR_COLOR_IDS[color_index]
|
||
)
|
||
var selected_color_id := str(_draft_appearance.get(
|
||
color_category_id,
|
||
"white",
|
||
))
|
||
var selected_color := CharacterCustomizationCatalog.option_color(
|
||
color_category_id,
|
||
selected_color_id,
|
||
)
|
||
var channel_button := Button.new()
|
||
channel_button.name = "FurColorSlot%d" % (color_index + 1)
|
||
channel_button.text = ""
|
||
channel_button.accessibility_name = "fur color %d" % (color_index + 1)
|
||
channel_button.tooltip_text = "Edit %s: %s" % [
|
||
CharacterCustomizationCatalog.fur_color_label(color_category_id),
|
||
_fur_option_label(options, selected_color_id),
|
||
]
|
||
channel_button.toggle_mode = true
|
||
channel_button.custom_minimum_size = Vector2(104.0, 42.0)
|
||
channel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
channel_button.button_pressed = (
|
||
_active_fur_color_id == color_category_id
|
||
)
|
||
channel_button.pressed.connect(
|
||
_select_fur_color_slot.bind(color_category_id)
|
||
)
|
||
UtilityPageStyle.apply_compact_ocean_button(channel_button)
|
||
channel_grid.add_child(channel_button)
|
||
var channel_contents := HBoxContainer.new()
|
||
channel_contents.name = "ChannelContents"
|
||
channel_contents.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
channel_contents.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
channel_contents.alignment = BoxContainer.ALIGNMENT_CENTER
|
||
channel_contents.add_theme_constant_override("separation", 8)
|
||
channel_button.add_child(channel_contents)
|
||
var channel_label := Label.new()
|
||
channel_label.name = "ChannelNumber"
|
||
channel_label.text = str(color_index + 1)
|
||
channel_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
channel_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
channel_label.add_theme_font_override(
|
||
"font", UtilityPageStyle.TuffyFont
|
||
)
|
||
channel_label.add_theme_color_override(
|
||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||
)
|
||
channel_contents.add_child(channel_label)
|
||
var channel_swatch := Panel.new()
|
||
channel_swatch.name = "ActiveColorSwatch"
|
||
channel_swatch.custom_minimum_size = (
|
||
Vector2.ONE * FUR_CHANNEL_SWATCH_SIZE
|
||
)
|
||
channel_swatch.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||
channel_swatch.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
channel_swatch.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
_set_fur_color_swatch(channel_swatch, selected_color)
|
||
channel_contents.add_child(channel_swatch)
|
||
_fur_color_channel_buttons[color_category_id] = channel_button
|
||
_fur_color_channel_swatches[color_category_id] = channel_swatch
|
||
|
||
_build_fur_palette(_active_fur_color_id, options, color_stack)
|
||
|
||
|
||
func _build_fur_palette(
|
||
category_id: String,
|
||
options: Array,
|
||
parent: VBoxContainer,
|
||
) -> void:
|
||
var selected_id: String = CharacterCustomizationCatalog.canonical_option_id(
|
||
category_id,
|
||
str(_draft_appearance.get(category_id, "white")),
|
||
)
|
||
var palette_center := CenterContainer.new()
|
||
palette_center.name = "FurPaletteCenter"
|
||
palette_center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
palette_center.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
parent.add_child(palette_center)
|
||
var grid := GridContainer.new()
|
||
grid.name = "FurPaletteGrid"
|
||
grid.columns = FUR_PALETTE_GRID_COLUMNS
|
||
grid.add_theme_constant_override("h_separation", 8)
|
||
grid.add_theme_constant_override("v_separation", 8)
|
||
palette_center.add_child(grid)
|
||
for option: Dictionary in options:
|
||
var option_id: String = str(option.get("id", ""))
|
||
var color: Color = CharacterCustomizationCatalog.option_color(
|
||
category_id, option_id
|
||
)
|
||
var button := Button.new()
|
||
button.text = ""
|
||
button.tooltip_text = "%s: %s" % [
|
||
CharacterCustomizationCatalog.fur_color_label(category_id),
|
||
str(option.get("label", option_id)),
|
||
]
|
||
button.toggle_mode = true
|
||
button.custom_minimum_size = (
|
||
Vector2.ONE * FUR_PALETTE_SWATCH_SIZE
|
||
)
|
||
button.button_pressed = selected_id == option_id
|
||
button.pressed.connect(_select_option.bind(category_id, option_id))
|
||
_apply_fur_swatch_style(button, color, button.button_pressed)
|
||
grid.add_child(button)
|
||
|
||
var selected_color := CharacterCustomizationCatalog.option_color(
|
||
category_id,
|
||
selected_id,
|
||
)
|
||
var custom_picker := ColorPickerButton.new()
|
||
custom_picker.name = "FurCustomColorPicker"
|
||
custom_picker.text = ""
|
||
custom_picker.accessibility_name = "custom color"
|
||
custom_picker.color = selected_color
|
||
custom_picker.edit_alpha = false
|
||
custom_picker.edit_intensity = false
|
||
custom_picker.custom_minimum_size = Vector2(0.0, 44.0)
|
||
custom_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
custom_picker.tooltip_text = "Choose a custom color for channel %d." % (
|
||
CharacterCustomizationCatalog.FUR_COLOR_IDS.find(category_id) + 1
|
||
)
|
||
custom_picker.color_changed.connect(
|
||
_select_custom_fur_color.bind(category_id)
|
||
)
|
||
custom_picker.popup_closed.connect(_refresh_options)
|
||
UtilityPageStyle.apply_compact_ocean_button(custom_picker)
|
||
_round_fur_color_picker_button(custom_picker)
|
||
_theme_fur_color_picker_popup(custom_picker)
|
||
parent.add_child(custom_picker)
|
||
var picker_display := Panel.new()
|
||
picker_display.name = "FurCustomColorDisplay"
|
||
picker_display.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
picker_display.offset_left = 4.0
|
||
picker_display.offset_top = 4.0
|
||
picker_display.offset_right = -4.0
|
||
picker_display.offset_bottom = -4.0
|
||
picker_display.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
picker_display.z_index = 1
|
||
custom_picker.add_child(picker_display)
|
||
var picker_label := Label.new()
|
||
picker_label.name = "FurCustomColorLabel"
|
||
picker_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||
picker_label.text = "custom color"
|
||
picker_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
picker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||
picker_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
picker_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||
picker_display.add_child(picker_label)
|
||
_fur_custom_color_display = picker_display
|
||
_fur_custom_color_label = picker_label
|
||
_update_fur_custom_color_display(selected_color)
|
||
|
||
|
||
func _fur_option_label(options: Array, option_id: String) -> String:
|
||
if CharacterCustomizationCatalog.is_custom_fur_color_id(option_id):
|
||
return "custom #%s" % option_id.trim_prefix(
|
||
CharacterCustomizationCatalog.CUSTOM_FUR_COLOR_PREFIX
|
||
)
|
||
for option: Dictionary in options:
|
||
if str(option.get("id", "")) == option_id:
|
||
return str(option.get("label", option_id))
|
||
return option_id
|
||
|
||
|
||
func _select_fur_color_slot(category_id: String) -> void:
|
||
if category_id not in CharacterCustomizationCatalog.FUR_COLOR_IDS:
|
||
return
|
||
_active_fur_color_id = category_id
|
||
_refresh_options()
|
||
|
||
|
||
func _select_fur_section(section_id: String) -> void:
|
||
if section_id not in [FUR_SECTION_PATTERNS, FUR_SECTION_COLORS]:
|
||
return
|
||
_active_fur_section = section_id
|
||
_refresh_options()
|
||
|
||
|
||
func _select_custom_fur_color(color: Color, category_id: String) -> void:
|
||
if category_id not in CharacterCustomizationCatalog.FUR_COLOR_IDS:
|
||
return
|
||
var option_id := CharacterCustomizationCatalog.custom_fur_color_id(color)
|
||
_draft_appearance[category_id] = option_id
|
||
_update_fur_channel_color(
|
||
category_id,
|
||
color,
|
||
"custom #%s" % option_id.trim_prefix(
|
||
CharacterCustomizationCatalog.CUSTOM_FUR_COLOR_PREFIX
|
||
),
|
||
)
|
||
_update_fur_custom_color_display(color)
|
||
_preview.apply_appearance_profile(_draft_appearance)
|
||
_dirty = _draft_differs()
|
||
_refresh_actions()
|
||
|
||
|
||
func _select_fur_pattern_part(category_id: String) -> void:
|
||
if category_id not in CharacterCustomizationCatalog.FUR_STYLE_IDS:
|
||
return
|
||
_active_fur_pattern_id = category_id
|
||
_refresh_options()
|
||
|
||
|
||
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 _set_fur_color_swatch(swatch: Panel, color: Color) -> void:
|
||
var swatch_style := UtilityPageStyle.rounded_style(
|
||
color,
|
||
roundi(FUR_CHANNEL_SWATCH_SIZE * 0.5),
|
||
)
|
||
swatch_style.anti_aliasing = true
|
||
swatch.add_theme_stylebox_override(
|
||
"panel", swatch_style
|
||
)
|
||
|
||
|
||
func _update_fur_channel_color(
|
||
category_id: String,
|
||
color: Color,
|
||
option_label: String,
|
||
) -> void:
|
||
var swatch: Panel = _fur_color_channel_swatches.get(category_id)
|
||
if swatch != null and is_instance_valid(swatch):
|
||
_set_fur_color_swatch(swatch, color)
|
||
var button: Button = _fur_color_channel_buttons.get(category_id)
|
||
if button != null and is_instance_valid(button):
|
||
button.tooltip_text = "Edit %s: %s" % [
|
||
CharacterCustomizationCatalog.fur_color_label(category_id),
|
||
option_label,
|
||
]
|
||
|
||
|
||
func _round_fur_color_picker_button(button: ColorPickerButton) -> void:
|
||
for state: StringName in [
|
||
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
||
]:
|
||
var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
|
||
if style == null:
|
||
continue
|
||
style.set_corner_radius_all(14)
|
||
style.content_margin_left = 14.0
|
||
style.content_margin_top = 14.0
|
||
style.content_margin_right = 14.0
|
||
style.content_margin_bottom = 14.0
|
||
button.add_theme_stylebox_override(state, style)
|
||
|
||
|
||
func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
|
||
var popup: PopupPanel = button.get_popup()
|
||
var picker: ColorPicker = button.get_picker()
|
||
popup.accessibility_name = "custom fur color picker"
|
||
popup.theme = GameTheme
|
||
popup.min_size = FUR_COLOR_PICKER_POPUP_SIZE
|
||
popup.size = FUR_COLOR_PICKER_POPUP_SIZE
|
||
var popup_style := UtilityPageStyle.rounded_style(
|
||
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
||
20,
|
||
)
|
||
popup_style.content_margin_left = 24.0
|
||
popup_style.content_margin_top = 24.0
|
||
popup_style.content_margin_right = 24.0
|
||
popup_style.content_margin_bottom = 24.0
|
||
popup.add_theme_stylebox_override("panel", popup_style)
|
||
picker.custom_minimum_size = Vector2(
|
||
FUR_COLOR_PICKER_POPUP_SIZE.x - 48,
|
||
FUR_COLOR_PICKER_POPUP_SIZE.y - 48,
|
||
)
|
||
picker.edit_alpha = false
|
||
picker.edit_intensity = false
|
||
picker.can_add_swatches = false
|
||
picker.presets_visible = false
|
||
picker.add_theme_constant_override("margin", 16)
|
||
picker.add_theme_constant_override("sv_width", FUR_COLOR_PICKER_SV_SIZE.x)
|
||
picker.add_theme_constant_override("sv_height", FUR_COLOR_PICKER_SV_SIZE.y)
|
||
picker.add_theme_constant_override("h_width", 38)
|
||
picker.add_theme_constant_override("label_width", 28)
|
||
|
||
|
||
func _update_fur_custom_color_display(color: Color) -> void:
|
||
if (
|
||
_fur_custom_color_display == null
|
||
or not is_instance_valid(_fur_custom_color_display)
|
||
or _fur_custom_color_label == null
|
||
or not is_instance_valid(_fur_custom_color_label)
|
||
):
|
||
return
|
||
_fur_custom_color_display.add_theme_stylebox_override(
|
||
"panel", UtilityPageStyle.rounded_style(color, 11)
|
||
)
|
||
_fur_custom_color_label.add_theme_color_override(
|
||
"font_color",
|
||
UtilityPageStyle.NAVY
|
||
if color.get_luminance() >= 0.52
|
||
else UtilityPageStyle.OCEAN_TEXT_PRIMARY,
|
||
)
|
||
|
||
|
||
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,
|
||
_draft_voice_id,
|
||
_draft_speech_speed_id,
|
||
_draft_call_id,
|
||
)
|
||
|
||
|
||
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()
|
||
_persisted_voice_id = _service.get_persisted_voice_id()
|
||
_persisted_speech_speed_id = (
|
||
_service.get_persisted_speech_speed_id()
|
||
)
|
||
_persisted_call_id = _service.get_persisted_call_id()
|
||
_draft_name = _persisted_name
|
||
_draft_appearance = _persisted_appearance.duplicate(true)
|
||
_draft_voice_id = _persisted_voice_id
|
||
_draft_speech_speed_id = _persisted_speech_speed_id
|
||
_draft_call_id = _persisted_call_id
|
||
TypewriterRevealType.set_characters_per_second(
|
||
VoiceProfilesType.speed_for(_persisted_speech_speed_id)
|
||
)
|
||
_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()
|
||
_draft_voice_id = VoiceProfilesType.DEFAULT_ID
|
||
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
|
||
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
|
||
_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
|
||
or _draft_voice_id != _persisted_voice_id
|
||
or _draft_speech_speed_id != _persisted_speech_speed_id
|
||
or _draft_call_id != _persisted_call_id
|
||
)
|
||
|
||
|
||
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()
|