fix: enforce player menu controller zones

This commit is contained in:
Alexander Sellite 2026-08-16 00:00:01 -04:00
parent a99916c38b
commit 108c91c352
2 changed files with 685 additions and 121 deletions

View file

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

View file

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