fix: enforce player menu controller zones
This commit is contained in:
parent
a99916c38b
commit
108c91c352
2 changed files with 685 additions and 121 deletions
|
|
@ -11,10 +11,17 @@ const TitleConfirmationScene = preload(
|
|||
"res://ui/title_confirmation_bubble_page.tscn"
|
||||
)
|
||||
const MailPageType = preload("res://ui/mail_page.gd")
|
||||
const LogbookPageType = preload("res://ui/logbook_page.gd")
|
||||
const ProfilePageType = preload("res://ui/profile_page.gd")
|
||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||
const BagItemSpriteScene = preload(
|
||||
"res://ui/components/bubble_menu/bag_item_sprite.tscn"
|
||||
)
|
||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const PlayersPageType = preload("res://ui/players_page.gd")
|
||||
const TheNetPageType = preload("res://ui/the_net_page.gd")
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
|
|
@ -35,6 +42,7 @@ func _run() -> void:
|
|||
await _validate_data_settings_navigation()
|
||||
await _validate_mail_navigation()
|
||||
await _validate_profile_confirmation_focus()
|
||||
await _validate_inventory_tab_zone_transitions()
|
||||
await _validate_player_menu_nested_controller_back()
|
||||
await _validate_confirmation_dialog_navigation()
|
||||
await _validate_bubble_confirmation_navigation()
|
||||
|
|
@ -389,6 +397,83 @@ func _validate_player_menu_nested_controller_back() -> void:
|
|||
)
|
||||
down.pressed = false
|
||||
Input.parse_input_event(down)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Profile category zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Profile from a nested zone.",
|
||||
)
|
||||
_expect(
|
||||
int(profile_page.get("_controller_zone"))
|
||||
== ProfilePageType.ControllerZone.ACCOUNT,
|
||||
"Global player-menu Back did not return Profile to its account zone.",
|
||||
)
|
||||
|
||||
var logbook_page := menu.get("_catalog_logbook") as Control
|
||||
logbook_page.set("_active", true)
|
||||
logbook_page.set("_interactive", true)
|
||||
logbook_page.set(
|
||||
"_controller_zone",
|
||||
LogbookPageType.ControllerZone.DETAILS,
|
||||
)
|
||||
menu.set("_current_section", PlayerMenuType.Section.LOGBOOK)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Logbook detail zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Logbook from a nested zone.",
|
||||
)
|
||||
_expect(
|
||||
int(logbook_page.get("_controller_zone"))
|
||||
== LogbookPageType.ControllerZone.ENTRIES,
|
||||
"Global player-menu Back did not return Logbook to creature selection.",
|
||||
)
|
||||
|
||||
var net_page := menu.get("_the_net_page") as Control
|
||||
net_page.set("_active", true)
|
||||
net_page.set("_interactive", true)
|
||||
net_page.set(
|
||||
"_controller_zone",
|
||||
TheNetPageType.ControllerZone.CLAIMS,
|
||||
)
|
||||
menu.set("_current_section", PlayerMenuType.Section.NET)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Fishnet claims zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Fishnet from a nested zone.",
|
||||
)
|
||||
_expect(
|
||||
int(net_page.get("_controller_zone"))
|
||||
== TheNetPageType.ControllerZone.TABS,
|
||||
"Global player-menu Back did not return Fishnet to its tabs.",
|
||||
)
|
||||
|
||||
var mail_page := menu.get("_mail_page") as Control
|
||||
mail_page.set("_active", true)
|
||||
mail_page.set("_interactive", true)
|
||||
(mail_page.get("_inbox") as Control).hide()
|
||||
(mail_page.get("_compose") as Control).show()
|
||||
(mail_page.get("_letter") as Control).hide()
|
||||
menu.set("_current_section", PlayerMenuType.Section.MAIL)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Mail compose zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Mail while composing.",
|
||||
)
|
||||
_expect(
|
||||
(mail_page.get("_inbox") as Control).visible,
|
||||
"Global player-menu Back did not return Mail to the inbox.",
|
||||
)
|
||||
|
||||
var mapping_manager := ControllerMappingManagerType.new()
|
||||
var bindings: Dictionary = ControllerMappingManagerType.default_bindings()
|
||||
|
|
@ -410,6 +495,22 @@ func _validate_player_menu_nested_controller_back() -> void:
|
|||
"_controller_zone", PlayersPageType.ControllerZone.BODY
|
||||
)
|
||||
menu.set("_current_section", PlayerMenuType.Section.PLAYERS)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Online body zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Online from its body zone.",
|
||||
)
|
||||
_expect(
|
||||
int(players_page.get("_controller_zone"))
|
||||
== PlayersPageType.ControllerZone.TABS,
|
||||
"Global player-menu Back did not return Online to its tabs.",
|
||||
)
|
||||
players_page.set(
|
||||
"_controller_zone", PlayersPageType.ControllerZone.BODY
|
||||
)
|
||||
var mapped_back := InputEventJoypadButton.new()
|
||||
mapped_back.device = 0
|
||||
mapped_back.button_index = JOY_BUTTON_Y
|
||||
|
|
@ -430,6 +531,303 @@ func _validate_player_menu_nested_controller_back() -> void:
|
|||
mapping_manager.free()
|
||||
|
||||
|
||||
func _validate_inventory_tab_zone_transitions() -> void:
|
||||
var menu := PlayerMenuScene.instantiate() as Control
|
||||
root.add_child(menu)
|
||||
await process_frame
|
||||
menu.visible = true
|
||||
menu.call(
|
||||
"_show_section_immediate",
|
||||
PlayerMenuType.Section.COOLER,
|
||||
)
|
||||
menu.call("_set_content_interactive", true)
|
||||
menu.call("_enter_inventory_tabs_zone")
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var cooler_tab := menu.get_node("%CoolerSubTab") as Button
|
||||
var equipment_tab := menu.get_node("%BagSubTab") as Button
|
||||
var items_tab := menu.get_node("%ItemsSubTab") as Button
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == cooler_tab,
|
||||
"Inventory tab zone did not begin on the active Cooler tab.",
|
||||
)
|
||||
|
||||
var right := InputEventAction.new()
|
||||
right.action = &"ui_right"
|
||||
right.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", right)),
|
||||
"Inventory tab zone did not consume controller Right.",
|
||||
)
|
||||
await _wait_for_player_menu_page_transition(menu)
|
||||
_expect(
|
||||
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
||||
"Controller Right did not switch from Cooler to Equipment.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
|
||||
"Changing Inventory tabs entered the item-content zone without A.",
|
||||
)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == equipment_tab,
|
||||
"Changing Inventory tabs did not keep focus on the selected tab.",
|
||||
)
|
||||
var down := InputEventAction.new()
|
||||
down.action = &"ui_down"
|
||||
down.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", down)),
|
||||
"Inventory tab zone did not consume controller Down.",
|
||||
)
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == equipment_tab,
|
||||
"Controller Down escaped the Inventory tab zone before A.",
|
||||
)
|
||||
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", right)),
|
||||
"Equipment-to-Items navigation did not consume controller Right.",
|
||||
)
|
||||
await _wait_for_player_menu_page_transition(menu)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == items_tab,
|
||||
"Items tab selection did not retain tab focus before A (focus=%s)."
|
||||
% root.gui_get_focus_owner(),
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
|
||||
"Items tab selection changed controller zones before A.",
|
||||
)
|
||||
var left := InputEventAction.new()
|
||||
left.action = &"ui_left"
|
||||
left.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", left)),
|
||||
"Items-to-Equipment navigation did not consume controller Left.",
|
||||
)
|
||||
await _wait_for_player_menu_page_transition(menu)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == equipment_tab,
|
||||
"Returning to Equipment did not retain tab focus before A.",
|
||||
)
|
||||
|
||||
# Add three representative equipment entries so entering the content zone
|
||||
# exercises the real three-column directional layout.
|
||||
var item_field := menu.get_node("%BagItemField") as Control
|
||||
var bag_nodes: Dictionary = menu.get("_bag_item_nodes")
|
||||
var owned_items: Array[OwnedItemType] = []
|
||||
for index: int in 3:
|
||||
var owned := OwnedItemType.new()
|
||||
owned.item_id = StringName("controller_test_item_%d" % index)
|
||||
owned_items.append(owned)
|
||||
var item_node := BagItemSpriteScene.instantiate() as Button
|
||||
item_node.set("item_id", owned.item_id)
|
||||
item_node.position = Vector2(60.0 + float(index) * 180.0, 40.0)
|
||||
item_field.add_child(item_node)
|
||||
bag_nodes[owned.item_id] = item_node
|
||||
menu.set("_sorted_bag_items", owned_items)
|
||||
menu.call("_set_content_interactive", true)
|
||||
menu.call("_configure_bag_item_focus")
|
||||
for item_node: Control in bag_nodes.values():
|
||||
_expect(
|
||||
item_node.focus_mode == Control.FOCUS_NONE,
|
||||
"Equipment content remained focusable while tabs owned the controller.",
|
||||
)
|
||||
|
||||
var accept := InputEventJoypadButton.new()
|
||||
accept.button_index = JOY_BUTTON_A
|
||||
accept.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", accept)),
|
||||
"Inventory tab zone did not consume controller A.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
"Controller A did not explicitly enter Inventory contents.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var first_item := bag_nodes[owned_items[0].item_id] as Control
|
||||
var second_item := bag_nodes[owned_items[1].item_id] as Control
|
||||
for item_node: Control in bag_nodes.values():
|
||||
_expect(
|
||||
item_node.focus_mode == Control.FOCUS_ALL,
|
||||
"Accepting Equipment did not enable its content focus zone.",
|
||||
)
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == first_item,
|
||||
"Entering Equipment did not focus its first item.",
|
||||
)
|
||||
_expect(
|
||||
first_item.focus_neighbor_right == first_item.get_path_to(second_item),
|
||||
"Equipment items do not provide horizontal controller navigation.",
|
||||
)
|
||||
Input.parse_input_event(right)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == second_item,
|
||||
"Controller Right did not move between Equipment items.",
|
||||
)
|
||||
right.pressed = false
|
||||
Input.parse_input_event(right)
|
||||
right.pressed = true
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume Equipment contents.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Inventory from its content zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
|
||||
"Global player-menu Back did not return Inventory contents to tabs.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", accept)),
|
||||
"Inventory tab zone did not re-enter Equipment contents.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
|
||||
var favorite := menu.get_node("%FavoriteBubble") as BaseButton
|
||||
var sell := menu.get_node("%SellBubble") as BaseButton
|
||||
var sell_all := menu.get_node("%SellAllBubble") as BaseButton
|
||||
for action: BaseButton in [favorite, sell, sell_all]:
|
||||
action.disabled = false
|
||||
action.focus_mode = Control.FOCUS_ALL
|
||||
var notepad_actions: Array[BaseButton] = [favorite, sell, sell_all]
|
||||
menu.call(
|
||||
"_configure_controller_notepad_action_focus",
|
||||
notepad_actions,
|
||||
)
|
||||
_expect(
|
||||
sell.get_node(sell.focus_neighbor_bottom) == sell_all,
|
||||
"Sell All is not reachable below Sell Fish in the notepad zone.",
|
||||
)
|
||||
_expect(
|
||||
sell_all.get_node(sell_all.focus_neighbor_top) == sell,
|
||||
"Sell All does not return to the upper notepad actions.",
|
||||
)
|
||||
menu.set(
|
||||
"_controller_ownership",
|
||||
PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
|
||||
)
|
||||
menu.set("_controller_source_section", PlayerMenuType.Section.BAG)
|
||||
menu.set("_controller_source_identity", owned_items[1].item_id)
|
||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume the Inventory notepad zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Inventory from its notepad zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
"Global player-menu Back did not return the notepad to Inventory contents.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
|
||||
var hotbar := PlayerHotbarType.new()
|
||||
menu.set("_hotbar", hotbar)
|
||||
var hotbar_slots: Array[StringName] = []
|
||||
hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
|
||||
hotbar_slots.fill(StringName())
|
||||
hotbar.set("_slots", hotbar_slots)
|
||||
var hotbar_fish_slots: Array[StringName] = []
|
||||
hotbar_fish_slots.resize(PlayerHotbarType.SLOT_COUNT)
|
||||
hotbar_fish_slots.fill(StringName())
|
||||
hotbar_fish_slots[0] = &"controller_test_fish"
|
||||
hotbar.set("_fish_slots", hotbar_fish_slots)
|
||||
var management_requests: Array[int] = [0]
|
||||
menu.controller_hotbar_management_requested.connect(
|
||||
func(_initial_slot: int) -> void:
|
||||
management_requests[0] += 1
|
||||
)
|
||||
var hotbar_owned := OwnedItemType.new()
|
||||
hotbar_owned.item_id = &"controller_hotbar_source"
|
||||
var hotbar_source := BagItemSpriteScene.instantiate() as Button
|
||||
hotbar_source.set("item_id", hotbar_owned.item_id)
|
||||
hotbar_source.position = Vector2(60.0, 40.0)
|
||||
item_field.add_child(hotbar_source)
|
||||
bag_nodes[hotbar_owned.item_id] = hotbar_source
|
||||
var hotbar_items: Array[OwnedItemType] = [hotbar_owned]
|
||||
menu.set("_sorted_bag_items", hotbar_items)
|
||||
menu.set(
|
||||
"_controller_ownership",
|
||||
PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
)
|
||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
||||
menu.call("_configure_bag_item_focus")
|
||||
hotbar_source.grab_focus()
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", down)),
|
||||
"Down from the final Equipment row did not enter the hotbar zone.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.HOTBAR_MANAGEMENT,
|
||||
"Equipment-to-hotbar navigation did not transfer controller ownership.",
|
||||
)
|
||||
_expect(
|
||||
management_requests[0] == 1,
|
||||
"Equipment-to-hotbar navigation did not open hotbar management.",
|
||||
)
|
||||
_expect(
|
||||
bool(menu.call("_handle_controller_ownership_input", accept)),
|
||||
"Hotbar management did not consume controller A.",
|
||||
)
|
||||
_expect(
|
||||
hotbar.get_fish_catch_id(0).is_empty(),
|
||||
"Controller A did not remove the selected fish hotbar assignment.",
|
||||
)
|
||||
_expect(
|
||||
bool(menu.call("consume_escape")),
|
||||
"Global player-menu Back did not consume hotbar management.",
|
||||
)
|
||||
_expect(
|
||||
menu.visible,
|
||||
"Global player-menu Back closed Inventory from hotbar management.",
|
||||
)
|
||||
_expect(
|
||||
menu.get("_controller_ownership")
|
||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||
"Global player-menu Back did not return the hotbar to Inventory contents.",
|
||||
)
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
hotbar.free()
|
||||
menu.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _wait_for_player_menu_page_transition(menu: Control) -> void:
|
||||
var frames_waited: int = 0
|
||||
while bool(menu.get("_page_transitioning")) and frames_waited < 120:
|
||||
await process_frame
|
||||
frames_waited += 1
|
||||
# Same-section Equipment/Items changes do not tween, but they restore the
|
||||
# active tab focus with a deferred call just like completed page changes.
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
not bool(menu.get("_page_transitioning")),
|
||||
"Inventory page transition did not finish within 120 frames.",
|
||||
)
|
||||
|
||||
|
||||
func _validate_mapping_capture_contract() -> void:
|
||||
var title_source: String = FileAccess.get_file_as_string(
|
||||
"res://ui/title_screen.gd"
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ var _controller_hotbar_identity: StringName
|
|||
var _controller_previous_hotbar_slot: int = 0
|
||||
var _controller_accept_held: bool = false
|
||||
var _controller_accept_hold_elapsed: float = 0.0
|
||||
var _content_interactive_enabled: bool = false
|
||||
var _sort_mode: SortMode = SortMode.CATCH_ORDER
|
||||
var _sort_descending: bool = true
|
||||
var _fish_selection := FishBatchSelectionType.new()
|
||||
|
|
@ -671,10 +672,9 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
JOY_BUTTON_B,
|
||||
)
|
||||
)
|
||||
if cancel_pressed:
|
||||
return _consume_player_menu_back()
|
||||
if _controller_ownership == ControllerOwnership.INVENTORY_TABS:
|
||||
if cancel_pressed:
|
||||
close_menu()
|
||||
return true
|
||||
if accept_pressed:
|
||||
_enter_inventory_content_zone()
|
||||
return true
|
||||
|
|
@ -684,37 +684,36 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
if event.is_action_pressed("ui_right"):
|
||||
_switch_inventory_tab_direction(1)
|
||||
return true
|
||||
if (
|
||||
event.is_action_pressed("ui_up")
|
||||
or event.is_action_pressed("ui_down")
|
||||
):
|
||||
# Inventory tabs are a closed controller zone. Content is entered
|
||||
# only by accepting the active tab, never by directional spillover.
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
if accept_pressed:
|
||||
if _hotbar != null:
|
||||
_hotbar.clear_slot(_hotbar.get_selected_slot())
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if accept_pressed:
|
||||
_confirm_controller_hotbar_placement()
|
||||
return true
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, true)
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
|
||||
if cancel_pressed:
|
||||
_release_controller_ownership(true, false)
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.SORT_FILTER:
|
||||
if cancel_pressed:
|
||||
_enter_inventory_content_zone()
|
||||
return true
|
||||
return false
|
||||
if _controller_ownership == ControllerOwnership.ITEM_LIST:
|
||||
if cancel_pressed:
|
||||
if (
|
||||
event.is_action_pressed("ui_down")
|
||||
and _controller_focus_is_on_last_inventory_row()
|
||||
):
|
||||
_cancel_controller_accept_hold()
|
||||
_enter_inventory_tabs_zone()
|
||||
return true
|
||||
return _enter_controller_hotbar_management()
|
||||
if _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_SELECT,
|
||||
|
|
@ -750,17 +749,7 @@ func _handle_active_page_controller_input(event: InputEvent) -> bool:
|
|||
JOY_BUTTON_B,
|
||||
)
|
||||
):
|
||||
# Child pages use ui_cancel for their zone transitions. Retry a mapped
|
||||
# B press as that semantic action before treating it as a request to
|
||||
# close the entire player menu.
|
||||
if not event.is_action_pressed("ui_cancel"):
|
||||
var cancel_event := InputEventAction.new()
|
||||
cancel_event.action = &"ui_cancel"
|
||||
cancel_event.pressed = true
|
||||
if _dispatch_active_page_controller_input(cancel_event):
|
||||
return true
|
||||
close_menu()
|
||||
return true
|
||||
return _consume_player_menu_back()
|
||||
return false
|
||||
|
||||
|
||||
|
|
@ -842,11 +831,100 @@ func _try_enter_notepad_controller_ownership() -> bool:
|
|||
_controller_source_identity = source_identity
|
||||
_controller_notepad_actions = actions
|
||||
_controller_ownership = ControllerOwnership.NOTEPAD_ACTIONS
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(actions)
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
_configure_controller_notepad_action_focus(actions)
|
||||
actions.front().call_deferred("grab_focus")
|
||||
return true
|
||||
|
||||
|
||||
func _configure_controller_notepad_action_focus(
|
||||
actions: Array[BaseButton],
|
||||
) -> void:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(actions)
|
||||
if actions.is_empty():
|
||||
return
|
||||
var favorite: BaseButton = (
|
||||
_favorite_bubble if _favorite_bubble in actions else null
|
||||
)
|
||||
var sell: BaseButton = _sell_bubble if _sell_bubble in actions else null
|
||||
var sell_all: BaseButton = (
|
||||
_sell_all_bubble if _sell_all_bubble in actions else null
|
||||
)
|
||||
if favorite != null and sell != null:
|
||||
favorite.focus_neighbor_right = favorite.get_path_to(sell)
|
||||
sell.focus_neighbor_left = sell.get_path_to(favorite)
|
||||
if sell_all == null:
|
||||
return
|
||||
for upper_action: BaseButton in [favorite, sell]:
|
||||
if upper_action != null:
|
||||
upper_action.focus_neighbor_bottom = (
|
||||
upper_action.get_path_to(sell_all)
|
||||
)
|
||||
var return_action: BaseButton = sell if sell != null else favorite
|
||||
if return_action != null:
|
||||
sell_all.focus_neighbor_top = sell_all.get_path_to(return_action)
|
||||
|
||||
|
||||
func _controller_focus_is_on_last_inventory_row() -> bool:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner == null:
|
||||
return false
|
||||
if _current_section == Section.COOLER:
|
||||
var fish_node := focus_owner as CoolerFishSpriteType
|
||||
if fish_node == null:
|
||||
return false
|
||||
var fish_index: int = _sorted_catches.find_custom(
|
||||
func(fish_catch: FishCatchType) -> bool:
|
||||
return fish_catch.catch_id == fish_node.catch_id
|
||||
)
|
||||
var fish_columns: int = 3 if _compact_layout else 9
|
||||
return (
|
||||
fish_index >= 0
|
||||
and fish_index + fish_columns >= _sorted_catches.size()
|
||||
)
|
||||
if _current_section == Section.BAG:
|
||||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node == null:
|
||||
return false
|
||||
var item_index: int = _sorted_bag_items.find_custom(
|
||||
func(owned: OwnedItemType) -> bool:
|
||||
return owned.item_id == item_node.item_id
|
||||
)
|
||||
return (
|
||||
item_index >= 0
|
||||
and item_index + 3 >= _sorted_bag_items.size()
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
func _enter_controller_hotbar_management() -> bool:
|
||||
if _hotbar == null or _current_section not in [Section.COOLER, Section.BAG]:
|
||||
return false
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
var source_identity: StringName
|
||||
if _current_section == Section.COOLER:
|
||||
var fish_node := focus_owner as CoolerFishSpriteType
|
||||
if fish_node == null:
|
||||
return false
|
||||
source_identity = fish_node.catch_id
|
||||
else:
|
||||
var item_node := focus_owner as BagItemSpriteType
|
||||
if item_node == null:
|
||||
return false
|
||||
source_identity = item_node.item_id
|
||||
if source_identity.is_empty():
|
||||
return false
|
||||
_controller_source_section = _current_section
|
||||
_controller_source_identity = source_identity
|
||||
_controller_previous_hotbar_slot = _hotbar.get_selected_slot()
|
||||
_controller_ownership = ControllerOwnership.HOTBAR_MANAGEMENT
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
controller_hotbar_management_requested.emit(
|
||||
_controller_previous_hotbar_slot
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func _try_begin_controller_hotbar_placement() -> bool:
|
||||
if _hotbar == null:
|
||||
return false
|
||||
|
|
@ -890,6 +968,7 @@ func _try_begin_controller_hotbar_placement() -> bool:
|
|||
if initial_slot < 0:
|
||||
initial_slot = _controller_previous_hotbar_slot
|
||||
_controller_ownership = ControllerOwnership.HOTBAR_PLACEMENT
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
controller_hotbar_placement_requested.emit(
|
||||
assignment_kind,
|
||||
identity,
|
||||
|
|
@ -958,6 +1037,7 @@ func _release_controller_ownership(
|
|||
_controller_source_identity = StringName()
|
||||
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
|
||||
_controller_hotbar_identity = StringName()
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
if prior_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
if restore_previous_hotbar_slot and _hotbar != null:
|
||||
_hotbar.select_slot(_controller_previous_hotbar_slot)
|
||||
|
|
@ -1092,6 +1172,7 @@ func _enter_inventory_tabs_zone() -> void:
|
|||
return
|
||||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.INVENTORY_TABS
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
var active_tab: Button = _inventory_tab_for_section(_current_section)
|
||||
if (
|
||||
active_tab != null
|
||||
|
|
@ -1106,6 +1187,7 @@ func _enter_inventory_content_zone() -> void:
|
|||
return
|
||||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.ITEM_LIST
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
call_deferred("_focus_current_section")
|
||||
|
||||
|
||||
|
|
@ -1115,6 +1197,7 @@ func _enter_inventory_sort_zone() -> void:
|
|||
_cancel_controller_accept_hold()
|
||||
_controller_ownership = ControllerOwnership.SORT_FILTER
|
||||
_controller_source_section = _current_section
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
var sort_controls: Array[Control] = [
|
||||
_cooler_sort_option,
|
||||
_cooler_sort_direction,
|
||||
|
|
@ -1144,36 +1227,6 @@ func _reset_controller_zone_for_section() -> void:
|
|||
_players_page.reset_controller_zone()
|
||||
|
||||
|
||||
func _collect_visible_toggle_buttons(
|
||||
root: Node,
|
||||
navigation_cluster: Control,
|
||||
grouped_buttons: Dictionary,
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var child_control := child as Control
|
||||
if child_control != null and not child_control.is_visible_in_tree():
|
||||
continue
|
||||
var button := child as BaseButton
|
||||
if (
|
||||
button != null
|
||||
and button.toggle_mode
|
||||
and (
|
||||
navigation_cluster == null
|
||||
or not navigation_cluster.is_ancestor_of(button)
|
||||
)
|
||||
):
|
||||
var group_key: Variant = button.get_parent()
|
||||
if button.button_group != null:
|
||||
group_key = button.button_group
|
||||
if not grouped_buttons.has(group_key):
|
||||
grouped_buttons[group_key] = []
|
||||
var buttons := grouped_buttons[group_key] as Array
|
||||
buttons.append(button)
|
||||
_collect_visible_toggle_buttons(
|
||||
child, navigation_cluster, grouped_buttons
|
||||
)
|
||||
|
||||
|
||||
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
|
|
@ -1247,16 +1300,43 @@ func _is_text_input_active() -> bool:
|
|||
func consume_escape() -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
return _consume_player_menu_back()
|
||||
|
||||
|
||||
func _consume_player_menu_back() -> bool:
|
||||
if _transitioning or _page_transitioning:
|
||||
return true
|
||||
if _sale_confirmation.visible:
|
||||
_close_sale_confirmation()
|
||||
elif _current_section == Section.MAIL and _mail_page.consume_escape():
|
||||
pass
|
||||
elif _current_section == Section.PROFILE and _profile_page.consume_escape():
|
||||
pass
|
||||
else:
|
||||
close_menu()
|
||||
return true
|
||||
_cancel_controller_accept_hold()
|
||||
if _is_inventory_section(_current_section):
|
||||
match _controller_ownership:
|
||||
ControllerOwnership.NOTEPAD_ACTIONS:
|
||||
_release_controller_ownership(true, false)
|
||||
ControllerOwnership.SORT_FILTER:
|
||||
_enter_inventory_content_zone()
|
||||
ControllerOwnership.HOTBAR_MANAGEMENT:
|
||||
_release_controller_ownership(true, false)
|
||||
ControllerOwnership.HOTBAR_PLACEMENT:
|
||||
_release_controller_ownership(true, true)
|
||||
ControllerOwnership.ITEM_LIST:
|
||||
_enter_inventory_tabs_zone()
|
||||
ControllerOwnership.INVENTORY_TABS:
|
||||
close_menu()
|
||||
_:
|
||||
close_menu()
|
||||
return true
|
||||
var cancel_event := InputEventAction.new()
|
||||
cancel_event.action = &"ui_cancel"
|
||||
cancel_event.pressed = true
|
||||
if _dispatch_active_page_controller_input(cancel_event):
|
||||
return true
|
||||
if _current_section == Section.MAIL and _mail_page.consume_escape():
|
||||
return true
|
||||
if _current_section == Section.PROFILE and _profile_page.consume_escape():
|
||||
return true
|
||||
close_menu()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -1510,8 +1590,16 @@ func _show_section(section: Section) -> void:
|
|||
and _profile_page.request_close_confirmation()
|
||||
):
|
||||
return
|
||||
_release_controller_ownership(false, true)
|
||||
var preserve_inventory_tab_zone: bool = (
|
||||
_controller_ownership == ControllerOwnership.INVENTORY_TABS
|
||||
and _is_inventory_section(_current_section)
|
||||
and _is_inventory_section(section)
|
||||
)
|
||||
if not preserve_inventory_tab_zone:
|
||||
_release_controller_ownership(false, true)
|
||||
_begin_page_transition(section)
|
||||
if preserve_inventory_tab_zone:
|
||||
_controller_ownership = ControllerOwnership.INVENTORY_TABS
|
||||
|
||||
|
||||
func _show_section_immediate(section: Section) -> void:
|
||||
|
|
@ -1745,41 +1833,21 @@ func _set_descendant_focus_disabled(root: Node) -> void:
|
|||
|
||||
|
||||
func _reserve_visible_secondary_navigation() -> void:
|
||||
# Inventory organizer tabs have dedicated left/right ownership. Other pages
|
||||
# manage their own controller zones, and their toggle buttons are content,
|
||||
# not secondary navigation that should be disabled here.
|
||||
if not _is_inventory_section(_current_section):
|
||||
return
|
||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||
var grouped_buttons: Dictionary = {}
|
||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
||||
var selected_tabs: Array = []
|
||||
var selected_group_y: float = INF
|
||||
for group_value: Variant in grouped_buttons.keys():
|
||||
var buttons := grouped_buttons[group_value] as Array
|
||||
if buttons.size() < 2:
|
||||
continue
|
||||
var group_y: float = INF
|
||||
for item: Variant in buttons:
|
||||
var button := item as BaseButton
|
||||
group_y = minf(group_y, button.global_position.y)
|
||||
if group_y < selected_group_y:
|
||||
selected_tabs = buttons
|
||||
selected_group_y = group_y
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
var displaced_focus: bool = selected_tabs.has(focus_owner)
|
||||
for item: Variant in selected_tabs:
|
||||
var tab := item as Control
|
||||
tab.focus_mode = Control.FOCUS_NONE
|
||||
if displaced_focus:
|
||||
focus_owner.release_focus()
|
||||
call_deferred("_focus_current_section")
|
||||
var inventory_tabs: Array[Button] = [
|
||||
_cooler_sub_tab,
|
||||
_bag_sub_tab,
|
||||
_items_sub_tab,
|
||||
_tackle_sub_tab,
|
||||
]
|
||||
_set_inventory_tab_focus_enabled(
|
||||
_content_interactive_enabled
|
||||
and visible
|
||||
and not _transitioning
|
||||
and not _page_transitioning
|
||||
and _controller_ownership == ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
for index: int in inventory_tabs.size():
|
||||
var tab: Button = inventory_tabs[index]
|
||||
tab.focus_neighbor_left = tab.get_path_to(
|
||||
|
|
@ -1795,6 +1863,113 @@ func _reserve_visible_secondary_navigation() -> void:
|
|||
_configure_tackle_item_focus()
|
||||
|
||||
|
||||
func _set_inventory_tab_focus_enabled(enabled: bool) -> void:
|
||||
for tab: Button in [
|
||||
_cooler_sub_tab,
|
||||
_bag_sub_tab,
|
||||
_items_sub_tab,
|
||||
_tackle_sub_tab,
|
||||
]:
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL if enabled else Control.FOCUS_NONE
|
||||
)
|
||||
|
||||
|
||||
func _apply_inventory_controller_zone_focus_modes() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
var regular_active: bool = (
|
||||
_content_interactive_enabled
|
||||
and visible
|
||||
and _is_inventory_section(_current_section)
|
||||
and not _transitioning
|
||||
and not _page_transitioning
|
||||
and not _sale_confirmation.visible
|
||||
)
|
||||
var shop_active: bool = (
|
||||
_content_interactive_enabled
|
||||
and _shop_cooler_context_active
|
||||
and _current_section == Section.COOLER
|
||||
)
|
||||
_set_inventory_tab_focus_enabled(
|
||||
regular_active
|
||||
and _controller_ownership == ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
var content_active: bool = (
|
||||
regular_active
|
||||
and _controller_ownership == ControllerOwnership.ITEM_LIST
|
||||
)
|
||||
var notepad_active: bool = (
|
||||
regular_active
|
||||
and _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS
|
||||
)
|
||||
var sort_active: bool = (
|
||||
regular_active
|
||||
and _controller_ownership == ControllerOwnership.SORT_FILTER
|
||||
)
|
||||
for sort_control: Control in [
|
||||
_cooler_sort_option,
|
||||
_cooler_sort_direction,
|
||||
]:
|
||||
sort_control.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if sort_active or shop_active
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
for fish_node: CoolerFishSpriteType in _fish_nodes.values():
|
||||
fish_node.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if (
|
||||
shop_active
|
||||
or (
|
||||
content_active
|
||||
and _current_section == Section.COOLER
|
||||
)
|
||||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
for action: NotepadInkActionType in [
|
||||
_favorite_bubble,
|
||||
_sell_bubble,
|
||||
_sell_all_bubble,
|
||||
]:
|
||||
action.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if (
|
||||
(shop_active or notepad_active)
|
||||
and _detail_constellation.visible
|
||||
and not action.disabled
|
||||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
for item_node: BagItemSpriteType in _bag_item_nodes.values():
|
||||
item_node.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if (
|
||||
content_active
|
||||
and _current_section == Section.BAG
|
||||
and not _bag_drag_active
|
||||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
for button: Button in _tackle_item_buttons.values():
|
||||
button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if content_active and _current_section == Section.TACKLE_BOX
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
_tackle_equip_button.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if (
|
||||
notepad_active
|
||||
and _current_section == Section.TACKLE_BOX
|
||||
and _tackle_equip_button.visible
|
||||
and not _tackle_equip_button.disabled
|
||||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
|
||||
|
||||
func _inventory_tab_for_section(section: Section) -> Button:
|
||||
match section:
|
||||
Section.COOLER:
|
||||
|
|
@ -1911,7 +2086,8 @@ func _show_bag_view(view: BagView) -> void:
|
|||
var preserve_tab_zone: bool = (
|
||||
_controller_ownership == ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
_release_controller_ownership(false, true)
|
||||
if not preserve_tab_zone:
|
||||
_release_controller_ownership(false, true)
|
||||
_bag_view = view
|
||||
_selected_bag_item_id = StringName()
|
||||
_refresh_bag()
|
||||
|
|
@ -2171,9 +2347,6 @@ func _tackle_is_interactive() -> bool:
|
|||
|
||||
func _apply_tackle_interactivity(interactive: bool) -> void:
|
||||
for button: Button in _tackle_item_buttons.values():
|
||||
button.focus_mode = (
|
||||
Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
|
||||
)
|
||||
button.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if interactive
|
||||
|
|
@ -2184,14 +2357,12 @@ func _apply_tackle_interactivity(interactive: bool) -> void:
|
|||
and _tackle_equip_button.visible
|
||||
and not _tackle_equip_button.disabled
|
||||
)
|
||||
_tackle_equip_button.focus_mode = (
|
||||
Control.FOCUS_ALL if action_interactive else Control.FOCUS_NONE
|
||||
)
|
||||
_tackle_equip_button.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if action_interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
|
||||
|
||||
func _toggle_active_tackle() -> void:
|
||||
|
|
@ -3003,6 +3174,7 @@ func _set_shell_interactive(interactive: bool) -> void:
|
|||
|
||||
|
||||
func _set_content_interactive(interactive: bool) -> void:
|
||||
_content_interactive_enabled = interactive
|
||||
_content_stage.mouse_filter = (
|
||||
Control.MOUSE_FILTER_PASS
|
||||
if interactive
|
||||
|
|
@ -3033,7 +3205,13 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
interactive and _is_inventory_section(_current_section)
|
||||
)
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL if tab_interactive else Control.FOCUS_NONE
|
||||
Control.FOCUS_ALL
|
||||
if (
|
||||
tab_interactive
|
||||
and _controller_ownership
|
||||
== ControllerOwnership.INVENTORY_TABS
|
||||
)
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
tab.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
|
|
@ -3068,11 +3246,6 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
_cooler_sort_option.refresh_ink_state()
|
||||
_cooler_sort_direction.refresh_ink_state()
|
||||
for fish_node: CoolerFishSpriteType in _fish_nodes.values():
|
||||
fish_node.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if cooler_interactive
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
fish_node.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if cooler_interactive
|
||||
|
|
@ -3087,11 +3260,6 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
_sell_all_bubble,
|
||||
]:
|
||||
var action_interactive: bool = detail_interactive and not action.disabled
|
||||
action.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if action_interactive
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
action.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if action_interactive
|
||||
|
|
@ -3104,9 +3272,6 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
and not _bag_drag_active
|
||||
)
|
||||
for item_node: BagItemSpriteType in _bag_item_nodes.values():
|
||||
item_node.focus_mode = (
|
||||
Control.FOCUS_ALL if bag_interactive else Control.FOCUS_NONE
|
||||
)
|
||||
item_node.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if bag_interactive
|
||||
|
|
@ -3116,6 +3281,7 @@ func _set_content_interactive(interactive: bool) -> void:
|
|||
interactive and _current_section == Section.TACKLE_BOX
|
||||
)
|
||||
_apply_tackle_interactivity(tackle_interactive)
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
|
||||
|
||||
func _cancel_presentation_tween() -> void:
|
||||
|
|
@ -4061,13 +4227,13 @@ func _refresh_cooler_notepad_action_interactivity() -> void:
|
|||
_sell_all_bubble,
|
||||
]:
|
||||
var action_interactive: bool = detail_interactive and not action.disabled
|
||||
if action_interactive:
|
||||
action.focus_mode = Control.FOCUS_ALL
|
||||
action.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
else:
|
||||
action.focus_mode = Control.FOCUS_NONE
|
||||
action.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
action.mouse_filter = (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if action_interactive
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
action.refresh_ink_state()
|
||||
_apply_inventory_controller_zone_focus_modes()
|
||||
|
||||
|
||||
func _update_sale_summary() -> void:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue