netfishing/ui/player_menu.gd

4501 lines
139 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name PlayerMenu
extends Control
const INPUT_OWNER: StringName = &"player_menu"
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishDataType = preload("res://fish/fish_data.gd")
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")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
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"
)
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
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"
)
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
const NAVIGATION_PRESENTATION_SCALE: float = 0.60
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
const NAVIGATION_SELECTED_SCALE: float = 1.02
const INVENTORY_TAB_LEFT_INSET: float = 96.0
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
signal menu_visibility_changed(is_open: bool)
signal inventory_hotbar_context_changed(show_hotbar: bool)
signal controller_hotbar_placement_requested(
assignment_kind: PlayerHotbarType.AssignmentKind,
identity: StringName,
initial_slot: int,
)
signal controller_hotbar_placement_ended
signal controller_hotbar_management_requested(initial_slot: int)
signal controller_hotbar_management_ended
signal menu_exit_started
signal shop_cooler_modal_changed(is_open: bool)
enum Section {
COOLER,
BAG,
TACKLE_BOX,
LOGBOOK,
NET,
MAIL,
PROFILE,
PLAYERS,
}
enum BagView {
EQUIPMENT,
CONSUMABLES,
}
enum TackleView {
BAIT,
LURES,
}
enum SortMode {
CATCH_ORDER,
NAME,
RARITY,
}
enum CloseReason {
USER,
BITE_STARTED,
WATER_RECOVERY,
GAME_MENU,
SESSION_END,
TEARDOWN,
}
enum ControllerOwnership {
ITEM_LIST,
NOTEPAD_ACTIONS,
HOTBAR_MANAGEMENT,
HOTBAR_PLACEMENT,
}
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)
@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
@onready var _cooler_water_surface: ColorRect = %WaterSurface
@onready var _fish_field: Control = %FishField
@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
@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
@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_equip_button: NotepadInkActionType = %TackleEquipButton
@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
@onready var _catalog_logbook: LogbookPage = %CatalogLogbook
@onready var _the_net_page: TheNetPage = %TheNetPage
@onready var _mail_page: MailPage = %MailPage
@onready var _profile_page: ProfilePage = %ProfilePage
@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
@onready var _inventory_tab: BubbleButtonType = %InventoryTab
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
@onready var _the_net_tab: BubbleButtonType = %TheNetTab
@onready var _mail_tab: BubbleButtonType = %MailTab
@onready var _profile_tab: BubbleButtonType = %ProfileTab
@onready var _players_tab: BubbleButtonType = %PlayersTab
@onready var _mail_unread_badge: Label = %MailUnreadBadge
@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
@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
@onready var _inventory_section: Control = %InventorySection
@onready var _bag_section: Control = %BagSection
@onready var _logbook_section: Control = %LogbookSection
@onready var _bag_empty: Label = %BagEmpty
@onready var _bag_grid: GridContainer = %BagGrid
@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
@onready var _sort_option: OptionButton = %SortOption
@onready var _sort_direction: Button = %SortDirection
@onready var _held_value: Label = %HeldValue
@onready var _cooler_count: Label = %CoolerCount
@onready var _inventory_empty: Label = %InventoryEmpty
@onready var _detail_texture: TextureRect = %DetailTexture
@onready var _detail_name: Label = %DetailName
@onready var _detail_data: Label = %DetailData
@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
@onready var _logbook_empty: Label = %LogbookEmpty
@onready var _logbook_grid: GridContainer = %LogbookGrid
var _compact_layout: bool = false
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
var _network_mail_service: NetworkMailService
var _reservations: PlayerAssetReservationService
var _network_profile_service: NetworkProfileService
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
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
var _controller_ownership: ControllerOwnership = ControllerOwnership.ITEM_LIST
var _controller_source_section: Section = Section.COOLER
var _controller_source_identity: StringName
var _controller_notepad_actions: Array[BaseButton] = []
var _controller_hotbar_assignment_kind: PlayerHotbarType.AssignmentKind = (
PlayerHotbarType.AssignmentKind.EMPTY
)
var _controller_hotbar_identity: StringName
var _controller_previous_hotbar_slot: int = 0
var _sort_mode: SortMode = SortMode.CATCH_ORDER
var _sort_descending: bool = true
var _fish_selection := FishBatchSelectionType.new()
var _selected_bag_item_id: StringName
var _prior_movement_enabled: bool = true
var _prior_camera_input_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _control_snapshot_stored: bool = false
var _mouse_snapshot_stored: bool = false
var _menu_generation: int = 0
var _confirmation_catch_ids: Array[StringName] = []
var _confirmation_buyer: FishBuyerProfileType
var _confirmation_buyer_id: StringName
var _confirmation_generation: int = -1
var _sale_in_progress: bool = false
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
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
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))
_tackle_equip_button.pressed.connect(_toggle_active_bait)
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
)
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
_close_button.pressed.connect(close_menu)
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
_sort_direction.pressed.connect(_on_sort_direction_pressed)
_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()
_sort_option.add_item("catch order", SortMode.CATCH_ORDER)
_sort_option.add_item("name", SortMode.NAME)
_sort_option.add_item("rarity", SortMode.RARITY)
_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)
_update_sort_direction_text()
_navigation_cluster.configure([
_inventory_tab,
_logbook_tab,
_the_net_tab,
_mail_tab,
_profile_tab,
_players_tab,
_close_button,
])
_configure_navigation_focus()
_apply_navigation_styles()
_apply_inventory_styles()
# 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()
_bag_filter_tabs.move_to_front()
_apply_cooler_control_styles()
_apply_cooler_wall_styles()
_apply_cooler_notepad_style()
_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()
resized.connect(_update_shell_layout)
_show_section_immediate(_current_section)
call_deferred("_update_shell_layout")
call_deferred("_update_cooler_water_mask")
set_process(false)
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
)
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,
)
func setup(
player: PlayerType,
inventory: FishInventoryType,
collection_log: CollectionLogType,
experience: PlayerExperienceType,
wallet: PlayerWalletType,
sale_service: FishSaleServiceType,
default_buyer: FishBuyerProfileType,
catalog: FishPoolType,
fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
network_sale_service: NetworkSaleService,
network_mail_service: NetworkMailService,
reservations: PlayerAssetReservationService,
network_profile_service: NetworkProfileService,
network_player_list: NetworkPlayerListService,
player_jobs: PlayerJobService,
world_time: WorldTimeService,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_player = player
_inventory = inventory
_collection_log = collection_log
_wallet = wallet
_sale_service = sale_service
_default_buyer = default_buyer
_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
_network_mail_service = network_mail_service
_reservations = reservations
_network_profile_service = network_profile_service
_network_player_list = network_player_list
_mail_page.setup(
network_mail_service, reservations, inventory, wallet, bag, item_catalog
)
_profile_page.setup(
network_profile_service,
experience,
world_environment,
world_sun,
)
_players_page.setup(network_player_list)
_catalog_logbook.setup(collection_log, inventory, catalog)
_the_net_page.setup(player_jobs, world_time)
_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
)
_fish_selection.clear()
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)
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 _player.active_bait_changed.is_connected(_on_active_bait_changed):
_player.active_bait_changed.connect(_on_active_bait_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)
_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)
var _left_page_trigger_held: bool = false
var _right_page_trigger_held: bool = false
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.echo:
return
if _handle_controller_ownership_input(event):
get_viewport().set_input_as_handled()
return
if visible:
_reserve_main_navigation_for_page_switching()
_reserve_visible_secondary_navigation()
if _handle_controller_page_switch(event):
get_viewport().set_input_as_handled()
return
if _handle_controller_secondary_switch(event):
get_viewport().set_input_as_handled()
return
if _handle_direct_page_shortcut(event):
get_viewport().set_input_as_handled()
return
if visible:
if event.is_action_pressed("open_backpack"):
if not _transitioning and not _page_transitioning:
close_menu()
get_viewport().set_input_as_handled()
return
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_ownership_input(event: InputEvent) -> bool:
if not visible:
return false
var button_event := event as InputEventJoypadButton
var accept_pressed: bool = (
button_event != null
and button_event.pressed
and _event_matches_controller_role(
event,
ControllerMappingManagerType.ROLE_A,
JOY_BUTTON_A,
)
)
var cancel_pressed: bool = (
button_event != null
and button_event.pressed
and _event_matches_controller_role(
event,
ControllerMappingManagerType.ROLE_B,
JOY_BUTTON_B,
)
)
var alternate_pressed: bool = (
button_event != null
and button_event.pressed
and _event_matches_controller_role(
event,
ControllerMappingManagerType.ROLE_Y,
JOY_BUTTON_Y,
)
)
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
if cancel_pressed:
_release_controller_ownership(true, false)
return true
if alternate_pressed:
if _hotbar != null:
_hotbar.clear_slot(_hotbar.get_selected_slot())
return true
if accept_pressed:
return true
return false
if _controller_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
if accept_pressed:
_confirm_controller_hotbar_placement()
return true
if cancel_pressed:
_release_controller_ownership(true, true)
return true
if alternate_pressed:
return true
return false
if _controller_ownership == ControllerOwnership.NOTEPAD_ACTIONS:
if cancel_pressed:
_release_controller_ownership(true, false)
return true
if alternate_pressed:
return true
var direction: int = 0
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
direction = -1
elif (
event.is_action_pressed("ui_right")
or event.is_action_pressed("ui_down")
):
direction = 1
if direction != 0:
_focus_next_notepad_action(direction)
return true
return false
if alternate_pressed:
return _try_begin_controller_hotbar_placement()
if accept_pressed:
return _try_enter_notepad_controller_ownership()
return false
func _event_matches_controller_role(
event: InputEvent,
role: StringName,
fallback_button: JoyButton,
) -> bool:
if _controller_mapping_manager != null:
return _controller_mapping_manager.event_matches_role(event, role)
var button_event := event as InputEventJoypadButton
return button_event != null and button_event.button_index == fallback_button
func _try_enter_notepad_controller_ownership() -> bool:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner == null:
return false
var source_identity: StringName
var actions: Array[BaseButton] = []
if _current_section == Section.COOLER:
var fish_node := focus_owner as CoolerFishSpriteType
if fish_node == null or fish_node.catch_id.is_empty():
return false
source_identity = fish_node.catch_id
_on_catch_card_pressed(source_identity)
if not _favorite_bubble.disabled:
actions.append(_favorite_bubble)
if not _sell_bubble.disabled:
actions.append(_sell_bubble)
elif _current_section == Section.TACKLE_BOX:
if not focus_owner.has_meta(&"controller_tackle_item_id"):
return false
source_identity = StringName(
str(focus_owner.get_meta(&"controller_tackle_item_id"))
)
_select_tackle_item(source_identity)
if _tackle_equip_button.visible and not _tackle_equip_button.disabled:
actions.append(_tackle_equip_button)
else:
return false
if actions.is_empty():
_restore_controller_item_focus(_current_section, source_identity)
return true
_controller_source_section = _current_section
_controller_source_identity = source_identity
_controller_notepad_actions = actions
_controller_ownership = ControllerOwnership.NOTEPAD_ACTIONS
actions.front().call_deferred("grab_focus")
return true
func _focus_next_notepad_action(direction: int) -> void:
var available: Array[BaseButton] = []
for action: BaseButton in _controller_notepad_actions:
if (
is_instance_valid(action)
and action.visible
and not action.disabled
and action.focus_mode != Control.FOCUS_NONE
):
available.append(action)
if available.is_empty():
return
var focused: Control = get_viewport().gui_get_focus_owner()
var current_index: int = available.find(focused)
if current_index < 0:
current_index = 0 if direction > 0 else available.size() - 1
else:
current_index = wrapi(
current_index + direction,
0,
available.size(),
)
available[current_index].grab_focus()
func _try_begin_controller_hotbar_placement() -> bool:
if _hotbar == null:
return false
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner == null:
return false
var assignment_kind: PlayerHotbarType.AssignmentKind = (
PlayerHotbarType.AssignmentKind.EMPTY
)
var identity: StringName
if _current_section == Section.COOLER:
var fish_node := focus_owner as CoolerFishSpriteType
if fish_node == null or fish_node.catch_id.is_empty():
return false
assignment_kind = PlayerHotbarType.AssignmentKind.FISH
identity = fish_node.catch_id
elif _current_section == Section.BAG:
var item_node := focus_owner as BagItemSpriteType
if item_node == null or item_node.item_id.is_empty():
return false
var item: ItemDataType = (
_item_catalog.get_item_by_id(item_node.item_id)
if _item_catalog != null
else null
)
if item == null or not item.hotbar_allowed:
return true
assignment_kind = PlayerHotbarType.AssignmentKind.ITEM
identity = item_node.item_id
else:
return false
_controller_source_section = _current_section
_controller_source_identity = identity
_controller_hotbar_assignment_kind = assignment_kind
_controller_hotbar_identity = identity
_controller_previous_hotbar_slot = _hotbar.get_selected_slot()
var initial_slot: int = _find_controller_hotbar_assignment(
assignment_kind,
identity,
)
if initial_slot < 0:
initial_slot = _controller_previous_hotbar_slot
_controller_ownership = ControllerOwnership.HOTBAR_PLACEMENT
controller_hotbar_placement_requested.emit(
assignment_kind,
identity,
initial_slot,
)
return true
func _find_controller_hotbar_assignment(
assignment_kind: PlayerHotbarType.AssignmentKind,
identity: StringName,
) -> int:
for slot_index: int in range(PlayerHotbarType.SLOT_COUNT):
if (
assignment_kind == PlayerHotbarType.AssignmentKind.FISH
and _hotbar.get_fish_catch_id(slot_index) == identity
):
return slot_index
if (
assignment_kind == PlayerHotbarType.AssignmentKind.ITEM
and _hotbar.get_item_id(slot_index) == identity
):
return slot_index
return -1
func _confirm_controller_hotbar_placement() -> void:
if (
_controller_ownership != ControllerOwnership.HOTBAR_PLACEMENT
or _hotbar == null
):
return
var slot_index: int = _hotbar.get_selected_slot()
var assigned: bool = false
if (
_controller_hotbar_assignment_kind
== PlayerHotbarType.AssignmentKind.FISH
):
assigned = _hotbar.assign_fish(
slot_index,
_controller_hotbar_identity,
)
elif (
_controller_hotbar_assignment_kind
== PlayerHotbarType.AssignmentKind.ITEM
):
assigned = _hotbar.assign_item(
slot_index,
_controller_hotbar_identity,
)
if assigned:
_release_controller_ownership(true, false)
func _release_controller_ownership(
restore_source_focus: bool,
restore_previous_hotbar_slot: bool,
) -> void:
if _controller_ownership == ControllerOwnership.ITEM_LIST:
return
var prior_ownership: ControllerOwnership = _controller_ownership
var source_section: Section = _controller_source_section
var source_identity: StringName = _controller_source_identity
_controller_ownership = ControllerOwnership.ITEM_LIST
_controller_notepad_actions.clear()
_controller_source_identity = StringName()
_controller_hotbar_assignment_kind = PlayerHotbarType.AssignmentKind.EMPTY
_controller_hotbar_identity = StringName()
if prior_ownership == ControllerOwnership.HOTBAR_PLACEMENT:
if restore_previous_hotbar_slot and _hotbar != null:
_hotbar.select_slot(_controller_previous_hotbar_slot)
controller_hotbar_placement_ended.emit()
if prior_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
controller_hotbar_management_ended.emit()
inventory_hotbar_context_changed.emit(
_current_section in [Section.COOLER, Section.BAG]
)
if restore_source_focus:
call_deferred(
"_restore_controller_item_focus",
source_section,
source_identity,
)
func _restore_controller_item_focus(
section: Section,
identity: StringName,
) -> void:
if not visible or section != _current_section:
return
if identity.is_empty():
_focus_current_section()
return
var target: Control
if section == Section.COOLER:
target = _fish_nodes.get(identity) as CoolerFishSpriteType
elif section == Section.BAG:
target = _bag_item_nodes.get(identity) as BagItemSpriteType
elif section == Section.TACKLE_BOX:
for child: Node in _tackle_item_list.get_children():
var button := child as BaseButton
if (
button != null
and button.has_meta(&"controller_tackle_item_id")
and StringName(str(button.get_meta(
&"controller_tackle_item_id"
))) == identity
):
target = button
break
if (
target != null
and is_instance_valid(target)
and target.is_visible_in_tree()
and target.focus_mode != Control.FOCUS_NONE
):
target.grab_focus()
else:
_focus_current_section()
func _handle_controller_page_switch(event: InputEvent) -> bool:
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
var use_mapping: bool = (
_controller_mapping_manager != null
)
var uses_left_trigger: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_POINTER_MODIFIER
)
if use_mapping
else (
motion_event != null
and motion_event.axis == JOY_AXIS_TRIGGER_LEFT
)
)
var uses_right_trigger: bool = (
_controller_mapping_manager.event_uses_role(
event, ControllerMappingManagerType.ROLE_CAMERA_ZOOM
)
if use_mapping
else (
motion_event != null
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
)
)
if (
not (uses_left_trigger or uses_right_trigger)
or not visible
):
return false
var is_pressed: bool = (
button_event.pressed
if button_event != null
else motion_event.axis_value > 0.5
)
if not is_pressed:
if uses_left_trigger:
_left_page_trigger_held = false
if uses_right_trigger:
_right_page_trigger_held = false
return true
if (
(uses_left_trigger and _left_page_trigger_held)
or (uses_right_trigger and _right_page_trigger_held)
):
return true
if uses_left_trigger:
_left_page_trigger_held = true
if uses_right_trigger:
_right_page_trigger_held = true
# Trigger input belongs to the Player Menu while it is visible, even when a
# transition or modal temporarily prevents changing pages.
if (
_transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
or _controller_ownership != ControllerOwnership.ITEM_LIST
):
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_trigger else 1
)
var next_index: int = wrapi(current_index + direction, 0, sections.size())
_show_section(sections[next_index])
return true
func _handle_controller_secondary_switch(event: InputEvent) -> bool:
var button_event := event as InputEventJoypadButton
if button_event == null or not visible:
return false
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.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.button_index == JOY_BUTTON_RIGHT_SHOULDER
)
if not (uses_left_bumper or uses_right_bumper):
return false
if not button_event.pressed:
return true
var direction: int = -1 if uses_left_bumper else 1
if _controller_ownership == ControllerOwnership.HOTBAR_MANAGEMENT:
_cycle_from_controller_hotbar_management(direction)
return true
if (
_transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
or _controller_ownership != ControllerOwnership.ITEM_LIST
):
return true
if _is_inventory_section(_current_section):
if (
(direction > 0 and _current_section == Section.BAG)
or (direction < 0 and _current_section == Section.COOLER)
):
_begin_controller_hotbar_management()
return true
var inventory_sections: Array[Section] = [
Section.COOLER,
Section.TACKLE_BOX,
Section.BAG,
]
var inventory_index: int = inventory_sections.find(_current_section)
_show_section(inventory_sections[wrapi(
inventory_index + direction,
0,
inventory_sections.size(),
)])
return true
_cycle_visible_secondary_tabs(direction)
return true
func _begin_controller_hotbar_management() -> void:
if _hotbar == null:
return
_controller_source_section = _current_section
_controller_source_identity = StringName()
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if _current_section == Section.COOLER:
var fish_node := focus_owner as CoolerFishSpriteType
if fish_node != null:
_controller_source_identity = fish_node.catch_id
elif _current_section == Section.BAG:
var item_node := focus_owner as BagItemSpriteType
if item_node != null:
_controller_source_identity = item_node.item_id
_controller_ownership = ControllerOwnership.HOTBAR_MANAGEMENT
controller_hotbar_management_requested.emit(_hotbar.get_selected_slot())
func _cycle_from_controller_hotbar_management(direction: int) -> void:
_release_controller_ownership(false, false)
var target_section: Section = (
Section.COOLER if direction > 0 else Section.BAG
)
if target_section == _current_section:
call_deferred("_focus_current_section")
else:
_show_section(target_section)
func _cycle_visible_secondary_tabs(direction: int) -> bool:
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
var grouped_buttons: Dictionary = {}
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
var selected_group: Variant = null
var selected_group_y: float = INF
for group_value: Variant in grouped_buttons.keys():
var buttons := grouped_buttons[group_value] as Array
if buttons.size() < 2:
continue
var group_y: float = INF
for item: Variant in buttons:
var button := item as BaseButton
group_y = minf(group_y, button.global_position.y)
if group_y < selected_group_y:
selected_group = group_value
selected_group_y = group_y
if selected_group == null:
return false
var tabs := grouped_buttons[selected_group] as Array
tabs.sort_custom(func(first: BaseButton, second: BaseButton) -> bool:
return first.global_position.x < second.global_position.x
)
var current_index: int = 0
for index: int in tabs.size():
var tab := tabs[index] as BaseButton
if tab.button_pressed:
current_index = index
break
var target := tabs[wrapi(
current_index + direction, 0, tabs.size()
)] as BaseButton
target.set_pressed_no_signal(true)
target.pressed.emit()
return true
func _collect_visible_toggle_buttons(
root: Node,
navigation_cluster: Control,
grouped_buttons: Dictionary,
) -> void:
for child: Node in root.get_children():
var child_control := child as Control
if child_control != null and not child_control.is_visible_in_tree():
continue
var button := child as BaseButton
if (
button != null
and button.toggle_mode
and (
navigation_cluster == null
or not navigation_cluster.is_ancestor_of(button)
)
):
var group_key: Variant = (
button.button_group
if button.button_group != null
else button.get_parent()
)
if not grouped_buttons.has(group_key):
grouped_buttons[group_key] = []
var buttons := grouped_buttons[group_key] as Array
buttons.append(button)
_collect_visible_toggle_buttons(
child, navigation_cluster, grouped_buttons
)
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
var key_event := event as InputEventKey
if (
key_event == null
or not key_event.pressed
or _is_text_input_active()
or _shortcut_blocked_by_modal_state()
):
return false
var physical_key: Key = key_event.physical_keycode
var inventory_requested: bool = physical_key == KEY_I
var logbook_requested: bool = physical_key == KEY_L
var tackle_requested: bool = (
physical_key == KEY_V
or event.is_action_pressed("open_tacklebox")
)
if not inventory_requested and not tackle_requested and not logbook_requested:
return false
var requested_section: Section = (
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
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
if _transitioning or _page_transitioning:
return true
if _sale_confirmation.visible:
_close_sale_confirmation()
elif _current_section == Section.MAIL and _mail_page.consume_escape():
pass
elif _current_section == Section.PROFILE and _profile_page.consume_escape():
pass
else:
close_menu()
return true
func open_menu() -> void:
if (
visible
or _shop_cooler_context_active
or _player == null
or _fishing_spot == null
or not _fishing_spot.can_open_player_menu()
):
return
_release_controller_ownership(false, true)
_menu_generation += 1
_transition_generation += 1
_cancel_presentation_tween()
_cancel_page_tween()
_prior_movement_enabled = _player.is_movement_enabled()
_prior_camera_input_enabled = _player.is_camera_input_enabled()
_prior_mouse_mode = Input.mouse_mode
_control_snapshot_stored = true
_mouse_snapshot_stored = true
_player.set_movement_enabled(false)
_player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
visible = true
_refresh_all()
_show_section_immediate(_current_section)
_update_shell_layout()
_begin_menu_entry()
menu_visibility_changed.emit(true)
inventory_hotbar_context_changed.emit(
_current_section in [Section.COOLER, Section.BAG]
)
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)
_cooler_sort_option.call_deferred("grab_focus")
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(
reason: CloseReason = CloseReason.USER,
restore_controls: bool = true,
) -> void:
if not visible:
return
if (
reason == CloseReason.USER
and _current_section == Section.PROFILE
and _profile_page.request_close_confirmation()
):
return
if _transitioning:
if reason != CloseReason.USER:
_finish_close(reason, restore_controls, _menu_generation)
return
_release_controller_ownership(false, true)
get_viewport().gui_cancel_drag()
_close_sale_confirmation()
if reason in [
CloseReason.BITE_STARTED,
CloseReason.WATER_RECOVERY,
CloseReason.SESSION_END,
CloseReason.TEARDOWN,
]:
_fish_selection.clear()
menu_exit_started.emit()
_begin_menu_exit(reason, restore_controls)
func close_for_water_recovery() -> void:
_fish_selection.clear()
close_menu(CloseReason.WATER_RECOVERY, false)
func close_for_game_menu() -> void:
close_menu(CloseReason.GAME_MENU)
func close_for_session_end() -> void:
_fish_selection.clear()
close_menu(CloseReason.SESSION_END, false)
func _exit_tree() -> void:
_release_controller_ownership(false, true)
_cancel_presentation_tween()
_cancel_page_tween()
if visible:
_finish_close(CloseReason.TEARDOWN, false, _menu_generation)
_fish_selection.clear()
_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
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
func _on_bite_activated() -> void:
if visible:
close_menu(CloseReason.BITE_STARTED)
func _show_section(section: Section) -> void:
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
_release_controller_ownership(false, true)
_begin_page_transition(section)
func _show_section_immediate(section: Section) -> void:
_current_section = section
if _is_inventory_section(section):
_last_inventory_section = section
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
_mail_page.visible = section == Section.MAIL
_profile_page.visible = section == Section.PROFILE
_players_page.visible = section == Section.PLAYERS
_content_shell.visible = false
_inventory_section.visible = false
_bag_section.visible = false
_content_shell.set_background_visible(true)
_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)
_catalog_logbook.deactivate()
else:
_catalog_logbook.activate()
if section == Section.NET:
_the_net_page.activate()
else:
_the_net_page.deactivate()
if section == Section.MAIL:
_mail_page.activate()
else:
_mail_page.deactivate()
if section == Section.PROFILE:
_profile_page.activate()
else:
_profile_page.deactivate()
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)
_refresh_inventory_organizer_tabs()
_logbook_tab.button_pressed = section == Section.LOGBOOK
_the_net_tab.button_pressed = section == Section.NET
_mail_tab.button_pressed = section == Section.MAIL
_profile_tab.button_pressed = section == Section.PROFILE
_players_tab.button_pressed = section == Section.PLAYERS
_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]
func _refresh_inventory_organizer_tabs() -> void:
for tab: OrganizerTabType in [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
]:
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,
]
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,
]:
tab.settle_for_close()
func _show_last_inventory_section() -> void:
_show_section(_last_inventory_section)
func _focus_current_section() -> void:
if _shop_cooler_context_active:
_focus_shop_cooler()
return
_reserve_main_navigation_for_page_switching()
_reserve_visible_secondary_navigation()
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
var candidates: Array[Control] = []
_collect_focusable_content(self, navigation_cluster, candidates)
if candidates.is_empty():
return
candidates.sort_custom(func(first: Control, second: Control) -> bool:
if not is_equal_approx(first.global_position.y, second.global_position.y):
return first.global_position.y < second.global_position.y
return first.global_position.x < second.global_position.x
)
candidates[0].grab_focus()
func _collect_focusable_content(
root: Node,
navigation_cluster: Control,
output: Array[Control],
) -> void:
for child: Node in root.get_children():
if child == navigation_cluster:
continue
var control := child as Control
if control != null and not control.is_visible_in_tree():
continue
if (
control != null
and control.focus_mode != Control.FOCUS_NONE
and not control is ScrollBar
):
output.append(control)
_collect_focusable_content(child, navigation_cluster, output)
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,
)
func _configure_navigation_focus() -> void:
_reserve_main_navigation_for_page_switching()
var navigation: Array[BubbleButtonType] = [
_inventory_tab,
_logbook_tab,
_the_net_tab,
_mail_tab,
_profile_tab,
_players_tab,
_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
func _reserve_main_navigation_for_page_switching() -> void:
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
if navigation_cluster == null:
return
_set_descendant_focus_disabled(navigation_cluster)
func _set_descendant_focus_disabled(root: Node) -> void:
for child: Node in root.get_children():
var control := child as Control
if control != null:
control.focus_mode = Control.FOCUS_NONE
_set_descendant_focus_disabled(child)
func _reserve_visible_secondary_navigation() -> void:
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
var grouped_buttons: Dictionary = {}
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
var selected_tabs: Array = []
var selected_group_y: float = INF
for group_value: Variant in grouped_buttons.keys():
var buttons := grouped_buttons[group_value] as Array
if buttons.size() < 2:
continue
var group_y: float = INF
for item: Variant in buttons:
var button := item as BaseButton
group_y = minf(group_y, button.global_position.y)
if group_y < selected_group_y:
selected_tabs = buttons
selected_group_y = group_y
var focus_owner: Control = get_viewport().gui_get_focus_owner()
var displaced_focus: bool = selected_tabs.has(focus_owner)
for item: Variant in selected_tabs:
var tab := item as Control
tab.focus_mode = Control.FOCUS_NONE
if displaced_focus:
focus_owner.release_focus()
call_deferred("_focus_current_section")
var inventory_tabs: Array[Button] = [
_cooler_sub_tab,
_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
)
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:
_catalog_logbook.focus_initial()
Section.NET:
_the_net_page.focus_initial()
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)
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:
var available_items: Array[OwnedItemType] = (
_bag.get_unlocked_bait_items()
if _tackle_view == TackleView.BAIT
else _bag.get_all_items()
)
for owned: OwnedItemType in available_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.icon = item.icon
row.tooltip_text = "%s ×%d" % [item.display_name, owned.quantity]
if item.is_bait() and item.icon != null:
row.custom_minimum_size = Vector2(72, 72)
row.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
row.expand_icon = true
row.alignment = HORIZONTAL_ALIGNMENT_CENTER
else:
row.text = "%s ×%d" % [item.display_name, owned.quantity]
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
row.toggle_mode = true
row.set_meta(&"controller_tackle_item_id", owned.item_id)
row.button_pressed = owned.item_id == _selected_tackle_item_id
if item.is_bait() and item.icon != null:
_apply_tackle_bait_button_style(row)
_add_tackle_quantity_badge(row, owned.quantity)
else:
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 _add_tackle_quantity_badge(row: Button, quantity: int) -> void:
var badge := Panel.new()
badge.name = "QuantityBadge"
badge.anchor_left = 1.0
badge.anchor_right = 1.0
badge.offset_left = -27.0
badge.offset_top = 3.0
badge.offset_right = -3.0
badge.offset_bottom = 27.0
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.z_index = 2
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
badge.add_theme_stylebox_override("panel", badge_style)
row.add_child(badge)
var quantity_label := Label.new()
quantity_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
quantity_label.offset_left = 1.0
quantity_label.offset_right = 1.0
quantity_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
quantity_label.text = str(quantity)
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
quantity_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
quantity_label.add_theme_font_size_override(
"font_size",
13 if quantity >= 100 else 15,
)
quantity_label.add_theme_color_override("font_color", Color("e7f5f4"))
badge.add_child(quantity_label)
func _apply_tackle_bait_button_style(button: Button) -> void:
var profile := BubbleMenuProfile.new()
var normal_style: StyleBoxFlat = profile.make_normal_style()
var hover_style: StyleBoxFlat = profile.make_hover_style()
var pressed_style: StyleBoxFlat = profile.make_pressed_style()
var disabled_style: StyleBoxFlat = profile.make_disabled_style()
for style: StyleBoxFlat in [
normal_style,
hover_style,
pressed_style,
disabled_style,
]:
style.set_corner_radius_all(36)
style.content_margin_left = 13.5
style.content_margin_top = 13.5
style.content_margin_right = 13.5
style.content_margin_bottom = 13.5
button.add_theme_stylebox_override("normal", normal_style)
button.add_theme_stylebox_override("hover", hover_style)
button.add_theme_stylebox_override("focus", hover_style)
button.add_theme_stylebox_override("pressed", pressed_style)
button.add_theme_stylebox_override("disabled", disabled_style)
button.add_theme_color_override("icon_normal_color", Color.WHITE)
button.add_theme_color_override("icon_hover_color", Color.WHITE)
button.add_theme_color_override("icon_focus_color", Color.WHITE)
button.add_theme_color_override("icon_pressed_color", Color.WHITE)
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_equip_button.visible = false
_tackle_equip_button.persistent_mark = false
_tackle_detail_text.text = (
"Select bait for details."
if _tackle_view == TackleView.BAIT
else "Select a lure for details."
)
return
_tackle_equip_button.visible = item.is_bait()
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 detail_lines: Array[String] = [
item.display_name,
"",
"quantity: %d" % quantity,
]
if assigned_slot >= 0:
detail_lines.append("hotbar slot %d" % (assigned_slot + 1))
detail_lines.append("")
detail_lines.append(item.description)
_tackle_detail_text.text = "\n".join(detail_lines)
if item.is_bait():
var is_equipped: bool = (
_player != null and _player.active_bait_id == item.item_id
)
_tackle_equip_button.text = (
"dequip %s" % item.display_name
if is_equipped
else "equip %s" % item.display_name
)
_tackle_equip_button.persistent_mark = is_equipped
_tackle_equip_button.disabled = quantity <= 0
_tackle_equip_button.refresh_ink_state()
func _toggle_active_bait() -> void:
if _player == null or _item_catalog == null:
return
var item: ItemDataType = _item_catalog.get_item_by_id(_selected_tackle_item_id)
if item == null or not item.is_bait():
return
if _player.active_bait_id == item.item_id:
_player.unequip_bait()
else:
_player.equip_bait(item)
_update_tackle_detail()
func _on_active_bait_changed(_item_id: StringName) -> void:
_update_tackle_detail()
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:
_detail_bubble.add_theme_stylebox_override(
"panel",
InventoryNotepadType.make_layout_style(false),
)
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,
_mail_tab,
_profile_tab,
_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,
_mail_tab,
_profile_tab,
_players_tab,
]:
if not bubble.button_pressed:
continue
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")
func _update_navigation_selection() -> void:
_set_navigation_target(_current_section)
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)
func _set_navigation_target(section: Section) -> void:
_inventory_tab.button_pressed = _is_inventory_section(section)
_logbook_tab.button_pressed = section == Section.LOGBOOK
_the_net_tab.button_pressed = section == Section.NET
_mail_tab.button_pressed = section == Section.MAIL
_profile_tab.button_pressed = section == Section.PROFILE
_players_tab.button_pressed = section == Section.PLAYERS
func _update_shell_layout() -> void:
if not is_node_ready():
return
var compact: bool = false
_compact_layout = compact
var reference_size := DESKTOP_REFERENCE_SIZE
_presentation_scale_root.size = reference_size
if not _transitioning:
_presentation_scale_root.scale = Vector2.ONE
_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)
_presentation_rest_position = _content_shell.position
var navigation_size := (
Vector2(840.0, 75.0) if compact else Vector2(840.0, 100.0)
)
_navigation_cluster.position = NAVIGATION_CANONICAL_POSITION
_navigation_cluster.size = navigation_size
_navigation_cluster.scale = (
Vector2.ONE * NAVIGATION_PRESENTATION_SCALE
)
_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)
_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)
_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)
_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 = (
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)
)
_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,
]:
status_value.add_theme_font_size_override(
"font_size",
10 if compact else 14,
)
_cooler_detail_texture.position = (
Vector2(5.0, 84.0) if compact else Vector2(80.0, 137.0)
)
_cooler_detail_texture.size = (
Vector2(74.0, 58.0) if compact else Vector2(128.0, 100.0)
)
_cooler_detail_name.position = (
Vector2(82.0, 82.0) if compact else Vector2(18.0, 241.0)
)
_cooler_detail_name.size = (
Vector2(214.0, 26.0) if compact else Vector2(252.0, 32.0)
)
_cooler_detail_name.add_theme_font_size_override(
"font_size", 18 if compact else 25,
)
_cooler_detail_stats.position = (
Vector2(82.0, 109.0) if compact else Vector2(18.0, 274.0)
)
_cooler_detail_stats.size = (
Vector2(214.0, 39.0) if compact else Vector2(252.0, 65.0)
)
_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 = (
Vector2(130.0, 35.0) if compact else Vector2(252.0, 40.0)
)
_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(
"font_size", 11 if compact else 17,
)
_sell_bubble.add_theme_font_size_override(
"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
_bag_outer_wall.position = INVENTORY_MAIN_POSITION
_bag_outer_wall.size = INVENTORY_MAIN_SIZE
_bag_filter_tabs.position = (
INVENTORY_MAIN_POSITION + INVENTORY_HEADER_INSET
)
_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
_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,
)
_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)
_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(
520.0 if compact else 800.0,
512.0 if compact else 420.0,
)
_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
_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
_mail_page.modulate.a = 1.0
_profile_page.modulate.a = 1.0
_players_page.modulate.a = 1.0
if not _page_transitioning:
_content_stage.position = _content_rest_position
_layout_cooler_fish(false)
_layout_bag_items()
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,
)
func _begin_menu_entry() -> void:
_transitioning = true
_set_shell_interactive(false)
_reset_page_transition_visuals()
_animate_inventory_tab_entry()
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
)
_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)
_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)
_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)
_settle_inventory_tabs_for_close()
if _current_section == Section.LOGBOOK:
_catalog_logbook.deactivate()
elif _current_section == Section.NET:
_the_net_page.deactivate()
var current_viewport: Viewport = get_viewport()
if current_viewport != null:
current_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)
_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)
_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()
_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
visible = false
set_process(false)
var current_viewport: Viewport = get_viewport()
if current_viewport != null:
current_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
Section.LOGBOOK:
return _logbook_page
Section.NET:
return _the_net_page
Section.MAIL:
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
Section.LOGBOOK:
return _logbook_rest_position
Section.NET:
return _the_net_rest_position
Section.MAIL:
return _mail_rest_position
Section.PROFILE:
return _profile_rest_position
_:
return Vector2.ZERO
func _begin_page_transition(section: Section) -> void:
_cancel_logbook_page_transition(true)
_page_transition_generation += 1
_cancel_page_tween()
_page_transitioning = true
_set_content_interactive(false)
var current_viewport: Viewport = get_viewport()
if current_viewport != null:
current_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:
_settle_inventory_tabs_for_close()
_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)
_page_tween.tween_property(
_page_outgoing_root,
"modulate:a",
0.0,
duration,
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_page_tween.tween_property(
_page_incoming_root,
"modulate:a",
1.0,
duration,
).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
if (
_page_outgoing_root != null
and _page_outgoing_root != _page_incoming_root
and _page_outgoing_root != _inventory_transition_group
):
_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()
_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,
_mail_tab,
_profile_tab,
_players_tab,
_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
)
_catalog_logbook.set_interactive(
interactive and _current_section == Section.LOGBOOK
)
_the_net_page.set_interactive(
interactive and _current_section == Section.NET
)
_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
)
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()
func _reset_page_transition_visuals() -> void:
for section: Section in [
Section.COOLER,
Section.BAG,
Section.TACKLE_BOX,
Section.LOGBOOK,
Section.NET,
Section.MAIL,
Section.PROFILE,
Section.PLAYERS,
]:
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
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)
_refresh_inventory()
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()
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
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"
)
SortMode.NAME:
direction_text = "za" if _sort_descending else "az"
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
func _refresh_all() -> void:
_refresh_economy_summary()
_refresh_inventory()
_refresh_bag()
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 = (
"drag to a hotbar slot."
if item.hotbar_allowed
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
else "select a bag item for details."
)
_bag_sprite_detail_texture.texture = item.icon if item != null else null
_bag_sprite_detail_name.text = (
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
)
func _on_inventory_changed() -> void:
_refresh_economy_summary()
_revalidate_confirmation()
_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
)
_wallet_balance.text = "wallet: $%d" % balance
_wallet_status.set_content("wallet", "$%d" % balance)
_notepad_wallet_value.text = "$%d" % balance
_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,
])
_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,
]
_held_value_status.set_content("held value", "$%d" % held_total)
_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()
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
_inventory_empty.visible = catches.is_empty()
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)
_sync_cooler_fish_nodes(catches)
var selected: FishCatchType = (
_inventory.get_catch(_fish_selection.get_focused_id())
if _inventory != null
else null
)
_update_sale_summary()
_update_inventory_detail(selected)
_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,
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,
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
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())
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)
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
func _on_catch_card_pressed(catch_id: StringName) -> void:
if _inventory == null:
return
var selected: FishCatchType = _inventory.get_catch(catch_id)
if selected == null:
return
_fish_selection.apply_click(
catch_id,
Input.is_key_pressed(KEY_CTRL),
Input.is_key_pressed(KEY_SHIFT)
)
_transaction_feedback.text = ""
_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"
_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()
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 = ""
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"
_clear_cooler_detail_stats()
_favorite_button.disabled = true
_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()
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()
var individual_preview: FishSaleResultType = (
_sale_service.preview_batch([fish_catch.catch_id], active_buyer)
if _sale_service != null and active_buyer != null
else null
)
var buyer_offer: int = (
individual_preview.payout
if individual_preview != null
else -1
)
_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"
_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)
_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 = (
"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()
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)
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
if selected_count == 0:
_selection_summary.text = "no fish selected"
_selection_status.set_content("selected", "none")
_offer_status.set_content(offer_label, "")
_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()
_set_cooler_selection_summary_empty()
_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
var preview: FishSaleResultType = (
_sale_service.preview_batch(selected_ids, active_buyer)
if _sale_service != null and active_buyer != null
else null
)
var count_text: String = (
"1 fish selected"
if selected_count == 1
else "%d fish selected" % selected_count
)
_selection_summary.text = count_text
_selection_status.set_content("selected", str(selected_count))
if (
preview != null
and preview.payout >= 0
and active_buyer != null
and (
preview.is_success()
or preview.status == FishSaleResultType.Status.FAVORITED
)
):
_selection_summary.text += "\n%s total offer: $%d" % [
active_buyer.display_name,
preview.payout,
]
_set_cooler_selection_summary(selected_count, preview.payout)
_offer_status.set_content(offer_label, "$%d" % preview.payout)
else:
_set_cooler_selection_summary(selected_count, -1)
_offer_status.set_content(offer_label, "unavailable")
_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()
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:
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(
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
var selected_ids: Array[StringName] = _fish_selection.get_selected_ids()
if (
_inventory == null
or _sale_service == null
or active_buyer == null
or selected_ids.is_empty()
):
return
var preview: FishSaleResultType = _sale_service.preview_batch(
selected_ids,
active_buyer
)
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
_confirmation_catch_ids = selected_ids.duplicate()
_confirmation_buyer = active_buyer
_confirmation_buyer_id = active_buyer.id
_confirmation_generation = _menu_generation
var fish_label: String = "fish"
_confirmation_message.text = (
"sell %d %s to the %s for $%d?\ncombined base value: $%d"
% [
preview.fish_count,
fish_label,
_get_buyer_display_group(active_buyer),
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
or _confirmation_catch_ids.is_empty()
or _confirmation_buyer == null
or _confirmation_buyer.id != _confirmation_buyer_id
or _confirmation_generation != _menu_generation
):
return
_sale_in_progress = true
var requested_catch_ids: Array[StringName] = (
_confirmation_catch_ids.duplicate()
)
var requested_buyer_id: StringName = _confirmation_buyer_id
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
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
_confirmation_catch_ids.clear()
_confirmation_buyer = null
_confirmation_buyer_id = StringName()
_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
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()
_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()
)
):
_close_sale_confirmation()
_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"
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"
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
_logbook_empty.visible = valid_species.is_empty()
_logbook_empty.text = (
"no fish catalog configured."
if valid_species.is_empty()
else "no species discovered yet."
)
_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)
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:
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(
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
var current_viewport: Viewport = get_viewport()
if current_viewport != null:
current_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),
)
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:
texture_frame.tooltip_text = "display texture unavailable"
return texture_frame
func _clear_container(container: Container) -> void:
for child: Node in container.get_children():
container.remove_child(child)
child.queue_free()