netfishing/ui/game_ui.gd

2157 lines
64 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 GameUI
extends CanvasLayer
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishQualityType = preload("res://fish/fish_quality.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const PlayerMenuType = preload("res://ui/player_menu.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 PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const HotbarUIType = preload("res://ui/hotbar.gd")
const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.gd")
const NetworkSessionType = preload("res://network/network_session.gd")
const FishingShopType = preload("res://ui/fishing_shop.gd")
const SettingsPanelType = preload("res://ui/settings_panel.gd")
const PlayerFishingUpgradesType = preload(
"res://progression/player_fishing_upgrades.gd"
)
const ShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const PlayerItemEffectsType = preload(
"res://progression/player_item_effects.gd"
)
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
const ChatUIType = preload("res://ui/chat_ui.gd")
const EmoteRadialMenuType = preload("res://ui/emote_radial_menu.gd")
const QuickRadialMenuType = preload("res://ui/quick_radial_menu.gd")
const ControllerVirtualCursorType = preload(
"res://ui/controller_virtual_cursor.gd"
)
const SurfaceDrawingToolbarType = preload(
"res://ui/surface_drawing_toolbar.gd"
)
const PlayerSettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldWeatherServiceType = preload(
"res://world/world_weather_service.gd"
)
const PlayerExperienceType = preload(
"res://progression/player_experience.gd"
)
const UIReferencePresentationType = preload(
"res://ui/ui_reference_presentation.gd"
)
const MAIN_BUBBLE_PROFILE: BubbleMenuProfile = preload(
"res://ui/components/bubble_menu/bubble_menu_profile.tres"
)
const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload(
"res://ui/icons/pictograms/x_dark.png"
)
signal pixelation_settings_visibility_changed(is_visible: bool)
signal crisp_reset_focus_requested
signal interactive_pointer_ui_changed(is_open: bool)
signal passive_pointer_ui_changed(is_enabled: bool)
signal player_menu_backdrop_visibility_changed(is_visible: bool)
signal shop_backdrop_visibility_changed(is_visible: bool)
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55
const VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD: float = 0.35
const VIRTUAL_MOUSE_STICK_DEADZONE: float = 0.18
const VIRTUAL_MOUSE_SPEED: float = 720.0
const CONTROLLER_MENU_SCROLL_DEADZONE: float = 0.25
const CONTROLLER_MENU_SCROLL_SPEED: float = 660.0
# Android controller mappings may expose LT on its own axis or as the negative
# half of the same signed axis used by RT. Support both without letting the RT
# zoom direction enter virtual-pointer mode.
const VIRTUAL_MOUSE_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_RIGHT
const VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
const VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
const FISHING_PANEL_DEFAULT_TOP_OFFSET: float = -210.0
const FISHING_PANEL_DEFAULT_BOTTOM_OFFSET: float = -140.0
const FISHING_PANEL_SHOWCASE_TOP_OFFSET: float = -174.0
const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0
@onready var _status_label: Label = %StatusLabel
@onready var _bite_prompt_button: Button = %BitePromptButton
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
@onready var _active_bait_button: Button = %ActiveBaitButton
@onready var _active_bait_quantity_badge: Panel = %ActiveBaitQuantityBadge
@onready var _active_bait_quantity: Label = %ActiveBaitQuantity
@onready var _experience_presentation: Control = %ExperiencePresentation
@onready var _catch_track: Control = %CatchTrack
@onready var _green_catch_progress: ProgressBar = %GreenCatchProgress
@onready var _red_chase_progress: ProgressBar = %RedChaseProgress
@onready var _barrier_markers: Control = %BarrierMarkers
@onready var _barrier_prompt_panel: PanelContainer = %BarrierPromptPanel
@onready var _barrier_prompt: Label = %BarrierPrompt
@onready var _barrier_health: Label = %BarrierHealth
@onready var _showcase_details: Label = %ShowcaseDetails
@onready var _fishing_panel: PanelContainer = %FishingPanel
@onready var _experience_panel: PanelContainer = %ExperienceProgressPanel
@onready var _experience_level_label: Label = %ExperienceLevelLabel
@onready var _experience_award_label: Label = %ExperienceAwardLabel
@onready var _experience_progress: ProgressBar = %ExperienceProgress
@onready var _experience_bubble: PanelContainer = %ExperienceBubble
@onready var _experience_bubble_label: Label = %ExperienceBubbleLabel
const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd")
const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
const SHOP_ANIMALESE_VOICE_ID: String = "natural"
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
@onready var _canonical_stage: Control = %CanonicalStage
@onready var _ui_root: Control = %UIRoot
@onready var _player_menu: PlayerMenuType = %PlayerMenu
@onready var _screen_fade: ScreenFade = %ScreenFade
@onready var _title_screen: TitleScreenType = %TitleScreen
@onready var _pause_menu: PauseMenuType = %PauseMenu
@onready var _hotbar_ui: HotbarUIType = %Hotbar
@onready var _fishing_shop: FishingShopType = %FishingShop
@onready var _shop_prompt: Control = %ShopPrompt
@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble
@onready var _shop_prompt_message: Label = %ShopPromptMessage
@onready var _shop_prompt_key_badge: Panel = %ShopPromptKeyBadge
@onready var _shop_prompt_key: Label = %ShopPromptKey
@onready var _shop_prompt_pointer: Polygon2D = %ShopPromptPointer
var _shop_animalese_voice: AnimaleseVoiceType
var _shop_npc_player_in_range: bool = false
var _shop_npc_spoken_for_current_visit: bool = false
@onready var _effect_status: Label = %EffectStatus
@onready var _chat_ui: ChatUIType = %ChatUI
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
@onready var _quick_radial_menu: QuickRadialMenuType = %QuickRadialMenu
@onready var _controller_virtual_cursor: ControllerVirtualCursorType = (
%ControllerVirtualCursor
)
@onready var _surface_drawing_toolbar: SurfaceDrawingToolbarType = (
%SurfaceDrawingToolbar
)
@onready var _title_settings_panel: SettingsPanelType = (
$UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel
)
@onready var _pause_settings_panel: SettingsPanelType = (
$UIRoot/CanonicalStage/PauseMenu/ResponsivePauseStage/PausePresentationScaleRoot/SettingsCenter/SettingsPanel
)
var _showcase_active: bool = false
var _player: PlayerType
var _bag: PlayerBagType
var _item_catalog: ItemCatalogType
var _player_menu_open: bool = false
var _gameplay_ui_enabled: bool = false
var _fishing_spot: FishingSpotType
var _system_menu_open: bool = false
var _shop_open: bool = false
var _chat_input_open: bool = false
var _player_menu_hotbar_visible: bool = false
var _item_effects: PlayerItemEffectsType
var _main_shop_buyer: FishBuyerProfileType
var _shop_interaction: ShopInteractionType
var _surface_drawing: NetworkSurfaceDrawingService
var _experience: PlayerExperienceType
var _experience_award_queue: Array[Dictionary] = []
var _experience_animation_active: bool = false
var _experience_animation_generation: int = 0
var _experience_panel_rest_y: float = 18.0
var _emote_prior_camera_input_enabled: bool = true
var _quick_prior_camera_input_enabled: bool = true
var _virtual_mouse_active: bool = false
var _virtual_mouse_prior_camera_input_enabled: bool = true
var _virtual_mouse_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
var _virtual_mouse_window_position: Vector2 = Vector2.ZERO
var _virtual_mouse_button_mask: int = 0
var _virtual_mouse_right_pressed: bool = false
var _virtual_mouse_device_id: int = 0
var _virtual_mouse_trigger_strength: float = 0.0
var _virtual_mouse_stick: Vector2 = Vector2.ZERO
var _virtual_mouse_trigger_rest_by_device: Dictionary[int, float] = {}
var _shared_trigger_rest_by_device: Dictionary[int, float] = {}
var _controller_mapping_manager: ControllerMappingManagerType
var _settings_manager: PlayerSettingsManagerType
func _ready() -> void:
_bite_prompt_button.pressed.connect(_on_bite_prompt_pressed)
_apply_active_bait_indicator_style()
_refresh_active_bait_indicator()
# Reward feedback must remain above full-screen canonical menus. Keeping the
# overlay as the final stage child makes that ownership explicit instead of
# relying on scene declaration order when another menu adds high-z children.
var presentation_parent: Node = _experience_presentation.get_parent()
if presentation_parent != null:
presentation_parent.move_child(
_experience_presentation,
presentation_parent.get_child_count() - 1,
)
_emote_radial_menu.emote_selected.connect(_on_emote_selected)
_quick_radial_menu.action_selected.connect(_on_quick_action_selected)
_chat_ui.text_entry_ownership_changed.connect(
_on_chat_text_entry_ownership_changed
)
_hotbar_ui.presentation_transition_finished.connect(
_on_hotbar_presentation_transition_finished
)
_player_menu.controller_hotbar_placement_requested.connect(
_on_controller_hotbar_placement_requested
)
_player_menu.controller_hotbar_placement_ended.connect(
_on_controller_hotbar_placement_ended
)
_player_menu.controller_hotbar_management_requested.connect(
_on_controller_hotbar_management_requested
)
_player_menu.controller_hotbar_management_ended.connect(
_on_controller_hotbar_management_ended
)
_title_settings_panel.panel_visibility_changed.connect(
_on_settings_visibility_changed
)
_pause_settings_panel.panel_visibility_changed.connect(
_on_settings_visibility_changed
)
_title_settings_panel.crisp_reset_focus_requested.connect(
crisp_reset_focus_requested.emit
)
_pause_settings_panel.crisp_reset_focus_requested.connect(
crisp_reset_focus_requested.emit
)
if not Input.joy_connection_changed.is_connected(
_on_controller_connection_changed
):
Input.joy_connection_changed.connect(_on_controller_connection_changed)
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,
main_shop_buyer: FishBuyerProfileType,
fishing_upgrades: PlayerFishingUpgradesType,
shop_interaction: ShopInteractionType,
item_effects: PlayerItemEffectsType,
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
network_sale_service: NetworkSaleService,
network_shop_service: NetworkShopService,
network_chat_service: NetworkChatService,
network_profile: NetworkProfilePreferences,
spawn_service: PlayerSpawnService,
network_mail_service: NetworkMailService,
reservations: PlayerAssetReservationService,
network_profile_service: NetworkProfileService,
network_player_list: NetworkPlayerListService,
settings_manager: PlayerSettingsManagerType,
surface_drawing: NetworkSurfaceDrawingService,
art_unlocks: PlayerArtUnlocks,
world_time: WorldTimeServiceType,
world_weather: WorldWeatherServiceType,
player_jobs: PlayerJobService,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
_player = player
_bag = bag
_item_catalog = item_catalog
_settings_manager = settings_manager
_fishing_spot = fishing_spot
_item_effects = item_effects
_experience = experience
if (
_player != null
and not _player.active_bait_changed.is_connected(
_on_hud_active_bait_changed
)
):
_player.active_bait_changed.connect(_on_hud_active_bait_changed)
if (
_bag != null
and not _bag.contents_changed.is_connected(
_on_hud_bait_inventory_changed
)
):
_bag.contents_changed.connect(_on_hud_bait_inventory_changed)
_refresh_active_bait_indicator()
if (
_experience != null
and not _experience.experience_awarded.is_connected(
_on_experience_awarded
)
):
_experience.experience_awarded.connect(_on_experience_awarded)
_chat_ui.setup(
network_chat_service, network_session, spawn_service, player,
fishing_spot, settings_manager, world_time, world_weather, item_effects
)
_title_settings_panel.setup_network_profile(
network_profile, network_session
)
_pause_settings_panel.setup_network_profile(
network_profile, network_session
)
fishing_spot.status_changed.connect(_on_fishing_status_changed)
fishing_spot.bite_prompt_changed.connect(_on_bite_prompt_changed)
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
fishing_spot.showcase_changed.connect(_on_showcase_changed)
fishing_spot.art_ui_toggle_requested.connect(_toggle_surface_drawing)
_player_menu.menu_visibility_changed.connect(
_on_player_menu_visibility_changed
)
_player_menu.menu_exit_started.connect(_on_player_menu_exit_started)
_player_menu.inventory_hotbar_context_changed.connect(
_on_inventory_hotbar_context_changed
)
_player_menu.shop_cooler_modal_changed.connect(
_on_shop_cooler_modal_changed
)
_player_menu.setup(
player,
inventory,
collection_log,
experience,
wallet,
sale_service,
default_buyer,
catalog,
fishing_spot,
bag,
hotbar,
item_catalog,
cooler_capacity,
network_session,
network_sale_service,
network_mail_service,
reservations,
network_profile_service,
network_player_list,
player_jobs,
world_time,
world_environment,
world_sun,
)
if (
_settings_manager != null
and not _settings_manager.settings_changed.is_connected(
_on_player_settings_changed
)
):
_settings_manager.settings_changed.connect(_on_player_settings_changed)
if _settings_manager != null:
_on_player_settings_changed(_settings_manager.current_settings)
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
_fishing_shop.setup(
player,
wallet,
main_shop_buyer,
fishing_upgrades,
fishing_spot,
shop_interaction,
bag,
item_catalog,
cooler_capacity,
art_unlocks,
network_shop_service,
)
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
_fishing_shop.shop_cooler_return_requested.connect(
_on_shop_cooler_return_requested
)
_fishing_shop.shop_cooler_confirmation_cancel_requested.connect(
_player_menu.cancel_shop_cooler_confirmation
)
_main_shop_buyer = main_shop_buyer
_shop_interaction = shop_interaction
_surface_drawing = surface_drawing
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
set_edge_docks(
settings_manager.current_settings.chat_dock_right,
settings_manager.current_settings.paint_dock_right,
settings_manager.current_settings.chat_mobile_mode,
)
func _input(event: InputEvent) -> void:
if _handle_virtual_mouse_input(event):
get_viewport().set_input_as_handled()
return
if _handle_controller_chat_controls(event):
get_viewport().set_input_as_handled()
return
if (
_surface_drawing_toolbar != null
and _surface_drawing_toolbar.owns_pointer_event(event)
):
return
var drawing_can_open: bool = (
_gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and not _showcase_active
and _fishing_spot != null
and _fishing_spot.can_use_surface_drawing()
)
if (
_surface_drawing != null
and _surface_drawing.handle_input(
event,
drawing_can_open,
_drawing_pointer_window_position(),
)
):
get_viewport().set_input_as_handled()
return
if _emote_radial_menu == null or _quick_radial_menu == null:
return
var can_open: bool = (
_gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and not _showcase_active
and not _virtual_mouse_active
)
var emote_was_open: bool = _emote_radial_menu.is_open()
if _emote_radial_menu.handle_input(
event,
can_open and not _quick_radial_menu.is_open(),
):
var emote_is_open: bool = _emote_radial_menu.is_open()
if emote_is_open != emote_was_open and _player != null:
if emote_is_open:
_emote_prior_camera_input_enabled = (
_player.is_camera_input_enabled()
)
_player.set_camera_input_enabled(false)
else:
_player.set_camera_input_enabled(
_emote_prior_camera_input_enabled
)
get_viewport().set_input_as_handled()
return
var quick_was_open: bool = _quick_radial_menu.is_open()
if _quick_radial_menu.handle_input(
event,
can_open and not _emote_radial_menu.is_open(),
):
var quick_is_open: bool = _quick_radial_menu.is_open()
if quick_is_open != quick_was_open and _player != null:
if quick_is_open:
_quick_prior_camera_input_enabled = (
_player.is_camera_input_enabled()
)
_player.set_camera_input_enabled(false)
else:
_player.set_camera_input_enabled(
_quick_prior_camera_input_enabled
)
get_viewport().set_input_as_handled()
func _handle_controller_chat_controls(event: InputEvent) -> bool:
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
if (
button_event == null
or not button_event.pressed
or not _gameplay_ui_enabled
or _system_menu_open
or _player_menu_open
or _shop_open
):
return false
var use_mapping: bool = (
_controller_mapping_manager != null
)
var select_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_SELECT
)
if use_mapping
else button_event.button_index == JOY_BUTTON_BACK
)
var focus_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_LB
)
if use_mapping
else button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
)
var accept_pressed: bool = (
_controller_mapping_manager.event_matches_role(
event, ControllerMappingManagerType.ROLE_A
)
if use_mapping
else button_event.button_index == JOY_BUTTON_A
)
if select_pressed:
# Handle Select before focused LineEdit controls consume it. Select owns
# chat visibility while LB only returns input ownership to the world.
_chat_ui.toggle_chat()
return true
if focus_pressed:
_chat_ui.refocus_gameplay()
return true
if accept_pressed:
return _chat_ui.request_virtual_keyboard()
return false
func _on_emote_selected(emote_id: StringName) -> void:
if emote_id == &"sit" and _player != null:
_player.toggle_sitting()
func _on_quick_action_selected(action_id: StringName) -> void:
match action_id:
&"stuff":
_player_menu.open_section(PlayerMenuType.Section.COOLER)
&"logbook":
_player_menu.open_section(PlayerMenuType.Section.LOGBOOK)
&"fishnet":
_player_menu.open_section(PlayerMenuType.Section.NET)
&"mail":
_player_menu.open_section(PlayerMenuType.Section.MAIL)
&"profile":
_player_menu.open_section(PlayerMenuType.Section.PROFILE)
&"online":
_player_menu.open_section(PlayerMenuType.Section.PLAYERS)
&"paint":
_toggle_surface_drawing()
&"chat":
_chat_ui.open_chat()
func _handle_virtual_mouse_input(event: InputEvent) -> bool:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
return _handle_mapped_virtual_mouse_input(event)
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
if motion_event != null:
if motion_event.axis in [
VIRTUAL_MOUSE_TRIGGER_AXIS,
VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS,
]:
if (
_virtual_mouse_active
and motion_event.device != _virtual_mouse_device_id
):
return false
_sample_trigger_rest_values(motion_event.device)
_virtual_mouse_trigger_strength = (
_virtual_mouse_strength_for_device(motion_event.device)
)
var was_active: bool = _virtual_mouse_active
if (
_virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
):
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse(motion_event.device)
elif (
_virtual_mouse_active
and _virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
elif _virtual_mouse_active:
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_secondary_click_strength_for_device(
motion_event.device
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return was_active or _virtual_mouse_active
if (
_virtual_mouse_active
and motion_event.device == _virtual_mouse_device_id
and motion_event.axis in [
JOY_AXIS_RIGHT_X,
JOY_AXIS_RIGHT_Y,
]
):
if motion_event.axis == JOY_AXIS_RIGHT_X:
_virtual_mouse_stick.x = motion_event.axis_value
else:
_virtual_mouse_stick.y = motion_event.axis_value
return true
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
if (
button_event != null
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
and (
_virtual_mouse_active
or _virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
)
):
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse(button_event.device)
if _virtual_mouse_active:
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
return true
return false
func _handle_mapped_virtual_mouse_input(event: InputEvent) -> bool:
var uses_activation: bool = _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_POINTER_MODIFIER,
)
if uses_activation:
var was_active: bool = _virtual_mouse_active
_virtual_mouse_trigger_strength = (
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_POINTER_MODIFIER
)
)
if (
_virtual_mouse_trigger_strength
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
and not _virtual_mouse_active
and _can_start_virtual_mouse()
):
_begin_virtual_mouse(
_controller_mapping_manager.get_active_device_id()
)
elif (
_virtual_mouse_active
and _virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
return was_active or _virtual_mouse_active
if not _virtual_mouse_active:
return false
if (
_controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RIGHT_STICK_X,
)
or _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y,
)
):
_virtual_mouse_stick = _mapped_virtual_mouse_stick()
return true
var button_event := event as InputEventJoypadButton
if button_event != null and _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_RB,
):
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
return true
if _controller_mapping_manager.event_uses_role(
event,
ControllerMappingManagerType.ROLE_CAMERA_ZOOM,
):
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_CAMERA_ZOOM
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return true
return false
func _mapped_virtual_mouse_stick() -> Vector2:
return Vector2(
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
),
_controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
),
)
func _can_start_virtual_mouse() -> bool:
return (
_gameplay_ui_enabled
and not _showcase_active
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _chat_input_open
and _player != null
and _fishing_spot != null
and _fishing_spot.can_open_system_menu()
and not _emote_radial_menu.is_open()
and not _quick_radial_menu.is_open()
)
func _begin_virtual_mouse(device_id: int) -> void:
if _virtual_mouse_active:
return
_virtual_mouse_active = true
_virtual_mouse_device_id = maxi(device_id, 0)
_virtual_mouse_stick = (
_mapped_virtual_mouse_stick()
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
)
else Vector2(
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_Y),
)
)
_virtual_mouse_prior_camera_input_enabled = (
_player.is_camera_input_enabled()
)
_player.set_camera_input_enabled(false)
if _fishing_spot != null:
_fishing_spot.set_local_menu_input_suppressed(
VIRTUAL_MOUSE_INPUT_OWNER,
true,
)
_virtual_mouse_prior_mouse_mode = Input.mouse_mode
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
_virtual_mouse_button_mask = 0
_virtual_mouse_right_pressed = false
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
Vector2(get_window().size) * 0.5
)
_controller_virtual_cursor.visible = true
_update_virtual_cursor_position(_virtual_mouse_window_position)
_emit_virtual_mouse_motion(Vector2.ZERO)
func _end_virtual_mouse() -> void:
if not _virtual_mouse_active:
return
if (_virtual_mouse_button_mask & MOUSE_BUTTON_MASK_LEFT) != 0:
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, false)
if _virtual_mouse_right_pressed:
_set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false)
_virtual_mouse_active = false
_virtual_mouse_trigger_strength = 0.0
_virtual_mouse_stick = Vector2.ZERO
_controller_virtual_cursor.visible = false
if _fishing_spot != null:
_fishing_spot.set_local_menu_input_suppressed(
VIRTUAL_MOUSE_INPUT_OWNER,
false,
)
if _player != null and is_instance_valid(_player):
_player.set_camera_input_enabled(
_virtual_mouse_prior_camera_input_enabled
)
Input.mouse_mode = _virtual_mouse_prior_mouse_mode
func _update_virtual_mouse(delta: float) -> void:
if not _virtual_mouse_active:
return
if (
_virtual_mouse_trigger_strength
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
):
_end_virtual_mouse()
return
var stick: Vector2 = _virtual_mouse_stick
var stick_length: float = stick.length()
if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE:
return
var adjusted_strength: float = (
(stick_length - VIRTUAL_MOUSE_STICK_DEADZONE)
/ (1.0 - VIRTUAL_MOUSE_STICK_DEADZONE)
)
var display_scale: float = UIReferencePresentationType.get_scale(
Vector2(get_window().size)
)
var relative_motion: Vector2 = (
stick.normalized()
* adjusted_strength
* VIRTUAL_MOUSE_SPEED
* display_scale
* delta
)
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
_virtual_mouse_window_position + relative_motion
)
_update_virtual_cursor_position(_virtual_mouse_window_position)
_emit_virtual_mouse_motion(relative_motion)
func _poll_virtual_mouse_controller_state() -> void:
if (
_controller_mapping_manager != null
and _controller_mapping_manager.has_custom_mapping()
):
_poll_mapped_virtual_mouse_controller_state()
return
var device_ids: Array[int] = Input.get_connected_joypads()
# Some Android controller backends deliver device-0 axes without including
# that device in get_connected_joypads(). Player camera input already uses
# this same primary-device fallback.
if device_ids.is_empty():
device_ids.append(0)
for device_id: int in device_ids:
_sample_trigger_rest_values(device_id)
if _virtual_mouse_active:
if not device_ids.has(_virtual_mouse_device_id):
_end_virtual_mouse()
return
_virtual_mouse_trigger_strength = _virtual_mouse_strength_for_device(
_virtual_mouse_device_id
)
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_secondary_click_strength_for_device(
_virtual_mouse_device_id
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
_virtual_mouse_stick = Vector2(
Input.get_joy_axis(
_virtual_mouse_device_id,
JOY_AXIS_RIGHT_X,
),
Input.get_joy_axis(
_virtual_mouse_device_id,
JOY_AXIS_RIGHT_Y,
),
)
return
if not _can_start_virtual_mouse():
return
for device_id: int in device_ids:
var trigger_strength: float = _virtual_mouse_strength_for_device(
device_id
)
if trigger_strength < VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
continue
_virtual_mouse_trigger_strength = trigger_strength
_begin_virtual_mouse(device_id)
return
func _poll_mapped_virtual_mouse_controller_state() -> void:
var trigger_strength: float = _controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_POINTER_MODIFIER
)
if _virtual_mouse_active:
_virtual_mouse_trigger_strength = trigger_strength
if trigger_strength < VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD:
_end_virtual_mouse()
return
_virtual_mouse_stick = _mapped_virtual_mouse_stick()
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
_controller_mapping_manager.get_role_strength(
ControllerMappingManagerType.ROLE_CAMERA_ZOOM
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
return
if (
trigger_strength >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
and _can_start_virtual_mouse()
):
_virtual_mouse_trigger_strength = trigger_strength
_begin_virtual_mouse(
_controller_mapping_manager.get_active_device_id()
)
func _sample_trigger_rest_values(device_id: int) -> void:
if not _virtual_mouse_trigger_rest_by_device.has(device_id):
_virtual_mouse_trigger_rest_by_device[device_id] = Input.get_joy_axis(
device_id,
VIRTUAL_MOUSE_TRIGGER_AXIS,
)
if not _shared_trigger_rest_by_device.has(device_id):
_shared_trigger_rest_by_device[device_id] = Input.get_joy_axis(
device_id,
VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS,
)
func _virtual_mouse_strength_for_device(device_id: int) -> float:
_sample_trigger_rest_values(device_id)
var dedicated_rest: float = (
_virtual_mouse_trigger_rest_by_device[device_id]
)
var shared_rest: float = _shared_trigger_rest_by_device[device_id]
var dedicated_strength: float = normalized_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_TRIGGER_AXIS),
dedicated_rest,
)
var shared_negative_strength: float = directional_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS),
shared_rest,
-1.0,
)
return maxf(dedicated_strength, shared_negative_strength)
func _secondary_click_strength_for_device(device_id: int) -> float:
_sample_trigger_rest_values(device_id)
return directional_trigger_strength(
Input.get_joy_axis(device_id, VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS),
_shared_trigger_rest_by_device[device_id],
1.0,
)
static func normalized_trigger_strength(
axis_value: float,
resting_value: float,
) -> float:
var negative_travel: float = absf(-1.0 - resting_value)
var positive_travel: float = absf(1.0 - resting_value)
var available_travel: float = maxf(negative_travel, positive_travel)
if available_travel <= 0.001:
return 0.0
return clampf(
absf(axis_value - resting_value) / available_travel,
0.0,
1.0,
)
static func directional_trigger_strength(
axis_value: float,
resting_value: float,
direction: float,
) -> float:
var normalized_direction: float = signf(direction)
if is_zero_approx(normalized_direction):
return 0.0
var endpoint: float = normalized_direction
var available_travel: float = absf(endpoint - resting_value)
if available_travel <= 0.001:
return 0.0
var directed_travel: float = (
(axis_value - resting_value) * normalized_direction
)
return clampf(directed_travel / available_travel, 0.0, 1.0)
func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void:
var mask: int = (
MOUSE_BUTTON_MASK_LEFT
if button == MOUSE_BUTTON_LEFT
else MOUSE_BUTTON_MASK_RIGHT
)
var already_pressed: bool = bool(_virtual_mouse_button_mask & mask)
if already_pressed == pressed:
return
if pressed:
_virtual_mouse_button_mask |= mask
else:
_virtual_mouse_button_mask &= ~mask
if button == MOUSE_BUTTON_RIGHT:
_virtual_mouse_right_pressed = pressed
var mouse_event := InputEventMouseButton.new()
mouse_event.position = _virtual_mouse_window_position
mouse_event.global_position = _virtual_mouse_window_position
mouse_event.button_index = button
mouse_event.button_mask = _virtual_mouse_button_mask
mouse_event.pressed = pressed
mouse_event.factor = 1.0
_parse_virtual_mouse_event(mouse_event)
func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void:
var motion_event := InputEventMouseMotion.new()
motion_event.position = _virtual_mouse_window_position
motion_event.global_position = _virtual_mouse_window_position
motion_event.relative = relative_motion
motion_event.button_mask = _virtual_mouse_button_mask
_parse_virtual_mouse_event(motion_event)
func _parse_virtual_mouse_event(event: InputEventMouse) -> void:
Input.parse_input_event(event)
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
var output_scale: float = UIReferencePresentationType.get_scale(
Vector2(get_window().size)
)
_controller_virtual_cursor.set_pointer_position(
viewport_position / output_scale
)
func _clamp_virtual_mouse_window_position(
window_position: Vector2,
) -> Vector2:
var bounds: Rect2 = get_virtual_mouse_window_bounds(
Vector2(get_window().size)
)
return Vector2(
clampf(window_position.x, bounds.position.x, bounds.end.x),
clampf(window_position.y, bounds.position.y, bounds.end.y),
)
static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
var output_scale: float = UIReferencePresentationType.get_scale(window_size)
var cursor_margin: Vector2 = (
ControllerVirtualCursorType.CURSOR_SIZE * output_scale * 0.5
)
var bounds_size: Vector2 = Vector2(
maxf(window_size.x - cursor_margin.x * 2.0, 0.0),
maxf(window_size.y - cursor_margin.y * 2.0, 0.0),
)
return Rect2(cursor_margin, bounds_size)
func _toggle_surface_drawing() -> void:
if _surface_drawing == null:
return
if _surface_drawing.is_active():
_surface_drawing.deactivate()
elif _surface_drawing.can_activate():
_surface_drawing.activate(_drawing_pointer_window_position())
func _drawing_pointer_window_position() -> Vector2:
if _virtual_mouse_active:
return _virtual_mouse_window_position
return get_window().get_mouse_position()
func setup_data_and_identity(
data_root: PlayerDataRoot,
identity_backups: IdentityBackupService,
player_identity: PlayerIdentityStore,
host_identity: HostIdentityStore,
network_session: NetworkSession,
interface_fonts: InterfaceFontController,
) -> void:
for panel: SettingsPanelType in [
_title_settings_panel, _pause_settings_panel
]:
panel.setup_data_and_identity(
data_root,
identity_backups,
player_identity,
host_identity,
network_session,
interface_fonts,
)
func setup_controller_mapping(
mapping_manager: ControllerMappingManagerType,
) -> void:
_controller_mapping_manager = mapping_manager
_player.set_controller_mapping_manager(_controller_mapping_manager)
_emote_radial_menu.setup_controller_mapping(_controller_mapping_manager)
_quick_radial_menu.setup_controller_mapping(_controller_mapping_manager)
_player_menu.setup_controller_mapping(_controller_mapping_manager)
for panel: SettingsPanelType in [
_title_settings_panel, _pause_settings_panel
]:
panel.setup_controller_mapping(_controller_mapping_manager)
func is_controller_mapping_capturing() -> bool:
return (
_title_settings_panel.is_controller_mapping_capturing()
or _pause_settings_panel.is_controller_mapping_capturing()
)
func _process(delta: float) -> void:
_poll_virtual_mouse_controller_state()
_update_virtual_mouse(delta)
_update_controller_menu_scroll(delta)
_update_experience_bubble_position()
if _item_effects == null or not _gameplay_ui_enabled:
_effect_status.hide()
return
var parts: Array[String] = []
for item_id: StringName in [
PlayerItemEffectsType.COFFEE_ID,
PlayerItemEffectsType.ENERGY_DRINK_ID,
PlayerItemEffectsType.SNACK_ID,
PlayerItemEffectsType.FISH_FINDER_ID,
]:
var remaining: float = _item_effects.get_remaining(item_id)
if remaining <= 0.0:
continue
var label: String = {
PlayerItemEffectsType.COFFEE_ID: "coffee",
PlayerItemEffectsType.ENERGY_DRINK_ID: "energy",
PlayerItemEffectsType.SNACK_ID: "snack",
PlayerItemEffectsType.FISH_FINDER_ID: "finder",
}.get(item_id, "")
parts.append(
"%s %d:%02d"
% [label, floori(remaining / 60.0), int(remaining) % 60]
)
_effect_status.text = " ".join(parts)
_effect_status.visible = not parts.is_empty()
func _update_controller_menu_scroll(delta: float) -> void:
if (
_controller_mapping_manager == null
or _virtual_mouse_active
or is_controller_mapping_capturing()
):
return
var stick_y: float = _controller_menu_scroll_axis()
var magnitude: float = absf(stick_y)
if magnitude <= CONTROLLER_MENU_SCROLL_DEADZONE:
return
var focus_owner: Control = get_viewport().gui_get_focus_owner()
var scroll: ScrollContainer = _focused_scroll_container(focus_owner)
if scroll == null:
return
var adjusted_axis: float = (
signf(stick_y)
* (magnitude - CONTROLLER_MENU_SCROLL_DEADZONE)
/ (1.0 - CONTROLLER_MENU_SCROLL_DEADZONE)
)
scroll.scroll_vertical += roundi(
adjusted_axis * CONTROLLER_MENU_SCROLL_SPEED * delta
)
func _focused_scroll_container(focus_owner: Control) -> ScrollContainer:
if focus_owner == null or not focus_owner.is_visible_in_tree():
return null
var current: Node = focus_owner
while current != null:
var scroll := current as ScrollContainer
if scroll != null:
if _controller_scroll_container_is_available(scroll):
return scroll
return null
var candidates: Array[ScrollContainer] = []
_collect_controller_scroll_containers(current, candidates)
if candidates.size() == 1:
return candidates[0]
if candidates.size() > 1:
return null
current = current.get_parent()
return null
func _collect_controller_scroll_containers(
root_node: Node,
result: Array[ScrollContainer],
) -> void:
for child: Node in root_node.get_children():
var canvas_item := child as CanvasItem
if canvas_item != null and not canvas_item.is_visible_in_tree():
continue
var scroll := child as ScrollContainer
if scroll != null and _controller_scroll_container_is_available(scroll):
result.append(scroll)
_collect_controller_scroll_containers(child, result)
func _controller_scroll_container_is_available(
scroll: ScrollContainer,
) -> bool:
return (
scroll.is_visible_in_tree()
and scroll.vertical_scroll_mode != ScrollContainer.SCROLL_MODE_DISABLED
)
func _controller_menu_scroll_axis() -> float:
if _controller_mapping_manager != null:
return _controller_mapping_manager.get_role_axis(
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
)
return Input.get_joy_axis(
0,
JOY_AXIS_RIGHT_Y,
)
func _on_controller_connection_changed(
device_id: int,
connected: bool,
) -> void:
_virtual_mouse_trigger_rest_by_device.erase(device_id)
_shared_trigger_rest_by_device.erase(device_id)
if not connected and _virtual_mouse_device_id == device_id:
_end_virtual_mouse()
func close_player_menu() -> void:
_player_menu.close_menu()
func close_player_menu_for_water_recovery() -> void:
_player_menu.close_for_water_recovery()
func close_player_menu_for_game_menu() -> void:
_player_menu.close_for_game_menu()
func close_player_menu_for_session_end() -> void:
_player_menu.close_for_session_end()
func consume_player_menu_escape() -> bool:
return _player_menu.consume_escape()
func get_pause_menu() -> PauseMenuType:
return _pause_menu
func _apply_active_bait_indicator_style() -> void:
var normal_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_normal_style()
var hover_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_hover_style()
var pressed_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_pressed_style()
var disabled_style: StyleBoxFlat = MAIN_BUBBLE_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
_active_bait_button.add_theme_stylebox_override("normal", normal_style)
_active_bait_button.add_theme_stylebox_override("hover", hover_style)
_active_bait_button.add_theme_stylebox_override("focus", hover_style)
_active_bait_button.add_theme_stylebox_override("pressed", pressed_style)
_active_bait_button.add_theme_stylebox_override("disabled", disabled_style)
for state: StringName in [
&"icon_normal_color",
&"icon_hover_color",
&"icon_focus_color",
&"icon_pressed_color",
]:
_active_bait_button.add_theme_color_override(state, Color.WHITE)
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
_active_bait_quantity_badge.add_theme_stylebox_override(
"panel",
badge_style,
)
func _refresh_active_bait_indicator() -> void:
if not is_node_ready():
return
var item: ItemDataType
if (
_player != null
and _item_catalog != null
and not _player.active_bait_id.is_empty()
):
item = _item_catalog.get_item_by_id(_player.active_bait_id)
var has_active_bait: bool = item != null and item.is_bait()
_active_bait_button.icon = (
item.icon
if has_active_bait and item.icon != null
else ACTIVE_BAIT_EMPTY_ICON
)
_active_bait_quantity_badge.visible = has_active_bait
if not has_active_bait:
_active_bait_button.tooltip_text = "no bait selected"
return
var quantity: int = (
_bag.get_quantity(item.item_id)
if _bag != null
else 0
)
_active_bait_quantity.text = str(quantity)
_active_bait_quantity.add_theme_font_size_override(
"font_size",
13 if quantity >= 100 else 15,
)
_active_bait_button.tooltip_text = "%s ×%d" % [
item.display_name,
quantity,
]
func _on_hud_active_bait_changed(_item_id: StringName) -> void:
_refresh_active_bait_indicator()
func _on_hud_bait_inventory_changed() -> void:
_refresh_active_bait_indicator()
func set_gameplay_ui_enabled(enabled: bool) -> void:
_gameplay_ui_enabled = enabled
if enabled:
_try_start_shop_npc_speech()
_gameplay_transient_hud.visible = enabled and not _player_menu_open
_experience_presentation.visible = enabled
_refresh_chat_availability()
if not enabled:
_end_virtual_mouse()
if _emote_radial_menu != null and _emote_radial_menu.is_open():
_emote_radial_menu.close_menu()
if _player != null:
_player.set_camera_input_enabled(
_emote_prior_camera_input_enabled
)
if _quick_radial_menu != null and _quick_radial_menu.is_open():
_quick_radial_menu.close_menu()
if _player != null:
_player.set_camera_input_enabled(
_quick_prior_camera_input_enabled
)
if _surface_drawing != null:
_surface_drawing.deactivate()
close_player_menu_for_session_end()
_fishing_shop.close_for_session_end()
_fishing_panel.visible = false
_shop_prompt.hide()
_hotbar_ui.set_presentation_visible(false, false)
_hotbar_ui.set_gameplay_input_enabled(false)
else:
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(true)
_refresh_fishing_panel_visibility()
call_deferred("_start_next_experience_animation")
func set_system_menu_open(is_open: bool) -> void:
_system_menu_open = is_open
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_refresh_chat_availability()
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not is_open
and not _player_menu_open
and not _shop_open
)
_hotbar_ui.set_drag_enabled(_player_menu_open and not is_open)
if is_open:
_hotbar_ui.set_drag_enabled(false)
_shop_prompt.hide()
_emit_interactive_pointer_ui_changed()
func get_fishing_shop() -> FishingShopType:
return _fishing_shop
func set_shop_prompt_visible(
is_visible: bool,
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
) -> void:
_apply_shop_prompt_style()
_shop_prompt.visible = (
is_visible
and _gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
)
if _shop_prompt.visible:
if world_anchor.is_finite():
_position_shop_prompt(world_anchor)
func set_shop_npc_player_in_range(in_range: bool) -> void:
if _shop_npc_player_in_range == in_range:
return
_shop_npc_player_in_range = in_range
if not in_range:
_shop_npc_spoken_for_current_visit = false
return
_try_start_shop_npc_speech()
func _try_start_shop_npc_speech() -> void:
if (
not _shop_npc_player_in_range
or _shop_npc_spoken_for_current_visit
or not _gameplay_ui_enabled
):
return
_shop_npc_spoken_for_current_visit = true
_ensure_shop_animalese_voice()
TypewriterRevealType.start(
_shop_prompt_message,
SHOP_SPEECH_CHARACTERS_PER_SECOND,
)
_shop_animalese_voice.speak_text(
_shop_prompt_message,
_shop_prompt_message.text,
"shopkeeper",
SHOP_ANIMALESE_VOICE_ID,
SHOP_SPEECH_CHARACTERS_PER_SECOND,
)
func _ensure_shop_animalese_voice() -> void:
if _shop_animalese_voice != null:
return
_shop_animalese_voice = AnimaleseVoiceType.new()
_shop_animalese_voice.name = "ShopAnimaleseVoice"
_shop_animalese_voice.base_pitch = SHOP_ANIMALESE_BASE_PITCH
add_child(_shop_animalese_voice)
func _apply_shop_prompt_style() -> void:
if _shop_prompt.has_meta(&"shop_prompt_styled"):
return
_shop_prompt.set_meta(&"shop_prompt_styled", true)
var bubble_style := StyleBoxFlat.new()
bubble_style.bg_color = Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96)
bubble_style.set_border_width_all(0)
bubble_style.set_corner_radius_all(12)
bubble_style.anti_aliasing = false
_shop_prompt_bubble.add_theme_stylebox_override("panel", bubble_style)
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_border_width_all(0)
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
_shop_prompt_key_badge.add_theme_stylebox_override("panel", badge_style)
for label: Label in [_shop_prompt_message, _shop_prompt_key]:
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
label.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_TEXT_PRIMARY,
)
_shop_prompt_pointer.color = Color(
UtilityPageStyle.OCEAN_PANEL_MID,
0.96,
)
func _position_shop_prompt(world_anchor: Vector3) -> void:
if _player == null:
_shop_prompt.hide()
return
var camera: Camera3D = _player.get_gameplay_camera()
if camera == null or camera.is_position_behind(world_anchor):
_shop_prompt.hide()
return
var camera_viewport_size := camera.get_viewport().get_visible_rect().size
var ui_viewport_size: Vector2 = _canonical_stage.size
if camera_viewport_size.x <= 0.0 or camera_viewport_size.y <= 0.0:
_shop_prompt.hide()
return
var screen_position := (
camera.unproject_position(world_anchor)
* ui_viewport_size
/ camera_viewport_size
)
if (
screen_position.x < -80.0
or screen_position.x > ui_viewport_size.x + 80.0
or screen_position.y < -80.0
or screen_position.y > ui_viewport_size.y + 80.0
):
_shop_prompt.hide()
return
var pointer_height := 10.0
var desired := screen_position - Vector2(
_shop_prompt.size.x * 0.5,
_shop_prompt.size.y + pointer_height,
)
_shop_prompt.position = Vector2(
clampf(
desired.x,
8.0,
ui_viewport_size.x - _shop_prompt.size.x - 8.0,
),
clampf(
desired.y,
8.0,
ui_viewport_size.y - _shop_prompt.size.y - pointer_height - 8.0,
),
)
_shop_prompt_pointer.position = Vector2(
clampf(
screen_position.x - _shop_prompt.position.x,
20.0,
_shop_prompt.size.x - 20.0,
),
_shop_prompt.size.y - 1.0,
)
func get_screen_fade() -> ScreenFade:
return _screen_fade
func get_title_screen() -> TitleScreenType:
return _title_screen
func set_effective_ui_pixel_size(pixel_size: int) -> void:
_title_settings_panel.set_effective_ui_pixel_size(pixel_size)
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)
func _on_player_settings_changed(settings: PlayerSettings) -> void:
if settings != null:
_player_menu.set_profile_preview_world_pixel_size(
settings.world_pixel_size
)
func set_edge_docks(
chat_dock_right: bool,
paint_dock_right: bool,
chat_mobile_mode: bool,
) -> void:
_chat_ui.set_dock_right(chat_dock_right)
_chat_ui.set_mobile_mode(chat_mobile_mode)
_surface_drawing_toolbar.set_dock_right(paint_dock_right)
func focus_open_settings_back_button() -> void:
if _title_settings_panel.visible:
_title_settings_panel.focus_back_button()
elif _pause_settings_panel.visible:
_pause_settings_panel.focus_back_button()
func refresh_open_settings_panel() -> void:
_title_settings_panel.refresh_open_panel()
_pause_settings_panel.refresh_open_panel()
func _on_settings_visibility_changed(_is_visible: bool) -> void:
pixelation_settings_visibility_changed.emit(
_title_settings_panel.visible or _pause_settings_panel.visible
)
func _on_fishing_status_changed(status: String) -> void:
if _showcase_active:
return
if _fishing_spot != null and _fishing_spot.is_fighting():
return
var normalized_status: String = status.strip_edges().to_lower()
if normalized_status in [
"fishing cancelled.",
"fishing cancelled",
"fishing failed.",
"fishing failed",
"fishing attempt ended.",
"fishing attempt ended",
]:
_set_fishing_status("")
return
_set_fishing_status(status)
func _set_fishing_status(text: String) -> void:
var normalized_text: String = text.strip_edges()
_status_label.text = normalized_text
_status_label.visible = not normalized_text.is_empty()
_refresh_fishing_panel_visibility()
func _refresh_fishing_panel_visibility() -> void:
var has_content: bool = (
not _status_label.text.strip_edges().is_empty()
or not _showcase_details.text.strip_edges().is_empty()
or _bite_prompt_button.visible
)
_fishing_panel.visible = (
_gameplay_ui_enabled
and has_content
)
func _on_bite_prompt_changed(is_visible: bool) -> void:
_bite_prompt_button.visible = is_visible
_refresh_fishing_panel_visibility()
func _on_bite_prompt_pressed() -> void:
if _fishing_spot != null:
_fishing_spot.confirm_pending_bite()
func _on_catch_display_changed(
progress: float,
chase_progress: float,
barrier_positions: PackedFloat32Array,
barrier_health: PackedInt32Array,
_barrier_max_health: PackedInt32Array,
active_barrier_index: int,
visible: bool,
) -> void:
var encounter_visible: bool = (
visible
and _fishing_spot != null
and _fishing_spot.is_fighting()
)
_hotbar_ui.set_item_name_suppressed(encounter_visible)
_green_catch_progress.value = progress * 100.0
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
_catch_track.visible = encounter_visible
_barrier_prompt_panel.visible = false
_barrier_prompt.visible = false
_barrier_health.visible = false
if not encounter_visible:
_barrier_health.visible = false
_barrier_prompt.visible = false
_clear_barrier_markers()
_barrier_health.text = ""
_refresh_fishing_panel_visibility()
return
_status_label.text = ""
_status_label.visible = false
_update_barrier_markers(
barrier_positions,
barrier_health,
active_barrier_index
)
if (
active_barrier_index >= 0
and active_barrier_index < barrier_health.size()
):
_barrier_prompt_panel.visible = true
_barrier_prompt.visible = true
_barrier_health.visible = true
_barrier_health.text = "%d" % barrier_health[active_barrier_index]
else:
_barrier_prompt.visible = false
_barrier_health.visible = false
_barrier_health.text = ""
_refresh_fishing_panel_visibility()
func _update_barrier_markers(
positions: PackedFloat32Array,
health: PackedInt32Array,
active_index: int,
) -> void:
_clear_barrier_markers()
for barrier_index: int in range(positions.size()):
var marker := ColorRect.new()
var defeated: bool = (
barrier_index < health.size()
and health[barrier_index] <= 0
)
marker.color = (
UIPalette.DISABLED
if defeated
else UIPalette.PRIMARY
)
if barrier_index == active_index:
marker.color = UIPalette.TEXT
marker.set_anchors_preset(Control.PRESET_TOP_LEFT)
marker.anchor_left = positions[barrier_index]
marker.anchor_right = positions[barrier_index]
marker.offset_left = -2.0
marker.offset_right = 2.0
marker.offset_top = 0.0
marker.offset_bottom = 18.0
marker.mouse_filter = Control.MOUSE_FILTER_IGNORE
_barrier_markers.add_child(marker)
func _clear_barrier_markers() -> void:
for marker: Node in _barrier_markers.get_children():
marker.queue_free()
func _on_showcase_changed(
fish_name: String,
rarity_name: String,
weight_lb: float,
quality: int,
visible: bool,
) -> void:
_showcase_active = visible
if not visible:
_set_fishing_panel_showcase_position(false)
_showcase_details.text = ""
_showcase_details.visible = false
_set_fishing_status("")
call_deferred("_start_next_experience_animation")
return
_set_fishing_panel_showcase_position(true)
_catch_track.visible = false
_barrier_prompt_panel.visible = false
_barrier_prompt.visible = false
_barrier_health.visible = false
_clear_barrier_markers()
_showcase_details.text = (
"%s%.1f lb"
% [rarity_name.to_lower(), weight_lb]
)
_showcase_details.visible = true
_status_label.text = "You caught %s!" % (
FishQualityType.qualified_name_with_article(fish_name, quality)
)
_status_label.visible = true
_refresh_fishing_panel_visibility()
func _set_fishing_panel_showcase_position(showcase: bool) -> void:
_fishing_panel.offset_top = (
FISHING_PANEL_SHOWCASE_TOP_OFFSET
if showcase
else FISHING_PANEL_DEFAULT_TOP_OFFSET
)
_fishing_panel.offset_bottom = (
FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET
if showcase
else FISHING_PANEL_DEFAULT_BOTTOM_OFFSET
)
func _on_experience_awarded(
amount: int,
previous_total: int,
new_total: int,
previous_level: int,
new_level: int,
) -> void:
if amount <= 0:
return
_experience_award_queue.append({
"amount": amount,
"previous_total": previous_total,
"new_total": new_total,
"previous_level": previous_level,
"new_level": new_level,
})
_start_next_experience_animation()
func _start_next_experience_animation() -> void:
if (
_experience_animation_active
or _showcase_active
or _experience_award_queue.is_empty()
or not _gameplay_ui_enabled
):
return
var award: Dictionary = _experience_award_queue.pop_front()
_experience_animation_active = true
_experience_animation_generation += 1
var generation: int = _experience_animation_generation
_play_experience_animation(award, generation)
func _play_experience_animation(
award: Dictionary,
generation: int,
) -> void:
var amount: int = int(award.get("amount", 0))
var previous_total: int = int(award.get("previous_total", 0))
var new_total: int = int(award.get("new_total", previous_total))
_experience_award_label.text = "+%d xp" % amount
_experience_bubble_label.text = "+%d xp!" % amount
_update_experience_progress(previous_total)
_experience_panel.position.y = -_experience_panel.size.y - 8.0
_experience_panel.modulate.a = 0.0
_experience_panel.show()
_experience_bubble.modulate.a = 0.0
_experience_bubble.scale = Vector2(0.72, 0.72)
_experience_bubble.pivot_offset = _experience_bubble.size * 0.5
_experience_bubble.show()
var entry_tween: Tween = create_tween()
entry_tween.set_parallel(true)
entry_tween.tween_property(
_experience_panel,
"position:y",
_experience_panel_rest_y,
0.26,
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
entry_tween.tween_property(
_experience_panel,
"modulate:a",
1.0,
0.18,
)
entry_tween.tween_property(
_experience_bubble,
"modulate:a",
1.0,
0.15,
)
entry_tween.tween_property(
_experience_bubble,
"scale",
Vector2.ONE,
0.28,
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
await entry_tween.finished
if generation != _experience_animation_generation:
return
var fill_duration: float = clampf(
0.8 + float(amount) * 0.006,
0.9,
1.65,
)
var fill_tween: Tween = create_tween()
fill_tween.tween_method(
Callable(self, "_set_experience_animation_progress").bind(
previous_total,
new_total,
),
0.0,
1.0,
fill_duration,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
await fill_tween.finished
if generation != _experience_animation_generation:
return
await get_tree().create_timer(0.75).timeout
if generation != _experience_animation_generation:
return
var exit_tween: Tween = create_tween()
exit_tween.set_parallel(true)
exit_tween.tween_property(
_experience_panel,
"modulate:a",
0.0,
0.24,
)
exit_tween.tween_property(
_experience_bubble,
"modulate:a",
0.0,
0.2,
)
exit_tween.tween_property(
_experience_bubble,
"scale",
Vector2(0.82, 0.82),
0.24,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
await exit_tween.finished
if generation != _experience_animation_generation:
return
_experience_panel.hide()
_experience_bubble.hide()
_experience_animation_active = false
call_deferred("_start_next_experience_animation")
func _set_experience_animation_progress(
progress: float,
previous_total: int,
new_total: int,
) -> void:
var displayed_total: int = roundi(lerpf(
float(previous_total),
float(new_total),
clampf(progress, 0.0, 1.0),
))
_update_experience_progress(displayed_total)
func _update_experience_progress(total_experience: int) -> void:
var level: int = PlayerExperienceType.level_for_total_experience(
total_experience
)
_experience_level_label.text = "level %d" % level
_experience_progress.value = (
PlayerExperienceType.progress_for_total_experience(total_experience)
* 100.0
)
func _update_experience_bubble_position() -> void:
if not _experience_animation_active or _player == null:
return
if _player_menu_open:
_experience_bubble.show()
_experience_bubble.position = Vector2(
(_canonical_stage.size.x - _experience_bubble.size.x) * 0.5,
_experience_panel_rest_y + _experience_panel.size.y + 10.0,
)
return
var camera: Camera3D = _player.get_gameplay_camera()
var anchor_position: Vector3 = _player.get_chat_anchor_position()
if camera == null or camera.is_position_behind(anchor_position):
_experience_bubble.hide()
return
_experience_bubble.show()
var window_size := Vector2(get_window().size)
var output_scale: float = UIReferencePresentationType.get_scale(
window_size
)
var stage_position: Vector2 = (
camera.unproject_position(anchor_position) / output_scale
- _canonical_stage.position
)
var desired: Vector2 = stage_position - Vector2(
_experience_bubble.size.x * 0.5,
_experience_bubble.size.y + 10.0,
)
_experience_bubble.position = Vector2(
clampf(
desired.x,
8.0,
_canonical_stage.size.x - _experience_bubble.size.x - 8.0,
),
clampf(
desired.y,
8.0,
_canonical_stage.size.y - _experience_bubble.size.y - 8.0,
),
)
func _on_player_menu_visibility_changed(is_open: bool) -> void:
_player_menu_open = is_open
if is_open:
_end_virtual_mouse()
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
_gameplay_transient_hud.visible = _gameplay_ui_enabled and not is_open
if is_open:
_hotbar_ui.set_drag_enabled(false)
_hotbar_ui.set_presentation_visible(false, false)
if _player_menu_hotbar_visible:
_hotbar_ui.set_player_menu_context(true)
_hotbar_ui.set_presentation_visible(
true,
true,
UIMotion.PLAYER_MENU_ENTER_DURATION,
)
player_menu_backdrop_visibility_changed.emit(true)
else:
_player_menu_hotbar_visible = false
_hotbar_ui.set_presentation_visible(false, false)
_hotbar_ui.set_player_menu_context(false)
_refresh_chat_availability()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled and not is_open
)
if not is_open:
_hotbar_ui.set_drag_enabled(false)
_refresh_hotbar_visibility()
_refresh_fishing_panel_visibility()
_emit_interactive_pointer_ui_changed()
if is_open:
_shop_prompt.hide()
else:
_refresh_fishing_panel_visibility()
func _on_player_menu_exit_started() -> void:
if not _player_menu_open:
return
_hotbar_ui.set_drag_enabled(false)
_hotbar_ui.set_presentation_visible(
false,
true,
UIMotion.PLAYER_MENU_EXIT_DURATION,
)
player_menu_backdrop_visibility_changed.emit(false)
func _on_shop_visibility_changed(is_open: bool) -> void:
_shop_open = is_open
if is_open and _surface_drawing != null:
_surface_drawing.deactivate()
shop_backdrop_visibility_changed.emit(is_open)
if not is_open and _player_menu.is_shop_cooler_mounted():
_player_menu.unmount_shop_cooler()
_refresh_chat_availability()
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not is_open
and not _system_menu_open
and not _player_menu_open
)
if is_open:
_shop_prompt.hide()
_emit_interactive_pointer_ui_changed()
func _on_shop_sell_fish_requested() -> void:
if (
not _fishing_shop.visible
or _main_shop_buyer == null
or not _main_shop_buyer.is_valid()
or _shop_interaction == null
or not _shop_interaction.is_local_player_in_range()
or _fishing_spot == null
or not _fishing_spot.is_ready_for_shop_transaction()
):
return
if _player_menu.mount_shop_cooler(
_fishing_shop.get_shop_cooler_mount(),
_main_shop_buyer,
):
_fishing_shop.activate_shop_cooler_page()
func _on_shop_cooler_return_requested() -> void:
_player_menu.unmount_shop_cooler()
_fishing_shop.deactivate_shop_cooler_page()
func _on_shop_cooler_modal_changed(is_open: bool) -> void:
_fishing_shop.set_shop_cooler_modal_open(is_open)
func _on_inventory_hotbar_context_changed(show_hotbar: bool) -> void:
var context_changed: bool = _player_menu_hotbar_visible != show_hotbar
_player_menu_hotbar_visible = show_hotbar
if _player_menu_open and context_changed:
_hotbar_ui.set_drag_enabled(false)
if show_hotbar:
_hotbar_ui.set_presentation_visible(false, false)
_hotbar_ui.set_player_menu_context(true)
_hotbar_ui.set_presentation_visible(
true,
true,
UIMotion.PLAYER_MENU_INVENTORY_DURATION,
)
else:
_hotbar_ui.set_presentation_visible(
false,
true,
UIMotion.PLAYER_MENU_INVENTORY_DURATION,
)
elif _player_menu_open and show_hotbar:
_hotbar_ui.set_drag_enabled(true)
if not context_changed:
return
if not _player_menu_open:
_refresh_hotbar_visibility()
func _on_controller_hotbar_placement_requested(
assignment_kind: PlayerHotbarType.AssignmentKind,
identity: StringName,
initial_slot: int,
) -> void:
_player_menu_hotbar_visible = true
_hotbar_ui.set_player_menu_context(true)
_hotbar_ui.set_drag_enabled(false)
_hotbar_ui.set_presentation_visible(true, false)
_hotbar_ui.begin_controller_placement(
assignment_kind,
identity,
initial_slot,
)
func _on_controller_hotbar_placement_ended() -> void:
_hotbar_ui.end_controller_placement()
_hotbar_ui.set_drag_enabled(
_player_menu_open
and _player_menu_hotbar_visible
and not _system_menu_open
)
func _on_controller_hotbar_management_requested(initial_slot: int) -> void:
_player_menu_hotbar_visible = true
_hotbar_ui.set_player_menu_context(true)
_hotbar_ui.set_drag_enabled(false)
_hotbar_ui.set_presentation_visible(true, false)
_hotbar_ui.begin_controller_management(initial_slot)
func _on_controller_hotbar_management_ended() -> void:
_hotbar_ui.end_controller_management()
_hotbar_ui.set_drag_enabled(
_player_menu_open
and _player_menu_hotbar_visible
and not _system_menu_open
)
func _on_hotbar_presentation_transition_finished(
is_visible: bool,
) -> void:
_hotbar_ui.set_drag_enabled(
is_visible
and _player_menu_open
and _player_menu_hotbar_visible
and not _system_menu_open
)
func _refresh_hotbar_visibility() -> void:
_hotbar_ui.set_presentation_visible(
_gameplay_ui_enabled
and not _system_menu_open
and not _shop_open
and (
not _player_menu_open
or _player_menu_hotbar_visible
),
_player_menu_open,
)
func _emit_interactive_pointer_ui_changed() -> void:
interactive_pointer_ui_changed.emit(
_system_menu_open or _player_menu_open or _shop_open or _chat_input_open
)
func _on_chat_text_entry_ownership_changed(active: bool) -> void:
_chat_input_open = active
if active and _surface_drawing != null:
_surface_drawing.deactivate()
_emit_interactive_pointer_ui_changed()
func _refresh_chat_availability() -> void:
if _chat_ui == null:
return
var chat_available := (
_gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
)
_chat_ui.set_available(chat_available)
passive_pointer_ui_changed.emit(chat_available)