Improve Android controller support

This commit is contained in:
Alexander Sellite 2026-08-03 09:31:13 -04:00
parent b47f131797
commit 9c89116713
16 changed files with 647 additions and 31 deletions

View file

@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://crci2on30ctoc"
path.s3tc="res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex"
path.etc2="res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://art/exported/environment/textures/grass_lite.png"
dest_files=["res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex"]
dest_files=["res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex", "res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.etc2.ctex"]
[params]

View file

@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://btxhq3rdlum2"
path.s3tc="res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex"
path.etc2="res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://art/exported/environment/textures/grass_olive_tile.png"
dest_files=["res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex"]
dest_files=["res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex", "res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.etc2.ctex"]
[params]

View file

@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://bnje415gq85nn"
path.s3tc="res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex"
path.etc2="res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://art/exported/environment/textures/sand.png"
dest_files=["res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex"]
dest_files=["res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex", "res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.etc2.ctex"]
[params]

View file

@ -94,6 +94,8 @@ class ShowcaseCameraSnapshot:
@export var maximum_zoom: float = 8.0
@export var zoom_step: float = 0.75
@export var zoom_smoothing: float = 12.0
@export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0
@export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55
@onready var _visuals: Node3D = %Visuals
@onready var _character_animation_player: AnimationPlayer = (
@ -290,16 +292,26 @@ func _process(delta: float) -> void:
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
)
if stick.length() > controller_camera_deadzone:
var adjusted_strength: float = (
(stick.length() - controller_camera_deadzone)
/ (1.0 - controller_camera_deadzone)
)
_rotate_camera(
stick.normalized()
* adjusted_strength
* controller_camera_speed
* delta
)
if (
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_RIGHT)
>= controller_trigger_threshold
):
var vertical_zoom_input: float = _apply_axis_deadzone(stick.y)
_set_target_zoom(
_target_zoom
+ vertical_zoom_input * controller_zoom_speed * delta
)
else:
var adjusted_strength: float = (
(stick.length() - controller_camera_deadzone)
/ (1.0 - controller_camera_deadzone)
)
_rotate_camera(
stick.normalized()
* adjusted_strength
* controller_camera_speed
* delta
)
var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
_spring_arm.spring_length = lerpf(
@ -435,6 +447,17 @@ func _set_target_zoom(value: float) -> void:
_target_zoom = clampf(value, minimum_zoom, maximum_zoom)
func _apply_axis_deadzone(value: float) -> float:
var magnitude: float = absf(value)
if magnitude <= controller_camera_deadzone:
return 0.0
return (
signf(value)
* (magnitude - controller_camera_deadzone)
/ (1.0 - controller_camera_deadzone)
)
func set_local_control(enabled: bool) -> void:
local_control_enabled = enabled
if is_node_ready():

View file

@ -80,13 +80,13 @@ slow_walk={
interact={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
]
}
fish_primary={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":5,"axis_value":1.0,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
]
}
camera_drag={
@ -97,13 +97,11 @@ camera_drag={
camera_zoom_in={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"canceled":false,"pressed":false,"double_click":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
]
}
camera_zoom_out={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
]
}
hotbar_1={
@ -178,6 +176,16 @@ open_emotes={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
]
}
open_quick_actions={
"deadzone": 0.2,
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
]
}
open_chat={
"deadzone": 0.2,
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
]
}
open_tacklebox={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
@ -213,6 +221,20 @@ alternate_fishing={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":92,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ui_accept={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
]
}
ui_cancel={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
]
}
[physics]

View file

@ -15,16 +15,30 @@ func _init() -> void:
assert(_has_joypad_motion(&"move_left", JOY_AXIS_LEFT_X, -1.0))
assert(_has_joypad_motion(&"move_right", JOY_AXIS_LEFT_X, 1.0))
assert(_has_joypad_button(&"jump", JOY_BUTTON_A))
assert(_has_joypad_button(&"interact", JOY_BUTTON_X))
assert(_has_joypad_motion(&"fish_primary", JOY_AXIS_TRIGGER_RIGHT, 1.0))
assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A))
assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B))
assert(_has_joypad_button(&"interact", JOY_BUTTON_Y))
assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER))
assert(not _has_joypad_motion(
&"fish_primary", JOY_AXIS_TRIGGER_LEFT, 1.0
))
assert(not _has_joypad_motion(
&"fish_primary", JOY_AXIS_TRIGGER_RIGHT, 1.0
))
assert(_has_joypad_button(&"sprint", JOY_BUTTON_LEFT_STICK))
assert(_has_joypad_button(&"camera_zoom_out", JOY_BUTTON_LEFT_SHOULDER))
assert(_has_joypad_button(&"camera_zoom_in", JOY_BUTTON_RIGHT_SHOULDER))
assert(not _has_joypad_button(
&"camera_zoom_out", JOY_BUTTON_LEFT_SHOULDER
))
assert(not _has_joypad_button(
&"camera_zoom_in", JOY_BUTTON_RIGHT_SHOULDER
))
assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT))
assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT))
assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_BACK))
assert(_has_joypad_button(&"open_system_menu", JOY_BUTTON_START))
assert(_has_joypad_button(&"open_emotes", JOY_BUTTON_DPAD_UP))
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_LEFT_SHOULDER))
print("android readiness validation passed")
quit()

View file

@ -194,6 +194,13 @@ func close_chat() -> void:
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("open_chat"):
if _opened:
close_chat()
elif _available:
open_chat()
get_viewport().set_input_as_handled()
return
if event is InputEventKey and event.pressed and not event.echo:
if event.keycode in [KEY_ENTER, KEY_KP_ENTER]:
if _opened:

View file

@ -0,0 +1,40 @@
class_name ControllerVirtualCursor
extends Control
const CURSOR_SIZE: Vector2 = Vector2(28.0, 28.0)
const OUTER_COLOR: Color = Color(0.02, 0.10, 0.14, 0.96)
const INNER_COLOR: Color = Color(0.34, 0.86, 0.91, 1.0)
const CENTER_COLOR: Color = Color(0.96, 1.0, 1.0, 1.0)
func _ready() -> void:
size = CURSOR_SIZE
mouse_filter = Control.MOUSE_FILTER_IGNORE
focus_mode = Control.FOCUS_NONE
visible = false
queue_redraw()
func set_pointer_position(pointer_position: Vector2) -> void:
position = pointer_position - CURSOR_SIZE * 0.5
func _draw() -> void:
var center: Vector2 = CURSOR_SIZE * 0.5
draw_circle(center, 8.0, OUTER_COLOR)
draw_circle(center, 5.0, INNER_COLOR)
draw_circle(center, 1.75, CENTER_COLOR)
draw_line(Vector2(center.x, 0.0), Vector2(center.x, 6.0), CENTER_COLOR, 2.0)
draw_line(
Vector2(center.x, CURSOR_SIZE.y - 6.0),
Vector2(center.x, CURSOR_SIZE.y),
CENTER_COLOR,
2.0,
)
draw_line(Vector2(0.0, center.y), Vector2(6.0, center.y), CENTER_COLOR, 2.0)
draw_line(
Vector2(CURSOR_SIZE.x - 6.0, center.y),
Vector2(CURSOR_SIZE.x, center.y),
CENTER_COLOR,
2.0,
)

View file

@ -0,0 +1 @@
uid://dehegl7ccn057

View file

@ -13,10 +13,12 @@ const SECTOR_COUNT: int = 8
const SIT_SECTOR: int = 0
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = SIT_SECTOR
var _controller_selection_mode: bool = false
func _ready() -> void:
@ -42,7 +44,7 @@ func handle_input(event: InputEvent, can_open: bool) -> bool:
if event.is_pressed():
if _is_open or not can_open:
return _is_open
open_menu()
open_menu(event is InputEventJoypadButton)
return true
if not _is_open:
return false
@ -53,9 +55,10 @@ func handle_input(event: InputEvent, can_open: bool) -> bool:
return true
func open_menu() -> void:
func open_menu(controller_selection: bool = false) -> void:
_is_open = true
_selected_sector = SIT_SECTOR
_controller_selection_mode = controller_selection
visible = true
_layout_bubbles()
_apply_selection_styles()
@ -74,7 +77,7 @@ func _process(_delta: float) -> void:
if not _is_open:
return
_layout_bubbles()
_update_mouse_selection()
_update_selection()
func _layout_bubbles() -> void:
@ -88,10 +91,27 @@ func _layout_bubbles() -> void:
bubble.pivot_offset = BUBBLE_SIZE * 0.5
func _update_selection() -> void:
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
if _controller_selection_mode:
return
_update_mouse_selection()
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:
return
_select_sector_from_offset(offset)
func _select_sector_from_offset(offset: Vector2) -> void:
var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU)
var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT))))
if sector == _selected_sector:

View file

