feat: sync live profile customization
This commit is contained in:
parent
d7c887db21
commit
a7837c3711
9 changed files with 842 additions and 49 deletions
|
|
@ -2,6 +2,7 @@ class_name ProfilePage
|
|||
extends Control
|
||||
|
||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||
const APPEARANCE_PREVIEW_INTERVAL_SECONDS: float = 0.08
|
||||
const OPTION_GRID_COLUMNS: int = 6
|
||||
const FUR_PALETTE_GRID_COLUMNS: int = 10
|
||||
const FUR_PATTERN_GRID_COLUMNS: int = 4
|
||||
|
|
@ -27,6 +28,13 @@ const VoiceProfilesType = preload(
|
|||
)
|
||||
const VOICE_CATEGORY_ID: String = "voice"
|
||||
|
||||
enum ControllerZone {
|
||||
ACCOUNT,
|
||||
CATEGORIES,
|
||||
OPTIONS,
|
||||
COLOR_PICKER,
|
||||
}
|
||||
|
||||
var _service: NetworkProfileService
|
||||
var _experience: PlayerExperience
|
||||
var _draft_name: String = ""
|
||||
|
|
@ -49,14 +57,18 @@ var _suggestions: HBoxContainer
|
|||
var _category_list: VBoxContainer
|
||||
var _option_list: VBoxContainer
|
||||
var _preview: ProfilePreview
|
||||
var _customize_button: Button
|
||||
var _apply_button: Button
|
||||
var _revert_button: Button
|
||||
var _defaults_button: Button
|
||||
var _reset_view_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 _appearance_preview_timer: Timer
|
||||
var _experience_level: Label
|
||||
var _experience_progress: ProgressBar
|
||||
var _experience_value: Label
|
||||
|
|
@ -76,6 +88,13 @@ 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
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _controller_zone: ControllerZone = ControllerZone.ACCOUNT
|
||||
var _controller_option_depth: int = 0
|
||||
var _active_color_picker_button: ColorPickerButton
|
||||
var _active_color_picker_return_depth: int = 0
|
||||
var _profile_active: bool = false
|
||||
var _profile_interactive: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -86,10 +105,17 @@ func _ready() -> void:
|
|||
_debounce.wait_time = CHECK_DEBOUNCE_SECONDS
|
||||
_debounce.timeout.connect(_request_conflict_check)
|
||||
add_child(_debounce)
|
||||
_appearance_preview_timer = Timer.new()
|
||||
_appearance_preview_timer.one_shot = true
|
||||
_appearance_preview_timer.wait_time = APPEARANCE_PREVIEW_INTERVAL_SECONDS
|
||||
_appearance_preview_timer.timeout.connect(_publish_draft_appearance)
|
||||
add_child(_appearance_preview_timer)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_cancel_feature_preview_requests()
|
||||
if _service != null:
|
||||
_service.restore_persisted_appearance()
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -127,6 +153,7 @@ func setup(
|
|||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
if _preview != null:
|
||||
_preview.setup_controller_mapping(mapping_manager)
|
||||
|
||||
|
|
@ -137,6 +164,7 @@ func set_world_pixel_size(pixel_size: int) -> void:
|
|||
|
||||
|
||||
func activate() -> void:
|
||||
_profile_active = true
|
||||
visible = true
|
||||
if _service != null:
|
||||
_load_persisted()
|
||||
|
|
@ -145,8 +173,12 @@ func activate() -> void:
|
|||
|
||||
|
||||
func deactivate() -> void:
|
||||
_profile_active = false
|
||||
visible = false
|
||||
_debounce.stop()
|
||||
_appearance_preview_timer.stop()
|
||||
if _service != null:
|
||||
_service.restore_persisted_appearance()
|
||||
_cancel_feature_preview_requests()
|
||||
if _voice_preview_tween != null and _voice_preview_tween.is_valid():
|
||||
_voice_preview_tween.kill()
|
||||
|
|
@ -154,16 +186,24 @@ func deactivate() -> void:
|
|||
|
||||
|
||||
func set_interactive(interactive: bool) -> void:
|
||||
_profile_interactive = interactive and _profile_active
|
||||
mouse_filter = (
|
||||
Control.MOUSE_FILTER_PASS if interactive else Control.MOUSE_FILTER_IGNORE
|
||||
Control.MOUSE_FILTER_PASS
|
||||
if _profile_interactive else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
_apply_controller_zone_focus()
|
||||
|
||||
|
||||
func reset_controller_zone() -> void:
|
||||
_controller_zone = ControllerZone.ACCOUNT
|
||||
_controller_option_depth = 0
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func consume_escape() -> bool:
|
||||
if _discard_confirmation.visible:
|
||||
_discard_confirmation.visible = false
|
||||
_confirmation_action = ""
|
||||
_name_edit.grab_focus()
|
||||
_close_discard_confirmation()
|
||||
return true
|
||||
if _name_edit.has_focus():
|
||||
_name_edit.release_focus()
|
||||
|
|
@ -174,6 +214,257 @@ func consume_escape() -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _profile_active or not _profile_interactive:
|
||||
return false
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
if _discard_confirmation.visible:
|
||||
_close_discard_confirmation()
|
||||
return true
|
||||
match _controller_zone:
|
||||
ControllerZone.COLOR_PICKER:
|
||||
_close_controller_color_picker()
|
||||
ControllerZone.OPTIONS:
|
||||
if _controller_option_depth > 0:
|
||||
_controller_option_depth -= 1
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
else:
|
||||
_controller_zone = ControllerZone.CATEGORIES
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
ControllerZone.CATEGORIES:
|
||||
_controller_zone = ControllerZone.ACCOUNT
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
ControllerZone.ACCOUNT:
|
||||
return false
|
||||
return true
|
||||
if _controller_zone == ControllerZone.ACCOUNT:
|
||||
return false
|
||||
if (
|
||||
_controller_zone == ControllerZone.CATEGORIES
|
||||
and event.is_action_pressed("ui_accept")
|
||||
):
|
||||
var focused_category := get_viewport().gui_get_focus_owner() as Button
|
||||
if focused_category != null and _category_list.is_ancestor_of(
|
||||
focused_category
|
||||
):
|
||||
focused_category.pressed.emit()
|
||||
_controller_zone = ControllerZone.OPTIONS
|
||||
_controller_option_depth = 0
|
||||
_apply_controller_zone_focus.call_deferred()
|
||||
_focus_controller_zone.call_deferred()
|
||||
return true
|
||||
if (
|
||||
_controller_zone == ControllerZone.OPTIONS
|
||||
and event.is_action_pressed("ui_accept")
|
||||
):
|
||||
var groups: Array = _controller_option_groups()
|
||||
if _controller_option_depth < groups.size() - 1:
|
||||
var focused_option := get_viewport().gui_get_focus_owner() as BaseButton
|
||||
_controller_option_depth += 1
|
||||
if focused_option != null:
|
||||
focused_option.pressed.emit()
|
||||
_apply_controller_zone_focus.call_deferred()
|
||||
_focus_controller_zone.call_deferred()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if (
|
||||
not _profile_active
|
||||
or not _profile_interactive
|
||||
):
|
||||
return
|
||||
var right_stick := Vector2(
|
||||
_controller_mapping_manager.get_role_axis(
|
||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||
_controller_mapping_manager.get_role_axis(
|
||||
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||
)
|
||||
if right_stick.length() < 0.16:
|
||||
return
|
||||
if _controller_zone == ControllerZone.COLOR_PICKER:
|
||||
_adjust_controller_color_gamut(right_stick, delta)
|
||||
else:
|
||||
_preview.apply_controller_orbit(right_stick, delta)
|
||||
|
||||
|
||||
func _apply_controller_zone_focus() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
var account_controls: Array[Control] = _account_controller_controls()
|
||||
var category_controls: Array[Control] = _controls_under(_category_list)
|
||||
var option_groups: Array = _controller_option_groups()
|
||||
var all_option_controls: Array[Control] = []
|
||||
for group_value: Variant in option_groups:
|
||||
var group := group_value as Array
|
||||
for item: Variant in group:
|
||||
var control := item as Control
|
||||
if control != null and control not in all_option_controls:
|
||||
all_option_controls.append(control)
|
||||
for control: Control in account_controls + category_controls + all_option_controls:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
for control: Control in [_confirmation_confirm, _keep_editing_button]:
|
||||
if control != null:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
if _preview != null:
|
||||
_preview.focus_mode = Control.FOCUS_NONE
|
||||
if _reset_view_button != null:
|
||||
_reset_view_button.focus_mode = Control.FOCUS_NONE
|
||||
if not _profile_interactive:
|
||||
return
|
||||
if _discard_confirmation != null and _discard_confirmation.visible:
|
||||
var confirmation_controls: Array[Control] = [
|
||||
_confirmation_confirm,
|
||||
_keep_editing_button,
|
||||
]
|
||||
for control: Control in confirmation_controls:
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
ControllerFocusNavigation.configure_spatial_neighbors(
|
||||
confirmation_controls
|
||||
)
|
||||
return
|
||||
var active_controls: Array[Control] = []
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
active_controls = account_controls
|
||||
ControllerZone.CATEGORIES:
|
||||
active_controls = category_controls
|
||||
ControllerZone.OPTIONS:
|
||||
if not option_groups.is_empty():
|
||||
_controller_option_depth = clampi(
|
||||
_controller_option_depth,
|
||||
0,
|
||||
option_groups.size() - 1,
|
||||
)
|
||||
for item: Variant in option_groups[_controller_option_depth]:
|
||||
var control := item as Control
|
||||
if control != null:
|
||||
active_controls.append(control)
|
||||
ControllerZone.COLOR_PICKER:
|
||||
active_controls = _color_picker_controller_controls()
|
||||
for control: Control in active_controls:
|
||||
var button := control as BaseButton
|
||||
control.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if button == null or not button.disabled
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
ControllerFocusNavigation.configure_spatial_neighbors(active_controls)
|
||||
|
||||
|
||||
func _focus_controller_zone() -> void:
|
||||
if not _profile_interactive:
|
||||
return
|
||||
var controls: Array[Control] = []
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
controls = _account_controller_controls()
|
||||
ControllerZone.CATEGORIES:
|
||||
controls = _controls_under(_category_list)
|
||||
ControllerZone.OPTIONS:
|
||||
var groups: Array = _controller_option_groups()
|
||||
if not groups.is_empty():
|
||||
for item: Variant in groups[clampi(
|
||||
_controller_option_depth, 0, groups.size() - 1
|
||||
)]:
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
controls.append(option_control)
|
||||
ControllerZone.COLOR_PICKER:
|
||||
controls = _color_picker_controller_controls()
|
||||
for control: Control in controls:
|
||||
var button := control as BaseButton
|
||||
if button != null and button.button_pressed:
|
||||
button.grab_focus()
|
||||
return
|
||||
for control: Control in controls:
|
||||
if control.focus_mode != Control.FOCUS_NONE:
|
||||
control.grab_focus()
|
||||
return
|
||||
|
||||
|
||||
func _account_controller_controls() -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
for control: Control in [
|
||||
_name_edit,
|
||||
_customize_button,
|
||||
_apply_button,
|
||||
_revert_button,
|
||||
_defaults_button,
|
||||
]:
|
||||
if control != null and control.is_visible_in_tree():
|
||||
controls.append(control)
|
||||
for control: Control in _controls_under(_suggestions):
|
||||
if control not in controls:
|
||||
controls.append(control)
|
||||
return controls
|
||||
|
||||
|
||||
func _controller_option_groups() -> Array:
|
||||
var groups: Array = []
|
||||
if _option_list == null:
|
||||
return groups
|
||||
if _category_id != "fur_pattern":
|
||||
groups.append(_controls_under(_option_list))
|
||||
return groups
|
||||
var section_tabs := _option_list.find_child(
|
||||
"FurSectionTabs", true, false
|
||||
) as Node
|
||||
groups.append(_controls_under(section_tabs))
|
||||
if _active_fur_section == FUR_SECTION_PATTERNS:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
"FurPatternPartTabs", true, false
|
||||
)))
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
"FurPatternGrid", true, false
|
||||
)))
|
||||
else:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
"FurColorChannelGrid", true, false
|
||||
)))
|
||||
var palette_controls: Array[Control] = _controls_under(
|
||||
_option_list.find_child("FurPaletteGrid", true, false)
|
||||
)
|
||||
var custom_picker := _option_list.find_child(
|
||||
"FurCustomColorPicker", true, false
|
||||
) as Control
|
||||
if custom_picker != null:
|
||||
palette_controls.append(custom_picker)
|
||||
groups.append(palette_controls)
|
||||
return groups
|
||||
|
||||
|
||||
func _controls_under(root: Node) -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
if root == null:
|
||||
return controls
|
||||
_collect_controller_controls(root, controls)
|
||||
return controls
|
||||
|
||||
|
||||
func _collect_controller_controls(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var control := child as Control
|
||||
if control != null and not control.is_visible_in_tree():
|
||||
continue
|
||||
if control is BaseButton or control is Slider or control is LineEdit:
|
||||
output.append(control)
|
||||
_collect_controller_controls(child, output)
|
||||
|
||||
|
||||
func has_unsaved_changes() -> bool:
|
||||
return _dirty
|
||||
|
||||
|
|
@ -295,6 +586,12 @@ func _build_ui() -> void:
|
|||
actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
actions.add_theme_constant_override("separation", 7)
|
||||
account_row.add_child(actions)
|
||||
_customize_button = Button.new()
|
||||
_customize_button.text = "customize"
|
||||
_customize_button.custom_minimum_size.x = 96.0
|
||||
UtilityPageStyle.apply_compact_ocean_button(_customize_button)
|
||||
_customize_button.pressed.connect(_enter_controller_customization)
|
||||
actions.add_child(_customize_button)
|
||||
_apply_button = Button.new()
|
||||
_apply_button.text = "apply"
|
||||
_apply_button.custom_minimum_size.x = 72.0
|
||||
|
|
@ -307,12 +604,12 @@ func _build_ui() -> void:
|
|||
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)
|
||||
_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
|
||||
|
|
@ -377,16 +674,17 @@ func _build_ui() -> void:
|
|||
_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)
|
||||
_reset_view_button = Button.new()
|
||||
_reset_view_button.text = "↶"
|
||||
_reset_view_button.tooltip_text = "reset view"
|
||||
_reset_view_button.custom_minimum_size = Vector2(36.0, 36.0)
|
||||
_reset_view_button.position = Vector2(-8.0, -8.0)
|
||||
_reset_view_button.z_index = 2
|
||||
_reset_view_button.focus_mode = Control.FOCUS_NONE
|
||||
_reset_view_button.pressed.connect(_preview.reset_view)
|
||||
UtilityPageStyle.apply_compact_ocean_button(_reset_view_button)
|
||||
_reset_view_button.add_theme_font_size_override("font_size", 22)
|
||||
preview_layer.add_child(_reset_view_button)
|
||||
|
||||
_discard_confirmation = PanelContainer.new()
|
||||
_discard_confirmation.visible = false
|
||||
|
|
@ -414,16 +712,22 @@ func _build_ui() -> void:
|
|||
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()
|
||||
)
|
||||
_keep_editing_button.pressed.connect(_close_discard_confirmation)
|
||||
UtilityPageStyle.apply_ocean_button(_keep_editing_button)
|
||||
confirm_buttons.add_child(_keep_editing_button)
|
||||
|
||||
_build_categories()
|
||||
|
||||
|
||||
func _enter_controller_customization() -> void:
|
||||
if not _profile_active or not _profile_interactive:
|
||||
return
|
||||
_controller_zone = ControllerZone.CATEGORIES
|
||||
_controller_option_depth = 0
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func _build_categories() -> void:
|
||||
for child: Node in _category_list.get_children():
|
||||
child.queue_free()
|
||||
|
|
@ -461,7 +765,9 @@ func _refresh_options() -> void:
|
|||
_fur_custom_color_display = null
|
||||
_fur_custom_color_label = null
|
||||
for child: Node in _option_list.get_children():
|
||||
_option_list.remove_child(child)
|
||||
child.queue_free()
|
||||
_refresh_controller_zone_after_options.call_deferred()
|
||||
if _category_id == VOICE_CATEGORY_ID:
|
||||
_build_voice_options()
|
||||
return
|
||||
|
|
@ -994,6 +1300,7 @@ func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
|
|||
|
||||
func _build_fur_options(options: Array) -> void:
|
||||
var section_tabs := HBoxContainer.new()
|
||||
section_tabs.name = "FurSectionTabs"
|
||||
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]:
|
||||
|
|
@ -1075,6 +1382,7 @@ func _build_fur_pattern_options() -> void:
|
|||
pattern_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
|
||||
pattern_margin.add_child(pattern_scroll)
|
||||
var pattern_grid := GridContainer.new()
|
||||
pattern_grid.name = "FurPatternGrid"
|
||||
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)
|
||||
|
|
@ -1270,6 +1578,7 @@ func _build_fur_palette(
|
|||
custom_picker.tooltip_text = "Choose a custom color for channel %d." % (
|
||||
CharacterCustomizationCatalog.FUR_COLOR_IDS.find(category_id) + 1
|
||||
)
|
||||
custom_picker.set_meta(&"fur_color_category_id", category_id)
|
||||
custom_picker.color_changed.connect(
|
||||
_select_custom_fur_color.bind(category_id)
|
||||
)
|
||||
|
|
@ -1341,6 +1650,7 @@ func _select_custom_fur_color(color: Color, category_id: String) -> void:
|
|||
)
|
||||
_update_fur_custom_color_display(color)
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_queue_draft_appearance_preview()
|
||||
_dirty = _draft_differs()
|
||||
_refresh_actions()
|
||||
|
||||
|
|
@ -1417,8 +1727,7 @@ func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
|
|||
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
|
||||
_fit_color_picker_popup(button)
|
||||
var popup_style := UtilityPageStyle.rounded_style(
|
||||
UtilityPageStyle.OCEAN_PANEL_DEEP,
|
||||
20,
|
||||
|
|
@ -1428,19 +1737,119 @@ func _theme_fur_color_picker_popup(button: ColorPickerButton) -> void:
|
|||
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)
|
||||
if not popup.about_to_popup.is_connected(
|
||||
_on_controller_color_picker_opened.bind(button)
|
||||
):
|
||||
popup.about_to_popup.connect(
|
||||
_on_controller_color_picker_opened.bind(button)
|
||||
)
|
||||
if not popup.popup_hide.is_connected(
|
||||
_on_controller_color_picker_closed.bind(button)
|
||||
):
|
||||
popup.popup_hide.connect(
|
||||
_on_controller_color_picker_closed.bind(button)
|
||||
)
|
||||
|
||||
|
||||
func _fit_color_picker_popup(button: ColorPickerButton) -> void:
|
||||
var popup: PopupPanel = button.get_popup()
|
||||
var picker: ColorPicker = button.get_picker()
|
||||
var window_size: Vector2i = get_window().size
|
||||
var available := Vector2i(
|
||||
maxi(window_size.x - 32, 420),
|
||||
maxi(window_size.y - 32, 340),
|
||||
)
|
||||
var target := Vector2i(
|
||||
mini(FUR_COLOR_PICKER_POPUP_SIZE.x, available.x),
|
||||
mini(FUR_COLOR_PICKER_POPUP_SIZE.y, available.y),
|
||||
)
|
||||
popup.min_size = target
|
||||
popup.size = target
|
||||
picker.custom_minimum_size = Vector2(
|
||||
target.x - 48,
|
||||
target.y - 48,
|
||||
)
|
||||
picker.add_theme_constant_override(
|
||||
"sv_width",
|
||||
mini(FUR_COLOR_PICKER_SV_SIZE.x, target.x - 110),
|
||||
)
|
||||
picker.add_theme_constant_override(
|
||||
"sv_height",
|
||||
mini(FUR_COLOR_PICKER_SV_SIZE.y, target.y - 180),
|
||||
)
|
||||
|
||||
|
||||
func _on_controller_color_picker_opened(button: ColorPickerButton) -> void:
|
||||
_active_color_picker_button = button
|
||||
_active_color_picker_return_depth = _controller_option_depth
|
||||
_controller_zone = ControllerZone.COLOR_PICKER
|
||||
_fit_color_picker_popup(button)
|
||||
_apply_controller_zone_focus.call_deferred()
|
||||
_focus_controller_zone.call_deferred()
|
||||
|
||||
|
||||
func _on_controller_color_picker_closed(button: ColorPickerButton) -> void:
|
||||
if _active_color_picker_button != button:
|
||||
return
|
||||
_active_color_picker_button = null
|
||||
_controller_zone = ControllerZone.OPTIONS
|
||||
_controller_option_depth = _active_color_picker_return_depth
|
||||
_apply_controller_zone_focus.call_deferred()
|
||||
_focus_controller_zone.call_deferred()
|
||||
|
||||
|
||||
func _close_controller_color_picker() -> void:
|
||||
if _active_color_picker_button != null:
|
||||
_active_color_picker_button.get_popup().hide()
|
||||
else:
|
||||
_controller_zone = ControllerZone.OPTIONS
|
||||
_controller_option_depth = _active_color_picker_return_depth
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func _color_picker_controller_controls() -> Array[Control]:
|
||||
if _active_color_picker_button == null:
|
||||
return []
|
||||
return _controls_under(_active_color_picker_button.get_picker())
|
||||
|
||||
|
||||
func _adjust_controller_color_gamut(stick: Vector2, delta: float) -> void:
|
||||
if _active_color_picker_button == null:
|
||||
return
|
||||
var color: Color = _active_color_picker_button.color
|
||||
var saturation: float = clampf(color.s + stick.x * delta * 0.72, 0.0, 1.0)
|
||||
var value: float = clampf(color.v - stick.y * delta * 0.72, 0.0, 1.0)
|
||||
var adjusted := Color.from_hsv(color.h, saturation, value, 1.0)
|
||||
_active_color_picker_button.color = adjusted
|
||||
var category_id := str(_active_color_picker_button.get_meta(
|
||||
&"fur_color_category_id",
|
||||
_active_fur_color_id,
|
||||
))
|
||||
_select_custom_fur_color(adjusted, category_id)
|
||||
|
||||
|
||||
func _refresh_controller_zone_after_options() -> void:
|
||||
if _controller_zone != ControllerZone.OPTIONS:
|
||||
return
|
||||
var groups: Array = _controller_option_groups()
|
||||
if groups.is_empty():
|
||||
_controller_zone = ControllerZone.CATEGORIES
|
||||
else:
|
||||
_controller_option_depth = clampi(
|
||||
_controller_option_depth,
|
||||
0,
|
||||
groups.size() - 1,
|
||||
)
|
||||
_apply_controller_zone_focus()
|
||||
_focus_controller_zone()
|
||||
|
||||
|
||||
func _update_fur_custom_color_display(color: Color) -> void:
|
||||
|
|
@ -1465,6 +1874,7 @@ func _update_fur_custom_color_display(color: Color) -> void:
|
|||
func _select_option(category_id: String, option_id: String) -> void:
|
||||
_draft_appearance[category_id] = option_id
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_queue_draft_appearance_preview()
|
||||
_dirty = _draft_differs()
|
||||
_refresh_options()
|
||||
_refresh_actions()
|
||||
|
|
@ -1479,6 +1889,7 @@ func _select_scale(value: float) -> void:
|
|||
)
|
||||
_update_scale_value_label(resolved_scale)
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_queue_draft_appearance_preview()
|
||||
_dirty = _draft_differs()
|
||||
_refresh_actions()
|
||||
|
||||
|
|
@ -1532,6 +1943,7 @@ func _on_conflict_result(
|
|||
)
|
||||
UtilityPageStyle.apply_compact_ocean_button(anyway)
|
||||
_suggestions.add_child(anyway)
|
||||
_apply_controller_zone_focus()
|
||||
|
||||
|
||||
func _on_experience_changed(_total_experience: int, _level: int) -> void:
|
||||
|
|
@ -1607,6 +2019,8 @@ func _load_persisted() -> void:
|
|||
)
|
||||
_name_edit.text = _draft_name
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_appearance_preview_timer.stop()
|
||||
_service.preview_appearance(_persisted_appearance)
|
||||
_dirty = false
|
||||
_allow_duplicate = false
|
||||
_name_status.text = ""
|
||||
|
|
@ -1633,9 +2047,17 @@ func _show_confirmation(action: String) -> void:
|
|||
else:
|
||||
_confirmation_label.text = "Discard unsaved profile changes?"
|
||||
_confirmation_confirm.text = "discard changes"
|
||||
_apply_controller_zone_focus()
|
||||
_keep_editing_button.grab_focus()
|
||||
|
||||
|
||||
func _close_discard_confirmation() -> void:
|
||||
_discard_confirmation.visible = false
|
||||
_confirmation_action = ""
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func _confirm_pending_action() -> void:
|
||||
_discard_confirmation.visible = false
|
||||
if _confirmation_action == "defaults":
|
||||
|
|
@ -1644,12 +2066,30 @@ func _confirm_pending_action() -> void:
|
|||
_draft_speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
|
||||
_draft_call_id = VoiceProfilesType.DEFAULT_CALL_ID
|
||||
_preview.apply_appearance_profile(_draft_appearance)
|
||||
_queue_draft_appearance_preview()
|
||||
_dirty = _draft_differs()
|
||||
_refresh_options()
|
||||
_refresh_actions()
|
||||
else:
|
||||
_confirm_discard()
|
||||
_confirmation_action = ""
|
||||
_apply_controller_zone_focus()
|
||||
call_deferred("_focus_controller_zone")
|
||||
|
||||
|
||||
func _queue_draft_appearance_preview() -> void:
|
||||
if _service == null or _appearance_preview_timer == null:
|
||||
return
|
||||
# Throttle continuous controls such as the custom color picker instead of
|
||||
# debouncing them. Remote players should see an in-progress drag, while the
|
||||
# reliable channel remains capped at one current snapshot per interval.
|
||||
if _appearance_preview_timer.is_stopped():
|
||||
_appearance_preview_timer.start()
|
||||
|
||||
|
||||
func _publish_draft_appearance() -> void:
|
||||
if _service != null:
|
||||
_service.preview_appearance(_draft_appearance)
|
||||
|
||||
|
||||
func _draft_differs() -> bool:
|
||||
|
|
@ -1672,4 +2112,6 @@ func _refresh_actions() -> void:
|
|||
|
||||
func _clear_suggestions() -> void:
|
||||
for child: Node in _suggestions.get_children():
|
||||
_suggestions.remove_child(child)
|
||||
child.queue_free()
|
||||
_apply_controller_zone_focus()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue