From f8e02e3844b8e04c64296d4ba100c45b81b1b609 Mon Sep 17 00:00:00 2001 From: Voyager Date: Wed, 19 Aug 2026 10:02:28 -0400 Subject: [PATCH] Polish interface artwork and controller behavior --- items/catalog/crab_net.tres | 2 +- items/catalog/standard_shovel.tres | 2 +- items/icons/equipment/net.png | Bin 0 -> 3274 bytes items/icons/equipment/net.png.import | 40 +++++ items/icons/equipment/shovel.png | Bin 0 -> 2211 bytes items/icons/equipment/shovel.png.import | 40 +++++ settings/player_settings.gd | 25 ++- settings/player_settings_manager.gd | 19 ++- tests/art_tools_validation.gd | 115 ++++++++++++-- ...ontroller_focus_presentation_validation.gd | 68 ++++++++- ...ontroller_menu_accessibility_validation.gd | 129 +++++++++++++++- tests/controller_ui_navigation_validation.gd | 10 ++ tests/economy_regression_validation.gd | 17 ++- tests/equipment_catalog_validation.gd | 18 ++- ui/components/general_inventory_slot.gd | 14 +- ui/components/locked_content_presentation.gd | 67 ++++++++ .../locked_content_presentation.gd.uid | 1 + ui/controller_focus_presentation.gd | 144 ++++++++++++++++-- ui/fishing_shop.gd | 45 ++++-- ui/game_ui.gd | 21 +++ ui/icons/main_menu/delete_save.png | Bin 0 -> 3158 bytes ui/icons/main_menu/delete_save.png.import | 40 +++++ .../moderation_options_ban_light.png | Bin 0 -> 2432 bytes .../moderation_options_ban_light.png.import | 40 +++++ .../moderation_options_block_light.png | Bin 0 -> 3423 bytes .../moderation_options_block_light.png.import | 40 +++++ .../moderation_options_kick_light.png | Bin 0 -> 2498 bytes .../moderation_options_kick_light.png.import | 40 +++++ .../moderation_options_mute_light.png | Bin 0 -> 1985 bytes .../moderation_options_mute_light.png.import | 40 +++++ ui/mail_page.gd | 9 +- ui/pause_menu.tscn | 7 +- ui/pixelation_reset_overlay.gd | 10 ++ ui/player_menu.gd | 70 +-------- ui/players_page.gd | 60 +++++++- ui/profile_page.gd | 21 +++ ui/settings_panel.gd | 7 + ui/surface_drawing_toolbar.gd | 88 ++++++++++- ui/title_screen.gd | 15 +- ui/title_screen.tscn | 23 +-- ui/ui_pixelation_presenter.gd | 3 + 41 files changed, 1119 insertions(+), 171 deletions(-) create mode 100644 items/icons/equipment/net.png create mode 100644 items/icons/equipment/net.png.import create mode 100644 items/icons/equipment/shovel.png create mode 100644 items/icons/equipment/shovel.png.import create mode 100644 ui/components/locked_content_presentation.gd create mode 100644 ui/components/locked_content_presentation.gd.uid create mode 100644 ui/icons/main_menu/delete_save.png create mode 100644 ui/icons/main_menu/delete_save.png.import create mode 100644 ui/icons/moderation_options/moderation_options_ban_light.png create mode 100644 ui/icons/moderation_options/moderation_options_ban_light.png.import create mode 100644 ui/icons/moderation_options/moderation_options_block_light.png create mode 100644 ui/icons/moderation_options/moderation_options_block_light.png.import create mode 100644 ui/icons/moderation_options/moderation_options_kick_light.png create mode 100644 ui/icons/moderation_options/moderation_options_kick_light.png.import create mode 100644 ui/icons/moderation_options/moderation_options_mute_light.png create mode 100644 ui/icons/moderation_options/moderation_options_mute_light.png.import diff --git a/items/catalog/crab_net.tres b/items/catalog/crab_net.tres index 2ed3cb0..a6e26b0 100644 --- a/items/catalog/crab_net.tres +++ b/items/catalog/crab_net.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] -[ext_resource type="Texture2D" path="res://items/icons/equipment/temp_net.png" id="2_icon"] +[ext_resource type="Texture2D" path="res://items/icons/equipment/net.png" id="2_icon"] [resource] script = ExtResource("1_item") diff --git a/items/catalog/standard_shovel.tres b/items/catalog/standard_shovel.tres index 5c44996..b30589c 100644 --- a/items/catalog/standard_shovel.tres +++ b/items/catalog/standard_shovel.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] [ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] -[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"] +[ext_resource type="Texture2D" path="res://items/icons/equipment/shovel.png" id="2_icon"] [resource] script = ExtResource("1_item") diff --git a/items/icons/equipment/net.png b/items/icons/equipment/net.png new file mode 100644 index 0000000000000000000000000000000000000000..522dd65e9e0c15f88a2a8c9221c021963ab0ad68 GIT binary patch literal 3274 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 EH^qzMPyhe` literal 0 HcmV?d00001 diff --git a/items/icons/equipment/net.png.import b/items/icons/equipment/net.png.import new file mode 100644 index 0000000..e32d295 --- /dev/null +++ b/items/icons/equipment/net.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c026rqygllmst" +path="res://.godot/imported/net.png-695e6b1511b4d1d2f6c885df7ccf0412.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://items/icons/equipment/net.png" +dest_files=["res://.godot/imported/net.png-695e6b1511b4d1d2f6c885df7ccf0412.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/items/icons/equipment/shovel.png b/items/icons/equipment/shovel.png new file mode 100644 index 0000000000000000000000000000000000000000..089f30c3fef98cd7f2fdd544defcfe5bac70054f GIT binary patch literal 2211 zcmaJ>3pkYN9)HKBk!3EKVGLu2&Wa^F<2GYlav3xy90o;pWsI3M7&FF~>#!rY8TGcqJwm@`PEO>mhr=wuR!W@>5#t7I~P08fE4rcj7@JjKX} zN;QT#gH9(B2@D3EN}*7x6b9XlOeR5~8O_wtkgQg#kBv5>kJc3t1cd@S?JxB)Z|Hjm zuf?r|`8rvf8Io|i*d@({$-Wcg8svn;>As&ZuZt_$TMnlzXz8yk-5)G9)Lh0ke7B`j zfzF7D-Qmwm_%QY;dZ#ujwnwG{V7D3Fn2fq@X|RYz_<_FIXF3LY7~`ByFCqXM%&f2o zL?BFOD3o%EG)c@rG8JM*hD-qf=Wt~>zw{JS@6vPfoL22Of>~4Q)EMu2O4T;=;c(k< zBwY`Oi*cz7*m~^pW%adlf3~dF+My~JO;QVN+*JpTEj{j`UUNU&U>ctk#(n%kJ=}37 z;cJ?uik{i$AJrMpYA(3ln!7t>@o%TacS}FmVRg7~D#7w)O^ zG4#(*^PTm|l~M!JX=kgjd-}Y4o&jf#(^MVx2Of-z+Wq4E$^dcTOz83Z5v^`|oZ!)( zUVHBmhoO{yE39B+U;1}h0ej1$Y`U+8k4*#!Ogai*B<}l%EAD2 z58Mq-?A8|SY@|%n21aUS378Ayf=tzMKckN7v@Z#~D2A&3g*)$NyjS<>#GA9agUFs1 ztAlYKeK!zii>f=e1<_AWV@i^`PnC#`Oj;h$v;NwDtJ22ZK6{5@VN&bFjR9*ta@H%v z`SR|W8&!AgCbKGe$Lg^fCg7u`DM{OrmLJ=YtLelVjnTw9_QQ&y!Mtq$mzmA;NvCFW z2IBrhywKWo@bl8^&f~S5GuCSCOKQa@j^)#i7r%Za{dn80lk9|QoN)92o7ky3llMcr z-}t#@Eu)~9@uMp)&GzTL-o!a_zc+WwYiWz7PU_4beNy|&u9W)9rToi=w(JS<1#?GR z??uPgvzkLkucQ-9_M4hFwe{si?7Z`XQ_7_YWizeGMU%N*@eWwJz zP9Gw^5lEa65pzXKF?7J5Y45z1$=S-{gt1(>PEK4$jt!H^Wis12?JMsQWb!zvB=a`} z2PZCzy+)|YXjMUi)w%*Am5EakA$%{_MO?yI_FN{5Ywxti**KCg1UW>MBhHXYxIXeE zxiVBC6o~-~e_VqPo0Rn?PcGUk6DK12&1Vlne~ zWe&hLW4#?5FGu7uF`|@;_{oS^sZfd$AtIG01N$0Pk@Gg|UrgkC7#s7*28f}fntTN!a)e)|ZwA8kQX=VcWn!2VNqTqs8o?+kxe0|LQVBY~ z44x=;O>W##n($6+wfleOcW~YR6~wF#xQh9m0XO$BAr@20=Xr%i^p1>t7TV)U*zl+& zrhhzJem~oV%Z3`k zKx|NM=oEJ8f&(CJOR)$uV)A=W`TgrjIV_hMYMU&fC*C)0@A+bdS$p zDxV})bd@}PUeT5J)TE-T=;`r_u7amg-+P>OUmzLO1inc-KQrXPdsP|muz&tvs^L(b zTcQ_#ztsE+Z~MheE3UD2!eDVRC&apO`1A8vV10AL&t^m~T7Cqi{2F`Zn-eVuC zuAEy@SM?iooB7 PlayerSettings: result.chat_dock_right = chat_dock_right result.chat_mobile_mode = chat_mobile_mode result.paint_dock_right = paint_dock_right + result.presentation_layout_customized = presentation_layout_customized result.fullscreen_enabled = fullscreen_enabled result.master_volume = master_volume result.music_volume = music_volume @@ -91,6 +96,20 @@ func copy() -> PlayerSettings: return result +func normalize_implicit_presentation_layout() -> bool: + if presentation_layout_customized: + return false + var changed: bool = ( + chat_dock_right != DEFAULT_CHAT_DOCK_RIGHT + or chat_mobile_mode != DEFAULT_CHAT_MOBILE_MODE + or paint_dock_right != DEFAULT_PAINT_DOCK_RIGHT + ) + chat_dock_right = DEFAULT_CHAT_DOCK_RIGHT + chat_mobile_mode = DEFAULT_CHAT_MOBILE_MODE + paint_dock_right = DEFAULT_PAINT_DOCK_RIGHT + return changed + + static func _is_valid_audio_volume(value: float) -> bool: return ( is_finite(value) diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd index 7032c6f..6c8cf4f 100644 --- a/settings/player_settings_manager.gd +++ b/settings/player_settings_manager.gd @@ -82,6 +82,10 @@ func load_settings() -> bool: presentation.has("paint_dock_right") and typeof(presentation["paint_dock_right"]) != TYPE_BOOL ) + or ( + presentation.has("layout_customized") + and typeof(presentation["layout_customized"]) != TYPE_BOOL + ) or ( presentation.has("fullscreen_enabled") and typeof(presentation["fullscreen_enabled"]) != TYPE_BOOL @@ -135,6 +139,12 @@ func load_settings() -> bool: presentation.get("chat_mobile_mode", false) ) loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true)) + loaded.presentation_layout_customized = bool( + presentation.get("layout_customized", false) + ) + var normalized_implicit_layout: bool = ( + loaded.normalize_implicit_presentation_layout() + ) loaded.fullscreen_enabled = bool( presentation.get("fullscreen_enabled", false) ) @@ -147,7 +157,7 @@ func load_settings() -> bool: if not loaded.is_valid(): return _use_defaults_after_corruption("Player settings values are invalid.") current_settings = loaded - _is_dirty = false + _is_dirty = normalized_implicit_layout _apply_audio_settings(current_settings) emit_signal("settings_changed", current_settings) return true @@ -157,7 +167,9 @@ func apply_settings(settings: PlayerSettings) -> bool: if settings == null or not settings.is_valid(): return false var previous: PlayerSettings = current_settings - current_settings = settings.copy() + var normalized_settings := settings.copy() + normalized_settings.normalize_implicit_presentation_layout() + current_settings = normalized_settings _is_dirty = true if not save_now(): current_settings = previous @@ -240,6 +252,9 @@ func save_now() -> bool: "chat_dock_right": current_settings.chat_dock_right, "chat_mobile_mode": current_settings.chat_mobile_mode, "paint_dock_right": current_settings.paint_dock_right, + "layout_customized": ( + current_settings.presentation_layout_customized + ), "fullscreen_enabled": current_settings.fullscreen_enabled, }, } diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index cafd16e..78a020f 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -87,14 +87,25 @@ func _run() -> void: assert(player != null and service != null and toolbar != null) assert(chat_ui != null) assert(bool(game_ui.call("_can_start_virtual_mouse"))) + var virtual_pointer_states: Array[bool] = [] + game_ui.virtual_pointer_mode_changed.connect( + func(active: bool) -> void: virtual_pointer_states.append(active) + ) game_ui.call("_begin_virtual_mouse", 0) var virtual_cursor := game_ui.get_node( "%ControllerVirtualCursor" ) as ControllerVirtualCursor assert(virtual_cursor.visible) + assert(virtual_pointer_states == [true]) + # PopupMenu subwindows can consume the physical shoulder release. The + # polled primary-button state must still release the synthetic mouse click. + game_ui.set("_virtual_mouse_button_mask", MOUSE_BUTTON_MASK_LEFT) + game_ui.call("_sync_virtual_mouse_primary_button", false) + assert(int(game_ui.get("_virtual_mouse_button_mask")) == 0) assert(not player.is_camera_input_enabled()) game_ui.call("_end_virtual_mouse") assert(not virtual_cursor.visible) + assert(virtual_pointer_states == [true, false]) assert(player.is_camera_input_enabled()) var settings_panel := game_ui.get( "_pause_settings_panel" @@ -131,6 +142,12 @@ func _run() -> void: changed_docks.paint_dock_right = false assert(settings_manager.apply_settings(changed_docks)) await process_frame + assert(not chat_ui.is_docked_right()) + assert(not chat_ui.is_mobile_mode()) + assert(toolbar.is_docked_right()) + changed_docks.presentation_layout_customized = true + assert(settings_manager.apply_settings(changed_docks)) + await process_frame assert(chat_ui.is_docked_right()) assert(chat_ui.is_mobile_mode()) assert(not toolbar.is_docked_right()) @@ -153,7 +170,31 @@ func _run() -> void: assert(reloaded_settings.current_settings.chat_dock_right) assert(reloaded_settings.current_settings.chat_mobile_mode) assert(not reloaded_settings.current_settings.paint_dock_right) + assert(reloaded_settings.current_settings.presentation_layout_customized) reloaded_settings.queue_free() + var legacy_data: Dictionary = JSON.parse_string( + FileAccess.get_file_as_string(PlayerSettingsManager.SETTINGS_PATH) + ) as Dictionary + var legacy_presentation := legacy_data.get("presentation", {}) as Dictionary + legacy_presentation.erase("layout_customized") + legacy_data["presentation"] = legacy_presentation + var legacy_file := FileAccess.open( + PlayerSettingsManager.SETTINGS_PATH, + FileAccess.WRITE, + ) + assert(legacy_file != null) + legacy_file.store_string(JSON.stringify(legacy_data, "\t")) + legacy_file.close() + var migrated_legacy_settings := PlayerSettingsManager.new() + root.add_child(migrated_legacy_settings) + assert(migrated_legacy_settings.load_settings()) + assert(not migrated_legacy_settings.current_settings.chat_dock_right) + assert(not migrated_legacy_settings.current_settings.chat_mobile_mode) + assert(migrated_legacy_settings.current_settings.paint_dock_right) + assert( + not migrated_legacy_settings.current_settings.presentation_layout_customized + ) + migrated_legacy_settings.queue_free() var default_docks: PlayerSettings = settings_manager.current_settings.copy() default_docks.chat_dock_right = false default_docks.chat_mobile_mode = false @@ -456,18 +497,39 @@ func _run() -> void: assert(grid_option.get_item_text(index).is_empty()) assert(brush_option.get_item_icon(index) != null) assert(grid_option.get_item_icon(index) != null) - assert( - str(brush_option.get_item_icon(index).get_meta( - &"channel_mask_source", "" - )).ends_with( - expected_brush_icons[index] + var brush_unlocked: bool = index == 0 + var grid_unlocked: bool = index == 0 + if brush_unlocked: + assert( + str(brush_option.get_item_icon(index).get_meta( + &"channel_mask_source", "" + )).ends_with( + expected_brush_icons[index] + ) ) - ) - assert( - grid_option.get_item_icon(index).resource_path.ends_with( - expected_grid_icons[index] + else: + assert(bool(brush_option.get_item_icon(index).get_meta( + &"locked_content_icon", false + ))) + if grid_unlocked: + assert( + grid_option.get_item_icon(index).resource_path.ends_with( + expected_grid_icons[index] + ) ) - ) + else: + assert(bool(grid_option.get_item_icon(index).get_meta( + &"locked_content_icon", false + ))) + var expected_lock_alpha: float = 1.0 if index == 0 else 0.72 + assert(is_equal_approx( + brush_option.get_popup().get_item_icon_modulate(index).a, + expected_lock_alpha, + )) + assert(is_equal_approx( + grid_option.get_popup().get_item_icon_modulate(index).a, + expected_lock_alpha, + )) assert(not brush_option.get_popup().is_item_disabled(0)) assert(brush_option.get_popup().is_item_disabled(1)) assert(not grid_option.get_popup().is_item_disabled(0)) @@ -478,6 +540,26 @@ func _run() -> void: assert(color_button.focus_mode == Control.FOCUS_NONE) assert(not (color_buttons[&"chalk_white"] as Button).disabled) assert((color_buttons[&"ocean_teal"] as Button).disabled) + var base_color_lock := ( + (color_buttons[&"chalk_white"] as Button).get_node( + "UnlockStateIcon" + ) as TextureRect + ) + var locked_color_lock := ( + (color_buttons[&"ocean_teal"] as Button).get_node( + "UnlockStateIcon" + ) as TextureRect + ) + assert(not base_color_lock.visible) + assert(locked_color_lock.visible) + assert(locked_color_lock.size == Vector2(18.0, 18.0)) + assert(is_equal_approx(locked_color_lock.modulate.a, 0.34)) + var popup_lock_icon := toolbar.get("_popup_lock_icon") as Texture2D + assert(popup_lock_icon.get_width() == 40) + assert(popup_lock_icon.get_height() == 40) + assert(int(popup_lock_icon.get_meta( + &"locked_content_icon_size", 0 + )) == 24) for product_id: StringName in [ &"marker_ocean_teal", &"brush_4x", &"grid_128x", @@ -487,6 +569,19 @@ func _run() -> void: assert(not (color_buttons[&"ocean_teal"] as Button).disabled) assert(not brush_option.get_popup().is_item_disabled(3)) assert(not grid_option.get_popup().is_item_disabled(3)) + assert(not locked_color_lock.visible) + assert(str(brush_option.get_item_icon(3).get_meta( + &"channel_mask_source", "" + )).ends_with(expected_brush_icons[3])) + assert(grid_option.get_item_icon(3).resource_path.ends_with( + expected_grid_icons[3] + )) + assert(is_equal_approx( + brush_option.get_popup().get_item_icon_modulate(3).a, 1.0 + )) + assert(is_equal_approx( + grid_option.get_popup().get_item_icon_modulate(3).a, 1.0 + )) assert(service.set_color_id(&"ocean_teal")) assert(service.set_brush_size(4)) assert(service.set_grid_size(128)) diff --git a/tests/controller_focus_presentation_validation.gd b/tests/controller_focus_presentation_validation.gd index ddb86a0..28ce0b4 100644 --- a/tests/controller_focus_presentation_validation.gd +++ b/tests/controller_focus_presentation_validation.gd @@ -42,11 +42,30 @@ func _run() -> void: stage.add_child(second_button) await process_frame + standard_button.grab_focus() + await process_frame + _expect( + root.gui_get_focus_owner() == null, + "a menu's programmatic first focus is visible before navigation input", + ) + _expect( + presentation.get("_neutral_focus_seed") == standard_button, + "the neutral menu focus seed did not remember the first action", + ) + var first_navigation := InputEventJoypadButton.new() + first_navigation.button_index = JOY_BUTTON_DPAD_DOWN + first_navigation.pressed = true + presentation._input(first_navigation) + await process_frame + _expect( + root.gui_get_focus_owner() == standard_button, + "the first directional input did not activate the neutral focus seed", + ) + var controller_event := InputEventJoypadButton.new() controller_event.button_index = JOY_BUTTON_A controller_event.pressed = true presentation._input(controller_event) - standard_button.grab_focus() await process_frame var focus_arrow := presentation.get("_focus_arrow") as TextureRect var focus_shadow := presentation.get("_focus_arrow_shadow") as TextureRect @@ -111,8 +130,8 @@ func _run() -> void: root.gui_get_hovered_control() == standard_button, "the pointer did not reach the hover-suppression test button", ) + presentation._input(first_navigation) presentation._input(controller_event) - standard_button.grab_focus() await process_frame _expect( standard_button.get_theme_stylebox("hover") == normal_style, @@ -154,6 +173,10 @@ func _run() -> void: not focus_shadow.visible, "mouse motion leaves the controller focus shadow visible", ) + _expect( + root.gui_get_focus_owner() == null, + "mouse motion leaves a stale menu action focused", + ) _expect( standard_button.get_theme_stylebox("focus") == old_focus_style, "native theme overrides were not restored after controller use", @@ -162,8 +185,6 @@ func _run() -> void: standard_button.get_theme_stylebox("hover") == old_hover_style, "mouse hover styling was not restored after controller use", ) - presentation._input(controller_event) - var item_list := ItemList.new() item_list.position = Vector2(80.0, 160.0) item_list.size = Vector2(240.0, 150.0) @@ -175,6 +196,9 @@ func _run() -> void: item_list.select(0) item_list.grab_focus() await process_frame + presentation._input(first_navigation) + presentation._input(controller_event) + await process_frame var first_item_arrow_y: float = focus_arrow.position.y item_list.select(2) await process_frame @@ -329,6 +353,42 @@ func _run() -> void: not focus_arrow.visible, "focus arrow remains after its control becomes unfocusable", ) + var event_focused_button := Button.new() + event_focused_button.text = "event focused" + event_focused_button.position = Vector2(700.0, 360.0) + event_focused_button.custom_minimum_size = Vector2(160.0, 50.0) + stage.add_child(event_focused_button) + await process_frame + presentation._input(first_navigation) + event_focused_button.grab_focus() + await process_frame + _expect( + root.gui_get_focus_owner() == event_focused_button, + ( + "a menu focus established by the current directional event was " + + "mistaken for an automatic default" + ), + ) + + presentation.set_virtual_pointer_active(true) + await process_frame + _expect( + not focus_arrow.visible, + "focus arrow competes with active virtual-pointer presentation", + ) + presentation._input(controller_event) + await process_frame + _expect( + not focus_arrow.visible, + "controller input stole presentation from the held virtual pointer", + ) + presentation.set_virtual_pointer_active(false) + presentation._input(controller_event) + await process_frame + _expect( + focus_arrow.visible, + "controller focus presentation did not resume after virtual-pointer use", + ) stage.queue_free() if _failures.is_empty(): diff --git a/tests/controller_menu_accessibility_validation.gd b/tests/controller_menu_accessibility_validation.gd index 103b98a..975ce79 100644 --- a/tests/controller_menu_accessibility_validation.gd +++ b/tests/controller_menu_accessibility_validation.gd @@ -3,6 +3,8 @@ extends SceneTree const JoinGamePageScene = preload( "res://ui/network/join_game_page.tscn" ) +const TitleScreenScene = preload("res://ui/title_screen.tscn") +const PauseMenuScene = preload("res://ui/pause_menu.tscn") const SettingsPanelScene = preload("res://ui/settings_panel.tscn") const BubbleConfirmationScene = preload( "res://ui/components/bubble_menu/bubble_confirmation_page.tscn" @@ -40,6 +42,7 @@ func _initialize() -> void: func _run() -> void: root.size = Vector2i(1280, 720) + await _validate_primary_menu_navigation() await _validate_join_game_navigation() await _validate_data_settings_navigation() await _validate_settings_adjustment_navigation() @@ -61,6 +64,49 @@ func _run() -> void: quit(1) +func _validate_primary_menu_navigation() -> void: + var title := TitleScreenScene.instantiate() as Control + root.add_child(title) + await process_frame + (title.get_node("%ButtonCenter") as Control).show() + (title.get_node("%BubbleField") as Control).show() + title.call("_update_title_layout") + title.call("_set_title_bubbles_interactive", true) + var title_controls: Array[Control] = [] + for candidate: Variant in title.call("_get_title_buttons"): + var control := candidate as Control + if control is BaseButton: + (control as BaseButton).disabled = false + title_controls.append(control) + _expect( + title_controls.size() == 7, + "Title menu does not expose all seven primary actions.", + ) + _assert_directionally_reachable(title_controls.front(), title_controls) + title.queue_free() + await process_frame + + var pause := PauseMenuScene.instantiate() as Control + root.add_child(pause) + pause.show() + var root_page := pause.get_node("%RootPage") as Control + root_page.call("show_page", false) + for _frame: int in 2: + await process_frame + var pause_controls: Array[Control] = [ + pause.get_node("%ResumeButton") as Control, + pause.get_node("%SaveButton") as Control, + pause.get_node("%JoinGameButton") as Control, + pause.get_node("%SettingsButton") as Control, + pause.get_node("%ReturnToTitleButton") as Control, + pause.get_node("%ResetProgressButton") as Control, + pause.get_node("%QuitButton") as Control, + ] + _assert_directionally_reachable(pause_controls.front(), pause_controls) + pause.queue_free() + await process_frame + + func _validate_join_game_navigation() -> void: var page := JoinGamePageScene.instantiate() as Control root.add_child(page) @@ -541,12 +587,21 @@ func _validate_settings_presentation_requires_apply() -> void: _expect( not settings_manager.current_settings.chat_dock_right and not settings_manager.current_settings.chat_mobile_mode - and settings_manager.current_settings.paint_dock_right, + and settings_manager.current_settings.paint_dock_right + and not settings_manager.current_settings.presentation_layout_customized, ( "Navigating presentation choices changed saved docking settings " + "before Apply." ), ) + panel.call("_apply_settings") + _expect( + settings_manager.current_settings.chat_dock_right + and settings_manager.current_settings.chat_mobile_mode + and not settings_manager.current_settings.paint_dock_right + and settings_manager.current_settings.presentation_layout_customized, + "Applying deliberate presentation choices did not retain them.", + ) panel.close_panel(true) panel.queue_free() settings_manager.queue_free() @@ -622,6 +677,7 @@ func _validate_profile_confirmation_focus() -> void: account_controls.append(item as Control) var preview := page.get("_preview") as Control var reset_view := page.get("_reset_view_button") as Button + var reset_view_hint := page.get("_reset_view_controller_hint") as Label _expect( suggestion in account_controls, "Profile name-conflict choices are outside the account zone.", @@ -638,6 +694,34 @@ func _validate_profile_confirmation_focus() -> void: reset_view.focus_mode == Control.FOCUS_NONE, "The profile reset-view button must not take controller focus.", ) + _expect( + reset_view_hint != null + and reset_view_hint.text == "Reset View: RS" + and reset_view_hint.focus_mode == Control.FOCUS_NONE, + "The profile preview does not present its right-stick reset hint.", + ) + preview.set("_camera_yaw", 1.0) + preview.set("_camera_pitch", 0.4) + preview.set("_camera_distance", 2.5) + var reset_view_event := InputEventJoypadButton.new() + reset_view_event.button_index = JOY_BUTTON_RIGHT_STICK + reset_view_event.pressed = true + _expect( + bool(page.call("handle_controller_input", reset_view_event)), + "Profile customization did not consume right-stick reset.", + ) + _expect( + is_equal_approx(float(preview.get("_camera_yaw")), 0.0) + and is_equal_approx( + float(preview.get("_camera_pitch")), + ProfilePreview.DEFAULT_CAMERA_PITCH, + ) + and is_equal_approx( + float(preview.get("_camera_distance")), + ProfilePreview.DEFAULT_CAMERA_DISTANCE, + ), + "Right-stick click did not restore the default profile preview view.", + ) page.call("_show_confirmation", "defaults") var confirmation := page.get("_discard_confirmation") as Control var confirm := page.get("_confirmation_confirm") as Button @@ -1087,11 +1171,13 @@ func _validate_inventory_tab_zone_transitions() -> void: inventory_state.add_child(node) bag.setup(ItemCatalogResource) layout.setup(bag, catches, ItemCatalogResource, storage_capacity) + assert(layout.restore_backpack_level(1)) bag.set_inventory_layout(layout) catches.set_inventory_layout(layout) hotbar.setup(bag, ItemCatalogResource, catches, layout) assert(bag.add_item(&"basic_fishing_rod", 1)) assert(bag.add_item(&"coffee", 1)) + assert(bag.add_item(&"worms", 1)) assert(layout.move_entry( PlayerInventoryLayout.EntryKind.ITEM, &"basic_fishing_rod", @@ -1415,6 +1501,47 @@ func _validate_inventory_tab_zone_transitions() -> void: ) for _frame: int in 2: await process_frame + + menu.call("_show_section_immediate", PlayerMenuType.Section.TACKLE_BOX) + menu.call("_set_content_interactive", true) + menu.set( + "_controller_ownership", + PlayerMenuType.ControllerOwnership.ITEM_LIST, + ) + menu.call("_apply_inventory_controller_zone_focus_modes") + menu.call("_configure_tackle_item_focus") + for _frame: int in 2: + await process_frame + var tackle_buttons: Dictionary = menu.get("_tackle_item_buttons") + var worms_button := tackle_buttons.get(&"worms") as Button + _expect(worms_button != null, "Tackle test could not find unlocked worms.") + if worms_button != null: + worms_button.grab_focus() + menu.call("_handle_controller_ownership_input", accept) + menu.call("_handle_controller_ownership_input", accept_release) + _expect( + StringName(menu.get("_selected_tackle_item_id")).is_empty(), + "Controller A still activates a bait or lure inventory action.", + ) + _expect( + not (menu.get_node("%TackleDetailPanel") as Control).visible, + "Controller A opened the tackle notepad reserved for Y.", + ) + _expect( + bool(menu.call( + "_handle_controller_ownership_input", context_press + )), + "Controller Y did not open the bait/lure equip notepad.", + ) + _expect( + menu.get("_controller_ownership") + == PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS, + "Tackle notepad did not take controller ownership after Y.", + ) + _expect( + (menu.get_node("%TackleDetailPanel") as Control).visible, + "Controller Y left the bait/lure equip notepad hidden.", + ) menu.queue_free() inventory_state.queue_free() await process_frame diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index ec6c0d4..e1169f9 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -215,6 +215,13 @@ func _validate_settings_navigation_contract() -> void: settings.crisp_reset_focus_requested.connect( reset_overlay.focus_reset_button ) + var reset_cursor_presentation := reset_overlay.get_node( + "ControllerFocusPresentation" + ) as ControllerFocusPresentation + var controller_right := InputEventJoypadButton.new() + controller_right.button_index = JOY_BUTTON_DPAD_RIGHT + controller_right.pressed = true + reset_cursor_presentation._input(controller_right) var focus_reset_event := InputEventAction.new() focus_reset_event.action = &"ui_right" focus_reset_event.pressed = true @@ -224,6 +231,9 @@ func _validate_settings_navigation_contract() -> void: root.gui_get_focus_owner() == reset_overlay.get_node("%ResetPixelationButton") ) + assert( + (reset_cursor_presentation.get("_focus_arrow") as TextureRect).visible + ) reset_overlay.return_to_settings_requested.connect( settings.focus_back_button ) diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index 1ef8c72..884ad9b 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -806,15 +806,15 @@ func _test_fishing_shop_sale_ui( assert(bait_receipt.contains(" spent on ")) assert(bait_receipt.ends_with(" worms")) assert(snails_button.get_node_or_null("SupplyQuantityBadge") == null) - assert(worms_button.get_node_or_null("UnlockStateIcon") == null) + assert(worms_button.get_node_or_null("UnlockStateBadge") == null) var snails_lock_icon := snails_button.get_node( - "UnlockStateIcon" + "UnlockStateBadge/UnlockStateIcon" ) as TextureRect assert(not bool(snails_lock_icon.get_meta(&"unlocked"))) assert(snails_lock_icon.texture.resource_path.ends_with( "/lock_light.png" )) - assert(is_equal_approx(snails_lock_icon.modulate.a, 0.75)) + assert(is_equal_approx(snails_lock_icon.modulate.a, 0.88)) await _activate_pointer_control(equipment_tab, ui_viewport) await process_frame var equipment_sections: Array[String] = [] @@ -836,7 +836,7 @@ func _test_fishing_shop_sale_ui( assert(basic_rod_button != null) assert(basic_rod_button.custom_minimum_size == Vector2(144.0, 144.0)) assert(basic_rod_button.icon != null and basic_rod_button.disabled) - assert(basic_rod_button.get_node_or_null("UnlockStateIcon") == null) + assert(basic_rod_button.get_node_or_null("UnlockStateBadge") == null) assert(rod_cards.find_child("Rod_aurora_rod", true, false) == null) await _activate_pointer_control(art_supplies_tab, ui_viewport) await process_frame @@ -886,8 +886,13 @@ func _test_fishing_shop_sale_ui( var product_unlocked: bool = ( player.art_unlocks.owns_product(product_id) ) - var state_icon := upgrade_button.get_node_or_null( - "UnlockStateIcon" + var state_badge := upgrade_button.get_node_or_null( + "UnlockStateBadge" + ) as Panel + var state_icon := ( + state_badge.get_node("UnlockStateIcon") as TextureRect + if state_badge != null + else null ) as TextureRect assert((state_icon == null) == product_unlocked) if state_icon != null: diff --git a/tests/equipment_catalog_validation.gd b/tests/equipment_catalog_validation.gd index a09221a..a74d066 100644 --- a/tests/equipment_catalog_validation.gd +++ b/tests/equipment_catalog_validation.gd @@ -20,20 +20,25 @@ func _initialize() -> void: shop.call( "_add_unlock_state_icon", locked_button, false, Vector2(72.0, 72.0) ) - var locked_icon := locked_button.get_node( + var locked_badge := locked_button.get_node( + "UnlockStateBadge" + ) as Panel + var locked_icon := locked_badge.get_node( "UnlockStateIcon" ) as TextureRect + assert(locked_badge.position == Vector2(4.0, 4.0)) + assert(locked_badge.size == Vector2(30.0, 30.0)) assert(locked_icon.texture.resource_path.ends_with("/lock_light.png")) - assert(locked_icon.size == Vector2(48.0, 48.0)) - assert(locked_icon.position == Vector2(12.0, 6.0)) - assert(is_equal_approx(locked_icon.modulate.a, 0.75)) + assert(locked_icon.size == Vector2(18.0, 18.0)) + assert(locked_icon.position == Vector2(6.0, 6.0)) + assert(is_equal_approx(locked_icon.modulate.a, 0.88)) assert(not bool(locked_icon.get_meta(&"unlocked"))) locked_button.free() var unlocked_button := Button.new() shop.call( "_add_unlock_state_icon", unlocked_button, true, Vector2(72.0, 72.0) ) - assert(unlocked_button.get_node_or_null("UnlockStateIcon") == null) + assert(unlocked_button.get_node_or_null("UnlockStateBadge") == null) unlocked_button.free() shop.free() @@ -54,13 +59,14 @@ func _initialize() -> void: ) assert(crab_net != null) assert(crab_net.icon != null) - assert(crab_net.icon.resource_path.ends_with("/equipment/temp_net.png")) + assert(crab_net.icon.resource_path.ends_with("/equipment/net.png")) var shovel: ItemDataType = ItemCatalogResource.get_available_item_by_id( &"standard_shovel" ) assert(shovel != null) assert(shovel.category == ItemDataType.Category.TOOL) assert(shovel.icon != null) + assert(shovel.icon.resource_path.ends_with("/equipment/shovel.png")) assert(shovel.equippable) assert(shovel.hotbar_allowed) assert(FishingShopStockType.get_price(&"standard_shovel") == 75) diff --git a/ui/components/general_inventory_slot.gd b/ui/components/general_inventory_slot.gd index dd3d1fd..bdb132f 100644 --- a/ui/components/general_inventory_slot.gd +++ b/ui/components/general_inventory_slot.gd @@ -7,7 +7,9 @@ signal context_requested(slot: GeneralInventorySlot) signal context_changed(slot: GeneralInventorySlot, text: String, active: bool) const ItemDataType = preload("res://items/item_data.gd") -const LOCK_ICON: Texture2D = preload("res://ui/icons/pictograms/lock_light.png") +const LockedContentPresentationType = preload( + "res://ui/components/locked_content_presentation.gd" +) var slot_index: int = -1 var container: int = -1 @@ -104,8 +106,8 @@ func refresh() -> void: disabled = _locked _apply_icon_geometry() if _locked: - _icon.texture = LOCK_ICON - _icon.modulate = Color(UtilityPageStyle.OCEAN_DISABLED, 0.18) + _icon.texture = LockedContentPresentationType.ICON + _icon.modulate = LockedContentPresentationType.icon_modulate() var storage_slot := ( container == PlayerInventoryLayout.InventoryContainer.STORAGE ) @@ -305,7 +307,7 @@ func _apply_style() -> void: Color(UtilityPageStyle.OCEAN_SELECTED, 0.92), radius ) var locked := UtilityPageStyle.rounded_style( - Color(UtilityPageStyle.OCEAN_FIELD, 0.38), radius + LockedContentPresentationType.disabled_background_color(), radius ) for state: StringName in [&"normal", &"pressed"]: add_theme_stylebox_override(state, normal) @@ -332,7 +334,9 @@ func _apply_icon_geometry() -> void: var is_large: bool = _presentation_size.x >= 70.0 var margin: float if _locked: - var lock_size: float = 24.0 if is_large else 18.0 + var lock_size: float = LockedContentPresentationType.icon_size_for( + _presentation_size + ).x margin = maxf( (_presentation_size.x - lock_size) * 0.5, 0.0, diff --git a/ui/components/locked_content_presentation.gd b/ui/components/locked_content_presentation.gd new file mode 100644 index 0000000..1342054 --- /dev/null +++ b/ui/components/locked_content_presentation.gd @@ -0,0 +1,67 @@ +class_name LockedContentPresentation +extends RefCounted + +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const ICON: Texture2D = preload("res://ui/icons/pictograms/lock_light.png") + +const LARGE_ICON_SIZE: float = 24.0 +const COMPACT_ICON_SIZE: float = 18.0 +const LARGE_PRESENTATION_THRESHOLD: float = 70.0 +const ICON_ALPHA: float = 0.18 +const BACKGROUND_ALPHA: float = 0.38 + + +static func icon_size_for(presentation_size: Vector2) -> Vector2: + var edge: float = ( + LARGE_ICON_SIZE + if minf(presentation_size.x, presentation_size.y) + >= LARGE_PRESENTATION_THRESHOLD + else COMPACT_ICON_SIZE + ) + return Vector2(edge, edge) + + +static func icon_modulate(alpha: float = ICON_ALPHA) -> Color: + return Color(UtilityPageStyleType.OCEAN_DISABLED, alpha) + + +static func disabled_background_color() -> Color: + return Color(UtilityPageStyleType.OCEAN_FIELD, BACKGROUND_ALPHA) + + +static func make_icon_texture( + icon_size: int = int(COMPACT_ICON_SIZE), + canvas_size: int = icon_size, +) -> Texture2D: + var source_image: Image = ICON.get_image() + if source_image == null or source_image.is_empty(): + return ICON + var image := source_image.duplicate() as Image + if image.is_compressed() and image.decompress() != OK: + return ICON + image.convert(Image.FORMAT_RGBA8) + image.resize( + icon_size, + icon_size, + Image.INTERPOLATE_NEAREST, + ) + if canvas_size > icon_size: + var canvas := Image.create( + canvas_size, + canvas_size, + false, + Image.FORMAT_RGBA8, + ) + canvas.fill(Color.TRANSPARENT) + var inset := Vector2i.ONE * ((canvas_size - icon_size) / 2) + canvas.blend_rect( + image, + Rect2i(Vector2i.ZERO, Vector2i(icon_size, icon_size)), + inset, + ) + image = canvas + var texture := ImageTexture.create_from_image(image) + texture.set_meta(&"locked_content_icon", true) + texture.set_meta(&"locked_content_icon_size", icon_size) + texture.set_meta(&"locked_content_canvas_size", canvas_size) + return texture diff --git a/ui/components/locked_content_presentation.gd.uid b/ui/components/locked_content_presentation.gd.uid new file mode 100644 index 0000000..95b2196 --- /dev/null +++ b/ui/components/locked_content_presentation.gd.uid @@ -0,0 +1 @@ +uid://c0188g32tfjk3 diff --git a/ui/controller_focus_presentation.gd b/ui/controller_focus_presentation.gd index 63982cd..aed03ef 100644 --- a/ui/controller_focus_presentation.gd +++ b/ui/controller_focus_presentation.gd @@ -43,6 +43,13 @@ const HOVER_COLOR_REPLACEMENTS: Dictionary[StringName, StringName] = { } var _controller_active: bool = false +var _virtual_pointer_active: bool = false +var _navigation_focus_active: bool = false +var _neutral_focus_seed: Control +var _releasing_neutral_focus: bool = false +var _focus_clear_generation: int = 0 +var _directional_input_generation: int = 0 +var _directional_input_in_flight: bool = false var _focused_control: Control var _focused_popup: PopupMenu var _popup_scroll_offset: float = 0.0 @@ -64,6 +71,9 @@ func _ready() -> void: get_viewport().gui_focus_changed.connect(_on_focus_changed) set_process_input(true) set_process(true) + var existing_focus: Control = _active_focus_owner() + if existing_focus != null: + _capture_neutral_focus_seed(existing_focus) func _exit_tree() -> void: @@ -72,6 +82,17 @@ func _exit_tree() -> void: func _input(event: InputEvent) -> void: + if _virtual_pointer_active and ( + event is InputEventJoypadButton or event is InputEventJoypadMotion + ): + return + var directional_navigation: bool = _is_directional_navigation(event) + if directional_navigation: + _directional_input_generation += 1 + _directional_input_in_flight = true + _clear_directional_input_in_flight.call_deferred( + _directional_input_generation + ) if event is InputEventJoypadButton: if (event as InputEventJoypadButton).pressed: _set_controller_active(true) @@ -82,15 +103,32 @@ func _input(event: InputEvent) -> void: _set_controller_active(true) elif event is InputEventMouseMotion: _set_controller_active(false) + _neutralize_current_navigation_focus() elif event is InputEventMouseButton: if (event as InputEventMouseButton).pressed: _set_controller_active(false) + _neutralize_current_navigation_focus() elif event is InputEventKey: if (event as InputEventKey).pressed: _set_controller_active(false) + if directional_navigation and _restore_neutral_focus_seed(): + get_viewport().set_input_as_handled() + + +func set_virtual_pointer_active(active: bool) -> void: + if _virtual_pointer_active == active: + return + _virtual_pointer_active = active + if active: + _set_controller_active(false) + _clear_focus_presentation() + _restore_native_hover_highlight() func _process(_delta: float) -> void: + if _virtual_pointer_active: + _set_controller_active(false) + return _update_hover_suppression() if _controller_active: var focused_popup: PopupMenu = _active_focused_popup(get_viewport()) @@ -164,6 +202,24 @@ func _set_controller_active(active: bool) -> void: func _on_focus_changed(control: Control) -> void: + if _releasing_neutral_focus and control == null: + return + if control == null: + _focus_clear_generation += 1 + _clear_focus_presentation() + _reset_navigation_session_after_focus_clear.call_deferred( + _focus_clear_generation + ) + return + # A normal transfer between controls briefly reports a null owner. Cancel + # that deferred reset so active directional navigation remains continuous. + _focus_clear_generation += 1 + if _directional_input_in_flight and _is_navigation_focus(control): + _navigation_focus_active = true + _neutral_focus_seed = null + elif not _navigation_focus_active and _is_navigation_focus(control): + _capture_neutral_focus_seed(control) + return if _controller_active: var focused_popup: PopupMenu = _active_focused_popup(get_viewport()) if focused_popup != null: @@ -174,6 +230,68 @@ func _on_focus_changed(control: Control) -> void: _queue_focus_visibility_update(control) +func _reset_navigation_session_after_focus_clear(generation: int) -> void: + if generation != _focus_clear_generation or _active_focus_owner() != null: + return + _navigation_focus_active = false + _neutral_focus_seed = null + + +func _clear_directional_input_in_flight(generation: int) -> void: + if generation == _directional_input_generation: + _directional_input_in_flight = false + + +func _is_directional_navigation(event: InputEvent) -> bool: + if event is InputEventKey and (event as InputEventKey).echo: + return false + return ( + event.is_action_pressed(&"ui_left") + or event.is_action_pressed(&"ui_right") + or event.is_action_pressed(&"ui_up") + or event.is_action_pressed(&"ui_down") + or event.is_action_pressed(&"ui_focus_next") + or event.is_action_pressed(&"ui_focus_prev") + ) + + +func _is_navigation_focus(control: Control) -> bool: + # Text entry is an explicit interaction rather than menu navigation. It must + # retain direct focus for typing, including Chat and dialog fields. + return not (control is LineEdit or control is TextEdit) + + +func _capture_neutral_focus_seed(control: Control) -> void: + if not _focus_is_presentable(control) or not _is_navigation_focus(control): + return + _navigation_focus_active = false + _neutral_focus_seed = control + _clear_focus_presentation() + _releasing_neutral_focus = true + control.release_focus() + _releasing_neutral_focus = false + + +func _neutralize_current_navigation_focus() -> void: + var focus_owner: Control = _active_focus_owner() + if focus_owner != null and _is_navigation_focus(focus_owner): + _capture_neutral_focus_seed(focus_owner) + else: + _navigation_focus_active = false + _clear_focus_presentation() + + +func _restore_neutral_focus_seed() -> bool: + if not _focus_is_presentable(_neutral_focus_seed): + _neutral_focus_seed = null + return false + var target: Control = _neutral_focus_seed + _neutral_focus_seed = null + _navigation_focus_active = true + target.grab_focus() + return true + + func _queue_focus_visibility_update(control: Control) -> void: if control == null: return @@ -314,15 +432,23 @@ func _bubble_focus_target_position(control: Control) -> Vector2: func _presentation_canvas_scale() -> Vector2: - var host := get_parent() as Control - if host == null: - return Vector2.ONE - var host_scale: Vector2 = ( - host.get_global_transform_with_canvas().get_scale().abs() - ) + var target: Control = _focused_control + var target_scale := Vector2.ONE + if target != null and is_instance_valid(target): + target_scale = ( + target.get_global_transform_with_canvas().get_scale().abs() + ) + else: + # PopupMenu is a Window rather than a Control. Its cursor retains the + # canonical host scale used before cross-viewport focus support. + var host := get_parent() as Control + if host != null: + target_scale = ( + host.get_global_transform_with_canvas().get_scale().abs() + ) return Vector2( - maxf(host_scale.x, 0.001), - maxf(host_scale.y, 0.001), + maxf(target_scale.x, 0.001), + maxf(target_scale.y, 0.001), ) @@ -535,8 +661,6 @@ func _update_hover_suppression() -> void: _restore_native_hover_highlight() if hovered != null: _suppress_native_hover_highlight(hovered) - - func _hovered_control_in_viewport(viewport: Viewport) -> Control: var embedded_windows: Array[Window] = viewport.get_embedded_subwindows() for index: int in range(embedded_windows.size() - 1, -1, -1): diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index ddc5d42..d76ec4a 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -27,6 +27,9 @@ const ShopInteractionType = preload( ) const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") +const LockedContentPresentationType = preload( + "res://ui/components/locked_content_presentation.gd" +) const UIMotionType = preload("res://ui/ui_motion.gd") const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" @@ -66,9 +69,6 @@ const ART_UPGRADE_ICONS: Dictionary[StringName, Texture2D] = { const CURRENCY_ICON: Texture2D = preload( "res://items/icons/shop/32_currency.png" ) -const LOCKED_ITEM_ICON: Texture2D = preload( - "res://ui/icons/pictograms/lock_light.png" -) signal menu_visibility_changed(is_open: bool) signal menu_exit_started @@ -120,8 +120,11 @@ const SUPPLY_PRICE_ICON_GAP: float = 3.0 const BAIT_SUPPLY_BADGE_Y: float = ( UtilityPageStyleType.SUPPLY_BADGE_EDGE_MARGIN ) -const LOCKED_ITEM_ICON_SIZE := Vector2(48.0, 48.0) -const LOCKED_ITEM_ICON_ALPHA: float = 0.75 +const LOCK_BADGE_SIZE := Vector2(30.0, 30.0) +const LOCK_BADGE_MARGIN := Vector2(4.0, 4.0) +const LOCK_BADGE_ICON_SIZE := Vector2(18.0, 18.0) +const LOCK_BADGE_ALPHA: float = 0.9 +const LOCK_BADGE_ICON_ALPHA: float = 0.88 const ROD_CARD_TILE_SIZE := Vector2(144.0, 144.0) const ROD_CARD_HOST_SIZE := Vector2(152.0, 198.0) const ROD_CAROUSEL_HEIGHT: float = 206.0 @@ -1434,27 +1437,41 @@ func _configure_marker_icon_tile( func _add_unlock_state_icon( button: Button, unlocked: bool, - tile_size: Vector2, - price_bubble_y: float = SUPPLY_PRICE_Y, + _tile_size: Vector2, + _price_bubble_y: float = SUPPLY_PRICE_Y, ) -> void: if unlocked: return + var badge := Panel.new() + badge.name = "UnlockStateBadge" + badge.position = LOCK_BADGE_MARGIN + badge.size = LOCK_BADGE_SIZE + badge.mouse_filter = Control.MOUSE_FILTER_IGNORE + badge.z_index = 3 + var badge_style := UtilityPageStyleType.rounded_style( + Color(UtilityPageStyleType.OCEAN_FIELD, LOCK_BADGE_ALPHA), + int(LOCK_BADGE_SIZE.x * 0.5), + ) + badge.add_theme_stylebox_override("panel", badge_style) + button.add_child(badge) var icon := TextureRect.new() icon.name = "UnlockStateIcon" - icon.texture = LOCKED_ITEM_ICON + icon.texture = LockedContentPresentationType.ICON icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST icon.mouse_filter = Control.MOUSE_FILTER_IGNORE - icon.z_index = 3 icon.set_meta(&"unlocked", false) - icon.size = LOCKED_ITEM_ICON_SIZE + icon.size = LOCK_BADGE_ICON_SIZE icon.position = Vector2( - (tile_size.x - LOCKED_ITEM_ICON_SIZE.x) * 0.5, - (price_bubble_y - LOCKED_ITEM_ICON_SIZE.y) * 0.5, + (LOCK_BADGE_SIZE.x - LOCK_BADGE_ICON_SIZE.x) * 0.5, + (LOCK_BADGE_SIZE.y - LOCK_BADGE_ICON_SIZE.y) * 0.5, ) - icon.modulate.a = LOCKED_ITEM_ICON_ALPHA - button.add_child(icon) + icon.modulate = Color( + UtilityPageStyleType.OCEAN_TEXT_PRIMARY, + LOCK_BADGE_ICON_ALPHA, + ) + badge.add_child(icon) func _add_supply_icon_tile( diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 65cf93b..9d9ab32 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -78,6 +78,7 @@ signal interactive_pointer_ui_changed(is_open: bool) signal passive_pointer_ui_changed(is_enabled: bool) signal player_menu_backdrop_visibility_changed(is_visible: bool) signal shop_backdrop_visibility_changed(is_visible: bool) +signal virtual_pointer_mode_changed(is_active: bool) const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse" const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55 @@ -881,6 +882,7 @@ func _begin_virtual_mouse(device_id: int) -> void: if _virtual_mouse_active: return _virtual_mouse_active = true + virtual_pointer_mode_changed.emit(true) _virtual_mouse_device_id = maxi(device_id, 0) _virtual_mouse_stick = ( _mapped_virtual_mouse_stick() @@ -921,6 +923,7 @@ func _end_virtual_mouse() -> void: if _virtual_mouse_right_pressed: _set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false) _virtual_mouse_active = false + virtual_pointer_mode_changed.emit(false) _virtual_mouse_trigger_strength = 0.0 _virtual_mouse_stick = Vector2.ZERO _controller_virtual_cursor.visible = false @@ -994,6 +997,12 @@ func _poll_virtual_mouse_controller_state() -> void: _virtual_mouse_trigger_strength = _virtual_mouse_strength_for_device( _virtual_mouse_device_id ) + _sync_virtual_mouse_primary_button( + Input.is_joy_button_pressed( + _virtual_mouse_device_id, + JOY_BUTTON_RIGHT_SHOULDER, + ) + ) _set_virtual_mouse_button( MOUSE_BUTTON_RIGHT, _secondary_click_strength_for_device( @@ -1034,6 +1043,11 @@ func _poll_mapped_virtual_mouse_controller_state() -> void: _end_virtual_mouse() return _virtual_mouse_stick = _mapped_virtual_mouse_stick() + _sync_virtual_mouse_primary_button( + _controller_mapping_manager.get_role_strength( + ControllerMappingManagerType.ROLE_RB + ) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD + ) _set_virtual_mouse_button( MOUSE_BUTTON_RIGHT, _controller_mapping_manager.get_role_strength( @@ -1151,6 +1165,13 @@ func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void: _update_virtual_cursor_position(_virtual_mouse_window_position) +func _sync_virtual_mouse_primary_button(pressed: bool) -> void: + # Embedded PopupMenu windows can consume the controller release event before + # GameUI._input receives it. Polling the physical role while virtual-pointer + # mode is held guarantees the synthetic left button cannot remain latched. + _set_virtual_mouse_button(MOUSE_BUTTON_LEFT, pressed) + + func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void: var motion_event := InputEventMouseMotion.new() motion_event.position = _virtual_mouse_window_position diff --git a/ui/icons/main_menu/delete_save.png b/ui/icons/main_menu/delete_save.png new file mode 100644 index 0000000000000000000000000000000000000000..3efa26f3b1a86c93d8677206bd12b76afee912a0 GIT binary patch literal 3158 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 P-vZz`xZ0Pj+7kb75e|GX literal 0 HcmV?d00001 diff --git a/ui/icons/main_menu/delete_save.png.import b/ui/icons/main_menu/delete_save.png.import new file mode 100644 index 0000000..4555a27 --- /dev/null +++ b/ui/icons/main_menu/delete_save.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cujpq4ow85uvo" +path="res://.godot/imported/delete_save.png-109b364ded207c94c5c900cd8f4a9bcb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/main_menu/delete_save.png" +dest_files=["res://.godot/imported/delete_save.png-109b364ded207c94c5c900cd8f4a9bcb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/moderation_options/moderation_options_ban_light.png b/ui/icons/moderation_options/moderation_options_ban_light.png new file mode 100644 index 0000000000000000000000000000000000000000..11c76b207ab510951bec43ccc328e0d66a60942a GIT binary patch literal 2432 zcmai02~<-_7OeyY*@O^eVpf`dX9gd~W93m`6|bOmhep6N4l&aZmE-n;j{_v+NI^K%0I zJ+(CqH2?tGUUXUzv>TLTo(h~#UrT!qjaf83$Pa*hmH?z?0Pr5VQilN8O$1-v>$+-IHTEj9&W`K zd=?7ke8%j97Q}I45MC69a@dg^uF}LSy*OLtG;3ALai0~SuN?oI0v?pBe0f*$8UR)g zSuTOT!R~O$m&%0_nLr}vi{%1|j1M2FOduWVT5U#C zE)dE1{e!=y`0Rut0NI$=hb==Tb+s^2ZADI$53bZExeB!yhX z=cF@bh=#8A_6j9~Q1kA<0Bp0dQ7H)bK`2({5=_sq=4QztOb0#9(m{x9?t_}jB`l!s z$AF*V_HD<4{23tq%=?%!X6|2h%sZg_p*dyy@(VWaC?Sh>QSpIWG#36#MU}uv;%nb! zU41I2xsAQ~H2$%o)aU2uwtCHho4AQ?`bg7l#ar1UhmEfvuiT1`J~->E=5*XhZ_eQ~ z|5tuuGBKjxdD&OB5j2V!Oo`j(<4H5zy?u}j7wTMkC_E0UER+M6Shao+bgE%qe(q|+ z^Ypb=YE(vCLIA+K>*`DL+5Nr*^6rkC|Y> zPc}9!vOUFJJ2Nl;a| z+jxNo%{ORYmZq>bd34*Nlh^Y`Mq)EWJ`95!#EROEyaeY?v8=C$awFOB@{;gGwLMjY zhuqjUeysRmcR+i-T^-NS{otDUrz-@}Zz;V++t;s`dpa6v8JSr3ebk+MYu>+UMd!F7 zde+j@!`1t3cibo_^m%({f%7kEj~$cyiZj^9+AXj36+ac%YdSZoUeEh@P<6~o-G5u- zW3FG9cHg;7lp7zRwl4pl!ijFeVp}r1|K8qfyeaP*ht=#eCHH*Ep?kJYmKs@wxhP^v z)1+}8(JveDYuH`LQ5jZn`F4qCo~1scZe8+$*Wij@#>?xMUz7{7lW(&#)5Cu{_knfO zHhH7YWL2kui|~QF*kr<#Y2HPAe(r21JXy;zcP17Sg(1{v9s&&@6UmNtL?=6vQ!vSi zN+wZ>jy6Ohl}IeoEMNWy0=&gg*53ahknE{M@(dwcC-*8OteDM!p>aq&76p&!Y!c^S z5}E2iqLPR+9HfEt732_<93&BCQQbJP93F!k6^#Ir?kT(%3@)1u(m2t(;}ACH9~FoE z$)(B`g)W77{aL&_R19FT;t>px$Yl5lC>t`H4F~SvV4OGvFI$m5X+5vvB?ui)H zm4_v8c)Nm8EcVYjaX9sL2&Bzd(lC^z+?F_4UHnXHu>-mjMyK~>I{JV1a2h8n@sRi{Nde<3)1~W(w+$BU@BRT0edx;AZuYj*>IA6B4raPjT+p7c|u0s%*RK z7O~swQ)4xnl%Vp~)9sFFE4}n!W>r;tyDOh3tExL@6{x7_CZ7rO;65A4##_|u5%SD= z>BmB9JiLLAXii9khp^QoLn9&o;h}qG%#gNKwbjS0S{yZ={ZO+ZB2?1Ra=yZ^%}$C> ztk~=wc|6zZyZm3SC4HmA5HI%i?JjpHf0oek-Ls?vqFXU_?~m$tywFt*{#gEEWLe>| zWcfmV(ZFWnwd&?V-3pV`9Rb%HFARTEv(i81-aiArnWiaXGWpPOBcE1^6-e5HB$ zlFP>S@V{eotlyyzp%=aV_U+yECOmAlVX+}Q@lz1iW2x5Bp3!z-kB*1H9AvAH3v1Ll zH!wy-7ASE&<)qobzq6ag=Hf7+#uTho69KgLVf literal 0 HcmV?d00001 diff --git a/ui/icons/moderation_options/moderation_options_ban_light.png.import b/ui/icons/moderation_options/moderation_options_ban_light.png.import new file mode 100644 index 0000000..c964243 --- /dev/null +++ b/ui/icons/moderation_options/moderation_options_ban_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjcoxu6kr8to0" +path="res://.godot/imported/moderation_options_ban_light.png-664b6bd4fe95df65f41d3a12c9ee6a40.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/moderation_options/moderation_options_ban_light.png" +dest_files=["res://.godot/imported/moderation_options_ban_light.png-664b6bd4fe95df65f41d3a12c9ee6a40.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/moderation_options/moderation_options_block_light.png b/ui/icons/moderation_options/moderation_options_block_light.png new file mode 100644 index 0000000000000000000000000000000000000000..3737b52282915676edccf313784bc5c82476a20e GIT binary patch literal 3423 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 ab6QS`=c-<`{tSK*5ajCUj(+6ef9~ISzQhaw literal 0 HcmV?d00001 diff --git a/ui/icons/moderation_options/moderation_options_block_light.png.import b/ui/icons/moderation_options/moderation_options_block_light.png.import new file mode 100644 index 0000000..87f4a6b --- /dev/null +++ b/ui/icons/moderation_options/moderation_options_block_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://htxi2g3qy1bh" +path="res://.godot/imported/moderation_options_block_light.png-48d6ddc186905a599655e8b153fa5b6c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/moderation_options/moderation_options_block_light.png" +dest_files=["res://.godot/imported/moderation_options_block_light.png-48d6ddc186905a599655e8b153fa5b6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/moderation_options/moderation_options_kick_light.png b/ui/icons/moderation_options/moderation_options_kick_light.png new file mode 100644 index 0000000000000000000000000000000000000000..b425cf945949f359b7a88a1a707215588598b361 GIT binary patch literal 2498 zcmah}3pmtS8$UB6gQ!r8QIXIEJ!4#A=%$&?CD&28q}*m0F*7^cpdwJuR< z*;ol_)7CVX+Gtm{Xe-Q&%M2-P2wlyd|LD`x^X>EP^Zd_w-}An|-+MXF`M=qIKAUv3 z2wDID9S?U(0F>*|rGbNc$jlsO1M5)pu*Z~({!02pwJ zoBUm&qnsY(!SMD5E1|9d)G_J+4TJ$$xapuf(1&X)h5%-%>;S}JjAwK%X8C9QK_*c9 zgh_=GB(S6D+z5opX6 zsqiT7?G?!seCdEtI&fF=Oe9s@>FX0q6(Z>X)S)8mkt>x-cofSN@Ds?O3))fd^aqJB zLPtl3>Np^jJ#Xyn6i84npRW|E6p?gLAd^E23<%>-6GbvP>Xo2&fo$-T5oVOYViYi} zpnFgS9#9o9omU}*1<2&h{r%H;Scxiy9M%9WkjR$~L52SWjUZBGzo=Bg%3)Y&7j*Zb z5n%B!Z%+>d2vvnai&%yNP!?=>>#gQs-wbW1mJ4Rg2zHK(Ue#J?OQqdCk%3;2)FJK6*b@LBD&e%sV$exOHIC~-M4qy=2s#kJe6!pfTGcRV{I>jbOV{PL_E4x0ySLyIrTN;sBGAxC6iT z=q2%Zhpv$|iPh7x=PFm}S#7_XRa(%pnG~EHK3QzMY`e=qba9p};p?bZ*Rigwc6z=X z$*sLvuqnq&6iH<<%lXw##h8peUl3N49pLwgu%mjP2_!6^$$&5u=!r-Ke4;Z+oC9r1WP4k(Epa*t{krW} zkfV!o=qXGFc_TZX&82Z7qUgYOZV}c4I?ZRC6n0cn0-c5UL?qDvVW_gYgO@zgN4w!94;MEO=&!)ciL!@3EHwooZ{Q-k+1y=$OeFrF zILC=FpU&}&J6)g?}+me@1npjUM-D(}DCOJOt*9XqPVRrvqY_);M;4^A6D z56XtXAkpyY;BD>e=k32_x+=4-nN-H0L~Qr>L$-4^;x|z?Z1)#@oKj}UJSyy-U1&Nv zvaqy#hh^B+A+My0i}%6%2@~Uu!mPzhoZ=%n?C~@Vr!Dbhx9;Ob7&(t+1>VK?_z_*z zIB|YeqC$>X!-b$$Ce63^(N~YG=(sEhBrEPP#Uolk+bBeLb$qGVT`kl9MATG{eGkv# z2!3urC1SR8^AGoFAOB$uVJi*C^4f@% z=JS0&cKP2=Byj%p`ME;OIFy&6b;8Qc1nDKLpZ`sUsqZ-3e8DU5ja#KYEmSwP8RJUP zE_-w=`W9C`r~w>Xksc@ZtSvfXR%V1TS6o!!w$gq*sc|brb3x@uT1W(*&~{>g{gdlX z+?gk>$+Q;?vQ0QRQzRLW+-4Wh*nM7N^ozU6>QUZzZcY4cvMoJQ^G-(VSD8BM;L_D3 zYA)Z=``O|oaN|JVQO(%r&hzwU-wG8eeL6~TlFt7y_}kW{6T3`8f2@7jZ1z?&$n-rX zE%{#Zu6Mn=2l_)MzAL{QyJzxBWA|(J#NtUt_tq)T(a>n^mN^D*a`UKTcz;%h)|*f_ u3$RxfgX05h`i*nN{N|FlXYVKe8lIaEES?jeOyt6M2t24hlp442eSZU0{$F?i literal 0 HcmV?d00001 diff --git a/ui/icons/moderation_options/moderation_options_kick_light.png.import b/ui/icons/moderation_options/moderation_options_kick_light.png.import new file mode 100644 index 0000000..5ba0e72 --- /dev/null +++ b/ui/icons/moderation_options/moderation_options_kick_light.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bkey68v7byxky" +path="res://.godot/imported/moderation_options_kick_light.png-3dc1d5aa8e3312c17f3a81edd965a73b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/icons/moderation_options/moderation_options_kick_light.png" +dest_files=["res://.godot/imported/moderation_options_kick_light.png-3dc1d5aa8e3312c17f3a81edd965a73b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/icons/moderation_options/moderation_options_mute_light.png b/ui/icons/moderation_options/moderation_options_mute_light.png new file mode 100644 index 0000000000000000000000000000000000000000..51770160ae64c2d94dcd714cc6e1f0dbd4c8c044 GIT binary patch literal 1985 zcmah}2~bm46nzOoSj2+Rg5VNRBeG}`79oP7CX&cvB-nyfAcUXtHCw|Mftm)KD2*u6 z793w6#L56%w(KfEHR?H-u{XepH-JJMB!~y*KxqbKigW|992g zxF|<^4|@QBBZtk5hdvTtwpP$qw-$^-M;5T-V*$vc0FbW&U;?J{M*!GH1>jy90E}_~ z++DwY#`Uh@l4 zFzqEL4|*V$W*~AthDs%=QW@^D@GQX}H&fn9LwH#M<}}1t6^KA%^*Y9NX90E!x&m%Y z0tXf!w@qF6Zx~uUwj^P!H5he<;nRJ6kfXJrI5dFMa1+(wmkLo4LdJ4s2suYA10ZzY zzKzi*2g%d>v-X4$8(3FX6xSB$zpOCMR;~*zRuAqNy{uh@MF(tBDK99y<|$2Kq+=oc z6Y7iJ4$X@{WtvTwx7-hQb*A`w?wWPwu{ok$&7Mx$yI8p+x~65wXG5n@>+f8;=caO} zhnxC&dhlnTB%W-rF{+ix4`)+&yrRT9PV;kiuvJ~Q{inNCKc%7sXKzR1$0rNew3}D& zp?wQ3+Uw zGm`LYfMmTFJFqlDipIu7Dt$E1t(|)4lt^fQw{IV8etVAq>k0A_! z9Dxqdsq~NlYG?p0G=Ub(pwk$^R6i<}L8bobHi=*O*8+H(QDN@i1VM~Is0n-_I8^!= zpB6q-J7B07$-?;X6P?;5EP+O61ko6@mzyAGX>UP|Gp<2$P$6TDR3w%2WPAYvXm3;! zIS^bh1u~_AZDK@%#qq_+zYG_NKFJI6U8eXfSaH2jn1x_~MyJCoun@xJDVUon#iU{c zlcNHT1ViL9If7ZH6`+5VT zr7srrfBI69p>l_O%fT)D|2X)3Ewuzrs3;C|RYGFd*pK>nE!D|-z<8YO<7?kw>(d}J zn62lm>sz32xg?y`*lj&_Eh4q2RCQqFMq>}aN^>JC-?8NquMwImLTZWFcb%A~d!dyi zWya8v8o~rRysU%lRr}@#Prv5Ye7Br>)MoYUs8dU=tN8m#+^wcx&bn*n&Pyiw5F@*u zUdgZxEu_?_=fk%#S$KEQW#vp#__Cf9@Q7|tJa|Nq;#5w^@ya_z$vh`=9qsIIOFkxY zo>tX%cj;D}m|eieE Control: page.add_child(_body) _salutation = OptionButton.new() _salutation.position = Vector2(12, 336) - _salutation.size = Vector2(180, 46) + _salutation.size = Vector2(230, 46) + _salutation.alignment = HORIZONTAL_ALIGNMENT_CENTER for id: String in NetworkMailProtocol.SALUTATIONS: _salutation.add_item(SALUTATION_LABELS[id]) _salutation.set_item_metadata(_salutation.item_count - 1, id) page.add_child(_salutation) _signature = Label.new() - _signature.position = Vector2(204, 342) - _signature.size = Vector2(288, 36) + _signature.position = Vector2(262, 336) + _signature.size = Vector2(230, 46) + _signature.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _signature.vertical_alignment = VERTICAL_ALIGNMENT_CENTER page.add_child(_signature) _attachment_kind = OptionButton.new() _attachment_kind.position = Vector2(ATTACHMENT_COLUMN_X, 48) diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index e1aa4ad..2ae8dc5 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=17 format=3] [ext_resource type="Script" path="res://ui/pause_menu.gd" id="1_script"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="2_settings"] @@ -15,6 +15,7 @@ [ext_resource type="Texture2D" path="res://ui/icons/pictograms/settings_dark.png" id="13_settings_dark"] [ext_resource type="Texture2D" path="res://ui/icons/main_menu/continue.png" id="14_continue"] [ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_dark.png" id="15_x_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/main_menu/delete_save.png" id="16_delete_save"] [node name="PauseMenu" type="Control"] unique_name_in_owner = true @@ -162,7 +163,9 @@ motion_phase = 3.0 [node name="ResetProgressButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0) -text = "reset\nprogress" +texture_filter = 1 +icon = ExtResource("16_delete_save") +tooltip_text = "reset progress" accessibility_name = "reset progress" neutral_size = Vector2(126, 120) desktop_anchor = Vector2(245, 365) diff --git a/ui/pixelation_reset_overlay.gd b/ui/pixelation_reset_overlay.gd index f837bd8..0c13d41 100644 --- a/ui/pixelation_reset_overlay.gd +++ b/ui/pixelation_reset_overlay.gd @@ -6,6 +6,9 @@ const FADE_DURATION: float = 0.55 const UIReferencePresentationType = preload( "res://ui/ui_reference_presentation.gd" ) +const ControllerFocusPresentationType = preload( + "res://ui/controller_focus_presentation.gd" +) signal reset_requested signal return_to_settings_requested @@ -16,9 +19,16 @@ signal return_to_settings_requested var _fade_tween: Tween var _fade_generation: int = 0 var _settings_open: bool = false +var _controller_focus_presentation: ControllerFocusPresentationType func _ready() -> void: + # This control intentionally lives outside the pixelated UI SubViewport, so + # it needs a cursor presenter in its own viewport. Reusing the shared class + # keeps its paw geometry and input policy identical to every other menu. + _controller_focus_presentation = ControllerFocusPresentationType.new() + _controller_focus_presentation.name = "ControllerFocusPresentation" + add_child(_controller_focus_presentation) _reset_button.pressed.connect(reset_requested.emit) _reset_button.gui_input.connect(_on_reset_button_gui_input) get_viewport().size_changed.connect(_update_responsive_layout) diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 4fe7074..263c5e4 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -357,7 +357,6 @@ var _selected_inventory_kind: int = -1 var _selected_inventory_identity: StringName var _inventory_move_kind: int = -1 var _inventory_move_identity: StringName -var _tackle_move_identity: StringName var _context_tooltip: PanelContainer var _context_tooltip_label: Label var _context_tooltip_source: Control @@ -796,6 +795,12 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool: ): _cancel_active_item_move() return _try_enter_notepad_controller_ownership() + if _current_section == Section.TACKLE_BOX and accept_event: + # Bait and lures are unlock collections, not movable inventory slots. + # Controller Y owns their contextual equip/notepad action; A is + # intentionally inert and consumed so it cannot activate the Button. + _cancel_controller_accept_hold() + return true if ( event.is_action_pressed("ui_down") and _controller_focus_is_on_last_inventory_row() @@ -862,13 +867,6 @@ func _activate_inventory_selection() -> void: var slot := focus_owner as GeneralInventorySlotType if slot != null: _on_general_inventory_slot_activated(slot) - elif _current_section == Section.TACKLE_BOX: - if focus_owner != null and focus_owner.has_meta( - &"controller_tackle_item_id" - ): - _activate_tackle_move(StringName(str( - focus_owner.get_meta(&"controller_tackle_item_id") - ))) func _cancel_controller_accept_hold() -> void: @@ -2500,7 +2498,7 @@ func _populate_tackle_column( item.max_stack, &"QuantityBadge", ) - button.pressed.connect(_activate_tackle_move.bind(owned.item_id)) + button.pressed.connect(_select_tackle_item.bind(owned.item_id)) button.gui_input.connect( _on_tackle_button_gui_input.bind(owned.item_id, button) ) @@ -2521,7 +2519,6 @@ func _populate_tackle_column( ) item_list.add_child(button) _tackle_item_buttons[owned.item_id] = button - _refresh_tackle_move_presentation() func _tackle_context_text(item: ItemDataType, quantity: int) -> String: @@ -2537,51 +2534,6 @@ func _tackle_context_text(item: ItemDataType, quantity: int) -> String: return "\n".join(lines) -func _activate_tackle_move(item_id: StringName) -> void: - if _bag == null or item_id.is_empty(): - return - var target_slot: int = _bag.get_storage_slot(item_id) - if _tackle_move_identity.is_empty(): - if target_slot < 0: - return - _tackle_move_identity = item_id - _selected_tackle_item_id = item_id - _update_tackle_detail() - _refresh_tackle_move_presentation() - var button := _tackle_item_buttons.get(item_id) as Button - if button != null: - _show_inventory_context_tooltip( - button, - "moving %s\nchoose another %s • B cancels" % [ - button.accessibility_name - if not button.accessibility_name.is_empty() - else str( - button.get_meta(&"inventory_context_text", "item") - ).get_slice("\n", 0), - "bait" if _item_catalog.get_item_by_id(item_id).is_bait() - else "lure", - ], - ) - return - var source_id: StringName = _tackle_move_identity - _tackle_move_identity = StringName() - _refresh_tackle_move_presentation() - _hide_inventory_context_tooltip() - if source_id == item_id or target_slot < 0: - return - _bag.move_item_to_storage_slot(source_id, target_slot) - - -func _refresh_tackle_move_presentation() -> void: - for item_id: StringName in _tackle_item_buttons: - var button: Button = _tackle_item_buttons[item_id] - button.modulate = ( - Color(1.0, 1.0, 1.0, 0.42) - if item_id == _tackle_move_identity - else Color.WHITE - ) - - func _on_tackle_button_gui_input( event: InputEvent, item_id: StringName, @@ -3912,13 +3864,7 @@ func _cancel_inventory_move(hide_tooltip: bool = true) -> bool: func _cancel_active_item_move() -> bool: - var canceled: bool = _cancel_inventory_move() - if not _tackle_move_identity.is_empty(): - _tackle_move_identity = StringName() - _refresh_tackle_move_presentation() - _hide_inventory_context_tooltip() - canceled = true - return canceled + return _cancel_inventory_move() func _open_inventory_notepad( diff --git a/ui/players_page.gd b/ui/players_page.gd index 87f3536..6748e6d 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -5,6 +5,20 @@ const TOGGLE_STATE_COLOR := Color("c3dfe6") const DialogControllerNavigationType = preload( "res://ui/file_dialog_controller_navigation.gd" ) +const MODERATION_BAN_ICON: Texture2D = preload( + "res://ui/icons/moderation_options/moderation_options_ban_light.png" +) +const MODERATION_BLOCK_ICON: Texture2D = preload( + "res://ui/icons/moderation_options/moderation_options_block_light.png" +) +const MODERATION_KICK_ICON: Texture2D = preload( + "res://ui/icons/moderation_options/moderation_options_kick_light.png" +) +const MODERATION_MUTE_ICON: Texture2D = preload( + "res://ui/icons/moderation_options/moderation_options_mute_light.png" +) +const MODERATION_BUTTON_SIZE := Vector2(52.0, 40.0) +const MODERATION_ICON_SIZE: int = 40 enum ControllerZone { TABS, @@ -492,16 +506,15 @@ func _build_active_rows() -> void: ) row.add_child(ping) var mute := Button.new() - mute.text = "unmute" if entry.muted else "mute" + var mute_action: String = "unmute" if entry.muted else "mute" mute.disabled = entry.is_local_player mute.pressed.connect(_toggle_mute.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(mute) + _configure_moderation_button(mute, MODERATION_MUTE_ICON, mute_action) row.add_child(mute) var block := Button.new() - block.text = "block" block.disabled = entry.is_local_player block.pressed.connect(_confirm_block.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(block) + _configure_moderation_button(block, MODERATION_BLOCK_ICON, "block") row.add_child(block) if entry.can_manage_operator: var operator := Button.new() @@ -510,19 +523,50 @@ func _build_active_rows() -> void: UtilityPageStyle.apply_compact_ocean_button(operator) row.add_child(operator) var kick := Button.new() - kick.text = "kick" kick.disabled = not entry.can_kick kick.pressed.connect(_confirm_kick.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(kick) + _configure_moderation_button(kick, MODERATION_KICK_ICON, "kick") row.add_child(kick) var ban := Button.new() - ban.text = "ban" ban.disabled = not entry.can_ban ban.pressed.connect(_confirm_ban.bind(entry)) - UtilityPageStyle.apply_compact_ocean_button(ban) + _configure_moderation_button(ban, MODERATION_BAN_ICON, "ban") row.add_child(ban) +func _configure_moderation_button( + button: Button, + button_icon: Texture2D, + action_label: String, +) -> void: + button.text = "" + button.icon = button_icon + button.tooltip_text = action_label + button.accessibility_name = action_label + button.expand_icon = true + button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER + button.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + UtilityPageStyle.apply_compact_ocean_button(button) + button.custom_minimum_size = MODERATION_BUTTON_SIZE + button.add_theme_constant_override("icon_max_width", MODERATION_ICON_SIZE) + button.add_theme_color_override( + "icon_disabled_color", + Color(1.0, 1.0, 1.0, 0.78), + ) + for state: StringName in [ + &"normal", &"hover", &"pressed", &"focus", &"disabled", + ]: + var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat + if style == null: + continue + style.content_margin_left = 6.0 + style.content_margin_right = 6.0 + style.content_margin_top = 0.0 + style.content_margin_bottom = 0.0 + button.add_theme_stylebox_override(state, style) + + func _build_session_artwork_controls() -> void: var counts: Vector2i = _service.get_session_artwork_counts() var row := _make_row() diff --git a/ui/profile_page.gd b/ui/profile_page.gd index 3824a72..be15a0f 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -71,6 +71,7 @@ var _apply_button: Button var _revert_button: Button var _defaults_button: Button var _reset_view_button: Button +var _reset_view_controller_hint: Label var _discard_confirmation: PanelContainer var _confirmation_label: Label var _confirmation_confirm: Button @@ -226,6 +227,15 @@ func consume_escape() -> bool: func handle_controller_input(event: InputEvent) -> bool: if not _profile_active or not _profile_interactive: return false + var reset_view_pressed := _event_matches_controller_press( + event, + &"sneak", + ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK, + JOY_BUTTON_RIGHT_STICK, + ) + if reset_view_pressed: + _preview.reset_view() + return true if ( event.is_action_pressed(&"ui_up") or event.is_action_pressed(&"ui_down") @@ -794,6 +804,17 @@ func _build_ui() -> void: 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) + _reset_view_controller_hint = Label.new() + _reset_view_controller_hint.name = "ResetViewControllerHint" + _reset_view_controller_hint.text = "Reset View: RS" + _reset_view_controller_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE + _reset_view_controller_hint.focus_mode = Control.FOCUS_NONE + _reset_view_controller_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _reset_view_controller_hint.add_theme_font_size_override("font_size", 14) + _reset_view_controller_hint.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY + ) + preview_stack.add_child(_reset_view_controller_hint) _discard_confirmation = PanelContainer.new() _discard_confirmation.visible = false diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index f8714ab..b49db11 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -116,6 +116,7 @@ var _on_screen_keyboard_enabled: bool = false var _chat_dock_right: bool = false var _chat_mobile_mode: bool = false var _paint_dock_right: bool = true +var _presentation_layout_edited: bool = false var _fullscreen_enabled: bool = false var _master_volume: float = 1.0 var _music_volume: float = 1.0 @@ -941,6 +942,8 @@ func _apply_settings() -> void: edited.chat_dock_right = _chat_dock_right edited.chat_mobile_mode = _chat_mobile_mode edited.paint_dock_right = _paint_dock_right + if _presentation_layout_edited: + edited.presentation_layout_customized = true edited.fullscreen_enabled = _fullscreen_enabled edited.master_volume = _master_volume edited.music_volume = _music_volume @@ -1000,6 +1003,7 @@ func _load_controls() -> void: _auto_click_interval_slider.set_value_no_signal( _auto_click_interval_value ) + _presentation_layout_edited = false _settings_manager.restore_audio_levels() _refresh_value_labels() @@ -1081,14 +1085,17 @@ func _set_ui_pixelation(pixel_size: int) -> void: func _on_chat_dock_selected(index: int) -> void: _chat_dock_right = _chat_dock.get_item_id(index) == 1 + _presentation_layout_edited = true func _on_chat_mode_selected(index: int) -> void: _chat_mobile_mode = _chat_mode.get_item_id(index) == 1 + _presentation_layout_edited = true func _on_paint_dock_selected(index: int) -> void: _paint_dock_right = _paint_dock.get_item_id(index) == 1 + _presentation_layout_edited = true func _set_fullscreen(enabled: bool) -> void: diff --git a/ui/surface_drawing_toolbar.gd b/ui/surface_drawing_toolbar.gd index 416274d..edd920e 100644 --- a/ui/surface_drawing_toolbar.gd +++ b/ui/surface_drawing_toolbar.gd @@ -5,6 +5,9 @@ const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") const ControllerVirtualCursorType = preload( "res://ui/controller_virtual_cursor.gd" ) +const LockedContentPresentationType = preload( + "res://ui/components/locked_content_presentation.gd" +) const MARKER_MODE_ICON: Texture2D = preload( "res://items/icons/art/art_kit_marker.png" ) @@ -29,6 +32,10 @@ const GRID_SIZE_ICONS: Dictionary[int, Texture2D] = { const TOOLBAR_WIDTH: float = 510.0 const TOOLBAR_HEIGHT: float = 373.0 const COLOR_RAIL_WIDTH: float = 46.0 +const POPUP_LOCK_ICON_SIZE: int = 24 +const POPUP_LOCK_CANVAS_SIZE: int = 40 +const POPUP_LOCK_ICON_ALPHA: float = 0.72 +const COLOR_LOCK_ICON_ALPHA: float = 0.34 @onready var _mode_button: Button = %ModeButton @onready var _brush_option: OptionButton = %BrushOption @@ -50,6 +57,8 @@ var _marker_mode_material: ShaderMaterial var _brush_popup_cursor: ControllerVirtualCursorType var _grid_popup_cursor: ControllerVirtualCursorType var _applied_marker_icon_color: Color = Color(-1.0, -1.0, -1.0, -1.0) +var _brush_option_icons: Dictionary[int, Texture2D] = {} +var _popup_lock_icon: Texture2D func _ready() -> void: @@ -98,6 +107,10 @@ func _ready() -> void: _configure_pointer_only_controls() _brush_popup_cursor = _add_popup_cursor(_brush_option.get_popup()) _grid_popup_cursor = _add_popup_cursor(_grid_option.get_popup()) + _popup_lock_icon = LockedContentPresentationType.make_icon_texture( + POPUP_LOCK_ICON_SIZE, + POPUP_LOCK_CANVAS_SIZE, + ) _build_options() _apply_marker_color_to_brush_icons( SurfaceDrawingPalette.get_color(SurfaceDrawingPalette.DEFAULT_COLOR_ID) @@ -307,19 +320,49 @@ func _build_options() -> void: button.pressed.connect(_select_color.bind(color_id)) _color_list.add_child(button) _color_buttons[color_id] = button + _add_color_lock_icon(button) _apply_color_button_style(button, color_id, false) +func _add_color_lock_icon(button: Button) -> void: + var icon_size := Vector2.ONE * ( + LockedContentPresentationType.COMPACT_ICON_SIZE + ) + var icon := TextureRect.new() + icon.name = "UnlockStateIcon" + icon.texture = LockedContentPresentationType.ICON + icon.set_anchors_preset(Control.PRESET_CENTER) + icon.offset_left = -icon_size.x * 0.5 + icon.offset_top = -icon_size.y * 0.5 + icon.offset_right = icon_size.x * 0.5 + icon.offset_bottom = icon_size.y * 0.5 + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + icon.mouse_filter = Control.MOUSE_FILTER_IGNORE + icon.modulate = Color( + UtilityPageStyleType.OCEAN_TEXT_SECONDARY, + COLOR_LOCK_ICON_ALPHA, + ) + icon.z_index = 1 + icon.visible = false + button.add_child(icon) + + func _apply_marker_color_to_brush_icons(marker_color: Color) -> void: if _applied_marker_icon_color.is_equal_approx(marker_color): return _applied_marker_icon_color = marker_color + _brush_option_icons.clear() for index: int in range(_brush_option.item_count): var brush_size: int = _brush_option.get_item_id(index) - _brush_option.set_item_icon( - index, - _channel_masked_icon(BRUSH_SIZE_ICONS[brush_size], marker_color), + _brush_option_icons[brush_size] = _channel_masked_icon( + BRUSH_SIZE_ICONS[brush_size], marker_color ) + if _unlocks == null or _unlocks.is_brush_size_unlocked(brush_size): + _brush_option.set_item_icon( + index, _brush_option_icons[brush_size] + ) func _channel_masked_icon(source: Texture2D, marker_color: Color) -> Texture2D: @@ -371,18 +414,26 @@ func _refresh_unlocks() -> void: var brush_popup: PopupMenu = _brush_option.get_popup() for index: int in range(_brush_option.item_count): var brush_size: int = _brush_option.get_item_id(index) - brush_popup.set_item_disabled( - index, not _unlocks.is_brush_size_unlocked(brush_size) + _apply_popup_unlock_state( + brush_popup, + index, + _unlocks.is_brush_size_unlocked(brush_size), + _brush_option_icons.get(brush_size, BRUSH_SIZE_ICONS[brush_size]), ) var grid_popup: PopupMenu = _grid_option.get_popup() for index: int in range(_grid_option.item_count): var grid_size: int = _grid_option.get_item_id(index) - grid_popup.set_item_disabled( - index, not _unlocks.is_grid_size_unlocked(grid_size) + _apply_popup_unlock_state( + grid_popup, + index, + _unlocks.is_grid_size_unlocked(grid_size), + GRID_SIZE_ICONS[grid_size], ) for color_id: StringName in _color_buttons: var button: Button = _color_buttons[color_id] button.disabled = not _unlocks.is_color_unlocked(color_id) + var lock_icon := button.get_node("UnlockStateIcon") as TextureRect + lock_icon.visible = button.disabled _apply_color_button_style( button, color_id, @@ -394,6 +445,25 @@ func _refresh_unlocks() -> void: ) +func _apply_popup_unlock_state( + popup: PopupMenu, + index: int, + unlocked: bool, + unlocked_icon: Texture2D, +) -> void: + popup.set_item_disabled(index, not unlocked) + popup.set_item_icon(index, unlocked_icon if unlocked else _popup_lock_icon) + popup.set_item_icon_modulate( + index, + Color.WHITE + if unlocked + else Color( + UtilityPageStyleType.OCEAN_TEXT_SECONDARY, + POPUP_LOCK_ICON_ALPHA, + ), + ) + + func _apply_color_button_style( button: Button, color_id: StringName, @@ -412,7 +482,9 @@ func _apply_color_button_style( style.set_corner_radius_all(17) button.add_theme_stylebox_override(style_name, style) var disabled_style := StyleBoxFlat.new() - disabled_style.bg_color = UtilityPageStyleType.OCEAN_DISABLED + disabled_style.bg_color = ( + LockedContentPresentationType.disabled_background_color() + ) disabled_style.set_corner_radius_all(17) button.add_theme_stylebox_override("disabled", disabled_style) diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 049c90e..23290db 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -10,6 +10,9 @@ const SettingsPanelType = preload("res://ui/settings_panel.gd") const FullscreenMenuPresentationType = preload( "res://ui/fullscreen_menu_presentation.gd" ) +const ControllerFocusNavigationType = preload( + "res://ui/controller_focus_navigation.gd" +) const BubbleButtonType = preload( "res://ui/components/bubble_menu/bubble_button.gd" ) @@ -85,7 +88,6 @@ const BUBBLE_COMPACT_HEIGHT_THRESHOLD: float = 560.0 const START_PROMPT_MIN_SCALE: float = 0.985 const START_PROMPT_MAX_SCALE: float = 1.015 const START_PROMPT_CYCLE_SECONDS: float = 3.0 -const DISABLED_BUBBLE_LABEL_ALPHA: float = 0.55 const INTRO_PROMPT_FADE_DURATION: float = 0.55 const INTRO_BUBBLE_TRAVEL_DURATION: float = 2.40 const INTRO_BRANDING_TRAVEL_DURATION: float = 2.0 @@ -110,7 +112,6 @@ enum ConfirmationAction { @onready var _delete_button: BubbleButtonType = %DeleteSaveButton @onready var _quit_button: BubbleButtonType = %QuitButton @onready var _join_game_button: BubbleButtonType = %JoinGameButton -@onready var _delete_save_label: Label = %DeleteSaveLabel @onready var _feedback_label: RichTextLabel = %FeedbackLabel @onready var _confirmation_page: TitleConfirmationBubblePageType = ( %ConfirmationPage @@ -446,9 +447,6 @@ func _update_title_layout() -> void: ) if not _title_settings_transition_active: call_deferred("_capture_title_bubble_rest_position") - _delete_save_label.text = ( - "delete\nsave" if compact_layout else "delete save" - ) if _awaiting_start_input and not _title_entry_transition_active: _schedule_intro_presentation() _update_world_preview_resolution() @@ -1415,6 +1413,10 @@ func _set_title_bubbles_interactive(interactive: bool) -> void: if interactive else Control.MOUSE_FILTER_IGNORE ) + if interactive: + ControllerFocusNavigationType.configure_spatial_neighbors( + _get_title_buttons() + ) func _capture_title_bubble_rest_position() -> void: @@ -1470,9 +1472,6 @@ func _refresh_save_inspection() -> void: _inspection = _save_manager.inspect_save() _continue_button.disabled = not _inspection.can_continue() _delete_button.disabled = not _inspection.can_delete() - _delete_save_label.modulate.a = ( - DISABLED_BUBBLE_LABEL_ALPHA if _delete_button.disabled else 1.0 - ) _feedback_label.text = _center_feedback_text(_inspection.message) if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: _feedback_label.text = _get_continue_stats_text() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index ab3efa6..bed5cc4 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=22 format=3] +[gd_scene load_steps=23 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -18,6 +18,7 @@ [ext_resource type="Texture2D" path="res://ui/icons/main_menu/continue.png" id="16_continue"] [ext_resource type="Texture2D" path="res://ui/icons/main_menu/new_game.png" id="17_new_game"] [ext_resource type="Texture2D" path="res://ui/icons/main_menu/credits.png" id="18_credits"] +[ext_resource type="Texture2D" path="res://ui/icons/main_menu/delete_save.png" id="19_delete_save"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] shader = ExtResource("4_water_shader") @@ -384,13 +385,16 @@ offset_left = -11.0 offset_top = 122.0 offset_right = 123.0 offset_bottom = 248.0 +texture_filter = 1 +icon = ExtResource("19_delete_save") +tooltip_text = "delete save" +accessibility_name = "delete save" script = ExtResource("7_bubble_button") profile = ExtResource("9_bubble_profile") neutral_size = Vector2(134, 126) desktop_anchor = Vector2(56, 185) compact_anchor = Vector2(53, 196) compact_minimum_size = Vector2(86, 86) -label_control_path = NodePath("DeleteSaveLabel") minimum_font_size = 15 maximum_font_size = 23 vertical_amplitude = 5.1 @@ -399,21 +403,6 @@ motion_phase = 3.7 deformation_amplitude = 0.018 deformation_period = 5.4 -[node name="DeleteSaveLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/DeleteSaveButton"] -unique_name_in_owner = true -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_constants/line_spacing = -3 -text = "delete save" -horizontal_alignment = 1 -vertical_alignment = 1 - [node name="QuitButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true offset_left = 275.0 diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index d609894..9123a7c 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -49,6 +49,9 @@ func _ready() -> void: ) var controller_focus_presentation := ControllerFocusPresentationType.new() _ui_root.add_child(controller_focus_presentation) + _game_ui.virtual_pointer_mode_changed.connect( + controller_focus_presentation.set_virtual_pointer_active + ) var root_viewport: Viewport = get_viewport() root_viewport.size_changed.connect(_resize_presentation) _resize_presentation()