@ -34,6 +34,10 @@ const PlayerCoolerCapacityType = preload(
)
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"
)
@ -58,6 +62,11 @@ 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_STICK_DEADZONE: float = 0.18
const VIRTUAL_MOUSE_SPEED: float = 720.0
@onready var _status_label: Label = %StatusLabel
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
@onready var _experience_presentation: Control = %ExperiencePresentation
@ -77,6 +86,7 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
@onready var _experience_bubble: PanelContainer = %ExperienceBubble
@onready var _experience_bubble_label: Label = %ExperienceBubbleLabel
@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
@ -87,6 +97,10 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
@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
)
@ -115,6 +129,15 @@ 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 _injecting_virtual_mouse_event: bool = false
func _ready() -> void:
@ -128,6 +151,7 @@ func _ready() -> void:
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
)
@ -274,6 +298,17 @@ func setup(
func _input(event: InputEvent) -> void:
if (
_virtual_mouse_active
and _injecting_virtual_mouse_event
and event is InputEventMouseMotion
):
_update_virtual_cursor_position(
(event as InputEventMouseMotion).position
)
if _handle_virtual_mouse_input(event):
get_viewport().set_input_as_handled()
return
if (
_surface_drawing_toolbar != null
and _surface_drawing_toolbar.owns_pointer_event(event)
@ -295,7 +330,7 @@ func _input(event: InputEvent) -> void:
):
get_viewport().set_input_as_handled()
return
if _emote_radial_menu == null:
if _emote_radial_menu == null or _quick_radial_menu == null:
return
var can_open: bool = (
_gameplay_ui_enabled
@ -304,8 +339,42 @@ func _input(event: InputEvent) -> void:
and not _shop_open
and not _chat_input_open
and not _showcase_active
and not _virtual_mouse_active
)
if _emote_radial_menu.handle_input(event, can_open):
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()
@ -314,6 +383,212 @@ func _on_emote_selected(emote_id: StringName) -> void:
_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:
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
if motion_event != null:
if motion_event.axis == JOY_AXIS_TRIGGER_LEFT:
var was_active: bool = _virtual_mouse_active
if motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse()
elif _virtual_mouse_active:
_end_virtual_mouse()
return was_active or _virtual_mouse_active
if (
_virtual_mouse_active
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
):
_set_virtual_mouse_button(
MOUSE_BUTTON_RIGHT,
motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
)
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 Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
)
):
if not _virtual_mouse_active and _can_start_virtual_mouse():
_begin_virtual_mouse()
if _virtual_mouse_active:
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
return true
return false
func _can_start_virtual_mouse() -> bool:
return (
_gameplay_ui_enabled
and not _showcase_active
and _player != null
and _fishing_spot != null
and _fishing_spot.can_use_surface_drawing()
and not _emote_radial_menu.is_open()
and not _quick_radial_menu.is_open()
)
func _begin_virtual_mouse() -> void:
if _virtual_mouse_active:
return
_virtual_mouse_active = true
_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 = Vector2(get_window().size) * 0.5
_controller_virtual_cursor.visible = true
_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
_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 (
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
< VIRTUAL_MOUSE_TRIGGER_THRESHOLD
):
_end_virtual_mouse()
return
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
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
)
var window_size: Vector2 = Vector2(get_window().size)
_virtual_mouse_window_position = Vector2(
clampf(
_virtual_mouse_window_position.x + relative_motion.x,
0.0,
window_size.x,
),
clampf(
_virtual_mouse_window_position.y + relative_motion.y,
0.0,
window_size.y,
),
)
_emit_virtual_mouse_motion(relative_motion)
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:
_injecting_virtual_mouse_event = true
Input.parse_input_event(event)
_injecting_virtual_mouse_event = false
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
var root_transform: Transform2D = _ui_root.get_global_transform_with_canvas()
_controller_virtual_cursor.set_pointer_position(
root_transform.affine_inverse() * viewport_position
)
func _toggle_surface_drawing() -> void:
if _surface_drawing == null:
return
@ -344,7 +619,8 @@ func setup_data_and_identity(
)
func _process(_delta: float) -> void:
func _process(delta: float) -> void:
_update_virtual_mouse(delta)
_update_experience_bubble_position()
if _item_effects == null or not _gameplay_ui_enabled:
_effect_status.hide()
@ -403,6 +679,19 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
_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()

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=19 format=3]
[gd_scene load_steps=21 format=3]
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
@ -11,6 +11,8 @@
[ext_resource type="Script" path="res://ui/chat_ui.gd" id="9_chat"]
[ext_resource type="PackedScene" path="res://ui/emote_radial_menu.tscn" id="10_emote"]
[ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"]
[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"]
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.032, 0.118, 0.15, 1)
@ -328,6 +330,9 @@ unique_name_in_owner = true
[node name="EmoteRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("10_emote")]
unique_name_in_owner = true
[node name="QuickRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("12_quick")]
unique_name_in_owner = true
[node name="ShopPrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
visible = false
@ -386,6 +391,12 @@ grow_vertical = 2
mouse_filter = 2
script = ExtResource("9_chat")
[node name="ControllerVirtualCursor" type="Control" parent="UIRoot"]
unique_name_in_owner = true
z_index = 500
mouse_filter = 2
script = ExtResource("13_cursor")
[node name="ScreenFade" type="ColorRect" parent="UIRoot"]
unique_name_in_owner = true
visible = false

View file

@ -701,6 +701,25 @@ func open_menu() -> void:
menu_visibility_changed.emit(true)
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,

152
ui/quick_radial_menu.gd Normal file
View file

@ -0,0 +1,152 @@
class_name QuickRadialMenu
extends Control
signal action_selected(action_id: StringName)
const BubbleButtonScene: PackedScene = preload(
"res://ui/components/bubble_menu/bubble_button.tscn"
)
const BubbleProfile: BubbleMenuProfile = preload(
"res://ui/components/bubble_menu/bubble_menu_profile.tres"
)
const SECTOR_COUNT: int = 8
const RING_RADIUS: float = 172.0
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
const ACTIONS: Array[StringName] = [
&"stuff",
&"logbook",
&"fishnet",
&"mail",
&"profile",
&"online",
&"paint",
&"chat",
]
const LABELS: Array[String] = [
"stuff",
"logbook",
"fishnet",
"mail",
"profile",
"online",
"paint",
"chat",
]
var _buttons: Array[BubbleButton] = []
var _is_open: bool = false
var _selected_sector: int = 0
var _controller_selection_mode: bool = false
func _ready() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
for sector: int in SECTOR_COUNT:
var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton
bubble.profile = BubbleProfile
bubble.focus_mode = Control.FOCUS_NONE
bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE
bubble.text = LABELS[sector]
add_child(bubble)
_buttons.append(bubble)
_apply_selection_styles()
func handle_input(event: InputEvent, can_open: bool) -> bool:
if not event.is_action("open_quick_actions"):
return false
var key_event: InputEventKey = event as InputEventKey
if key_event != null and key_event.echo:
return false
if event.is_pressed():
if _is_open or not can_open:
return _is_open
open_menu(event is InputEventJoypadButton)
return true
if not _is_open:
return false
var selected: int = _selected_sector
close_menu()
call_deferred("_emit_selected_action", ACTIONS[selected])
return true
func open_menu(controller_selection: bool = false) -> void:
_is_open = true
_selected_sector = 0
_controller_selection_mode = controller_selection
visible = true
_layout_bubbles()
_apply_selection_styles()
func close_menu() -> void:
_is_open = false
visible = false
func is_open() -> bool:
return _is_open
func _emit_selected_action(action_id: StringName) -> void:
action_selected.emit(action_id)
func _process(_delta: float) -> void:
if not _is_open:
return
_layout_bubbles()
_update_selection()
func _layout_bubbles() -> void:
var center: Vector2 = size * 0.5
for sector: int in SECTOR_COUNT:
var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT)
var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS
var bubble: BubbleButton = _buttons[sector]
bubble.position = bubble_center - BUBBLE_SIZE * 0.5
bubble.size = BUBBLE_SIZE
bubble.pivot_offset = BUBBLE_SIZE * 0.5
func _update_selection() -> void:
var stick: Vector2 = Vector2(
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
)
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
_select_sector_from_offset(stick)
return
if _controller_selection_mode:
return
_update_mouse_selection()
func _update_mouse_selection() -> void:
var offset: Vector2 = get_local_mouse_position() - size * 0.5
if offset.length() < 24.0:
return
_select_sector_from_offset(offset)
func _select_sector_from_offset(offset: Vector2) -> void:
var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU)
var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT))))
if sector == _selected_sector:
return
_selected_sector = sector
_apply_selection_styles()
func _apply_selection_styles() -> void:
for sector: int in _buttons.size():
var bubble: BubbleButton = _buttons[sector]
bubble.apply_profile()
if sector == _selected_sector:
bubble.add_theme_stylebox_override(
"normal", BubbleProfile.make_hover_style()
)

View file

@ -0,0 +1 @@
uid://5nr1c8x881sw

14
ui/quick_radial_menu.tscn Normal file
View file

@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/quick_radial_menu.gd" id="1_quick"]
[node name="QuickRadialMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
z_index = 90
script = ExtResource("1_quick")