Polish interface artwork and controller behavior

This commit is contained in:
Alexander Sellite 2026-08-19 10:02:28 -04:00
parent c6b897718c
commit f8e02e3844
41 changed files with 1119 additions and 171 deletions

View file

@ -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))

View file

@ -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():

View file

@ -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

View file

@ -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
)

View file

@ -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:

View file

@ -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)