netfishing/ui/player_menu.gd

3792 lines
117 KiB
GDScript3
Raw Normal View History

2026-07-25 16:02:34 -04:00
class_name PlayerMenu
extends Control
const INPUT_OWNER: StringName = &"player_menu"
2026-07-25 16:02:34 -04:00
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishDataType = preload("res://fish/fish_data.gd")
2026-08-02 17:44:11 -04:00
const FishQualityType = preload("res://fish/fish_quality.gd")
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const NetworkSessionType = preload("res://network/network_session.gd")
2026-07-25 16:02:34 -04:00
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
2026-07-31 09:29:27 -04:00
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
2026-07-31 10:20:18 -04:00
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
2026-07-25 16:02:34 -04:00
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const PlayerType = preload("res://player/player.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
const PlayerExperienceType = preload(
"res://progression/player_experience.gd"
)
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
2026-07-26 23:18:51 -04:00
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
2026-07-28 16:29:02 -04:00
const BubbleButtonType = preload(
"res://ui/components/bubble_menu/bubble_button.gd"
)
const BubbleClusterType = preload(
"res://ui/components/bubble_menu/bubble_cluster.gd"
)
const BubbleContentShellType = preload(
"res://ui/components/bubble_menu/bubble_content_shell.gd"
)
const BubbleStatusBubbleType = preload(
"res://ui/components/bubble_menu/bubble_status_bubble.gd"
)
const NotepadInkActionType = preload(
"res://ui/components/bubble_menu/notepad_ink_action.gd"
)
const NotepadInkChoiceType = preload(
"res://ui/components/bubble_menu/notepad_ink_choice.gd"
)
const CoolerFishSpriteType = preload(
"res://ui/components/bubble_menu/cooler_fish_sprite.gd"
)
const CoolerFishSpriteScene = preload(
"res://ui/components/bubble_menu/cooler_fish_sprite.tscn"
)
const BagItemSpriteType = preload(
"res://ui/components/bubble_menu/bag_item_sprite.gd"
)
const BagItemSpriteScene = preload(
"res://ui/components/bubble_menu/bag_item_sprite.tscn"
)
const LogbookEntryType = preload(
"res://ui/components/bubble_menu/logbook_entry.gd"
)
const LogbookEntryScene = preload(
"res://ui/components/bubble_menu/logbook_entry.tscn"
)
2026-07-28 16:29:02 -04:00
const LOGBOOK_PAGE_DURATION: float = 0.18
const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
const COMPACT_HEIGHT_THRESHOLD: float = 560.0
2026-07-29 10:53:46 -04:00
const NAVIGATION_PRESENTATION_SCALE: float = 0.60
2026-07-29 20:10:44 -04:00
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
2026-07-29 10:53:46 -04:00
const NAVIGATION_SELECTED_SCALE: float = 1.02
const INVENTORY_TAB_LEFT_INSET: float = 96.0
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
2026-07-25 16:02:34 -04:00
signal menu_visibility_changed(is_open: bool)
signal inventory_hotbar_context_changed(show_hotbar: bool)
signal menu_exit_started
signal shop_cooler_modal_changed(is_open: bool)
2026-07-25 16:02:34 -04:00
enum Section {
COOLER,
BAG,
TACKLE_BOX,
2026-07-25 16:02:34 -04:00
LOGBOOK,
NET,
2026-07-29 20:10:44 -04:00
MAIL,
PROFILE,
2026-07-29 23:10:51 -04:00
PLAYERS,
2026-07-25 16:02:34 -04:00
}
enum BagView {
EQUIPMENT,
CONSUMABLES,
}
enum TackleView {
BAIT,
LURES,
}
2026-07-25 16:02:34 -04:00
enum SortMode {
CATCH_ORDER,
NAME,
RARITY,
}
enum CloseReason {
USER,
BITE_STARTED,
WATER_RECOVERY,
GAME_MENU,
SESSION_END,
TEARDOWN,
}
2026-07-31 09:29:27 -04:00
const INVENTORY_MAIN_POSITION := Vector2(54.0, 166.0)
const INVENTORY_MAIN_SIZE := Vector2(882.0, 484.0)
const INVENTORY_PANEL_GAP := 16.0
const INVENTORY_NOTEPAD_POSITION := Vector2(
INVENTORY_MAIN_POSITION.x + INVENTORY_MAIN_SIZE.x + INVENTORY_PANEL_GAP,
INVENTORY_MAIN_POSITION.y,
)
const INVENTORY_NOTEPAD_SIZE := Vector2(278.0, 484.0)
const INVENTORY_HEADER_INSET := Vector2(24.0, 20.0)
const INVENTORY_HEADER_HEIGHT := 40.0
const INVENTORY_MAIN_CORNER_RADIUS: int = 58
const INVENTORY_INNER_CORNER_RADIUS: int = 45
const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
2026-07-31 09:29:27 -04:00
2026-07-28 16:29:02 -04:00
@onready var _navigation_cluster: BubbleClusterType = %NavigationCluster
@onready var _presentation_scale_root: Control = %PlayerMenuPresentationScaleRoot
@onready var _cooler_page: Control = %CoolerPage
@onready var _cooler_outer_wall: PanelContainer = %CoolerOuterWall
@onready var _cooler_inner_liner: PanelContainer = %CoolerInnerLiner
2026-07-29 10:53:46 -04:00
@onready var _cooler_water_surface: ColorRect = %WaterSurface
@onready var _fish_field: Control = %FishField
@onready var _cooler_empty: Label = %CoolerEmpty
@onready var _cooler_sort_controls: HBoxContainer = %CoolerSortControls
@onready var _cooler_sort_option: NotepadInkChoiceType = %CoolerSortOption
@onready var _cooler_sort_direction: NotepadInkActionType = %CoolerSortDirection
@onready var _detail_constellation: Control = %DetailConstellation
@onready var _detail_bubble: Control = %DetailBubble
@onready var _notepad_binding: Label = %BindingDecoration
@onready var _notepad_title: Label = %NotepadTitle
@onready var _notepad_rule: ColorRect = %NotepadRule
2026-07-29 10:53:46 -04:00
@onready var _notepad_wallet_value: Label = %NotepadWalletValue
@onready var _notepad_capacity_value: Label = %NotepadCapacityValue
@onready var _cooler_detail_texture: TextureRect = %CoolerDetailTexture
@onready var _cooler_detail_name: Label = %CoolerDetailName
2026-07-29 10:53:46 -04:00
@onready var _cooler_detail_stats: Control = %CoolerDetailStats
@onready var _cooler_weight_row: HBoxContainer = %CoolerWeightRow
@onready var _cooler_weight_value: Label = %CoolerWeightValue
@onready var _cooler_weight_unit: Label = %CoolerWeightUnit
@onready var _cooler_offer_row: HBoxContainer = %CoolerOfferRow
@onready var _cooler_offer_label: Label = %CoolerOfferLabel
@onready var _cooler_offer_value: Label = %CoolerOfferValue
@onready var _cooler_selection_summary: Control = %CoolerSelectionSummary
@onready var _cooler_selection_empty: Label = %CoolerSelectionEmpty
@onready var _cooler_selected_count_row: HBoxContainer = %CoolerSelectedCountRow
@onready var _cooler_selected_count_value: Label = %CoolerSelectedCountValue
@onready var _cooler_selected_count_label: Label = %CoolerSelectedCountLabel
@onready var _cooler_combined_offer_row: HBoxContainer = %CoolerCombinedOfferRow
@onready var _cooler_combined_offer_label: Label = %CoolerCombinedOfferLabel
@onready var _cooler_combined_offer_value: Label = %CoolerCombinedOfferValue
@onready var _favorite_bubble: NotepadInkActionType = %FavoriteBubble
@onready var _sell_bubble: NotepadInkActionType = %SellBubble
@onready var _bag_page: Control = %BagPage
@onready var _tackle_box_page: Control = %TackleBoxPage
@onready var _inventory_sub_tabs: HBoxContainer = %InventorySubTabs
@onready var _cooler_sub_tab: Button = %CoolerSubTab
@onready var _bag_sub_tab: Button = %BagSubTab
@onready var _tackle_sub_tab: Button = %TackleSubTab
@onready var _bag_filter_tabs: HBoxContainer = %BagFilterTabs
@onready var _equipment_filter: Button = %EquipmentFilter
@onready var _consumables_filter: Button = %ConsumablesFilter
@onready var _tackle_main_panel: PanelContainer = %TackleMainPanel
@onready var _tackle_detail_panel: PanelContainer = %TackleDetailPanel
@onready var _bait_filter: Button = %BaitFilter
@onready var _lures_filter: Button = %LuresFilter
@onready var _tackle_empty: Label = %TackleEmpty
@onready var _tackle_detail_text: Label = %TackleDetailText
@onready var _tackle_item_list: VBoxContainer = %TackleItemList
@onready var _bag_outer_wall: PanelContainer = %BagOuterWall
@onready var _bag_inner_liner: PanelContainer = %BagInnerLiner
@onready var _bag_scroll: ScrollContainer = %BagScroll
@onready var _bag_host: Control = %BagHost
@onready var _bag_item_field: Control = %BagItemField
@onready var _bag_empty_state: Label = %BagEmptyState
@onready var _bag_detail_constellation: Control = %BagDetailConstellation
@onready var _bag_sprite_detail_bubble: Control = %BagDetailBubble
@onready var _bag_sprite_detail_texture: TextureRect = %BagSpriteDetailTexture
@onready var _bag_sprite_detail_name: Label = %BagSpriteDetailName
@onready var _bag_sprite_detail_data: Label = %BagSpriteDetailData
@onready var _logbook_page: Control = %LogbookPage
2026-07-30 23:16:28 -04:00
@onready var _catalog_logbook: LogbookPage = %CatalogLogbook
@onready var _the_net_page: TheNetPage = %TheNetPage
2026-07-29 20:10:44 -04:00
@onready var _mail_page: MailPage = %MailPage
@onready var _profile_page: ProfilePage = %ProfilePage
2026-07-29 23:10:51 -04:00
@onready var _players_page: PlayersPage = %PlayersPage
@onready var _book_backing: PanelContainer = %BookBacking
@onready var _book_spread: BoxContainer = %BookSpread
@onready var _left_page: PanelContainer = %LeftPage
@onready var _right_page: PanelContainer = %RightPage
@onready var _book_gutter: ColorRect = %BookGutter
@onready var _left_heading: Label = %LeftHeading
@onready var _right_heading: Label = %RightHeading
@onready var _left_entry_field: BoxContainer = %LeftEntryField
@onready var _right_entry_field: BoxContainer = %RightEntryField
@onready var _logbook_empty_state: Label = %LogbookEmptyState
@onready var _logbook_previous: BubbleButtonType = %LogbookPrevious
@onready var _logbook_next: BubbleButtonType = %LogbookNext
@onready var _logbook_page_status: BubbleStatusBubbleType = %LogbookPageStatus
2026-07-28 16:29:02 -04:00
@onready var _inventory_tab: BubbleButtonType = %InventoryTab
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
@onready var _the_net_tab: BubbleButtonType = %TheNetTab
2026-07-29 20:10:44 -04:00
@onready var _mail_tab: BubbleButtonType = %MailTab
@onready var _profile_tab: BubbleButtonType = %ProfileTab
2026-07-29 23:10:51 -04:00
@onready var _players_tab: BubbleButtonType = %PlayersTab
2026-07-29 20:10:44 -04:00
@onready var _mail_unread_badge: Label = %MailUnreadBadge
2026-07-28 16:29:02 -04:00
@onready var _close_button: BubbleButtonType = %CloseButton
@onready var _content_shell: BubbleContentShellType = %MenuPanel
@onready var _content_stage: Control = %Content
@onready var _cooler_scroll: ScrollContainer = %CoolerScroll
@onready var _cooler_host: Control = %CoolerHost
@onready var _wallet_balance: Label = %WalletBalance
2026-07-28 16:29:02 -04:00
@onready var _header: Control = %Header
@onready var _separator: Control = %Separator
@onready var _wallet_status: BubbleStatusBubbleType = %WalletStatus
@onready var _capacity_status: BubbleStatusBubbleType = %CapacityStatus
@onready var _held_value_status: BubbleStatusBubbleType = %HeldValueStatus
@onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus
@onready var _offer_status: BubbleStatusBubbleType = %OfferStatus
2026-07-25 16:02:34 -04:00
@onready var _inventory_section: Control = %InventorySection
@onready var _bag_section: Control = %BagSection
2026-07-25 16:02:34 -04:00
@onready var _logbook_section: Control = %LogbookSection
@onready var _bag_empty: Label = %BagEmpty
@onready var _bag_grid: GridContainer = %BagGrid
2026-07-28 16:29:02 -04:00
@onready var _bag_list: Control = %BagList
@onready var _bag_detail: Control = %BagDetail
@onready var _bag_detail_texture: TextureRect = %BagDetailTexture
@onready var _bag_detail_name: Label = %BagDetailName
@onready var _bag_detail_data: Label = %BagDetailData
2026-07-25 16:02:34 -04:00
@onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection
@onready var _held_value: Label = %HeldValue
@onready var _cooler_count: Label = %CoolerCount
2026-07-25 16:02:34 -04:00
@onready var _inventory_empty: Label = %InventoryEmpty
@onready var _detail_texture: TextureRect = %DetailTexture
@onready var _detail_name: Label = %DetailName
@onready var _detail_data: Label = %DetailData
2026-07-26 23:18:51 -04:00
@onready var _selection_summary: Label = %SelectionSummary
@onready var _favorite_button: Button = %FavoriteButton
@onready var _sell_button: Button = %SellButton
@onready var _sale_unavailable: Label = %SaleUnavailable
@onready var _transaction_feedback: Label = %TransactionFeedback
@onready var _sale_confirmation: PanelContainer = %SaleConfirmation
@onready var _confirmation_message: Label = %ConfirmationMessage
@onready var _confirm_sale_button: Button = %ConfirmSaleButton
@onready var _cancel_sale_button: Button = %CancelSaleButton
2026-07-25 16:02:34 -04:00
@onready var _logbook_empty: Label = %LogbookEmpty
@onready var _logbook_grid: GridContainer = %LogbookGrid
2026-07-28 16:29:02 -04:00
var _compact_layout: bool = false
2026-07-25 16:02:34 -04:00
var _player: PlayerType
var _inventory: FishInventoryType
var _collection_log: CollectionLogType
var _wallet: PlayerWalletType
var _sale_service: FishSaleServiceType
var _network_session: NetworkSessionType
var _network_sale_service: NetworkSaleService
2026-07-29 20:10:44 -04:00
var _network_mail_service: NetworkMailService
var _reservations: PlayerAssetReservationService
var _network_profile_service: NetworkProfileService
2026-07-29 23:10:51 -04:00
var _network_player_list: NetworkPlayerListService
var _controller_mapping_manager: ControllerMappingManagerType
var _default_buyer: FishBuyerProfileType
var _sale_buyer_override: FishBuyerProfileType
var _shop_cooler_context_active: bool = false
var _cooler_original_parent: Node
var _cooler_original_index: int = -1
var _confirmation_original_parent: Node
var _confirmation_original_index: int = -1
2026-07-25 16:02:34 -04:00
var _catalog: FishPoolType
var _fishing_spot: FishingSpotType
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _cooler_capacity: PlayerCoolerCapacityType
var _current_section: Section = Section.COOLER
var _last_inventory_section: Section = Section.COOLER
var _bag_view: BagView = BagView.EQUIPMENT
var _tackle_view: TackleView = TackleView.BAIT
var _selected_tackle_item_id: StringName
2026-07-25 16:02:34 -04:00
var _sort_mode: SortMode = SortMode.CATCH_ORDER
var _sort_descending: bool = true
2026-07-26 23:18:51 -04:00
var _fish_selection := FishBatchSelectionType.new()
var _selected_bag_item_id: StringName
2026-07-25 16:02:34 -04:00
var _prior_movement_enabled: bool = true
var _prior_camera_input_enabled: bool = true
2026-07-25 18:56:47 -04:00
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
2026-07-25 16:02:34 -04:00
var _control_snapshot_stored: bool = false
2026-07-25 18:56:47 -04:00
var _mouse_snapshot_stored: bool = false
2026-07-25 16:02:34 -04:00
var _menu_generation: int = 0
2026-07-26 23:18:51 -04:00
var _confirmation_catch_ids: Array[StringName] = []
var _confirmation_buyer: FishBuyerProfileType
var _confirmation_buyer_id: StringName
2026-07-26 23:18:51 -04:00
var _confirmation_generation: int = -1
var _sale_in_progress: bool = false
2026-07-28 16:29:02 -04:00
var _presentation_tween: Tween
var _page_tween: Tween
var _transition_generation: int = 0
var _page_transition_generation: int = 0
var _transitioning: bool = false
var _page_transitioning: bool = false
var _presentation_rest_position: Vector2 = Vector2.ZERO
var _content_rest_position: Vector2 = Vector2.ZERO
var _cooler_rest_position: Vector2 = Vector2.ZERO
var _bag_rest_position: Vector2 = Vector2.ZERO
var _tackle_rest_position: Vector2 = Vector2.ZERO
var _logbook_rest_position: Vector2 = Vector2.ZERO
var _the_net_rest_position: Vector2 = Vector2.ZERO
2026-07-29 20:10:44 -04:00
var _mail_rest_position: Vector2 = Vector2.ZERO
var _profile_rest_position: Vector2 = Vector2.ZERO
var _page_outgoing_root: Control
var _page_incoming_root: Control
var _page_outgoing_content_root: Control
var _inventory_transition_group: Control
var _fish_nodes: Dictionary[StringName, CoolerFishSpriteType] = {}
var _sorted_catches: Array[FishCatchType] = []
var _bag_item_nodes: Dictionary[StringName, BagItemSpriteType] = {}
var _sorted_bag_items: Array[OwnedItemType] = []
var _bag_drag_active: bool = false
var _motion_elapsed: float = 0.0
var _logbook_species: Array[FishDataType] = []
var _logbook_current_page: int = 0
var _logbook_page_count: int = 1
var _logbook_page_transitioning: bool = false
var _logbook_page_generation: int = 0
var _logbook_page_tween: Tween
2026-07-25 16:02:34 -04:00
func _ready() -> void:
_configure_inventory_transition_group()
_cooler_original_parent = _cooler_page.get_parent()
_cooler_original_index = _cooler_page.get_index()
_confirmation_original_parent = _sale_confirmation.get_parent()
_confirmation_original_index = _sale_confirmation.get_index()
_inventory_sub_tabs.move_child(_tackle_sub_tab, 1)
_inventory_sub_tabs.move_child(_bag_sub_tab, 2)
_inventory_tab.pressed.connect(_show_last_inventory_section)
_cooler_sub_tab.pressed.connect(_show_section.bind(Section.COOLER))
_bag_sub_tab.pressed.connect(_show_section.bind(Section.BAG))
_tackle_sub_tab.pressed.connect(_show_section.bind(Section.TACKLE_BOX))
_equipment_filter.pressed.connect(_set_bag_view.bind(BagView.EQUIPMENT))
_consumables_filter.pressed.connect(
_set_bag_view.bind(BagView.CONSUMABLES)
)
_bait_filter.pressed.connect(_set_tackle_view.bind(TackleView.BAIT))
_lures_filter.pressed.connect(_set_tackle_view.bind(TackleView.LURES))
2026-07-25 16:02:34 -04:00
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
)
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
2026-07-29 20:10:44 -04:00
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
2026-07-29 23:10:51 -04:00
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
2026-07-25 16:02:34 -04:00
_close_button.pressed.connect(close_menu)
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
2026-07-25 16:02:34 -04:00
_sort_direction.pressed.connect(_on_sort_direction_pressed)
2026-07-29 10:53:46 -04:00
_cooler_sort_option.item_selected.connect(_on_cooler_sort_selected)
_cooler_sort_direction.pressed.connect(_on_sort_direction_pressed)
_favorite_button.pressed.connect(_on_favorite_pressed)
_sell_button.pressed.connect(_on_sell_pressed)
_favorite_bubble.pressed.connect(_on_favorite_pressed)
_sell_bubble.pressed.connect(_on_sell_pressed)
_confirm_sale_button.pressed.connect(_on_confirm_sale_pressed)
_cancel_sale_button.pressed.connect(_close_sale_confirmation)
_configure_sale_confirmation_focus()
2026-07-26 23:18:51 -04:00
_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_sort_option.add_item("name", SortMode.NAME)
_sort_option.add_item("rarity", SortMode.RARITY)
2026-07-25 16:02:34 -04:00
_sort_option.select(SortMode.CATCH_ORDER)
_cooler_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_cooler_sort_option.add_item("name", SortMode.NAME)
_cooler_sort_option.add_item("rarity", SortMode.RARITY)
_cooler_sort_option.select(SortMode.CATCH_ORDER)
2026-07-25 16:02:34 -04:00
_update_sort_direction_text()
2026-07-28 16:29:02 -04:00
_navigation_cluster.configure([
_inventory_tab,
_logbook_tab,
_the_net_tab,
2026-07-29 20:10:44 -04:00
_mail_tab,
_profile_tab,
2026-07-29 23:10:51 -04:00
_players_tab,
2026-07-28 16:29:02 -04:00
_close_button,
])
_configure_navigation_focus()
_apply_navigation_styles()
_apply_inventory_styles()
2026-07-31 10:20:18 -04:00
# Keep the exposed tab hitboxes above the full-page roots. The individual
# main panels retain a higher z-index and cover the tab bodies below y=166.
_inventory_sub_tabs.move_to_front()
2026-07-31 09:29:27 -04:00
_bag_filter_tabs.move_to_front()
2026-07-28 16:29:02 -04:00
_apply_cooler_control_styles()
_apply_cooler_wall_styles()
_apply_cooler_notepad_style()
2026-07-29 10:53:46 -04:00
_cooler_water_surface.resized.connect(_update_cooler_water_mask)
_fish_field.gui_input.connect(_on_fish_field_gui_input)
_apply_bag_styles()
_bag_item_field.gui_input.connect(_on_bag_field_gui_input)
_apply_mail_notification_style()
2026-07-28 16:29:02 -04:00
resized.connect(_update_shell_layout)
_show_section_immediate(_current_section)
call_deferred("_update_shell_layout")
2026-07-29 10:53:46 -04:00
call_deferred("_update_cooler_water_mask")
2026-07-28 16:29:02 -04:00
set_process(false)
2026-07-25 16:02:34 -04:00
func _configure_inventory_transition_group() -> void:
# Keep the organizer row and the panel that masks its lower flange under one
# alpha owner. Top-level page crossfades can then never expose a complete tab.
var original_layer_index: int = mini(
_inventory_sub_tabs.get_index(),
mini(
_cooler_page.get_index(),
mini(_bag_page.get_index(), _tackle_box_page.get_index()),
),
)
_inventory_transition_group = Control.new()
_inventory_transition_group.name = "InventoryTransitionGroup"
_inventory_transition_group.set_anchors_and_offsets_preset(
Control.PRESET_FULL_RECT
)
_inventory_transition_group.mouse_filter = Control.MOUSE_FILTER_IGNORE
_presentation_scale_root.add_child(_inventory_transition_group)
# Adding a runtime container normally places it after NavigationCluster in
# GUI picking order. Retain the pages' authored layer so their full-screen
# PASS roots cannot cover the top-level navigation hitboxes.
_presentation_scale_root.move_child(
_inventory_transition_group,
original_layer_index,
)
for control: Control in [
_inventory_sub_tabs,
_cooler_page,
_bag_page,
_tackle_box_page,
]:
control.reparent(_inventory_transition_group, true)
func _apply_mail_notification_style() -> void:
var background := StyleBoxFlat.new()
background.bg_color = Color(0.95, 0.88, 0.63, 0.98)
background.border_color = Color(0.20, 0.14, 0.08, 0.92)
background.set_border_width_all(2)
background.set_corner_radius_all(12)
background.content_margin_left = 5
background.content_margin_right = 5
background.content_margin_top = 2
background.content_margin_bottom = 2
_mail_unread_badge.add_theme_stylebox_override("normal", background)
_mail_unread_badge.add_theme_font_override(
"font", UtilityPageStyle.TuffyFont
)
2026-07-29 10:53:46 -04:00
func _update_cooler_water_mask() -> void:
var shader_material := _cooler_water_surface.material as ShaderMaterial
if shader_material == null:
return
shader_material.set_shader_parameter(
"rendered_size",
_cooler_water_surface.size,
)
2026-07-25 16:02:34 -04:00
func setup(
player: PlayerType,
inventory: FishInventoryType,
collection_log: CollectionLogType,
experience: PlayerExperienceType,
wallet: PlayerWalletType,
sale_service: FishSaleServiceType,
default_buyer: FishBuyerProfileType,
2026-07-25 16:02:34 -04:00
catalog: FishPoolType,
fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
network_sale_service: NetworkSaleService,
2026-07-29 20:10:44 -04:00
network_mail_service: NetworkMailService,
reservations: PlayerAssetReservationService,
network_profile_service: NetworkProfileService,
2026-07-29 23:10:51 -04:00
network_player_list: NetworkPlayerListService,
player_jobs: PlayerJobService,
world_time: WorldTimeService,
2026-08-04 22:34:43 -04:00
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
2026-07-25 16:02:34 -04:00
) -> void:
_player = player
_inventory = inventory
_collection_log = collection_log
_wallet = wallet
_sale_service = sale_service
_default_buyer = default_buyer
2026-07-25 16:02:34 -04:00
_catalog = catalog
_fishing_spot = fishing_spot
_bag = bag
_hotbar = hotbar
_item_catalog = item_catalog
_cooler_capacity = cooler_capacity
_network_session = network_session
_network_sale_service = network_sale_service
2026-07-29 20:10:44 -04:00
_network_mail_service = network_mail_service
_reservations = reservations
_network_profile_service = network_profile_service
2026-07-29 23:10:51 -04:00
_network_player_list = network_player_list
2026-07-29 20:10:44 -04:00
_mail_page.setup(
network_mail_service, reservations, inventory, wallet, bag, item_catalog
)
2026-08-04 22:34:43 -04:00
_profile_page.setup(
network_profile_service,
experience,
world_environment,
world_sun,
)
2026-07-29 23:10:51 -04:00
_players_page.setup(network_player_list)
2026-07-30 23:16:28 -04:00
_catalog_logbook.setup(collection_log, inventory, catalog)
_the_net_page.setup(player_jobs, world_time)
2026-07-29 20:10:44 -04:00
_network_mail_service.unread_count_changed.connect(
_on_mail_unread_count_changed
)
_on_mail_unread_count_changed(_network_mail_service.get_unread_count())
if (
_network_sale_service != null
and not _network_sale_service.local_sale_pending.is_connected(
_on_network_sale_pending
)
):
_network_sale_service.local_sale_pending.connect(
_on_network_sale_pending
)
_network_sale_service.local_sale_finished.connect(
_on_network_sale_finished
)
2026-07-26 23:18:51 -04:00
_fish_selection.clear()
2026-07-25 16:02:34 -04:00
if not _inventory.catches_changed.is_connected(_on_inventory_changed):
_inventory.catches_changed.connect(_on_inventory_changed)
if not _wallet.balance_changed.is_connected(_on_wallet_balance_changed):
_wallet.balance_changed.connect(_on_wallet_balance_changed)
2026-07-25 16:02:34 -04:00
if not _fishing_spot.bite_activated.is_connected(_on_bite_activated):
_fishing_spot.bite_activated.connect(_on_bite_activated)
if not _bag.contents_changed.is_connected(_on_bag_changed):
_bag.contents_changed.connect(_on_bag_changed)
if not _hotbar.slots_changed.is_connected(_on_hotbar_changed):
_hotbar.slots_changed.connect(_on_hotbar_changed)
if not _cooler_capacity.capacity_changed.is_connected(
_on_cooler_capacity_changed
):
_cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed)
2026-07-25 16:02:34 -04:00
_refresh_all()
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
_profile_page.setup_controller_mapping(_controller_mapping_manager)
func set_profile_preview_world_pixel_size(pixel_size: int) -> void:
_profile_page.set_world_pixel_size(pixel_size)
2026-07-25 16:02:34 -04:00
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.echo:
return
if _handle_controller_page_switch(event):
get_viewport().set_input_as_handled()
return
2026-07-30 23:16:28 -04:00
if _handle_direct_page_shortcut(event):
get_viewport().set_input_as_handled()
return
2026-07-25 16:02:34 -04:00
if visible:
if event.is_action_pressed("open_backpack"):
2026-07-28 16:29:02 -04:00
if not _transitioning and not _page_transitioning:
close_menu()
2026-07-25 16:02:34 -04:00
get_viewport().set_input_as_handled()
return
2026-07-25 16:02:34 -04:00
if (
event.is_action_pressed("open_backpack")
and _fishing_spot != null
and _fishing_spot.can_open_player_menu()
):
open_menu()
get_viewport().set_input_as_handled()
func _handle_controller_page_switch(event: InputEvent) -> bool:
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
var use_mapping: bool = (
_controller_mapping_manager != null
)
var uses_left_bumper: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_LB
)
if use_mapping
else (
button_event != null
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
)
)
var uses_right_bumper: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_RB
)
if use_mapping
else (
button_event != null
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
)
)
if (
button_event == null
or not (uses_left_bumper or uses_right_bumper)
or not visible
):
return false
if not button_event.pressed:
return true
# Shoulder input belongs to the Player Menu while it is visible, even when
# a transition or modal temporarily prevents changing pages. This keeps LB
# from opening Chat and RB from leaking into gameplay behind the menu.
if (
_transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
):
return true
var sections: Array[Section] = [
_last_inventory_section,
Section.LOGBOOK,
Section.NET,
Section.MAIL,
Section.PROFILE,
Section.PLAYERS,
]
var current_index: int = 0
if not _is_inventory_section(_current_section):
current_index = sections.find(_current_section)
if current_index < 0:
current_index = 0
var direction: int = (
-1 if uses_left_bumper else 1
)
var next_index: int = wrapi(current_index + direction, 0, sections.size())
_show_section(sections[next_index])
return true
2026-07-30 23:16:28 -04:00
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
var key_event := event as InputEventKey
if (
key_event == null
or not key_event.pressed
or _is_text_input_active()
2026-07-30 23:16:28 -04:00
or _shortcut_blocked_by_modal_state()
):
return false
var physical_key: Key = key_event.physical_keycode
var inventory_requested: bool = physical_key == KEY_I
2026-07-30 23:16:28 -04:00
var logbook_requested: bool = physical_key == KEY_L
var tackle_requested: bool = (
physical_key == KEY_V
or event.is_action_pressed("open_tacklebox")
)
2026-07-30 23:16:28 -04:00
if not inventory_requested and not tackle_requested and not logbook_requested:
return false
var requested_section: Section = (
2026-07-30 23:16:28 -04:00
Section.LOGBOOK
if logbook_requested
else (
Section.TACKLE_BOX
if tackle_requested
else _last_inventory_section
)
)
if visible:
if (
_current_section == requested_section
and not _transitioning
and not _page_transitioning
):
close_menu()
elif not _transitioning and not _page_transitioning:
_show_section(requested_section)
else:
if (
_fishing_spot == null
or not _fishing_spot.can_open_player_menu()
):
return false
_current_section = requested_section
open_menu()
return true
2026-07-30 23:16:28 -04:00
func _shortcut_blocked_by_modal_state() -> bool:
return (
_sale_confirmation.visible
or (
_current_section == Section.MAIL
and _mail_page.is_composing_letter()
)
or (
_current_section == Section.PROFILE
and _profile_page.has_modal_confirmation()
)
)
func _is_text_input_active() -> bool:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
return (
focus_owner != null
and focus_owner.is_visible_in_tree()
and (focus_owner is LineEdit or focus_owner is TextEdit)
)
func consume_escape() -> bool:
if not visible:
return false
2026-07-28 16:29:02 -04:00
if _transitioning or _page_transitioning:
return true
if _sale_confirmation.visible:
_close_sale_confirmation()
2026-07-29 20:10:44 -04:00
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
2026-07-25 16:02:34 -04:00
func open_menu() -> void:
if (
visible
or _shop_cooler_context_active
2026-07-25 16:02:34 -04:00
or _player == null
or _fishing_spot == null
or not _fishing_spot.can_open_player_menu()
):
return
_menu_generation += 1
2026-07-28 16:29:02 -04:00
_transition_generation += 1
_cancel_presentation_tween()
_cancel_page_tween()
2026-07-25 16:02:34 -04:00
_prior_movement_enabled = _player.is_movement_enabled()
_prior_camera_input_enabled = _player.is_camera_input_enabled()
2026-07-25 18:56:47 -04:00
_prior_mouse_mode = Input.mouse_mode
2026-07-25 16:02:34 -04:00
_control_snapshot_stored = true
2026-07-25 18:56:47 -04:00
_mouse_snapshot_stored = true
2026-07-25 16:02:34 -04:00
_player.set_movement_enabled(false)
_player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
2026-07-25 18:56:47 -04:00
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
2026-07-25 16:02:34 -04:00
visible = true
_refresh_all()
2026-07-28 16:29:02 -04:00
_show_section_immediate(_current_section)
_update_shell_layout()
_begin_menu_entry()
2026-07-25 16:02:34 -04:00
menu_visibility_changed.emit(true)
2026-08-03 09:31:13 -04:00
func open_section(section: Section) -> bool:
if (
_shop_cooler_context_active
or _player == null
or _fishing_spot == null
or not _fishing_spot.can_open_player_menu()
):
return false
if visible:
if _transitioning or _page_transitioning or _sale_confirmation.visible:
return false
if section != _current_section:
_show_section(section)
return true
_current_section = section
open_menu()
return visible
func mount_shop_cooler(
host: Control,
buyer: FishBuyerProfileType,
) -> bool:
if (
visible
or _shop_cooler_context_active
or host == null
or not is_instance_valid(host)
or buyer == null
or not buyer.is_valid()
or buyer.id != MAIN_SHOP_BUYER_ID
):
return false
_menu_generation += 1
_cancel_page_tween()
_transitioning = false
_page_transitioning = false
_sale_buyer_override = buyer
_shop_cooler_context_active = true
_current_section = Section.COOLER
_last_inventory_section = Section.COOLER
_cooler_page.reparent(host, false)
_sale_confirmation.reparent(host, false)
_cooler_page.position = Vector2.ZERO
_cooler_page.size = DESKTOP_REFERENCE_SIZE
_cooler_page.scale = Vector2.ONE
_cooler_page.modulate = Color.WHITE
_cooler_page.visible = true
_sale_confirmation.visible = false
_update_shell_layout()
_refresh_inventory()
_set_content_interactive(true)
_update_cooler_water_mask()
set_process(true)
call_deferred("_focus_shop_cooler")
return true
func unmount_shop_cooler() -> void:
if not _shop_cooler_context_active:
return
_close_sale_confirmation()
_set_content_interactive(false)
_cooler_page.visible = false
_cooler_page.reparent(_cooler_original_parent, false)
_cooler_original_parent.move_child(
_cooler_page,
mini(_cooler_original_index, _cooler_original_parent.get_child_count() - 1),
)
_sale_confirmation.reparent(_confirmation_original_parent, false)
_confirmation_original_parent.move_child(
_sale_confirmation,
mini(
_confirmation_original_index,
_confirmation_original_parent.get_child_count() - 1,
),
)
_sale_buyer_override = null
_shop_cooler_context_active = false
_menu_generation += 1
if not visible:
set_process(false)
func is_shop_cooler_mounted() -> bool:
return _shop_cooler_context_active
func cancel_shop_cooler_confirmation() -> void:
if _shop_cooler_context_active and _sale_confirmation.visible:
_close_sale_confirmation()
func _focus_shop_cooler() -> void:
if not _shop_cooler_context_active or not _cooler_page.is_visible_in_tree():
return
var focused_node := _fish_nodes.get(
_fish_selection.get_focused_id()
) as CoolerFishSpriteType
if focused_node != null and focused_node.focus_mode != Control.FOCUS_NONE:
focused_node.grab_focus()
return
for fish_catch: FishCatchType in _sorted_catches:
var fish_node := _fish_nodes.get(
fish_catch.catch_id
) as CoolerFishSpriteType
if fish_node != null and fish_node.focus_mode != Control.FOCUS_NONE:
fish_node.grab_focus()
return
_cooler_sort_option.grab_focus()
func close_menu(
2026-07-25 18:56:47 -04:00
reason: CloseReason = CloseReason.USER,
restore_controls: bool = true,
) -> void:
2026-07-25 16:02:34 -04:00
if not visible:
return
if (
reason == CloseReason.USER
and _current_section == Section.PROFILE
and _profile_page.request_close_confirmation()
):
return
2026-07-28 16:29:02 -04:00
if _transitioning:
if reason != CloseReason.USER:
_finish_close(reason, restore_controls, _menu_generation)
return
get_viewport().gui_cancel_drag()
_close_sale_confirmation()
2026-07-26 23:18:51 -04:00
if reason in [
CloseReason.BITE_STARTED,
CloseReason.WATER_RECOVERY,
CloseReason.SESSION_END,
CloseReason.TEARDOWN,
]:
_fish_selection.clear()
menu_exit_started.emit()
2026-07-28 16:29:02 -04:00
_begin_menu_exit(reason, restore_controls)
2026-07-25 16:02:34 -04:00
func close_for_water_recovery() -> void:
2026-07-26 23:18:51 -04:00
_fish_selection.clear()
2026-07-25 18:56:47 -04:00
close_menu(CloseReason.WATER_RECOVERY, false)
func close_for_game_menu() -> void:
close_menu(CloseReason.GAME_MENU)
func close_for_session_end() -> void:
2026-07-26 23:18:51 -04:00
_fish_selection.clear()
close_menu(CloseReason.SESSION_END, false)
2026-07-25 16:02:34 -04:00
func _exit_tree() -> void:
2026-07-28 16:29:02 -04:00
_cancel_presentation_tween()
_cancel_page_tween()
2026-07-25 16:02:34 -04:00
if visible:
2026-07-28 16:29:02 -04:00
_finish_close(CloseReason.TEARDOWN, false, _menu_generation)
2026-07-26 23:18:51 -04:00
_fish_selection.clear()
2026-07-25 16:02:34 -04:00
_menu_generation += 1
func _restore_player_controls(generation: int) -> void:
if (
generation != _menu_generation
or not _control_snapshot_stored
):
return
if _player != null and is_instance_valid(_player):
_player.set_movement_enabled(_prior_movement_enabled)
_player.set_camera_input_enabled(_prior_camera_input_enabled)
_control_snapshot_stored = false
2026-07-25 18:56:47 -04:00
func _apply_mouse_close_policy(reason: CloseReason) -> void:
if not _mouse_snapshot_stored:
return
match reason:
CloseReason.USER, CloseReason.BITE_STARTED:
Input.mouse_mode = _prior_mouse_mode
CloseReason.GAME_MENU:
# The Game Menu immediately assumes visible pointer ownership.
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
CloseReason.WATER_RECOVERY:
# Recovery remains authoritative over local input.
pass
CloseReason.SESSION_END:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
CloseReason.TEARDOWN:
pass
_mouse_snapshot_stored = false
2026-07-25 16:02:34 -04:00
func _on_bite_activated() -> void:
if visible:
close_menu(CloseReason.BITE_STARTED)
2026-07-25 16:02:34 -04:00
func _show_section(section: Section) -> void:
2026-07-28 16:29:02 -04:00
if (
section == _current_section
or _transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
):
return
if (
_current_section == Section.PROFILE
and _profile_page.request_close_confirmation()
):
return
2026-07-28 16:29:02 -04:00
_begin_page_transition(section)
func _show_section_immediate(section: Section) -> void:
2026-07-25 16:02:34 -04:00
_current_section = section
if _is_inventory_section(section):
_last_inventory_section = section
2026-07-25 16:02:34 -04:00
if not is_node_ready():
return
_configure_navigation_focus()
_cooler_page.visible = section == Section.COOLER
_bag_page.visible = section == Section.BAG
_tackle_box_page.visible = section == Section.TACKLE_BOX
_inventory_sub_tabs.visible = _is_inventory_section(section)
_logbook_page.visible = section == Section.LOGBOOK
_the_net_page.visible = section == Section.NET
2026-07-29 20:10:44 -04:00
_mail_page.visible = section == Section.MAIL
_profile_page.visible = section == Section.PROFILE
2026-07-29 23:10:51 -04:00
_players_page.visible = section == Section.PLAYERS
_content_shell.visible = false
_inventory_section.visible = false
_bag_section.visible = false
_content_shell.set_background_visible(true)
2026-07-28 16:29:02 -04:00
_header.visible = section != Section.COOLER
_separator.visible = section != Section.COOLER
if section == Section.COOLER:
_refresh_inventory()
else:
_fish_selection.clear()
_close_detail_constellation()
if section != Section.BAG:
_close_bag_detail()
else:
_update_bag_detail()
_refresh_tackle_box()
if section != Section.LOGBOOK:
_cancel_logbook_page_transition(true)
2026-07-30 23:16:28 -04:00
_catalog_logbook.deactivate()
else:
_catalog_logbook.activate()
if section == Section.NET:
_the_net_page.activate()
else:
_the_net_page.deactivate()
2026-07-29 20:10:44 -04:00
if section == Section.MAIL:
_mail_page.activate()
else:
_mail_page.deactivate()
if section == Section.PROFILE:
_profile_page.activate()
else:
_profile_page.deactivate()
2026-07-29 23:10:51 -04:00
if section == Section.PLAYERS:
_players_page.activate()
else:
_players_page.deactivate()
_inventory_tab.button_pressed = _is_inventory_section(section)
_cooler_sub_tab.set_selected(section == Section.COOLER)
_bag_sub_tab.set_selected(section == Section.BAG)
_tackle_sub_tab.set_selected(section == Section.TACKLE_BOX)
2026-07-31 10:20:18 -04:00
_refresh_inventory_organizer_tabs()
2026-07-25 16:02:34 -04:00
_logbook_tab.button_pressed = section == Section.LOGBOOK
_the_net_tab.button_pressed = section == Section.NET
2026-07-29 20:10:44 -04:00
_mail_tab.button_pressed = section == Section.MAIL
_profile_tab.button_pressed = section == Section.PROFILE
2026-07-29 23:10:51 -04:00
_players_tab.button_pressed = section == Section.PLAYERS
2026-07-28 16:29:02 -04:00
_update_navigation_selection()
_configure_active_page_focus()
inventory_hotbar_context_changed.emit(
section in [Section.COOLER, Section.BAG]
)
func _is_inventory_section(section: Section) -> bool:
return section in [Section.COOLER, Section.BAG, Section.TACKLE_BOX]
2026-07-31 10:20:18 -04:00
func _refresh_inventory_organizer_tabs() -> void:
for tab: OrganizerTabType in [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
2026-07-31 10:20:18 -04:00
]:
tab.refresh_state()
func _animate_inventory_tab_entry() -> void:
if not _is_inventory_section(_current_section):
return
var tabs: Array[OrganizerTabType] = [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
2026-07-31 10:20:18 -04:00
]
for index: int in tabs.size():
tabs[index].animate_entrance(float(index) * 0.025)
func _settle_inventory_tabs_for_close() -> void:
for tab: OrganizerTabType in [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
2026-07-31 10:20:18 -04:00
]:
tab.settle_for_close()
func _show_last_inventory_section() -> void:
_show_section(_last_inventory_section)
2026-07-25 16:02:34 -04:00
func _focus_current_section() -> void:
if _shop_cooler_context_active:
_focus_shop_cooler()
return
if _current_section == Section.COOLER:
_cooler_sub_tab.grab_focus()
elif _current_section == Section.BAG:
_bag_sub_tab.grab_focus()
elif _current_section == Section.TACKLE_BOX:
_tackle_sub_tab.grab_focus()
2026-07-29 20:10:44 -04:00
elif _current_section == Section.LOGBOOK:
2026-07-30 23:16:28 -04:00
_catalog_logbook.focus_initial()
elif _current_section == Section.NET:
_the_net_page.focus_initial()
elif _current_section == Section.MAIL:
2026-07-29 20:10:44 -04:00
_mail_tab.grab_focus()
2026-07-29 23:10:51 -04:00
elif _current_section == Section.PLAYERS:
_players_tab.grab_focus()
else:
_profile_tab.grab_focus()
2026-07-25 16:02:34 -04:00
2026-07-28 16:29:02 -04:00
func _process(delta: float) -> void:
if visible or _shop_cooler_context_active:
_motion_elapsed += delta
if visible:
_navigation_cluster.advance_motion(delta)
_apply_navigation_selection_presentation()
for fish_node: CoolerFishSpriteType in _fish_nodes.values():
fish_node.advance_presentation(delta, _motion_elapsed)
if not visible:
return
var bag_motion_enabled: bool = (
not _bag_drag_active and not get_viewport().gui_is_dragging()
)
for item_node: BagItemSpriteType in _bag_item_nodes.values():
item_node.advance_presentation(
_motion_elapsed,
bag_motion_enabled,
)
2026-07-28 16:29:02 -04:00
func _configure_navigation_focus() -> void:
var navigation: Array[BubbleButtonType] = [
_inventory_tab,
_logbook_tab,
_the_net_tab,
2026-07-29 20:10:44 -04:00
_mail_tab,
_profile_tab,
2026-07-29 23:10:51 -04:00
_players_tab,
2026-07-28 16:29:02 -04:00
_close_button,
]
for index: int in navigation.size():
var bubble: BubbleButtonType = navigation[index]
var previous: BubbleButtonType = navigation[
(index - 1 + navigation.size()) % navigation.size()
]
var next: BubbleButtonType = navigation[
(index + 1) % navigation.size()
]
bubble.focus_neighbor_left = bubble.get_path_to(previous)
bubble.focus_neighbor_right = bubble.get_path_to(next)
bubble.focus_neighbor_top = bubble.focus_neighbor_left
bubble.focus_neighbor_bottom = bubble.focus_neighbor_right
var inventory_tabs: Array[Button] = [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
]
for index: int in inventory_tabs.size():
var tab: Button = inventory_tabs[index]
tab.focus_neighbor_left = tab.get_path_to(
inventory_tabs[maxi(index - 1, 0)]
)
tab.focus_neighbor_right = tab.get_path_to(
inventory_tabs[mini(index + 1, inventory_tabs.size() - 1)]
)
tab.focus_neighbor_top = tab.get_path_to(_inventory_tab)
_inventory_tab.focus_neighbor_bottom = _inventory_tab.get_path_to(
_inventory_tab_for_section(_last_inventory_section)
)
_tackle_sub_tab.focus_neighbor_bottom = _tackle_sub_tab.get_path_to(
_bait_filter
)
2026-07-28 16:29:02 -04:00
func _inventory_tab_for_section(section: Section) -> Button:
match section:
Section.COOLER:
return _cooler_sub_tab
Section.BAG:
return _bag_sub_tab
_:
return _tackle_sub_tab
func _configure_sale_confirmation_focus() -> void:
_confirm_sale_button.focus_neighbor_left = (
_confirm_sale_button.get_path_to(_cancel_sale_button)
)
_confirm_sale_button.focus_neighbor_right = (
_confirm_sale_button.focus_neighbor_left
)
_confirm_sale_button.focus_neighbor_top = (
_confirm_sale_button.focus_neighbor_left
)
_confirm_sale_button.focus_neighbor_bottom = (
_confirm_sale_button.focus_neighbor_left
)
_cancel_sale_button.focus_neighbor_left = (
_cancel_sale_button.get_path_to(_confirm_sale_button)
)
_cancel_sale_button.focus_neighbor_right = (
_cancel_sale_button.focus_neighbor_left
)
_cancel_sale_button.focus_neighbor_top = (
_cancel_sale_button.focus_neighbor_left
)
_cancel_sale_button.focus_neighbor_bottom = (
_cancel_sale_button.focus_neighbor_left
)
if not _sale_confirmation.visible:
for button: Button in [_confirm_sale_button, _cancel_sale_button]:
button.focus_mode = Control.FOCUS_NONE
button.mouse_filter = Control.MOUSE_FILTER_IGNORE
func _configure_active_page_focus() -> void:
match _current_section:
Section.COOLER:
_configure_cooler_fish_focus()
Section.BAG:
_configure_bag_item_focus()
Section.TACKLE_BOX:
_bait_filter.grab_focus()
Section.LOGBOOK:
2026-07-30 23:16:28 -04:00
_catalog_logbook.focus_initial()
Section.NET:
_the_net_page.focus_initial()
2026-07-29 20:10:44 -04:00
Section.MAIL:
_mail_page.activate()
Section.PROFILE:
_profile_page.activate()
func _apply_inventory_styles() -> void:
for button: Button in [
_equipment_filter,
_consumables_filter,
_bait_filter,
_lures_filter,
]:
UtilityPageStyle.apply_ocean_button(button)
2026-07-31 09:29:27 -04:00
for panel: PanelContainer in [_tackle_main_panel]:
var panel_style := UtilityPageStyle.panel_style()
panel_style.set_corner_radius_all(INVENTORY_MAIN_CORNER_RADIUS)
panel.add_theme_stylebox_override(
"panel",
panel_style,
)
for label: Label in [_tackle_empty]:
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
label.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
)
_sale_confirmation.add_theme_stylebox_override(
"panel",
UtilityPageStyle.panel_style(),
)
_confirmation_message.add_theme_font_override(
"font",
UtilityPageStyle.TuffyFont,
)
_confirmation_message.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
)
UtilityPageStyle.apply_ocean_button(_confirm_sale_button)
UtilityPageStyle.apply_ocean_button(_cancel_sale_button)
func _set_bag_view(view: BagView) -> void:
if _bag_view == view:
return
_bag_view = view
_selected_bag_item_id = StringName()
_refresh_bag()
func _set_tackle_view(view: TackleView) -> void:
_tackle_view = view
_selected_tackle_item_id = StringName()
_refresh_tackle_box()
func _refresh_tackle_box() -> void:
if not is_node_ready():
return
_bait_filter.button_pressed = _tackle_view == TackleView.BAIT
_lures_filter.button_pressed = _tackle_view == TackleView.LURES
for child: Node in _tackle_item_list.get_children():
child.queue_free()
var matching_items: Array[OwnedItemType] = []
if _bag != null and _item_catalog != null:
for owned: OwnedItemType in _bag.get_all_items():
var item: ItemDataType = _item_catalog.get_item_by_id(
owned.item_id
)
if item == null:
continue
if (
_tackle_view == TackleView.BAIT
and item.category == ItemDataType.Category.BAIT
):
matching_items.append(owned)
elif (
_tackle_view == TackleView.LURES
and item.category == ItemDataType.Category.LURE
):
matching_items.append(owned)
matching_items.sort_custom(_sort_bag_items)
for owned: OwnedItemType in matching_items:
var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id)
var row := Button.new()
row.text = "%s ×%d" % [item.display_name, owned.quantity]
row.icon = item.icon
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
row.toggle_mode = true
row.button_pressed = owned.item_id == _selected_tackle_item_id
UtilityPageStyle.apply_ocean_button(row)
row.pressed.connect(_select_tackle_item.bind(owned.item_id))
_tackle_item_list.add_child(row)
_tackle_empty.text = (
"No bait collected."
if _tackle_view == TackleView.BAIT
else "No lures collected."
)
_tackle_empty.visible = matching_items.is_empty()
_update_tackle_detail()
func _select_tackle_item(item_id: StringName) -> void:
_selected_tackle_item_id = item_id
_refresh_tackle_box()
func _update_tackle_detail() -> void:
var item: ItemDataType = (
_item_catalog.get_item_by_id(_selected_tackle_item_id)
if _item_catalog != null and not _selected_tackle_item_id.is_empty()
else null
)
if item == null:
_tackle_detail_text.text = (
"Select bait for details."
if _tackle_view == TackleView.BAIT
else "Select a lure for details."
)
return
var quantity: int = _bag.get_quantity(item.item_id) if _bag != null else 0
var assigned_slot: int = -1
if _hotbar != null:
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
if _hotbar.get_item_id(slot_index) == item.item_id:
assigned_slot = slot_index
break
var state_text: String = (
"hotbar slot %d" % (assigned_slot + 1)
if assigned_slot >= 0
else "not assigned"
)
_tackle_detail_text.text = (
"%s\n\nType: %s\nQuantity: %d\n%s\n\n%s"
% [
item.display_name,
item.get_category_name(),
quantity,
state_text,
item.description,
]
)
2026-07-28 16:29:02 -04:00
func _apply_cooler_control_styles() -> void:
var profile: BubbleMenuProfile = _inventory_tab.profile
if profile == null:
return
_sort_option.add_theme_stylebox_override(
"normal",
profile.make_normal_style(),
)
_sort_option.add_theme_stylebox_override(
"hover",
profile.make_hover_style(),
)
_sort_option.add_theme_stylebox_override(
"focus",
profile.make_hover_style(),
)
_sort_option.add_theme_stylebox_override(
"pressed",
profile.make_pressed_style(),
)
_sort_option.add_theme_color_override("font_color", profile.text_color)
_sort_option.add_theme_color_override(
"font_hover_color",
profile.text_hover_color,
)
_sort_option.add_theme_color_override(
"font_focus_color",
profile.text_hover_color,
)
func _apply_cooler_wall_styles() -> void:
var outer := StyleBoxFlat.new()
outer.bg_color = Color(0.76, 0.9, 0.96, 1.0)
outer.border_color = Color(0.91, 0.98, 1.0, 1.0)
outer.set_border_width_all(7)
outer.set_corner_radius_all(INVENTORY_MAIN_CORNER_RADIUS)
_cooler_outer_wall.add_theme_stylebox_override("panel", outer)
var inner := StyleBoxFlat.new()
inner.bg_color = Color(0.018, 0.16, 0.25, 1.0)
inner.border_color = Color(0.56, 0.78, 0.86, 1.0)
inner.set_border_width_all(5)
inner.set_corner_radius_all(INVENTORY_INNER_CORNER_RADIUS)
_cooler_inner_liner.add_theme_stylebox_override("panel", inner)
var scroll_bar := _cooler_scroll.get_v_scroll_bar()
var scroll_grabber := StyleBoxFlat.new()
scroll_grabber.bg_color = Color(0.56, 0.78, 0.86, 0.58)
scroll_grabber.set_corner_radius_all(6)
scroll_bar.add_theme_stylebox_override("grabber", scroll_grabber)
scroll_bar.add_theme_stylebox_override("grabber_highlight", scroll_grabber)
scroll_bar.add_theme_stylebox_override("grabber_pressed", scroll_grabber)
var scroll_track := StyleBoxFlat.new()
scroll_track.bg_color = Color(0.02, 0.1, 0.14, 0.28)
scroll_track.set_corner_radius_all(6)
scroll_bar.add_theme_stylebox_override("scroll", scroll_track)
func _apply_cooler_notepad_style() -> void:
2026-07-31 09:29:27 -04:00
_detail_bubble.add_theme_stylebox_override(
"panel",
2026-07-31 16:51:18 -04:00
InventoryNotepadType.make_layout_style(false),
2026-07-31 09:29:27 -04:00
)
InventoryNotepadType.apply_handwritten_to(_detail_constellation)
func _apply_navigation_styles() -> void:
var profile: BubbleMenuProfile = _inventory_tab.profile
if profile == null:
return
var normal := _make_navigation_style(profile.normal_fill, 2, 0.24)
var hover := _make_navigation_style(
profile.hover_fill.lightened(0.025),
4,
0.34,
)
var pressed := _make_navigation_style(
profile.pressed_fill.lightened(0.055),
5,
0.40,
)
var disabled := _make_navigation_style(profile.disabled_fill, 1, 0.14)
for bubble: BubbleButtonType in [
_inventory_tab,
_logbook_tab,
_the_net_tab,
2026-07-29 20:10:44 -04:00
_mail_tab,
_profile_tab,
2026-07-29 23:10:51 -04:00
_players_tab,
_close_button,
]:
bubble.add_theme_stylebox_override("normal", normal)
bubble.add_theme_stylebox_override("hover", hover)
bubble.add_theme_stylebox_override("focus", hover)
bubble.add_theme_stylebox_override("pressed", pressed)
bubble.add_theme_stylebox_override("disabled", disabled)
func _make_navigation_style(
fill: Color,
shadow_size: int,
shadow_alpha: float,
) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = fill
style.set_corner_radius_all(128)
style.set_border_width_all(0)
style.shadow_color = Color(0.015, 0.06, 0.09, shadow_alpha)
style.shadow_size = shadow_size
style.shadow_offset = Vector2(0.0, 2.0)
return style
func _apply_navigation_selection_presentation() -> void:
for bubble: BubbleButtonType in [
_inventory_tab,
_logbook_tab,
_the_net_tab,
2026-07-29 20:10:44 -04:00
_mail_tab,
_profile_tab,
2026-07-29 23:10:51 -04:00
_players_tab,
]:
if not bubble.button_pressed:
continue
2026-07-29 10:53:46 -04:00
bubble.scale *= NAVIGATION_SELECTED_SCALE
func _apply_bag_styles() -> void:
var outer := StyleBoxFlat.new()
outer.bg_color = UtilityPageStyle.OCEAN_PANEL_DEEP
outer.set_border_width_all(0)
outer.set_corner_radius_all(INVENTORY_MAIN_CORNER_RADIUS)
_bag_outer_wall.add_theme_stylebox_override("panel", outer)
var inner := StyleBoxFlat.new()
inner.bg_color = UtilityPageStyle.OCEAN_PANEL_MID
inner.set_border_width_all(0)
inner.set_corner_radius_all(INVENTORY_INNER_CORNER_RADIUS)
_bag_inner_liner.add_theme_stylebox_override("panel", inner)
func _apply_logbook_styles() -> void:
var backing := StyleBoxFlat.new()
backing.bg_color = Color(0.54, 0.42, 0.27, 1.0)
backing.border_color = Color(0.72, 0.59, 0.39, 1.0)
backing.set_border_width_all(6)
backing.corner_radius_top_left = 42
backing.corner_radius_top_right = 34
backing.corner_radius_bottom_right = 46
backing.corner_radius_bottom_left = 36
_book_backing.add_theme_stylebox_override("panel", backing)
var left_paper := StyleBoxFlat.new()
left_paper.bg_color = Color(0.95, 0.91, 0.79, 1.0)
left_paper.border_color = Color(0.80, 0.70, 0.52, 1.0)
left_paper.set_border_width_all(3)
left_paper.corner_radius_top_left = 30
left_paper.corner_radius_bottom_left = 38
left_paper.corner_radius_top_right = 8
left_paper.corner_radius_bottom_right = 6
_left_page.add_theme_stylebox_override("panel", left_paper)
var right_paper := StyleBoxFlat.new()
right_paper.bg_color = Color(0.925, 0.875, 0.74, 1.0)
right_paper.border_color = Color(0.77, 0.66, 0.48, 1.0)
right_paper.set_border_width_all(3)
right_paper.corner_radius_top_left = 8
right_paper.corner_radius_bottom_left = 6
right_paper.corner_radius_top_right = 34
right_paper.corner_radius_bottom_right = 40
_right_page.add_theme_stylebox_override("panel", right_paper)
for label: Label in [_left_heading, _right_heading, _logbook_empty_state]:
label.add_theme_color_override(
"font_color",
Color(0.22, 0.16, 0.09, 1.0),
)
var page_profile: BubbleMenuProfile = _logbook_previous.profile
if page_profile != null:
var disabled_style: StyleBoxFlat = page_profile.make_normal_style()
disabled_style.bg_color.a = 0.82
disabled_style.border_color.a = 0.72
for control: BubbleButtonType in [
_logbook_previous,
_logbook_next,
]:
control.add_theme_stylebox_override(
"disabled",
disabled_style.duplicate(),
)
control.add_theme_color_override(
"font_disabled_color",
Color(0.035, 0.145, 0.22, 0.68),
)
_logbook_page_status.set_content("pages", "1 / 1")
2026-07-28 16:29:02 -04:00
func _update_navigation_selection() -> void:
_set_navigation_target(_current_section)
2026-07-29 20:10:44 -04:00
func _on_mail_unread_count_changed(count: int) -> void:
_mail_unread_badge.visible = count > 0
_mail_unread_badge.text = "99+" if count > 99 else str(count)
2026-07-28 16:29:02 -04:00
func _set_navigation_target(section: Section) -> void:
_inventory_tab.button_pressed = _is_inventory_section(section)
2026-07-28 16:29:02 -04:00
_logbook_tab.button_pressed = section == Section.LOGBOOK
_the_net_tab.button_pressed = section == Section.NET
2026-07-29 20:10:44 -04:00
_mail_tab.button_pressed = section == Section.MAIL
_profile_tab.button_pressed = section == Section.PROFILE
2026-07-29 23:10:51 -04:00
_players_tab.button_pressed = section == Section.PLAYERS
2026-07-28 16:29:02 -04:00
func _update_shell_layout() -> void:
if not is_node_ready():
return
2026-07-29 10:53:46 -04:00
var compact: bool = false
2026-07-28 16:29:02 -04:00
_compact_layout = compact
2026-07-29 10:53:46 -04:00
var reference_size := DESKTOP_REFERENCE_SIZE
_presentation_scale_root.size = reference_size
if not _transitioning:
_presentation_scale_root.scale = Vector2.ONE
2026-07-29 10:53:46 -04:00
_presentation_scale_root.position = Vector2.ZERO
_presentation_scale_root.pivot_offset = reference_size * 0.5
_content_shell.position = Vector2(14.0, 92.0) if compact else Vector2(42.0, 104.0)
_content_shell.size = Vector2(612.0, 286.0) if compact else Vector2(1196.0, 478.0)
2026-07-28 16:29:02 -04:00
_presentation_rest_position = _content_shell.position
var navigation_size := (
Vector2(840.0, 75.0) if compact else Vector2(840.0, 100.0)
2026-07-28 16:29:02 -04:00
)
2026-07-29 10:53:46 -04:00
_navigation_cluster.position = NAVIGATION_CANONICAL_POSITION
2026-07-28 16:29:02 -04:00
_navigation_cluster.size = navigation_size
2026-07-29 10:53:46 -04:00
_navigation_cluster.scale = (
Vector2.ONE * NAVIGATION_PRESENTATION_SCALE
)
2026-07-28 16:29:02 -04:00
_navigation_cluster.apply_layout(navigation_size, compact)
_cooler_page.size = reference_size
_cooler_page.position = Vector2.ZERO
_cooler_rest_position = Vector2.ZERO
# The Bag shell has the widest 88 px upper-left curve. Eight more pixels
# place every tab flange behind a straight section of the shared panel.
_inventory_sub_tabs.position = (
INVENTORY_MAIN_POSITION
+ Vector2(INVENTORY_TAB_LEFT_INSET, -30.0)
)
_layout_cooler_fish(false)
2026-07-31 09:29:27 -04:00
_cooler_outer_wall.position = INVENTORY_MAIN_POSITION
_cooler_outer_wall.size = INVENTORY_MAIN_SIZE
_detail_constellation.position = INVENTORY_NOTEPAD_POSITION
_detail_constellation.size = INVENTORY_NOTEPAD_SIZE
_notepad_binding.position = Vector2(44.0, 1.0) if compact else Vector2(28.0, 4.0)
_notepad_binding.size = Vector2(218.0, 20.0) if compact else Vector2(232.0, 27.0)
_notepad_binding.add_theme_font_size_override("font_size", 13 if compact else 20)
2026-07-29 10:53:46 -04:00
_notepad_title.position = Vector2(10.0, 14.0) if compact else Vector2(20.0, 31.0)
_notepad_title.size = Vector2(286.0, 18.0) if compact else Vector2(248.0, 28.0)
2026-07-29 10:53:46 -04:00
_notepad_title.add_theme_font_size_override("font_size", 14 if compact else 20)
_notepad_rule.position = Vector2(10.0, 49.0) if compact else Vector2(18.0, 87.0)
_notepad_rule.size = Vector2(286.0, 2.0) if compact else Vector2(252.0, 2.0)
_cooler_sort_controls.position = (
2026-07-29 10:53:46 -04:00
Vector2(10.0, 51.0) if compact else Vector2(18.0, 98.0)
)
_cooler_sort_controls.size = (
Vector2(286.0, 31.0) if compact else Vector2(252.0, 38.0)
)
_cooler_sort_controls.get_child(0).visible = false
_cooler_sort_option.custom_minimum_size = (
Vector2(140.0, 31.0) if compact else Vector2(123.0, 38.0)
)
_cooler_sort_direction.custom_minimum_size = (
Vector2(140.0, 31.0) if compact else Vector2(123.0, 38.0)
)
2026-07-29 10:53:46 -04:00
_cooler_sort_option.set_choice_font_size(12 if compact else 15)
_cooler_sort_direction.add_theme_font_size_override(
"font_size",
12 if compact else 14,
)
_notepad_wallet_value.position = (
Vector2(10.0, 33.0) if compact else Vector2(18.0, 61.0)
)
_notepad_wallet_value.size = (
Vector2(138.0, 17.0) if compact else Vector2(120.0, 23.0)
)
_notepad_capacity_value.position = (
Vector2(154.0, 33.0) if compact else Vector2(146.0, 61.0)
)
_notepad_capacity_value.size = (
Vector2(142.0, 17.0) if compact else Vector2(124.0, 23.0)
)
for status_value: Label in [
_notepad_wallet_value,
_notepad_capacity_value,
]:
2026-07-29 10:53:46 -04:00
status_value.add_theme_font_size_override(
"font_size",
2026-07-29 10:53:46 -04:00
10 if compact else 14,
)
_cooler_detail_texture.position = (
2026-07-29 10:53:46 -04:00
Vector2(5.0, 84.0) if compact else Vector2(80.0, 137.0)
)
_cooler_detail_texture.size = (
2026-07-29 10:53:46 -04:00
Vector2(74.0, 58.0) if compact else Vector2(128.0, 100.0)
)
_cooler_detail_name.position = (
2026-07-29 10:53:46 -04:00
Vector2(82.0, 82.0) if compact else Vector2(18.0, 241.0)
)
_cooler_detail_name.size = (
2026-07-29 10:53:46 -04:00
Vector2(214.0, 26.0) if compact else Vector2(252.0, 32.0)
)
_cooler_detail_name.add_theme_font_size_override(
2026-07-29 10:53:46 -04:00
"font_size", 18 if compact else 25,
)
2026-07-29 10:53:46 -04:00
_cooler_detail_stats.position = (
Vector2(82.0, 109.0) if compact else Vector2(18.0, 274.0)
)
2026-07-29 10:53:46 -04:00
_cooler_detail_stats.size = (
Vector2(214.0, 39.0) if compact else Vector2(252.0, 65.0)
)
2026-07-29 10:53:46 -04:00
_layout_cooler_detail_text(compact)
_cooler_selection_summary.position = (
Vector2(10.0, 151.0) if compact else Vector2(18.0, 346.0)
)
_cooler_selection_summary.size = (
2026-07-29 10:53:46 -04:00
Vector2(130.0, 35.0) if compact else Vector2(252.0, 40.0)
)
2026-07-29 10:53:46 -04:00
_layout_cooler_selection_text(compact)
_favorite_bubble.custom_minimum_size = (
Vector2(72.0, 42.0) if compact else Vector2(108.0, 50.0)
)
_sell_bubble.custom_minimum_size = (
Vector2(74.0, 42.0) if compact else Vector2(118.0, 50.0)
)
_favorite_bubble.position = (
Vector2(144.0, 148.0) if compact else Vector2(26.0, 378.0)
)
_favorite_bubble.size = (
Vector2(72.0, 42.0) if compact else Vector2(108.0, 50.0)
)
_sell_bubble.position = (
Vector2(222.0, 148.0) if compact else Vector2(144.0, 378.0)
)
_sell_bubble.size = (
Vector2(74.0, 42.0) if compact else Vector2(118.0, 50.0)
)
_favorite_bubble.add_theme_font_size_override(
2026-07-29 10:53:46 -04:00
"font_size", 11 if compact else 17,
)
_sell_bubble.add_theme_font_size_override(
2026-07-29 10:53:46 -04:00
"font_size", 11 if compact else 17,
)
_update_sort_direction_text()
_bag_page.size = reference_size
_bag_page.position = Vector2.ZERO
_bag_rest_position = Vector2.ZERO
_tackle_box_page.size = reference_size
_tackle_box_page.position = Vector2.ZERO
_tackle_rest_position = Vector2.ZERO
2026-07-31 09:29:27 -04:00
_bag_outer_wall.position = INVENTORY_MAIN_POSITION
_bag_outer_wall.size = INVENTORY_MAIN_SIZE
_bag_filter_tabs.position = (
INVENTORY_MAIN_POSITION + INVENTORY_HEADER_INSET
)
2026-07-31 09:29:27 -04:00
_bag_filter_tabs.size = Vector2(320.0, INVENTORY_HEADER_HEIGHT)
_bag_host.custom_minimum_size = (
Vector2(520.0, 152.0) if compact else Vector2(788.0, 358.0)
)
_bag_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
2026-07-31 09:29:27 -04:00
_bag_detail_constellation.position = INVENTORY_NOTEPAD_POSITION
_bag_detail_constellation.size = INVENTORY_NOTEPAD_SIZE
_bag_sprite_detail_texture.custom_minimum_size = (
Vector2(72.0, 48.0) if compact else Vector2(100.0, 72.0)
)
_bag_sprite_detail_name.add_theme_font_size_override(
"font_size",
18 if compact else 22,
)
_bag_sprite_detail_data.add_theme_font_size_override(
"font_size",
12 if compact else 15,
)
2026-07-31 09:29:27 -04:00
_tackle_main_panel.position = INVENTORY_MAIN_POSITION
_tackle_main_panel.size = INVENTORY_MAIN_SIZE
_tackle_detail_panel.position = INVENTORY_NOTEPAD_POSITION
_tackle_detail_panel.size = INVENTORY_NOTEPAD_SIZE
_logbook_page.size = reference_size
_logbook_page.position = Vector2.ZERO
_logbook_rest_position = Vector2.ZERO
_the_net_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
_the_net_page.size = reference_size
_the_net_page.position = Vector2.ZERO
_the_net_rest_position = Vector2.ZERO
_mail_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
2026-07-29 20:10:44 -04:00
_mail_page.size = reference_size
_mail_page.position = Vector2.ZERO
_mail_rest_position = Vector2.ZERO
_profile_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
_profile_page.position = Vector2.ZERO
_profile_page.size = reference_size
_profile_rest_position = Vector2.ZERO
_players_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
_players_page.position = Vector2.ZERO
_players_page.size = reference_size
_sale_confirmation.size = SALE_CONFIRMATION_SIZE
_sale_confirmation.position = (
(reference_size - SALE_CONFIRMATION_SIZE) * 0.5
)
_cooler_host.custom_minimum_size = Vector2(
2026-07-31 09:29:27 -04:00
520.0 if compact else 800.0,
512.0 if compact else 420.0,
)
2026-07-28 16:29:02 -04:00
_bag_grid.columns = 2 if compact else 3
_bag_list.custom_minimum_size.x = 300.0 if compact else 360.0
_bag_detail.custom_minimum_size.x = 176.0 if compact else 220.0
_content_stage.custom_minimum_size.y = 220.0 if compact else 260.0
_content_rest_position = _content_stage.position
if not _transitioning:
_content_shell.position = _presentation_rest_position
_cooler_page.position = _cooler_rest_position
_bag_page.position = _bag_rest_position
_tackle_box_page.position = _tackle_rest_position
_logbook_page.position = _logbook_rest_position
_the_net_page.position = _the_net_rest_position
2026-07-29 20:10:44 -04:00
_mail_page.position = _mail_rest_position
_profile_page.position = _profile_rest_position
_players_page.position = Vector2.ZERO
_cooler_page.modulate.a = 1.0
_bag_page.modulate.a = 1.0
_tackle_box_page.modulate.a = 1.0
_logbook_page.modulate.a = 1.0
_the_net_page.modulate.a = 1.0
2026-07-29 20:10:44 -04:00
_mail_page.modulate.a = 1.0
_profile_page.modulate.a = 1.0
_players_page.modulate.a = 1.0
2026-07-28 16:29:02 -04:00
if not _page_transitioning:
_content_stage.position = _content_rest_position
_layout_cooler_fish(false)
_layout_bag_items()
2026-07-28 16:29:02 -04:00
2026-07-29 10:53:46 -04:00
func _layout_cooler_detail_text(compact: bool) -> void:
var weight_row_height: float = 17.0 if compact else 24.0
_cooler_weight_row.position = Vector2.ZERO
_cooler_weight_row.size = Vector2(
_cooler_detail_stats.size.x,
weight_row_height,
)
_cooler_offer_row.position = Vector2(
0.0,
17.0 if compact else 27.0,
)
_cooler_offer_row.size = Vector2(
_cooler_detail_stats.size.x,
22.0 if compact else 30.0,
)
_cooler_weight_value.add_theme_font_size_override(
"font_size",
9 if compact else 12,
)
_cooler_weight_unit.add_theme_font_size_override(
"font_size",
10 if compact else 14,
)
_cooler_offer_label.add_theme_font_size_override(
"font_size",
13 if compact else 18,
)
_cooler_offer_value.add_theme_font_size_override(
"font_size",
10 if compact else 15,
)
func _layout_cooler_selection_text(compact: bool) -> void:
var row_height: float = 17.5 if compact else 20.0
for numeric_label: Label in [
_cooler_selected_count_value,
_cooler_combined_offer_value,
]:
numeric_label.add_theme_font_size_override(
"font_size",
10 if compact else 14,
)
for word_label: Label in [
_cooler_selection_empty,
_cooler_selected_count_label,
_cooler_combined_offer_label,
]:
word_label.add_theme_font_size_override(
"font_size",
12 if compact else 18,
)
_cooler_selected_count_row.position = Vector2.ZERO
_cooler_selected_count_row.size = Vector2(
_cooler_selection_summary.size.x,
row_height,
)
_cooler_combined_offer_row.position = Vector2(0.0, row_height)
_cooler_combined_offer_row.size = Vector2(
_cooler_selection_summary.size.x,
row_height,
)
2026-07-28 16:29:02 -04:00
func _begin_menu_entry() -> void:
_transitioning = true
_set_shell_interactive(false)
_reset_page_transition_visuals()
2026-07-31 10:20:18 -04:00
_animate_inventory_tab_entry()
2026-07-28 16:29:02 -04:00
set_process(true)
var generation: int = _transition_generation
_presentation_scale_root.pivot_offset = (
_presentation_scale_root.size * 0.5
)
_presentation_scale_root.modulate.a = 0.0
_presentation_scale_root.scale = (
Vector2.ONE * UIMotion.PLAYER_MENU_ENTER_SCALE
)
2026-07-28 16:29:02 -04:00
_presentation_tween = create_tween().set_parallel(true)
_presentation_tween.tween_property(
_presentation_scale_root,
"modulate:a",
1.0,
UIMotion.PLAYER_MENU_ENTER_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
2026-07-28 16:29:02 -04:00
_presentation_tween.tween_property(
_presentation_scale_root,
"scale",
Vector2.ONE,
UIMotion.PLAYER_MENU_ENTER_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
2026-07-28 16:29:02 -04:00
_presentation_tween.chain().tween_callback(
_finish_menu_entry.bind(generation)
)
func _finish_menu_entry(generation: int) -> void:
if generation != _transition_generation or not visible:
return
_presentation_tween = null
_transitioning = false
_set_shell_interactive(true)
_focus_current_section()
func _begin_menu_exit(reason: CloseReason, restore_controls: bool) -> void:
if reason != CloseReason.USER:
_finish_close(reason, restore_controls, _menu_generation)
return
_transition_generation += 1
_cancel_presentation_tween()
_transitioning = true
_set_shell_interactive(false)
2026-07-31 10:20:18 -04:00
_settle_inventory_tabs_for_close()
if _current_section == Section.LOGBOOK:
_catalog_logbook.deactivate()
elif _current_section == Section.NET:
_the_net_page.deactivate()
2026-07-28 16:29:02 -04:00
get_viewport().gui_release_focus()
var generation: int = _transition_generation
var closing_generation: int = _menu_generation
_presentation_tween = create_tween().set_parallel(true)
_presentation_tween.tween_property(
_presentation_scale_root,
"modulate:a",
0.0,
UIMotion.PLAYER_MENU_EXIT_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
2026-07-28 16:29:02 -04:00
_presentation_tween.tween_property(
_presentation_scale_root,
"scale",
Vector2.ONE * UIMotion.PLAYER_MENU_EXIT_SCALE,
UIMotion.PLAYER_MENU_EXIT_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
2026-07-28 16:29:02 -04:00
_presentation_tween.chain().tween_callback(
_finish_menu_exit.bind(
generation,
reason,
restore_controls,
closing_generation,
)
)
func _finish_menu_exit(
generation: int,
reason: CloseReason,
restore_controls: bool,
closing_generation: int,
) -> void:
if generation != _transition_generation or not visible:
return
_presentation_tween = null
_finish_close(reason, restore_controls, closing_generation)
func _finish_close(
reason: CloseReason,
restore_controls: bool,
closing_generation: int,
) -> void:
_cancel_presentation_tween()
_cancel_page_tween()
_cancel_logbook_page_transition(true)
_the_net_page.deactivate()
2026-07-28 16:29:02 -04:00
_transitioning = false
_page_transitioning = false
_bag_drag_active = false
_reset_page_transition_visuals()
_presentation_scale_root.modulate.a = 1.0
_presentation_scale_root.scale = Vector2.ONE
2026-07-28 16:29:02 -04:00
visible = false
set_process(false)
get_viewport().gui_release_focus()
if _fishing_spot != null and is_instance_valid(_fishing_spot):
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
if restore_controls:
_restore_player_controls(closing_generation)
else:
_control_snapshot_stored = false
_apply_mouse_close_policy(reason)
_menu_generation += 1
_transition_generation += 1
menu_visibility_changed.emit(false)
func _get_section_root(section: Section) -> Control:
match section:
Section.COOLER:
return _cooler_page
Section.BAG:
return _bag_page
Section.TACKLE_BOX:
return _tackle_box_page
2026-07-29 20:10:44 -04:00
Section.LOGBOOK:
return _logbook_page
Section.NET:
return _the_net_page
Section.MAIL:
2026-07-29 20:10:44 -04:00
return _mail_page
Section.PROFILE:
return _profile_page
_:
return _players_page
func _get_section_rest_position(section: Section) -> Vector2:
match section:
Section.COOLER:
return _cooler_rest_position
Section.BAG:
return _bag_rest_position
Section.TACKLE_BOX:
return _tackle_rest_position
2026-07-29 20:10:44 -04:00
Section.LOGBOOK:
return _logbook_rest_position
Section.NET:
return _the_net_rest_position
Section.MAIL:
2026-07-29 20:10:44 -04:00
return _mail_rest_position
Section.PROFILE:
return _profile_rest_position
_:
return Vector2.ZERO
2026-07-28 16:29:02 -04:00
func _begin_page_transition(section: Section) -> void:
_cancel_logbook_page_transition(true)
2026-07-28 16:29:02 -04:00
_page_transition_generation += 1
_cancel_page_tween()
_page_transitioning = true
_set_content_interactive(false)
get_viewport().gui_release_focus()
var outgoing_section: Section = _current_section
var outgoing_inventory: bool = _is_inventory_section(outgoing_section)
var incoming_inventory: bool = _is_inventory_section(section)
if outgoing_inventory and not incoming_inventory:
2026-07-31 10:20:18 -04:00
_settle_inventory_tabs_for_close()
2026-07-28 16:29:02 -04:00
_set_navigation_target(section)
var generation: int = _page_transition_generation
_page_outgoing_content_root = _get_section_root(outgoing_section)
_page_outgoing_root = (
_inventory_transition_group
if outgoing_inventory and not incoming_inventory
else _page_outgoing_content_root
)
_page_incoming_root = (
_inventory_transition_group
if incoming_inventory and not outgoing_inventory
else _get_section_root(section)
)
var outgoing_rest: Vector2 = _get_section_rest_position(outgoing_section)
var incoming_rest: Vector2 = _get_section_rest_position(section)
_show_section_immediate(section)
if outgoing_inventory and not incoming_inventory:
# Immediate page selection hides Inventory. Restore the outgoing content
# beneath its still-opaque masking group for the coordinated fade-out.
_page_outgoing_content_root.visible = true
_inventory_sub_tabs.visible = true
_inventory_transition_group.visible = true
_page_outgoing_root.visible = true
_page_incoming_root.visible = true
_page_outgoing_root.position = outgoing_rest
_page_incoming_root.position = incoming_rest
_page_outgoing_root.modulate.a = 1.0
_page_incoming_root.modulate.a = 0.0
if incoming_inventory and not outgoing_inventory:
_animate_inventory_tab_entry()
var duration: float = (
UIMotion.PLAYER_MENU_INVENTORY_DURATION
if outgoing_inventory and incoming_inventory
else UIMotion.PLAYER_MENU_PAGE_DURATION
)
_page_tween = create_tween().set_parallel(true)
2026-07-28 16:29:02 -04:00
_page_tween.tween_property(
_page_outgoing_root,
2026-07-28 16:29:02 -04:00
"modulate:a",
0.0,
duration,
2026-07-28 16:29:02 -04:00
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_page_tween.tween_property(
_page_incoming_root,
2026-07-28 16:29:02 -04:00
"modulate:a",
1.0,
duration,
2026-07-28 16:29:02 -04:00
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_page_tween.finished.connect(
_finish_page_transition.bind(generation),
CONNECT_ONE_SHOT
)
func _finish_page_transition(generation: int) -> void:
if generation != _page_transition_generation or not visible:
return
_page_tween = null
_page_transitioning = false
2026-07-31 09:29:27 -04:00
if (
_page_outgoing_root != null
and _page_outgoing_root != _page_incoming_root
and _page_outgoing_root != _inventory_transition_group
2026-07-31 09:29:27 -04:00
):
_page_outgoing_root.visible = false
if (
_page_outgoing_content_root != null
and not _is_inventory_section(_current_section)
):
_page_outgoing_content_root.visible = false
_inventory_sub_tabs.visible = _is_inventory_section(_current_section)
_inventory_sub_tabs.modulate.a = 1.0
_reset_page_transition_visuals()
2026-07-28 16:29:02 -04:00
_set_content_interactive(true)
_focus_current_section()
func _set_shell_interactive(interactive: bool) -> void:
_set_content_interactive(interactive)
for bubble: BubbleButtonType in [
_inventory_tab,
_logbook_tab,
_the_net_tab,
2026-07-29 20:10:44 -04:00
_mail_tab,
_profile_tab,
2026-07-29 23:10:51 -04:00
_players_tab,
2026-07-28 16:29:02 -04:00
_close_button,
]:
bubble.focus_mode = Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
bubble.mouse_filter = (
Control.MOUSE_FILTER_STOP
if interactive
else Control.MOUSE_FILTER_IGNORE
)
func _set_content_interactive(interactive: bool) -> void:
_content_stage.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive
else Control.MOUSE_FILTER_IGNORE
)
_cooler_page.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive and _current_section == Section.COOLER
else Control.MOUSE_FILTER_IGNORE
)
_bag_page.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive and _current_section == Section.BAG
else Control.MOUSE_FILTER_IGNORE
)
_tackle_box_page.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive and _current_section == Section.TACKLE_BOX
else Control.MOUSE_FILTER_IGNORE
)
for tab: Button in [
_cooler_sub_tab, _bag_sub_tab, _tackle_sub_tab,
]:
var tab_interactive: bool = (
interactive and _is_inventory_section(_current_section)
)
tab.focus_mode = (
Control.FOCUS_ALL if tab_interactive else Control.FOCUS_NONE
)
tab.mouse_filter = (
Control.MOUSE_FILTER_STOP
if tab_interactive and not tab.button_pressed
else Control.MOUSE_FILTER_IGNORE
)
_logbook_page.mouse_filter = (
Control.MOUSE_FILTER_PASS
if interactive and _current_section == Section.LOGBOOK
else Control.MOUSE_FILTER_IGNORE
)
2026-07-30 23:16:28 -04:00
_catalog_logbook.set_interactive(
interactive and _current_section == Section.LOGBOOK
)
_the_net_page.set_interactive(
interactive and _current_section == Section.NET
)
2026-07-29 20:10:44 -04:00
_mail_page.set_interactive(
interactive and _current_section == Section.MAIL
)
_profile_page.set_interactive(
interactive and _current_section == Section.PROFILE
)
var cooler_interactive: bool = (
interactive and _current_section == Section.COOLER
)
_cooler_sort_option.disabled = not cooler_interactive
_cooler_sort_direction.disabled = not cooler_interactive
_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
else Control.MOUSE_FILTER_IGNORE
)
var detail_interactive: bool = (
cooler_interactive and _detail_constellation.visible
)
for action: NotepadInkActionType in [_favorite_bubble, _sell_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
else Control.MOUSE_FILTER_IGNORE
)
action.refresh_ink_state()
var bag_interactive: bool = (
interactive
and _current_section == Section.BAG
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
else Control.MOUSE_FILTER_IGNORE
)
2026-07-28 16:29:02 -04:00
func _cancel_presentation_tween() -> void:
if _presentation_tween != null:
_presentation_tween.kill()
_presentation_tween = null
func _cancel_page_tween() -> void:
if _page_tween != null:
_page_tween.kill()
_page_tween = null
_reset_page_transition_visuals()
2026-07-28 16:29:02 -04:00
func _reset_page_transition_visuals() -> void:
2026-07-29 20:10:44 -04:00
for section: Section in [
Section.COOLER,
Section.BAG,
Section.TACKLE_BOX,
Section.LOGBOOK,
Section.NET,
Section.MAIL,
Section.PROFILE,
Section.PLAYERS,
2026-07-29 20:10:44 -04:00
]:
var page: Control = _get_section_root(section)
page.position = _get_section_rest_position(section)
page.modulate.a = 1.0
if _inventory_transition_group != null:
_inventory_transition_group.modulate.a = 1.0
_inventory_transition_group.visible = true
_inventory_sub_tabs.modulate.a = 1.0
_page_outgoing_root = null
_page_incoming_root = null
_page_outgoing_content_root = null
2026-07-28 16:29:02 -04:00
func _on_sort_selected(index: int, source: OptionButton) -> void:
var selected_id: int = source.get_item_id(index)
if selected_id < SortMode.CATCH_ORDER or selected_id > SortMode.RARITY:
return
_sort_mode = selected_id as SortMode
_sort_option.select(_sort_mode)
_cooler_sort_option.select(_sort_mode)
2026-07-25 16:02:34 -04:00
_refresh_inventory()
2026-07-29 10:53:46 -04:00
func _on_cooler_sort_selected(sort_id: int) -> void:
_sort_mode = sort_id
_sort_option.select(_sort_mode)
_cooler_sort_option.select(_sort_mode)
_refresh_inventory()
2026-07-25 16:02:34 -04:00
func _on_sort_direction_pressed() -> void:
_sort_descending = not _sort_descending
_update_sort_direction_text()
_refresh_inventory()
func _update_sort_direction_text() -> void:
var direction_text: String
2026-07-25 16:02:34 -04:00
match _sort_mode:
SortMode.CATCH_ORDER:
if _compact_layout:
direction_text = "newest" if _sort_descending else "oldest"
else:
direction_text = (
"newest first" if _sort_descending else "oldest first"
)
2026-07-25 16:02:34 -04:00
SortMode.NAME:
direction_text = "za" if _sort_descending else "az"
2026-07-25 16:02:34 -04:00
SortMode.RARITY:
if _compact_layout:
direction_text = "highlow" if _sort_descending else "lowhigh"
else:
direction_text = (
"high to low" if _sort_descending else "low to high"
)
_sort_direction.text = direction_text
_cooler_sort_direction.text = direction_text
2026-07-25 16:02:34 -04:00
func _refresh_all() -> void:
_refresh_economy_summary()
2026-07-25 16:02:34 -04:00
_refresh_inventory()
_refresh_bag()
2026-07-25 16:02:34 -04:00
func _on_bag_changed() -> void:
_refresh_bag()
_refresh_tackle_box()
func _on_hotbar_changed() -> void:
_update_bag_detail()
_update_tackle_detail()
func _refresh_bag() -> void:
if not is_node_ready():
return
var owned_items: Array[OwnedItemType] = (
_bag.get_all_items() if _bag != null else []
)
var filtered_items: Array[OwnedItemType] = []
for owned: OwnedItemType in owned_items:
var item: ItemDataType = (
_item_catalog.get_item_by_id(owned.item_id)
if _item_catalog != null
else null
)
if item == null:
continue
var is_consumable: bool = (
item.category == ItemDataType.Category.CONSUMABLE
)
if (
(_bag_view == BagView.CONSUMABLES and is_consumable)
or (
_bag_view == BagView.EQUIPMENT
and not is_consumable
and item.category != ItemDataType.Category.BAIT
and item.category != ItemDataType.Category.LURE
)
):
filtered_items.append(owned)
owned_items = filtered_items
owned_items.sort_custom(_sort_bag_items)
_sorted_bag_items = owned_items
_equipment_filter.button_pressed = _bag_view == BagView.EQUIPMENT
_consumables_filter.button_pressed = (
_bag_view == BagView.CONSUMABLES
)
_bag_empty.visible = owned_items.is_empty()
_bag_empty_state.visible = owned_items.is_empty()
_bag_empty_state.text = (
"No equipment in your Bag."
if _bag_view == BagView.EQUIPMENT
else "No consumables in your Bag."
)
if (
not _selected_bag_item_id.is_empty()
and (_bag == null or not _bag.owns_item(_selected_bag_item_id))
):
_selected_bag_item_id = StringName()
_sync_bag_item_nodes(owned_items)
_update_bag_detail()
func _sync_bag_item_nodes(owned_items: Array[OwnedItemType]) -> void:
var retained: Dictionary[StringName, bool] = {}
for owned: OwnedItemType in owned_items:
var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id)
if item == null:
continue
retained[owned.item_id] = true
var item_node := _bag_item_nodes.get(
owned.item_id
) as BagItemSpriteType
if item_node == null:
item_node = BagItemSpriteScene.instantiate()
_bag_item_field.add_child(item_node)
_bag_item_nodes[owned.item_id] = item_node
item_node.pressed.connect(
_select_bag_item.bind(owned.item_id)
)
item_node.focus_entered.connect(
_select_bag_item.bind(owned.item_id)
)
item_node.drag_started.connect(_on_bag_drag_started)
item_node.drag_finished.connect(_on_bag_drag_finished)
var identity_hash: int = absi(String(owned.item_id).hash())
item_node.configure_item(
item.item_id,
item.display_name,
item.icon,
owned.quantity,
item.hotbar_allowed,
float(identity_hash % 628) / 100.0,
0.97 + float(identity_hash % 7) * 0.01,
)
item_node.tooltip_text = (
2026-07-26 23:18:51 -04:00
"drag to a hotbar slot."
if item.hotbar_allowed
2026-07-26 23:18:51 -04:00
else "this item cannot be assigned to the hotbar."
)
item_node.disabled = false
item_node.set_selected(item.item_id == _selected_bag_item_id)
for item_id: StringName in _bag_item_nodes.keys():
if retained.has(item_id):
continue
var removed := _bag_item_nodes[item_id] as BagItemSpriteType
_bag_item_nodes.erase(item_id)
_release_focus_from(removed, _bag_sub_tab)
removed.queue_free()
_layout_bag_items()
_configure_bag_item_focus()
func _layout_bag_items() -> void:
if not is_node_ready() or _sorted_bag_items.is_empty():
return
_bag_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
var columns: int = 3
var cell_size := (
Vector2(166.0, 72.0)
if _compact_layout
else Vector2(246.0, 154.0)
)
var item_size := (
Vector2(86.0, 62.0)
if _compact_layout
else Vector2(138.0, 102.0)
)
var origin := (
Vector2(16.0, 4.0)
if _compact_layout
else Vector2(54.0, 44.0)
)
_bag_item_field.custom_minimum_size = (
Vector2(520.0, 140.0)
if _compact_layout
else Vector2(820.0, 240.0)
)
for index: int in _sorted_bag_items.size():
var owned: OwnedItemType = _sorted_bag_items[index]
var item_node := _bag_item_nodes.get(
owned.item_id
) as BagItemSpriteType
if item_node == null:
continue
var identity_hash: int = absi(String(owned.item_id).hash())
var column: int = index % columns
var row: int = floori(float(index) / float(columns))
var stable_offset := Vector2(
float(identity_hash % 13) - 6.0,
float(floori(float(identity_hash) / 19.0) % 11) - 5.0,
)
var lane_offset: float = 18.0 if row % 2 == 1 else 0.0
item_node.custom_minimum_size = item_size
item_node.size = item_size
item_node.position = (
origin
+ Vector2(
float(column) * cell_size.x + lane_offset,
float(row) * cell_size.y,
)
+ stable_offset
)
func _configure_bag_item_focus() -> void:
if _current_section != Section.BAG:
return
var controls: Array[BagItemSpriteType] = []
for owned: OwnedItemType in _sorted_bag_items:
var item_node := _bag_item_nodes.get(
owned.item_id
) as BagItemSpriteType
if item_node != null:
controls.append(item_node)
if controls.is_empty():
_bag_sub_tab.focus_neighbor_bottom = NodePath()
return
_bag_sub_tab.focus_neighbor_bottom = _bag_sub_tab.get_path_to(
controls.front()
)
for index: int in controls.size():
var control: BagItemSpriteType = controls[index]
var column: int = index % 3
var row: int = floori(float(index) / 3.0)
var left_index: int = row * 3 + maxi(column - 1, 0)
var right_index: int = mini(index + 1, controls.size() - 1)
control.focus_neighbor_left = control.get_path_to(
controls[left_index]
)
control.focus_neighbor_right = control.get_path_to(
controls[right_index]
)
control.focus_neighbor_top = (
control.get_path_to(_bag_sub_tab)
if index < 3
else control.get_path_to(controls[index - 3])
)
control.focus_neighbor_bottom = control.get_path_to(
controls[mini(index + 3, controls.size() - 1)]
)
func _sort_bag_items(a: OwnedItemType, b: OwnedItemType) -> bool:
var item_a: ItemDataType = _item_catalog.get_item_by_id(a.item_id)
var item_b: ItemDataType = _item_catalog.get_item_by_id(b.item_id)
if item_a == null:
return false
if item_b == null:
return true
if item_a.category != item_b.category:
return item_a.category < item_b.category
return item_a.display_name.naturalnocasecmp_to(item_b.display_name) < 0
func _select_bag_item(item_id: StringName) -> void:
if _bag_drag_active or get_viewport().gui_is_dragging():
return
_selected_bag_item_id = item_id
for candidate_id: StringName in _bag_item_nodes:
var item_node := _bag_item_nodes[candidate_id] as BagItemSpriteType
item_node.set_selected(candidate_id == item_id)
_update_bag_detail()
func _update_bag_detail() -> void:
var item: ItemDataType = (
_item_catalog.get_item_by_id(_selected_bag_item_id)
if _item_catalog != null and not _selected_bag_item_id.is_empty()
else null
)
var quantity: int = (
_bag.get_quantity(_selected_bag_item_id)
if _bag != null and item != null
else 0
)
var hotbar_assignment: String = "not assigned"
if item != null and _hotbar != null:
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
if _hotbar.get_item_id(slot_index) == item.item_id:
hotbar_assignment = "hotbar slot %d" % (slot_index + 1)
break
var item_state: String = ""
if item != null:
if item.equippable:
item_state = "equippable"
elif item.usable:
item_state = "usable"
_bag_detail_texture.texture = item.icon if item != null else null
_bag_detail_name.text = item.display_name if item != null else ""
_bag_detail_data.text = (
"%s\nquantity: %d\n%s\n%s\n%s"
% [
item.get_category_name(),
quantity,
item_state,
hotbar_assignment,
item.description,
]
if item != null
2026-07-26 23:18:51 -04:00
else "select a bag item for details."
)
_bag_sprite_detail_texture.texture = item.icon if item != null else null
_bag_sprite_detail_name.text = (
2026-07-31 09:29:27 -04:00
item.display_name if item != null else ""
)
_bag_sprite_detail_data.text = (
"%s • quantity: %d\n%s%s\n%s"
% [
item.get_category_name(),
quantity,
item_state,
hotbar_assignment,
item.description,
]
if item != null
else "select an item for details."
)
_bag_detail_constellation.visible = (
_current_section == Section.BAG
)
func _close_bag_detail() -> void:
_bag_detail_constellation.visible = false
func _on_bag_field_gui_input(event: InputEvent) -> void:
if (
event is InputEventMouseButton
and event.button_index == MOUSE_BUTTON_LEFT
and event.pressed
and not get_viewport().gui_is_dragging()
):
_selected_bag_item_id = StringName()
for item_node: BagItemSpriteType in _bag_item_nodes.values():
item_node.set_selected(false)
_update_bag_detail()
func _on_bag_drag_started() -> void:
_bag_drag_active = true
_close_bag_detail()
_set_content_interactive(false)
func _on_bag_drag_finished() -> void:
_bag_drag_active = false
if visible and _current_section == Section.BAG:
_set_content_interactive(
not _transitioning and not _page_transitioning
)
2026-07-25 16:02:34 -04:00
func _on_inventory_changed() -> void:
_refresh_economy_summary()
_revalidate_confirmation()
2026-07-25 16:02:34 -04:00
_refresh_inventory()
func _on_wallet_balance_changed(
_new_balance: int,
_delta: int,
) -> void:
_refresh_economy_summary()
func _refresh_economy_summary() -> void:
if not is_node_ready():
return
var balance: int = _wallet.get_balance() if _wallet != null else 0
var held_total: int = (
_inventory.get_total_sale_value()
if _inventory != null
else 0
)
2026-07-26 23:18:51 -04:00
_wallet_balance.text = "wallet: $%d" % balance
2026-07-28 16:29:02 -04:00
_wallet_status.set_content("wallet", "$%d" % balance)
2026-07-29 10:53:46 -04:00
_notepad_wallet_value.text = "$%d" % balance
2026-07-28 16:29:02 -04:00
_capacity_status.set_content("cooler", "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
])
2026-07-29 10:53:46 -04:00
_notepad_capacity_value.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
]
2026-07-28 16:29:02 -04:00
_held_value_status.set_content("held value", "$%d" % held_total)
2026-07-26 23:18:51 -04:00
_held_value.text = "held fish base value: $%d" % held_total
_cooler_count.text = "%d / %d" % [
_inventory.get_all_catches().size() if _inventory != null else 0,
_cooler_capacity.get_capacity() if _cooler_capacity != null else 0,
]
func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
_refresh_economy_summary()
2026-07-25 16:02:34 -04:00
func _refresh_inventory() -> void:
if not is_node_ready():
return
var catches: Array[FishCatchType] = []
if _inventory != null:
for fish_catch: FishCatchType in _inventory.get_all_catches():
if fish_catch != null and fish_catch.is_valid():
catches.append(fish_catch)
catches.sort_custom(_compare_catches)
_sorted_catches = catches
2026-07-25 16:02:34 -04:00
_inventory_empty.visible = catches.is_empty()
_cooler_empty.visible = catches.is_empty()
2026-07-26 23:18:51 -04:00
var visible_ids: Array[StringName] = []
for fish_catch: FishCatchType in catches:
visible_ids.append(fish_catch.catch_id)
_fish_selection.set_visible_order(visible_ids)
2026-07-25 16:02:34 -04:00
_sync_cooler_fish_nodes(catches)
var selected: FishCatchType = (
_inventory.get_catch(_fish_selection.get_focused_id())
if _inventory != null
else null
)
2026-07-26 23:18:51 -04:00
_update_sale_summary()
2026-07-29 10:53:46 -04:00
_update_inventory_detail(selected)
2026-07-25 16:02:34 -04:00
_update_sort_direction_text()
func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
var retained: Dictionary[StringName, bool] = {}
for fish_catch: FishCatchType in catches:
retained[fish_catch.catch_id] = true
var fish_node := _fish_nodes.get(
fish_catch.catch_id
) as CoolerFishSpriteType
var is_new: bool = fish_node == null
if is_new:
fish_node = CoolerFishSpriteScene.instantiate()
_fish_field.add_child(fish_node)
_fish_nodes[fish_catch.catch_id] = fish_node
fish_node.set_meta("new_cooler_fish", true)
fish_node.pressed.connect(
_on_catch_card_pressed.bind(fish_catch.catch_id)
)
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
fish_node.configure(
fish_catch.catch_id,
2026-08-02 17:44:11 -04:00
FishQualityType.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
),
fish_catch.fish.display_texture,
float(identity_hash % 628) / 100.0,
0.96 + float(identity_hash % 9) * 0.01,
2026-08-02 17:44:11 -04:00
UIPalette.get_quality_color(fish_catch.quality),
)
fish_node.set_item_state(
_fish_selection.is_selected(fish_catch.catch_id),
fish_catch.catch_id == _fish_selection.get_focused_id(),
fish_catch.is_favorited,
)
for catch_id: StringName in _fish_nodes.keys():
if retained.has(catch_id):
continue
var removed := _fish_nodes[catch_id] as CoolerFishSpriteType
_fish_nodes.erase(catch_id)
_release_focus_from(removed, _inventory_tab)
removed.queue_free()
_layout_cooler_fish(true)
_configure_cooler_fish_focus()
func _layout_cooler_fish(animate: bool = true) -> void:
if not is_node_ready():
return
var columns: int = 3 if _compact_layout else 9
var cell_size := (
Vector2(68.0, 58.0) if _compact_layout else Vector2(84.0, 80.0)
)
var fish_size := (
Vector2(52.0, 52.0) if _compact_layout else Vector2(72.0, 72.0)
)
var visible_field_size := (
Vector2(218.0, 124.0)
if _compact_layout
2026-07-31 09:29:27 -04:00
else Vector2(800.0, 280.0)
)
var side_margin: float = 2.0 if _compact_layout else 8.0
var top_margin: float = 4.0 if _compact_layout else 6.0
var required_rows: int = ceili(
float(_sorted_catches.size()) / float(columns)
) if not _sorted_catches.is_empty() else 0
var required_height: float = (
top_margin * 2.0 + float(required_rows) * cell_size.y
)
var content_size := Vector2(
visible_field_size.x,
maxf(visible_field_size.y, required_height),
)
_cooler_host.custom_minimum_size = content_size
_fish_field.custom_minimum_size = content_size
_cooler_scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO
for index: int in _sorted_catches.size():
var fish_catch: FishCatchType = _sorted_catches[index]
var fish_node := _fish_nodes.get(
fish_catch.catch_id
) as CoolerFishSpriteType
if fish_node == null:
continue
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
2026-07-28 16:29:02 -04:00
var column: int = index % columns
var row: int = floori(float(index) / float(columns))
var offset_span: int = 5 if _compact_layout else 7
var offset_center: float = 2.0 if _compact_layout else 3.0
var stable_offset := Vector2(
float(identity_hash % offset_span) - offset_center,
float(floori(float(identity_hash) / 17.0) % offset_span) - offset_center,
)
var lane_offset: float = 5.0 if row % 2 == 1 else 0.0
var target := Vector2(
side_margin + float(column) * cell_size.x + lane_offset,
top_margin + float(row) * cell_size.y,
) + stable_offset
fish_node.custom_minimum_size = fish_size
fish_node.size = fish_size
var immediate: bool = (
not animate or fish_node.has_meta("new_cooler_fish")
)
fish_node.set_target_position(target, immediate)
if fish_node.has_meta("new_cooler_fish"):
fish_node.remove_meta("new_cooler_fish")
func _configure_cooler_fish_focus() -> void:
if _current_section != Section.COOLER:
return
var controls: Array[CoolerFishSpriteType] = []
for fish_catch: FishCatchType in _sorted_catches:
var fish_node := _fish_nodes.get(
fish_catch.catch_id
) as CoolerFishSpriteType
if fish_node != null:
controls.append(fish_node)
var columns: int = 3 if _compact_layout else 9
if controls.is_empty():
if not _shop_cooler_context_active:
_cooler_sub_tab.focus_neighbor_bottom = NodePath()
return
var top_control: Control = (
_cooler_sort_option
if _shop_cooler_context_active
else _cooler_sub_tab
)
if not _shop_cooler_context_active:
_cooler_sub_tab.focus_neighbor_bottom = _cooler_sub_tab.get_path_to(
controls.front()
)
for index: int in controls.size():
var control: CoolerFishSpriteType = controls[index]
control.focus_neighbor_left = control.get_path_to(
controls[maxi(index - 1, 0)]
)
control.focus_neighbor_right = control.get_path_to(
controls[mini(index + 1, controls.size() - 1)]
)
control.focus_neighbor_top = (
control.get_path_to(top_control)
if index < columns
else control.get_path_to(controls[index - columns])
)
control.focus_neighbor_bottom = (
control.get_path_to(_favorite_bubble)
if (
not _favorite_bubble.disabled
and index + columns >= controls.size()
)
else control.get_path_to(
controls[mini(index + columns, controls.size() - 1)]
)
)
var focused_node := _fish_nodes.get(
_fish_selection.get_focused_id()
) as CoolerFishSpriteType
if focused_node == null:
focused_node = controls.front()
_favorite_bubble.focus_neighbor_left = (
_favorite_bubble.get_path_to(focused_node)
)
_favorite_bubble.focus_neighbor_right = (
_favorite_bubble.get_path_to(_sell_bubble)
)
_favorite_bubble.focus_neighbor_top = (
_favorite_bubble.get_path_to(focused_node)
)
_sell_bubble.focus_neighbor_left = (
_sell_bubble.get_path_to(_favorite_bubble)
)
_sell_bubble.focus_neighbor_right = (
_sell_bubble.get_path_to(_close_button)
)
_sell_bubble.focus_neighbor_top = _sell_bubble.get_path_to(focused_node)
2026-07-28 16:29:02 -04:00
2026-07-25 16:02:34 -04:00
func _compare_catches(left: FishCatchType, right: FishCatchType) -> bool:
var comparison: int = 0
match _sort_mode:
SortMode.CATCH_ORDER:
comparison = left.catch_sequence - right.catch_sequence
SortMode.NAME:
comparison = left.fish.display_name.naturalnocasecmp_to(
right.fish.display_name
)
SortMode.RARITY:
comparison = left.fish.rarity - right.fish.rarity
if comparison == 0:
comparison = left.catch_sequence - right.catch_sequence
return comparison > 0 if _sort_descending else comparison < 0
2026-07-26 23:18:51 -04:00
func _on_catch_card_pressed(catch_id: StringName) -> void:
2026-07-25 16:02:34 -04:00
if _inventory == null:
return
var selected: FishCatchType = _inventory.get_catch(catch_id)
if selected == null:
return
2026-07-26 23:18:51 -04:00
_fish_selection.apply_click(
catch_id,
Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT)
)
_transaction_feedback.text = ""
2026-07-25 16:02:34 -04:00
_refresh_inventory()
func _on_fish_field_gui_input(event: InputEvent) -> void:
if (
event is InputEventMouseButton
and event.button_index == MOUSE_BUTTON_LEFT
and event.pressed
and not get_viewport().gui_is_dragging()
):
_fish_selection.clear()
_transaction_feedback.text = ""
_refresh_inventory()
func _close_detail_constellation() -> void:
_detail_constellation.visible = _current_section == Section.COOLER
_cooler_detail_texture.texture = null
_cooler_detail_texture.visible = false
_cooler_detail_name.text = "select a fish"
2026-07-29 10:53:46 -04:00
_clear_cooler_detail_stats()
_set_cooler_selection_summary_empty()
_favorite_bubble.disabled = true
_favorite_bubble.text = "favorite"
_favorite_bubble.persistent_mark = false
_sell_bubble.disabled = true
_sell_bubble.text = "sell fish"
_sell_bubble.persistent_mark = false
_favorite_bubble.focus_mode = Control.FOCUS_NONE
_sell_bubble.focus_mode = Control.FOCUS_NONE
_favorite_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
_sell_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
_favorite_bubble.refresh_ink_state()
_sell_bubble.refresh_ink_state()
_configure_cooler_fish_focus()
2026-07-29 10:53:46 -04:00
func _clear_cooler_detail_stats() -> void:
for label: Label in [
_cooler_weight_value,
_cooler_weight_unit,
_cooler_offer_label,
_cooler_offer_value,
]:
label.text = ""
func _set_cooler_selection_summary_empty() -> void:
_cooler_selection_empty.visible = true
_cooler_selected_count_row.visible = false
_cooler_combined_offer_row.visible = false
for label: Label in [
_cooler_selected_count_value,
_cooler_selected_count_label,
_cooler_combined_offer_label,
_cooler_combined_offer_value,
]:
label.visible = false
label.text = ""
func _set_cooler_selection_summary(
selected_count: int,
offer_value: int,
) -> void:
_cooler_selection_empty.visible = false
_cooler_selected_count_row.visible = true
_cooler_combined_offer_row.visible = true
_cooler_selected_count_value.visible = true
_cooler_selected_count_label.visible = true
_cooler_combined_offer_label.visible = true
_cooler_combined_offer_value.visible = offer_value >= 0
_cooler_selected_count_value.text = str(selected_count)
_cooler_selected_count_label.text = "fish selected"
if offer_value >= 0:
_cooler_combined_offer_label.text = "combined offer"
_cooler_combined_offer_value.text = "$%d" % offer_value
else:
_cooler_combined_offer_label.text = "offer unavailable"
_cooler_combined_offer_value.text = ""
2026-07-25 16:02:34 -04:00
func _update_inventory_detail(fish_catch: FishCatchType) -> void:
if fish_catch == null:
_cooler_detail_texture.texture = null
_cooler_detail_texture.visible = false
_cooler_detail_name.text = "select a fish"
2026-07-29 10:53:46 -04:00
_clear_cooler_detail_stats()
_favorite_button.disabled = true
2026-07-26 23:18:51 -04:00
_favorite_button.text = "favorite"
_favorite_bubble.disabled = true
_favorite_bubble.text = "favorite"
_favorite_bubble.persistent_mark = false
_detail_constellation.visible = _current_section == Section.COOLER
_favorite_bubble.focus_mode = Control.FOCUS_NONE
_sell_bubble.focus_mode = Control.FOCUS_NONE
_favorite_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
_sell_bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
_favorite_bubble.refresh_ink_state()
_sell_bubble.refresh_ink_state()
_configure_cooler_fish_focus()
2026-07-25 16:02:34 -04:00
return
_cooler_detail_texture.texture = fish_catch.fish.display_texture
_cooler_detail_texture.visible = fish_catch.fish.display_texture != null
_cooler_detail_name.text = fish_catch.fish.display_name
var active_buyer: FishBuyerProfileType = _get_active_sale_buyer()
2026-07-26 23:18:51 -04:00
var individual_preview: FishSaleResultType = (
_sale_service.preview_batch([fish_catch.catch_id], active_buyer)
if _sale_service != null and active_buyer != null
2026-07-26 23:18:51 -04:00
else null
)
var buyer_offer: int = (
2026-07-26 23:18:51 -04:00
individual_preview.payout
if individual_preview != null
else -1
)
2026-07-29 10:53:46 -04:00
_cooler_weight_value.text = "%.2f" % fish_catch.weight_lb
if (
_reservations != null
and _reservations.is_fish_reserved(fish_catch.catch_id)
):
_cooler_detail_name.text += " • reserved in mail"
2026-08-02 17:44:11 -04:00
_cooler_weight_unit.text = "lb • %s%s" % [
FishQualityType.display_name(fish_catch.quality),
fish_catch.fish.get_rarity_name(),
]
if buyer_offer >= 0 and active_buyer != null:
_cooler_offer_label.text = _get_offer_label(active_buyer)
2026-07-29 10:53:46 -04:00
_cooler_offer_value.text = "$%d" % buyer_offer
else:
_cooler_offer_label.text = "buyer unavailable"
_cooler_offer_value.text = ""
_favorite_button.disabled = false
_favorite_button.text = (
2026-07-26 23:18:51 -04:00
"unfavorite" if fish_catch.is_favorited else "favorite"
)
_favorite_bubble.disabled = false
_favorite_bubble.text = _favorite_button.text
_favorite_bubble.persistent_mark = fish_catch.is_favorited
_favorite_bubble.refresh_ink_state()
_detail_constellation.visible = _current_section == Section.COOLER
_cooler_sort_controls.visible = true
var detail_interactive: bool = (
_detail_constellation.visible
and not _transitioning
and not _page_transitioning
)
for action: NotepadInkActionType in [_favorite_bubble, _sell_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
else Control.MOUSE_FILTER_IGNORE
)
action.refresh_ink_state()
_configure_cooler_fish_focus()
2026-07-26 23:18:51 -04:00
func _update_sale_summary() -> void:
var active_buyer: FishBuyerProfileType = _get_active_sale_buyer()
var buyer_id: StringName = (
active_buyer.id if active_buyer != null else StringName()
)
var offer_label: String = _get_offer_label(active_buyer)
2026-07-26 23:18:51 -04:00
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
var selected_count: int = selected_ids.size()
_sell_button.text = (
"sell fish"
if selected_count == 1
else "sell %d fish" % selected_count
)
_sell_bubble.text = _sell_button.text
2026-07-26 23:18:51 -04:00
if selected_count == 0:
_selection_summary.text = "no fish selected"
2026-07-28 16:29:02 -04:00
_selection_status.set_content("selected", "none")
_offer_status.set_content(offer_label, "")
2026-07-26 23:18:51 -04:00
_sell_button.text = "sell fish"
_sell_button.disabled = true
_sell_bubble.disabled = true
_sell_bubble.text = "sell fish"
_sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state()
2026-07-29 10:53:46 -04:00
_set_cooler_selection_summary_empty()
2026-07-26 23:18:51 -04:00
_sale_unavailable.text = ""
_sale_unavailable.visible = false
return
if (
_sale_in_progress
or (
_network_sale_service != null
and _network_sale_service.is_local_sale_pending()
)
):
_selection_summary.text = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_status.set_content("selected", str(selected_count))
_offer_status.set_content(offer_label, "pending")
_sell_button.disabled = true
_sell_bubble.disabled = true
_sale_unavailable.text = "Selling…"
_sale_unavailable.visible = true
return
if (
_network_sale_service == null
or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id)
):
_selection_summary.text = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_status.set_content("selected", str(selected_count))
_offer_status.set_content(offer_label, "unavailable")
_sell_button.disabled = true
_sell_bubble.disabled = true
_sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state()
_sale_unavailable.text = (
"Selling is not supported by this server."
)
_sale_unavailable.visible = true
return
2026-07-26 23:18:51 -04:00
var preview: FishSaleResultType = (
_sale_service.preview_batch(selected_ids, active_buyer)
if _sale_service != null and active_buyer != null
2026-07-26 23:18:51 -04:00
else null
)
2026-07-26 23:18:51 -04:00
var count_text: String = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
2026-07-26 23:18:51 -04:00
_selection_summary.text = count_text
2026-07-28 16:29:02 -04:00
_selection_status.set_content("selected", str(selected_count))
2026-07-26 23:18:51 -04:00
if (
preview != null
and preview.payout >= 0
and active_buyer != null
2026-07-26 23:18:51 -04:00
and (
preview.is_success()
or preview.status == FishSaleResultType.Status.FAVORITED
)
):
_selection_summary.text += "\n%s total offer: $%d" % [
active_buyer.display_name,
2026-07-26 23:18:51 -04:00
preview.payout,
]
2026-07-29 10:53:46 -04:00
_set_cooler_selection_summary(selected_count, preview.payout)
_offer_status.set_content(offer_label, "$%d" % preview.payout)
2026-07-28 16:29:02 -04:00
else:
2026-07-29 10:53:46 -04:00
_set_cooler_selection_summary(selected_count, -1)
_offer_status.set_content(offer_label, "unavailable")
2026-07-26 23:18:51 -04:00
_sell_button.disabled = preview == null or not preview.is_success()
_sell_bubble.disabled = _sell_button.disabled
_sell_bubble.persistent_mark = false
_sell_bubble.refresh_ink_state()
2026-07-26 23:18:51 -04:00
if preview != null and preview.status == FishSaleResultType.Status.FAVORITED:
_sale_unavailable.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
)
elif preview != null and not preview.is_success():
_sale_unavailable.text = preview.get_message()
else:
_sale_unavailable.text = ""
_sale_unavailable.visible = not _sale_unavailable.text.is_empty()
func _on_favorite_pressed() -> void:
2026-07-26 23:18:51 -04:00
var focused_id: StringName = _fish_selection.get_focused_id()
if _inventory == null or focused_id.is_empty():
return
var fish_catch: FishCatchType = _inventory.get_catch_by_id(
2026-07-26 23:18:51 -04:00
focused_id
)
if fish_catch == null:
_refresh_inventory()
return
if _inventory.set_catch_favorited(
fish_catch.catch_id,
not fish_catch.is_favorited
):
_transaction_feedback.text = (
"%s %s."
% [
fish_catch.fish.display_name,
"favorited" if fish_catch.is_favorited else "unfavorited",
]
)
func _on_sell_pressed() -> void:
var active_buyer: FishBuyerProfileType = _get_active_sale_buyer()
var buyer_id: StringName = (
active_buyer.id if active_buyer != null else StringName()
)
if (
_sale_in_progress
or (
_network_sale_service != null
and _network_sale_service.is_local_sale_pending()
)
):
_transaction_feedback.text = "Selling…"
return
if (
_network_sale_service == null
or buyer_id.is_empty()
or not _network_sale_service.can_request_sale(buyer_id)
):
_transaction_feedback.text = (
"Selling is not supported by this server."
)
return
2026-07-26 23:18:51 -04:00
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
if (
_inventory == null
or _sale_service == null
or active_buyer == null
2026-07-26 23:18:51 -04:00
or selected_ids.is_empty()
):
return
2026-07-26 23:18:51 -04:00
var preview: FishSaleResultType = _sale_service.preview_batch(
selected_ids,
active_buyer
)
2026-07-26 23:18:51 -04:00
if not preview.is_success():
_transaction_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
_refresh_inventory()
return
2026-07-26 23:18:51 -04:00
_confirmation_catch_ids = selected_ids.duplicate()
_confirmation_buyer = active_buyer
_confirmation_buyer_id = active_buyer.id
2026-07-26 23:18:51 -04:00
_confirmation_generation = _menu_generation
var fish_label: String = "fish"
_confirmation_message.text = (
2026-07-26 23:18:51 -04:00
"sell %d %s to the %s for $%d?\ncombined base value: $%d"
% [
2026-07-26 23:18:51 -04:00
preview.fish_count,
fish_label,
_get_buyer_display_group(active_buyer),
2026-07-26 23:18:51 -04:00
preview.payout,
preview.base_value,
]
)
_sale_confirmation.visible = true
if _shop_cooler_context_active:
shop_cooler_modal_changed.emit(true)
_set_shell_interactive(false)
for button: Button in [_confirm_sale_button, _cancel_sale_button]:
button.focus_mode = Control.FOCUS_ALL
button.mouse_filter = Control.MOUSE_FILTER_STOP
_confirm_sale_button.disabled = false
_cancel_sale_button.grab_focus()
func _on_confirm_sale_pressed() -> void:
if (
_sale_in_progress
or _network_sale_service == null
or not _network_sale_service.can_request_sale(
_confirmation_buyer_id
)
or _sale_service == null
2026-07-26 23:18:51 -04:00
or _confirmation_catch_ids.is_empty()
or _confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
2026-07-26 23:18:51 -04:00
or _confirmation_generation != _menu_generation
):
return
_sale_in_progress = true
2026-07-26 23:18:51 -04:00
var requested_catch_ids: Array[StringName] = (
_confirmation_catch_ids.duplicate()
)
var requested_buyer_id: StringName = _confirmation_buyer_id
2026-07-26 23:18:51 -04:00
var transaction_generation: int = _menu_generation
_confirm_sale_button.disabled = true
_close_sale_confirmation()
var request_id: String = _network_sale_service.request_local_sale(
requested_catch_ids,
requested_buyer_id,
)
if request_id.is_empty():
_sale_in_progress = false
2026-07-26 23:18:51 -04:00
return
if (
_network_sale_service.is_local_sale_pending()
and transaction_generation == _menu_generation
and (visible or _shop_cooler_context_active)
):
_transaction_feedback.text = "Selling…"
func _can_use_shared_world_actions() -> bool:
return (
_network_sale_service != null
and _network_sale_service.can_request_sale()
)
func _on_network_sale_pending(_request_id: String) -> void:
_sale_in_progress = true
if visible or _shop_cooler_context_active:
_transaction_feedback.text = "Selling…"
_update_sale_summary()
func _on_network_sale_finished(
_request_id: String,
accepted: bool,
message: String,
catch_ids: Array[StringName],
payout: int,
) -> void:
_sale_in_progress = false
if accepted:
_fish_selection.remove_ids(catch_ids)
if not visible and not _shop_cooler_context_active:
return
_refresh_all()
var feedback_message: String = (
"Sale complete. $%d received." % payout
if accepted
else message
)
_transaction_feedback.text = feedback_message
if not accepted:
_sale_unavailable.text = feedback_message
_sale_unavailable.visible = true
if _current_section == Section.COOLER:
call_deferred("_restore_inventory_tab_focus")
func _restore_inventory_tab_focus() -> void:
if _shop_cooler_context_active:
_focus_shop_cooler()
return
if (
visible
and _current_section == Section.COOLER
and _inventory_tab.focus_mode != Control.FOCUS_NONE
and _inventory_tab.is_visible_in_tree()
):
_inventory_tab.grab_focus()
func _close_sale_confirmation() -> void:
var was_visible: bool = _sale_confirmation.visible
2026-07-26 23:18:51 -04:00
_confirmation_catch_ids.clear()
_confirmation_buyer = null
_confirmation_buyer_id = StringName()
2026-07-26 23:18:51 -04:00
_confirmation_generation = -1
_sale_confirmation.visible = false
if was_visible and _shop_cooler_context_active:
shop_cooler_modal_changed.emit(false)
_confirmation_message.text = ""
_confirm_sale_button.disabled = false
for button: Button in [_confirm_sale_button, _cancel_sale_button]:
button.focus_mode = Control.FOCUS_NONE
button.mouse_filter = Control.MOUSE_FILTER_IGNORE
if (
was_visible
and (visible or _shop_cooler_context_active)
and not _transitioning
and not _page_transitioning
):
_set_shell_interactive(true)
_focus_current_section()
func _revalidate_confirmation() -> void:
if not _sale_confirmation.visible:
return
2026-07-26 23:18:51 -04:00
if (
_confirmation_generation != _menu_generation
or _confirmation_catch_ids.is_empty()
or _sale_service == null
or _confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
):
_close_sale_confirmation()
2026-07-26 23:18:51 -04:00
_transaction_feedback.text = "sale selection is no longer available."
return
var preview: FishSaleResultType = _sale_service.preview_batch(
_confirmation_catch_ids,
_confirmation_buyer
)
if (
not preview.is_success()
or (
_confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
or not _confirmation_buyer.is_valid()
2026-07-26 23:18:51 -04:00
)
):
_close_sale_confirmation()
2026-07-26 23:18:51 -04:00
_transaction_feedback.text = (
"favorited fish cannot be sold. "
+ "remove them from the selection first."
if preview.status == FishSaleResultType.Status.FAVORITED
else preview.get_message()
)
func _get_buyer_display_group(
buyer: FishBuyerProfileType,
) -> String:
if buyer == null:
return "buyer"
if not buyer.animal_name_plural.is_empty():
return buyer.animal_name_plural
if not buyer.display_name.is_empty():
return buyer.display_name
return "buyer"
2026-07-25 16:02:34 -04:00
func _get_active_sale_buyer() -> FishBuyerProfileType:
if _sale_buyer_override != null and _sale_buyer_override.is_valid():
return _sale_buyer_override
return _default_buyer
func _get_offer_label(buyer: FishBuyerProfileType) -> String:
if buyer == null:
return "buyer offer"
if buyer.id == MAIN_SHOP_BUYER_ID:
return "shop offer"
if buyer.id == NetworkSaleService.PELICAN_BUYER_ID:
return "pelican offer"
return "buyer offer"
2026-07-25 16:02:34 -04:00
func _refresh_logbook() -> void:
if not is_node_ready():
return
var valid_species: Array[FishDataType] = []
if _catalog != null:
for fish: FishDataType in _catalog.candidates:
if fish != null and not fish.id.is_empty():
valid_species.append(fish)
_logbook_species = valid_species
2026-07-25 16:02:34 -04:00
_logbook_empty.visible = valid_species.is_empty()
_logbook_empty.text = (
2026-07-26 23:18:51 -04:00
"no fish catalog configured."
2026-07-25 16:02:34 -04:00
if valid_species.is_empty()
2026-07-26 23:18:51 -04:00
else "no species discovered yet."
2026-07-25 16:02:34 -04:00
)
_logbook_empty_state.visible = valid_species.is_empty()
_logbook_empty_state.text = (
"no fish catalog configured"
if valid_species.is_empty()
else ""
)
_refresh_logbook_page(false)
2026-07-25 16:02:34 -04:00
func _refresh_logbook_page(keep_transition_state: bool = true) -> void:
if not is_node_ready():
return
var entries_per_spread: int = 2 if _compact_layout else 4
_logbook_page_count = maxi(
1,
ceili(float(_logbook_species.size()) / float(entries_per_spread)),
)
_logbook_current_page = clampi(
_logbook_current_page,
0,
_logbook_page_count - 1,
)
_clear_logbook_entries()
var start_index: int = _logbook_current_page * entries_per_spread
var end_index: int = mini(
start_index + entries_per_spread,
_logbook_species.size(),
)
var visible_species: Array[FishDataType] = []
for index: int in range(start_index, end_index):
visible_species.append(_logbook_species[index])
if _compact_layout:
for fish: FishDataType in visible_species:
_left_entry_field.add_child(_create_logbook_entry(fish))
else:
var left_count: int = mini(2, visible_species.size())
for index: int in visible_species.size():
var destination: BoxContainer = (
_left_entry_field
if index < left_count
else _right_entry_field
)
destination.add_child(_create_logbook_entry(
visible_species[index]
))
_logbook_page_status.set_content(
"pages",
"%d / %d" % [_logbook_current_page + 1, _logbook_page_count],
)
_logbook_page_status.visible = _logbook_page_count > 1
_update_logbook_page_control_state(
keep_transition_state
and visible
and _current_section == Section.LOGBOOK
and not _transitioning
and not _page_transitioning
and not _logbook_page_transitioning
)
_configure_logbook_focus()
func _create_logbook_entry(fish: FishDataType) -> LogbookEntryType:
2026-07-25 16:02:34 -04:00
var discovered: bool = (
_collection_log != null
and _collection_log.has_discovered(fish.id)
)
var entry := LogbookEntryScene.instantiate() as LogbookEntryType
entry.set_meta("fish_id", fish.id)
entry.configure(
2026-07-25 16:02:34 -04:00
fish.display_texture,
fish.display_name,
fish.get_rarity_name(),
_inventory.get_count(fish.id) if _inventory != null else 0,
discovered,
)
entry.apply_compact_layout(_compact_layout)
return entry
func _clear_logbook_entries() -> void:
for field: BoxContainer in [_left_entry_field, _right_entry_field]:
for child: Node in field.get_children():
_release_focus_from(child, _logbook_tab)
field.remove_child(child)
child.queue_free()
func _release_focus_from(node: Node, fallback: Control) -> void:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if (
focus_owner == null
or (
focus_owner != node
and not node.is_ancestor_of(focus_owner)
)
):
return
get_viewport().gui_release_focus()
if (
visible
and not _transitioning
and not _page_transitioning
and fallback != null
and fallback.focus_mode != Control.FOCUS_NONE
):
fallback.call_deferred("grab_focus")
func _request_logbook_page(direction: int) -> void:
if (
_current_section != Section.LOGBOOK
or _logbook_page_transitioning
or _transitioning
or _page_transitioning
):
return
var target_page: int = clampi(
_logbook_current_page + direction,
0,
_logbook_page_count - 1,
)
if target_page == _logbook_current_page:
return
_logbook_page_generation += 1
_logbook_page_transitioning = true
_update_logbook_page_control_state(false)
var generation: int = _logbook_page_generation
var travel: float = -12.0 if direction > 0 else 12.0
_logbook_page_tween = create_tween()
_logbook_page_tween.tween_property(
_book_spread,
"modulate:a",
0.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.parallel().tween_property(
_book_spread,
"position:x",
travel,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.tween_callback(
_swap_logbook_page.bind(target_page, travel, generation)
)
_logbook_page_tween.tween_property(
_book_spread,
"modulate:a",
1.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.parallel().tween_property(
_book_spread,
"position:x",
0.0,
LOGBOOK_PAGE_DURATION * 0.5,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_logbook_page_tween.finished.connect(
_finish_logbook_page_transition.bind(generation),
CONNECT_ONE_SHOT,
)
func _swap_logbook_page(
target_page: int,
travel: float,
generation: int,
) -> void:
if (
generation != _logbook_page_generation
or not visible
or _current_section != Section.LOGBOOK
):
return
_logbook_current_page = target_page
_refresh_logbook_page(false)
_book_spread.position.x = -travel
_book_spread.modulate.a = 0.0
func _finish_logbook_page_transition(generation: int) -> void:
if generation != _logbook_page_generation:
return
_logbook_page_tween = null
_logbook_page_transitioning = false
_book_spread.position.x = 0.0
_book_spread.modulate.a = 1.0
_update_logbook_page_control_state(
visible and _current_section == Section.LOGBOOK
)
_configure_logbook_focus()
if _logbook_next.visible and not _logbook_next.disabled:
_logbook_next.grab_focus()
elif _logbook_previous.visible and not _logbook_previous.disabled:
_logbook_previous.grab_focus()
func _cancel_logbook_page_transition(reset_visuals: bool) -> void:
_logbook_page_generation += 1
if _logbook_page_tween != null:
_logbook_page_tween.kill()
_logbook_page_tween = null
_logbook_page_transitioning = false
if reset_visuals and is_instance_valid(_book_spread):
_book_spread.position.x = 0.0
_book_spread.modulate.a = 1.0
func _update_logbook_page_control_state(interactive: bool) -> void:
var has_multiple_pages: bool = _logbook_page_count > 1
_logbook_previous.visible = has_multiple_pages
_logbook_next.visible = has_multiple_pages
_logbook_previous.disabled = (
not interactive or _logbook_current_page <= 0
)
_logbook_next.disabled = (
not interactive
or _logbook_current_page >= _logbook_page_count - 1
)
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
control.focus_mode = (
Control.FOCUS_ALL
if control.visible and not control.disabled
else Control.FOCUS_NONE
)
control.mouse_filter = (
Control.MOUSE_FILTER_STOP
if control.visible and not control.disabled
else Control.MOUSE_FILTER_IGNORE
)
func _configure_logbook_focus() -> void:
if _current_section != Section.LOGBOOK:
return
_logbook_tab.focus_neighbor_bottom = _logbook_tab.focus_neighbor_right
if _logbook_page_count <= 1:
return
var first_control: BubbleButtonType = (
_logbook_previous
if not _logbook_previous.disabled
else _logbook_next
)
_logbook_tab.focus_neighbor_bottom = _logbook_tab.get_path_to(
first_control
)
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
control.focus_neighbor_top = control.get_path_to(_logbook_tab)
control.focus_neighbor_bottom = control.focus_neighbor_top
control.focus_neighbor_left = control.get_path_to(
_logbook_previous
)
control.focus_neighbor_right = control.get_path_to(_logbook_next)
func _advance_logbook_page_controls(delta: float) -> void:
for control: BubbleButtonType in [_logbook_previous, _logbook_next]:
if not control.visible:
continue
control.advance_emphasis(delta)
control.apply_presentation(
control.calculate_target(_motion_elapsed, 1.0, 1.0),
control.calculate_visual_scale(_motion_elapsed, 1.0),
minf(1.0, delta * 10.0),
2026-07-25 16:02:34 -04:00
)
func _create_texture_frame(
texture: Texture2D,
minimum_size: Vector2,
) -> TextureRect:
var texture_frame := TextureRect.new()
texture_frame.custom_minimum_size = minimum_size
texture_frame.texture = texture
texture_frame.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
texture_frame.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture_frame.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
texture_frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
if texture == null:
2026-07-26 23:18:51 -04:00
texture_frame.tooltip_text = "display texture unavailable"
2026-07-25 16:02:34 -04:00
return texture_frame
func _clear_container(container: Container) -> void:
for child: Node in container.get_children():
container.remove_child(child)
child.queue_free()