Improve Android controller and chat support
This commit is contained in:
parent
3dc8ced5d2
commit
ceef03eba5
24 changed files with 2485 additions and 128 deletions
21
main/main.gd
21
main/main.gd
|
|
@ -16,6 +16,9 @@ const PlayerSettingsManagerType = preload(
|
||||||
"res://settings/player_settings_manager.gd"
|
"res://settings/player_settings_manager.gd"
|
||||||
)
|
)
|
||||||
const PlayerSettingsType = preload("res://settings/player_settings.gd")
|
const PlayerSettingsType = preload("res://settings/player_settings.gd")
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
const TitleScreenType = preload("res://ui/title_screen.gd")
|
const TitleScreenType = preload("res://ui/title_screen.gd")
|
||||||
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
||||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
|
|
@ -111,6 +114,9 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
|
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
|
||||||
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
|
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
|
||||||
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
|
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
|
||||||
|
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
|
||||||
|
%ControllerMappingManager
|
||||||
|
)
|
||||||
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
|
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
|
||||||
@onready var _title_music: AudioStreamPlayer = %TitleMusic
|
@onready var _title_music: AudioStreamPlayer = %TitleMusic
|
||||||
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
|
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
|
||||||
|
|
@ -534,6 +540,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_network_session,
|
_network_session,
|
||||||
_interface_fonts,
|
_interface_fonts,
|
||||||
)
|
)
|
||||||
|
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||||
_data_root.conflict_detected.connect(_on_portable_conflict)
|
_data_root.conflict_detected.connect(_on_portable_conflict)
|
||||||
_data_root.status_changed.connect(_on_data_root_status)
|
_data_root.status_changed.connect(_on_data_root_status)
|
||||||
if (
|
if (
|
||||||
|
|
@ -927,6 +934,8 @@ func _on_peer_identity_observed(_peer_id: int, status: String) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
|
if _game_ui.is_controller_mapping_capturing():
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
not (
|
not (
|
||||||
event.is_action_pressed("ui_cancel")
|
event.is_action_pressed("ui_cancel")
|
||||||
|
|
@ -935,6 +944,7 @@ func _input(event: InputEvent) -> void:
|
||||||
or (event is InputEventKey and event.echo)
|
or (event is InputEventKey and event.echo)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
var pause_open_requested: bool = _is_pause_open_request(event)
|
||||||
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
||||||
if title_screen.visible or not _gameplay_started:
|
if title_screen.visible or not _gameplay_started:
|
||||||
return
|
return
|
||||||
|
|
@ -953,7 +963,8 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
not _water_recovery.is_recovery_active()
|
pause_open_requested
|
||||||
|
and not _water_recovery.is_recovery_active()
|
||||||
and _fishing_spot.can_open_system_menu()
|
and _fishing_spot.can_open_system_menu()
|
||||||
):
|
):
|
||||||
_game_ui.close_player_menu_for_game_menu()
|
_game_ui.close_player_menu_for_game_menu()
|
||||||
|
|
@ -961,6 +972,13 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _is_pause_open_request(event: InputEvent) -> bool:
|
||||||
|
return (
|
||||||
|
event.is_action_pressed("open_system_menu")
|
||||||
|
or event is InputEventKey
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if (
|
if (
|
||||||
not _gameplay_started
|
not _gameplay_started
|
||||||
|
|
@ -1002,6 +1020,7 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
_game_ui.set_edge_docks(
|
_game_ui.set_edge_docks(
|
||||||
settings.chat_dock_right,
|
settings.chat_dock_right,
|
||||||
settings.paint_dock_right,
|
settings.paint_dock_right,
|
||||||
|
settings.chat_mobile_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"]
|
[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"]
|
||||||
[ext_resource type="Script" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
|
[ext_resource type="Script" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
|
||||||
[ext_resource type="AudioStream" path="res://audio/ambience/waves.wav" id="55_waves"]
|
[ext_resource type="AudioStream" path="res://audio/ambience/waves.wav" id="55_waves"]
|
||||||
|
[ext_resource type="Script" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||||
shader = ExtResource("40_title_water")
|
shader = ExtResource("40_title_water")
|
||||||
|
|
@ -273,6 +274,10 @@ script = ExtResource("9_save")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("10_settings")
|
script = ExtResource("10_settings")
|
||||||
|
|
||||||
|
[node name="ControllerMappingManager" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("56_controller_mapping")
|
||||||
|
|
||||||
[node name="TitleMusic" type="AudioStreamPlayer" parent="." unique_id=1292939956]
|
[node name="TitleMusic" type="AudioStreamPlayer" parent="." unique_id=1292939956]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
stream = ExtResource("13_title_music")
|
stream = ExtResource("13_title_music")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ const PlayerArtUnlocksType = preload(
|
||||||
const PlayerExperienceType = preload(
|
const PlayerExperienceType = preload(
|
||||||
"res://progression/player_experience.gd"
|
"res://progression/player_experience.gd"
|
||||||
)
|
)
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
const FishingRodAttachmentScene = preload(
|
const FishingRodAttachmentScene = preload(
|
||||||
"res://player/fishing_rod_attachment.tscn"
|
"res://player/fishing_rod_attachment.tscn"
|
||||||
)
|
)
|
||||||
|
|
@ -32,6 +35,10 @@ const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
||||||
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
||||||
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
||||||
const BASE_REEL_SPEED: float = 0.16
|
const BASE_REEL_SPEED: float = 0.16
|
||||||
|
# The target Android handheld exposes its physical right trigger through
|
||||||
|
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
||||||
|
# remains isolated from camera behavior.
|
||||||
|
const CONTROLLER_ZOOM_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
|
||||||
|
|
||||||
var appearance_snapshot: Dictionary = (
|
var appearance_snapshot: Dictionary = (
|
||||||
CharacterCustomizationCatalog.default_snapshot()
|
CharacterCustomizationCatalog.default_snapshot()
|
||||||
|
|
@ -160,6 +167,7 @@ var _character_animation_name: StringName = &""
|
||||||
var _sitting: bool = false
|
var _sitting: bool = false
|
||||||
var _fishing_rod: Node3D
|
var _fishing_rod: Node3D
|
||||||
var _fishing_rod_tip: Marker3D
|
var _fishing_rod_tip: Marker3D
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -169,6 +177,12 @@ func _ready() -> void:
|
||||||
_camera.current = local_control_enabled
|
_camera.current = local_control_enabled
|
||||||
|
|
||||||
|
|
||||||
|
func set_controller_mapping_manager(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
|
||||||
|
|
||||||
func _initialize_fishing_rod() -> void:
|
func _initialize_fishing_rod() -> void:
|
||||||
var skeleton := get_node_or_null(
|
var skeleton := get_node_or_null(
|
||||||
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||||
|
|
@ -287,13 +301,10 @@ func _process(delta: float) -> void:
|
||||||
_camera_dragging = false
|
_camera_dragging = false
|
||||||
|
|
||||||
if _camera_input_enabled:
|
if _camera_input_enabled:
|
||||||
var stick: Vector2 = Vector2(
|
var stick: Vector2 = _get_controller_camera_stick()
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
|
|
||||||
)
|
|
||||||
if stick.length() > controller_camera_deadzone:
|
if stick.length() > controller_camera_deadzone:
|
||||||
if (
|
if (
|
||||||
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_RIGHT)
|
_get_controller_zoom_strength()
|
||||||
>= controller_trigger_threshold
|
>= controller_trigger_threshold
|
||||||
):
|
):
|
||||||
var vertical_zoom_input: float = _apply_axis_deadzone(stick.y)
|
var vertical_zoom_input: float = _apply_axis_deadzone(stick.y)
|
||||||
|
|
@ -321,6 +332,36 @@ func _process(delta: float) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_controller_camera_stick() -> Vector2:
|
||||||
|
if (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
):
|
||||||
|
return Vector2(
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||||
|
),
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Vector2(
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_controller_zoom_strength() -> float:
|
||||||
|
if (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
):
|
||||||
|
return _controller_mapping_manager.get_role_strength(
|
||||||
|
ControllerMappingManagerType.ROLE_RT
|
||||||
|
)
|
||||||
|
return Input.get_joy_axis(0, CONTROLLER_ZOOM_TRIGGER_AXIS)
|
||||||
|
|
||||||
|
|
||||||
func _update_character_animation() -> void:
|
func _update_character_animation() -> void:
|
||||||
if _character_animation_player == null:
|
if _character_animation_player == null:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ hotbar_next={
|
||||||
open_backpack={
|
open_backpack={
|
||||||
"deadzone": 0.2,
|
"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":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"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":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
open_system_menu={
|
open_system_menu={
|
||||||
|
|
@ -183,6 +183,11 @@ open_quick_actions={
|
||||||
}
|
}
|
||||||
open_chat={
|
open_chat={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
focus_gameplay={
|
||||||
|
"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)
|
"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
751
settings/controller_mapping_manager.gd
Normal file
751
settings/controller_mapping_manager.gd
Normal file
|
|
@ -0,0 +1,751 @@
|
||||||
|
class_name ControllerMappingManager
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal active_profile_changed
|
||||||
|
signal active_controller_changed(controller_name: String)
|
||||||
|
|
||||||
|
const FORMAT_VERSION: int = 1
|
||||||
|
const PROFILE_PATH: String = "user://controller_mappings.json"
|
||||||
|
const PROFILE_TEMP_PATH: String = "user://controller_mappings.json.tmp"
|
||||||
|
const PROFILE_BACKUP_PATH: String = "user://controller_mappings.json.backup"
|
||||||
|
const MAX_PROFILE_BYTES: int = 1024 * 1024
|
||||||
|
const CAPTURE_AXIS_THRESHOLD: float = 0.55
|
||||||
|
const ROLE_A: StringName = &"a"
|
||||||
|
const ROLE_B: StringName = &"b"
|
||||||
|
const ROLE_X: StringName = &"x"
|
||||||
|
const ROLE_Y: StringName = &"y"
|
||||||
|
const ROLE_LB: StringName = &"lb"
|
||||||
|
const ROLE_RB: StringName = &"rb"
|
||||||
|
const ROLE_LT: StringName = &"lt"
|
||||||
|
const ROLE_RT: StringName = &"rt"
|
||||||
|
const ROLE_SELECT: StringName = &"select"
|
||||||
|
const ROLE_START: StringName = &"start"
|
||||||
|
const ROLE_LEFT_STICK_CLICK: StringName = &"left_stick_click"
|
||||||
|
const ROLE_RIGHT_STICK_CLICK: StringName = &"right_stick_click"
|
||||||
|
const ROLE_DPAD_UP: StringName = &"dpad_up"
|
||||||
|
const ROLE_DPAD_DOWN: StringName = &"dpad_down"
|
||||||
|
const ROLE_DPAD_LEFT: StringName = &"dpad_left"
|
||||||
|
const ROLE_DPAD_RIGHT: StringName = &"dpad_right"
|
||||||
|
const ROLE_LEFT_STICK_X: StringName = &"left_stick_x"
|
||||||
|
const ROLE_LEFT_STICK_Y: StringName = &"left_stick_y"
|
||||||
|
const ROLE_RIGHT_STICK_X: StringName = &"right_stick_x"
|
||||||
|
const ROLE_RIGHT_STICK_Y: StringName = &"right_stick_y"
|
||||||
|
|
||||||
|
const ROLE_ORDER: Array[StringName] = [
|
||||||
|
ROLE_A,
|
||||||
|
ROLE_B,
|
||||||
|
ROLE_X,
|
||||||
|
ROLE_Y,
|
||||||
|
ROLE_LB,
|
||||||
|
ROLE_RB,
|
||||||
|
ROLE_LT,
|
||||||
|
ROLE_RT,
|
||||||
|
ROLE_SELECT,
|
||||||
|
ROLE_START,
|
||||||
|
ROLE_LEFT_STICK_CLICK,
|
||||||
|
ROLE_RIGHT_STICK_CLICK,
|
||||||
|
ROLE_DPAD_UP,
|
||||||
|
ROLE_DPAD_DOWN,
|
||||||
|
ROLE_DPAD_LEFT,
|
||||||
|
ROLE_DPAD_RIGHT,
|
||||||
|
ROLE_LEFT_STICK_X,
|
||||||
|
ROLE_LEFT_STICK_Y,
|
||||||
|
ROLE_RIGHT_STICK_X,
|
||||||
|
ROLE_RIGHT_STICK_Y,
|
||||||
|
]
|
||||||
|
|
||||||
|
const ROLE_LABELS: Dictionary = {
|
||||||
|
ROLE_A: "jump / menu accept",
|
||||||
|
ROLE_B: "menu back",
|
||||||
|
ROLE_X: "player menu",
|
||||||
|
ROLE_Y: "interact",
|
||||||
|
ROLE_LB: "focus chat or world",
|
||||||
|
ROLE_RB: "primary action",
|
||||||
|
ROLE_LT: "virtual mouse",
|
||||||
|
ROLE_RT: "camera zoom",
|
||||||
|
ROLE_SELECT: "chat",
|
||||||
|
ROLE_START: "pause",
|
||||||
|
ROLE_LEFT_STICK_CLICK: "sprint",
|
||||||
|
ROLE_RIGHT_STICK_CLICK: "unassigned stick click",
|
||||||
|
ROLE_DPAD_UP: "emote wheel",
|
||||||
|
ROLE_DPAD_DOWN: "quick menu",
|
||||||
|
ROLE_DPAD_LEFT: "previous hotbar slot",
|
||||||
|
ROLE_DPAD_RIGHT: "next hotbar slot",
|
||||||
|
ROLE_LEFT_STICK_X: "move left / right",
|
||||||
|
ROLE_LEFT_STICK_Y: "move up / down",
|
||||||
|
ROLE_RIGHT_STICK_X: "camera left / right",
|
||||||
|
ROLE_RIGHT_STICK_Y: "camera up / down",
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROLE_PROMPTS: Dictionary = {
|
||||||
|
ROLE_A: "press the a button",
|
||||||
|
ROLE_B: "press the b button",
|
||||||
|
ROLE_X: "press the x button",
|
||||||
|
ROLE_Y: "press the y button",
|
||||||
|
ROLE_LB: "press the left bumper",
|
||||||
|
ROLE_RB: "press the right bumper",
|
||||||
|
ROLE_LT: "squeeze the left trigger",
|
||||||
|
ROLE_RT: "squeeze the right trigger",
|
||||||
|
ROLE_SELECT: "press select / back",
|
||||||
|
ROLE_START: "press start",
|
||||||
|
ROLE_LEFT_STICK_CLICK: "click the left stick",
|
||||||
|
ROLE_RIGHT_STICK_CLICK: "click the right stick",
|
||||||
|
ROLE_DPAD_UP: "press d-pad up",
|
||||||
|
ROLE_DPAD_DOWN: "press d-pad down",
|
||||||
|
ROLE_DPAD_LEFT: "press d-pad left",
|
||||||
|
ROLE_DPAD_RIGHT: "press d-pad right",
|
||||||
|
ROLE_LEFT_STICK_X: "move the left stick left",
|
||||||
|
ROLE_LEFT_STICK_Y: "move the left stick up",
|
||||||
|
ROLE_RIGHT_STICK_X: "move the right stick left",
|
||||||
|
ROLE_RIGHT_STICK_Y: "move the right stick up",
|
||||||
|
}
|
||||||
|
|
||||||
|
const STICK_AXIS_ROLES: Array[StringName] = [
|
||||||
|
ROLE_LEFT_STICK_X,
|
||||||
|
ROLE_LEFT_STICK_Y,
|
||||||
|
ROLE_RIGHT_STICK_X,
|
||||||
|
ROLE_RIGHT_STICK_Y,
|
||||||
|
]
|
||||||
|
const TRIGGER_ROLES: Array[StringName] = [ROLE_LT, ROLE_RT]
|
||||||
|
|
||||||
|
const BUTTON_ACTION_ROLES: Dictionary = {
|
||||||
|
&"jump": ROLE_A,
|
||||||
|
&"ui_accept": ROLE_A,
|
||||||
|
&"ui_cancel": ROLE_B,
|
||||||
|
&"open_backpack": ROLE_X,
|
||||||
|
&"interact": ROLE_Y,
|
||||||
|
&"focus_gameplay": ROLE_LB,
|
||||||
|
&"fish_primary": ROLE_RB,
|
||||||
|
&"open_chat": ROLE_SELECT,
|
||||||
|
&"open_system_menu": ROLE_START,
|
||||||
|
&"sprint": ROLE_LEFT_STICK_CLICK,
|
||||||
|
&"open_emotes": ROLE_DPAD_UP,
|
||||||
|
&"open_quick_actions": ROLE_DPAD_DOWN,
|
||||||
|
&"hotbar_previous": ROLE_DPAD_LEFT,
|
||||||
|
&"hotbar_next": ROLE_DPAD_RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AXIS_ACTION_ROLES: Dictionary = {
|
||||||
|
&"move_left": [ROLE_LEFT_STICK_X, -1.0],
|
||||||
|
&"move_right": [ROLE_LEFT_STICK_X, 1.0],
|
||||||
|
&"move_forward": [ROLE_LEFT_STICK_Y, -1.0],
|
||||||
|
&"move_backward": [ROLE_LEFT_STICK_Y, 1.0],
|
||||||
|
&"ui_left": [ROLE_LEFT_STICK_X, -1.0],
|
||||||
|
&"ui_right": [ROLE_LEFT_STICK_X, 1.0],
|
||||||
|
&"ui_up": [ROLE_LEFT_STICK_Y, -1.0],
|
||||||
|
&"ui_down": [ROLE_LEFT_STICK_Y, 1.0],
|
||||||
|
}
|
||||||
|
|
||||||
|
const UI_DPAD_ACTION_ROLES: Dictionary = {
|
||||||
|
&"ui_up": ROLE_DPAD_UP,
|
||||||
|
&"ui_down": ROLE_DPAD_DOWN,
|
||||||
|
&"ui_left": ROLE_DPAD_LEFT,
|
||||||
|
&"ui_right": ROLE_DPAD_RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _profiles: Dictionary = {}
|
||||||
|
var _active_device_id: int = 0
|
||||||
|
var _active_profile_key: String = "default"
|
||||||
|
var _active_controller_name: String = "controller"
|
||||||
|
var _default_joy_events: Dictionary = {}
|
||||||
|
var _axis_rest_by_device: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_capture_project_defaults()
|
||||||
|
load_profiles()
|
||||||
|
_refresh_active_controller()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
var device_id: int = -1
|
||||||
|
var button_event := event as InputEventJoypadButton
|
||||||
|
if button_event != null and button_event.pressed:
|
||||||
|
device_id = button_event.device
|
||||||
|
var motion_event := event as InputEventJoypadMotion
|
||||||
|
if (
|
||||||
|
motion_event != null
|
||||||
|
and absf(motion_event.axis_value) >= CAPTURE_AXIS_THRESHOLD
|
||||||
|
):
|
||||||
|
device_id = motion_event.device
|
||||||
|
if device_id >= 0 and device_id != _active_device_id:
|
||||||
|
_set_active_controller(device_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
var connected: Array[int] = Input.get_connected_joypads()
|
||||||
|
if connected.is_empty():
|
||||||
|
connected.append(0)
|
||||||
|
if not connected.has(_active_device_id):
|
||||||
|
_set_active_controller(connected[0])
|
||||||
|
elif (
|
||||||
|
_active_controller_name == "controller"
|
||||||
|
and Input.get_connected_joypads().has(_active_device_id)
|
||||||
|
):
|
||||||
|
_set_active_controller(_active_device_id)
|
||||||
|
_sample_axis_rest_values(_active_device_id)
|
||||||
|
|
||||||
|
|
||||||
|
func load_profiles() -> bool:
|
||||||
|
_recover_interrupted_write()
|
||||||
|
if not FileAccess.file_exists(PROFILE_PATH):
|
||||||
|
_profiles = {}
|
||||||
|
return true
|
||||||
|
var file := FileAccess.open(PROFILE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return false
|
||||||
|
if file.get_length() > MAX_PROFILE_BYTES:
|
||||||
|
file.close()
|
||||||
|
push_warning("Controller mappings are too large; using defaults.")
|
||||||
|
_profiles = {}
|
||||||
|
return false
|
||||||
|
var json := JSON.new()
|
||||||
|
var error: Error = json.parse(file.get_as_text())
|
||||||
|
file.close()
|
||||||
|
if error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||||
|
push_warning("Controller mappings are malformed; using defaults.")
|
||||||
|
_profiles = {}
|
||||||
|
return false
|
||||||
|
var data: Dictionary = json.data
|
||||||
|
if int(data.get("format_version", -1)) != FORMAT_VERSION:
|
||||||
|
push_warning("Controller mapping version is unsupported; using defaults.")
|
||||||
|
_profiles = {}
|
||||||
|
return false
|
||||||
|
var raw_profiles: Variant = data.get("profiles", {})
|
||||||
|
if typeof(raw_profiles) != TYPE_DICTIONARY:
|
||||||
|
_profiles = {}
|
||||||
|
return false
|
||||||
|
_profiles = _validated_profiles(raw_profiles as Dictionary)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _validated_profiles(raw_profiles: Dictionary) -> Dictionary:
|
||||||
|
var validated: Dictionary = {}
|
||||||
|
for key_value: Variant in raw_profiles:
|
||||||
|
var raw_profile: Variant = raw_profiles[key_value]
|
||||||
|
if typeof(raw_profile) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var profile := raw_profile as Dictionary
|
||||||
|
var raw_bindings: Variant = profile.get("bindings", {})
|
||||||
|
if typeof(raw_bindings) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var bindings := raw_bindings as Dictionary
|
||||||
|
var complete: bool = true
|
||||||
|
for role: StringName in ROLE_ORDER:
|
||||||
|
var raw_binding: Variant = bindings.get(str(role), {})
|
||||||
|
if (
|
||||||
|
typeof(raw_binding) != TYPE_DICTIONARY
|
||||||
|
or not validate_binding(role, raw_binding as Dictionary)
|
||||||
|
):
|
||||||
|
complete = false
|
||||||
|
break
|
||||||
|
if not complete:
|
||||||
|
continue
|
||||||
|
validated[str(key_value)] = {
|
||||||
|
"controller_name": str(
|
||||||
|
profile.get("controller_name", "controller")
|
||||||
|
),
|
||||||
|
"bindings": bindings.duplicate(true),
|
||||||
|
}
|
||||||
|
return validated
|
||||||
|
|
||||||
|
|
||||||
|
func has_custom_mapping() -> bool:
|
||||||
|
return _profiles.has(_active_profile_key)
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_device_id() -> int:
|
||||||
|
return _active_device_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_controller_name() -> String:
|
||||||
|
return _active_controller_name
|
||||||
|
|
||||||
|
|
||||||
|
func get_role_label(role: StringName) -> String:
|
||||||
|
return str(ROLE_LABELS.get(role, str(role)))
|
||||||
|
|
||||||
|
|
||||||
|
func get_role_prompt(role: StringName) -> String:
|
||||||
|
return str(ROLE_PROMPTS.get(role, "press or move the requested input"))
|
||||||
|
|
||||||
|
|
||||||
|
func role_expects_axis(role: StringName) -> bool:
|
||||||
|
return role in STICK_AXIS_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
func role_accepts_axis(role: StringName) -> bool:
|
||||||
|
return role in STICK_AXIS_ROLES or role in TRIGGER_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
func get_active_bindings() -> Dictionary:
|
||||||
|
var raw_profile: Variant = _profiles.get(_active_profile_key, {})
|
||||||
|
var profile: Dictionary = (
|
||||||
|
raw_profile as Dictionary
|
||||||
|
if typeof(raw_profile) == TYPE_DICTIONARY else {}
|
||||||
|
)
|
||||||
|
var bindings: Variant = profile.get("bindings", {})
|
||||||
|
if (
|
||||||
|
typeof(bindings) == TYPE_DICTIONARY
|
||||||
|
and not (bindings as Dictionary).is_empty()
|
||||||
|
):
|
||||||
|
return (bindings as Dictionary).duplicate(true)
|
||||||
|
return default_bindings()
|
||||||
|
|
||||||
|
|
||||||
|
func get_binding(role: StringName) -> Dictionary:
|
||||||
|
var bindings: Dictionary = get_active_bindings()
|
||||||
|
var binding: Variant = bindings.get(str(role), {})
|
||||||
|
if typeof(binding) == TYPE_DICTIONARY:
|
||||||
|
return (binding as Dictionary).duplicate(true)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func set_binding(role: StringName, binding: Dictionary) -> bool:
|
||||||
|
if role not in ROLE_ORDER or not validate_binding(role, binding):
|
||||||
|
return false
|
||||||
|
var bindings: Dictionary = get_active_bindings()
|
||||||
|
bindings[str(role)] = binding.duplicate(true)
|
||||||
|
return replace_active_bindings(bindings)
|
||||||
|
|
||||||
|
|
||||||
|
func replace_active_bindings(bindings: Dictionary) -> bool:
|
||||||
|
for role: StringName in ROLE_ORDER:
|
||||||
|
var binding: Variant = bindings.get(str(role), {})
|
||||||
|
if typeof(binding) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
if not validate_binding(role, binding as Dictionary):
|
||||||
|
return false
|
||||||
|
var previous: Dictionary = _profiles.duplicate(true)
|
||||||
|
_profiles[_active_profile_key] = {
|
||||||
|
"controller_name": _active_controller_name,
|
||||||
|
"bindings": bindings.duplicate(true),
|
||||||
|
}
|
||||||
|
if not _save_profiles():
|
||||||
|
_profiles = previous
|
||||||
|
return false
|
||||||
|
_apply_active_profile()
|
||||||
|
active_profile_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func reset_active_profile() -> bool:
|
||||||
|
if not _profiles.has(_active_profile_key):
|
||||||
|
_restore_project_defaults()
|
||||||
|
active_profile_changed.emit()
|
||||||
|
return true
|
||||||
|
var previous: Dictionary = _profiles.duplicate(true)
|
||||||
|
_profiles.erase(_active_profile_key)
|
||||||
|
if not _save_profiles():
|
||||||
|
_profiles = previous
|
||||||
|
return false
|
||||||
|
_restore_project_defaults()
|
||||||
|
active_profile_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func binding_from_event(role: StringName, event: InputEvent) -> Dictionary:
|
||||||
|
var button := event as InputEventJoypadButton
|
||||||
|
if role in TRIGGER_ROLES and button != null and button.pressed:
|
||||||
|
return {
|
||||||
|
"kind": "button",
|
||||||
|
"button": int(button.button_index),
|
||||||
|
}
|
||||||
|
if role_accepts_axis(role):
|
||||||
|
var motion := event as InputEventJoypadMotion
|
||||||
|
if motion == null:
|
||||||
|
return {}
|
||||||
|
_sample_axis_rest_values(motion.device)
|
||||||
|
var rest: float = _axis_rest_value(motion.device, motion.axis)
|
||||||
|
var travel: float = motion.axis_value - rest
|
||||||
|
if absf(travel) < CAPTURE_AXIS_THRESHOLD:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"kind": "axis",
|
||||||
|
"axis": int(motion.axis),
|
||||||
|
"direction": signf(travel),
|
||||||
|
"rest": rest,
|
||||||
|
}
|
||||||
|
if button == null or not button.pressed:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"kind": "button",
|
||||||
|
"button": int(button.button_index),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func validate_binding(role: StringName, binding: Dictionary) -> bool:
|
||||||
|
var binding_kind: String = str(binding.get("kind", ""))
|
||||||
|
if role_expects_axis(role) and binding_kind != "axis":
|
||||||
|
return false
|
||||||
|
if role not in STICK_AXIS_ROLES and role not in TRIGGER_ROLES:
|
||||||
|
if binding_kind != "button":
|
||||||
|
return false
|
||||||
|
if role in TRIGGER_ROLES and binding_kind not in ["axis", "button"]:
|
||||||
|
return false
|
||||||
|
if binding_kind == "button":
|
||||||
|
var button_index: int = int(binding.get("button", -1))
|
||||||
|
return button_index >= 0 and button_index < JOY_BUTTON_MAX
|
||||||
|
var axis_index: int = int(binding.get("axis", -1))
|
||||||
|
var direction: float = float(binding.get("direction", 0.0))
|
||||||
|
var rest: float = float(binding.get("rest", 0.0))
|
||||||
|
return (
|
||||||
|
axis_index >= 0
|
||||||
|
and axis_index < JOY_AXIS_MAX
|
||||||
|
and not is_zero_approx(direction)
|
||||||
|
and rest >= -1.0
|
||||||
|
and rest <= 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func binding_label(binding: Dictionary) -> String:
|
||||||
|
if str(binding.get("kind", "")) == "button":
|
||||||
|
return "button %d" % int(binding.get("button", -1))
|
||||||
|
if str(binding.get("kind", "")) == "axis":
|
||||||
|
return "axis %d %s" % [
|
||||||
|
int(binding.get("axis", -1)),
|
||||||
|
"+" if float(binding.get("direction", 0.0)) > 0.0 else "−",
|
||||||
|
]
|
||||||
|
return "unmapped"
|
||||||
|
|
||||||
|
|
||||||
|
func get_role_strength(role: StringName) -> float:
|
||||||
|
if not has_custom_mapping():
|
||||||
|
return 0.0
|
||||||
|
var binding: Dictionary = get_binding(role)
|
||||||
|
if str(binding.get("kind", "")) == "button":
|
||||||
|
return (
|
||||||
|
1.0
|
||||||
|
if Input.is_joy_button_pressed(
|
||||||
|
_active_device_id,
|
||||||
|
int(binding.get("button", -1)),
|
||||||
|
)
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
return _axis_binding_strength(binding)
|
||||||
|
|
||||||
|
|
||||||
|
func get_role_axis(role: StringName) -> float:
|
||||||
|
if not has_custom_mapping():
|
||||||
|
return 0.0
|
||||||
|
var binding: Dictionary = get_binding(role)
|
||||||
|
if str(binding.get("kind", "")) != "axis":
|
||||||
|
return 0.0
|
||||||
|
var raw: float = Input.get_joy_axis(
|
||||||
|
_active_device_id,
|
||||||
|
int(binding.get("axis", -1)),
|
||||||
|
)
|
||||||
|
var direction: float = signf(float(binding.get("direction", -1.0)))
|
||||||
|
var rest: float = float(binding.get("rest", 0.0))
|
||||||
|
var delta: float = raw - rest
|
||||||
|
if is_zero_approx(delta):
|
||||||
|
return 0.0
|
||||||
|
var toward_captured_direction: bool = signf(delta) == direction
|
||||||
|
var endpoint: float = direction if toward_captured_direction else -direction
|
||||||
|
var available: float = absf(endpoint - rest)
|
||||||
|
if available <= 0.001:
|
||||||
|
return 0.0
|
||||||
|
var magnitude: float = clampf(absf(delta) / available, 0.0, 1.0)
|
||||||
|
return -magnitude if toward_captured_direction else magnitude
|
||||||
|
|
||||||
|
|
||||||
|
func event_matches_role(event: InputEvent, role: StringName) -> bool:
|
||||||
|
if not has_custom_mapping():
|
||||||
|
return false
|
||||||
|
var binding: Dictionary = get_binding(role)
|
||||||
|
var button := event as InputEventJoypadButton
|
||||||
|
if button != null and str(binding.get("kind", "")) == "button":
|
||||||
|
return button.button_index == int(binding.get("button", -1))
|
||||||
|
var motion := event as InputEventJoypadMotion
|
||||||
|
if motion == null or str(binding.get("kind", "")) != "axis":
|
||||||
|
return false
|
||||||
|
if motion.axis != int(binding.get("axis", -1)):
|
||||||
|
return false
|
||||||
|
return _axis_binding_strength(binding, motion.axis_value) >= 0.5
|
||||||
|
|
||||||
|
|
||||||
|
func event_uses_role(event: InputEvent, role: StringName) -> bool:
|
||||||
|
if not has_custom_mapping():
|
||||||
|
return false
|
||||||
|
var binding: Dictionary = get_binding(role)
|
||||||
|
var button := event as InputEventJoypadButton
|
||||||
|
if button != null and str(binding.get("kind", "")) == "button":
|
||||||
|
return button.button_index == int(binding.get("button", -1))
|
||||||
|
var motion := event as InputEventJoypadMotion
|
||||||
|
return (
|
||||||
|
motion != null
|
||||||
|
and str(binding.get("kind", "")) == "axis"
|
||||||
|
and motion.axis == int(binding.get("axis", -1))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func default_bindings() -> Dictionary:
|
||||||
|
return {
|
||||||
|
str(ROLE_A): _button_binding(JOY_BUTTON_A),
|
||||||
|
str(ROLE_B): _button_binding(JOY_BUTTON_B),
|
||||||
|
str(ROLE_X): _button_binding(JOY_BUTTON_X),
|
||||||
|
str(ROLE_Y): _button_binding(JOY_BUTTON_Y),
|
||||||
|
str(ROLE_LB): _button_binding(JOY_BUTTON_LEFT_SHOULDER),
|
||||||
|
str(ROLE_RB): _button_binding(JOY_BUTTON_RIGHT_SHOULDER),
|
||||||
|
str(ROLE_LT): _axis_binding(JOY_AXIS_TRIGGER_RIGHT, 1.0, 0.0),
|
||||||
|
str(ROLE_RT): _axis_binding(JOY_AXIS_TRIGGER_LEFT, 1.0, 0.0),
|
||||||
|
str(ROLE_SELECT): _button_binding(JOY_BUTTON_BACK),
|
||||||
|
str(ROLE_START): _button_binding(JOY_BUTTON_START),
|
||||||
|
str(ROLE_LEFT_STICK_CLICK): _button_binding(JOY_BUTTON_LEFT_STICK),
|
||||||
|
str(ROLE_RIGHT_STICK_CLICK): _button_binding(JOY_BUTTON_RIGHT_STICK),
|
||||||
|
str(ROLE_DPAD_UP): _button_binding(JOY_BUTTON_DPAD_UP),
|
||||||
|
str(ROLE_DPAD_DOWN): _button_binding(JOY_BUTTON_DPAD_DOWN),
|
||||||
|
str(ROLE_DPAD_LEFT): _button_binding(JOY_BUTTON_DPAD_LEFT),
|
||||||
|
str(ROLE_DPAD_RIGHT): _button_binding(JOY_BUTTON_DPAD_RIGHT),
|
||||||
|
str(ROLE_LEFT_STICK_X): _axis_binding(JOY_AXIS_LEFT_X, -1.0, 0.0),
|
||||||
|
str(ROLE_LEFT_STICK_Y): _axis_binding(JOY_AXIS_LEFT_Y, -1.0, 0.0),
|
||||||
|
str(ROLE_RIGHT_STICK_X): _axis_binding(JOY_AXIS_RIGHT_X, -1.0, 0.0),
|
||||||
|
str(ROLE_RIGHT_STICK_Y): _axis_binding(JOY_AXIS_RIGHT_Y, -1.0, 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func _button_binding(button: JoyButton) -> Dictionary:
|
||||||
|
return {"kind": "button", "button": int(button)}
|
||||||
|
|
||||||
|
|
||||||
|
static func _axis_binding(
|
||||||
|
axis: JoyAxis,
|
||||||
|
direction: float,
|
||||||
|
rest: float,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"kind": "axis",
|
||||||
|
"axis": int(axis),
|
||||||
|
"direction": direction,
|
||||||
|
"rest": rest,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _axis_binding_strength(
|
||||||
|
binding: Dictionary,
|
||||||
|
raw_override: float = INF,
|
||||||
|
) -> float:
|
||||||
|
var axis: int = int(binding.get("axis", -1))
|
||||||
|
if axis < 0:
|
||||||
|
return 0.0
|
||||||
|
var raw: float = (
|
||||||
|
Input.get_joy_axis(_active_device_id, axis)
|
||||||
|
if is_inf(raw_override) else raw_override
|
||||||
|
)
|
||||||
|
var rest: float = float(binding.get("rest", 0.0))
|
||||||
|
var direction: float = signf(float(binding.get("direction", 0.0)))
|
||||||
|
var endpoint: float = direction
|
||||||
|
var available: float = absf(endpoint - rest)
|
||||||
|
if available <= 0.001:
|
||||||
|
return 0.0
|
||||||
|
return clampf((raw - rest) * direction / available, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_active_controller() -> void:
|
||||||
|
var connected: Array[int] = Input.get_connected_joypads()
|
||||||
|
_set_active_controller(connected[0] if not connected.is_empty() else 0)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_active_controller(device_id: int) -> void:
|
||||||
|
var previous_profile_key: String = _active_profile_key
|
||||||
|
_active_device_id = maxi(device_id, 0)
|
||||||
|
var controller_is_listed: bool = Input.get_connected_joypads().has(
|
||||||
|
_active_device_id
|
||||||
|
)
|
||||||
|
var guid: String = (
|
||||||
|
Input.get_joy_guid(_active_device_id).strip_edges()
|
||||||
|
if controller_is_listed else ""
|
||||||
|
)
|
||||||
|
var controller_name: String = (
|
||||||
|
Input.get_joy_name(_active_device_id).strip_edges()
|
||||||
|
if controller_is_listed else "controller"
|
||||||
|
)
|
||||||
|
if controller_name.is_empty():
|
||||||
|
controller_name = "controller"
|
||||||
|
_active_controller_name = controller_name
|
||||||
|
_active_profile_key = (
|
||||||
|
guid
|
||||||
|
if not guid.is_empty()
|
||||||
|
else "name:" + controller_name
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
previous_profile_key == "name:controller"
|
||||||
|
and _active_profile_key != previous_profile_key
|
||||||
|
and _profiles.has(previous_profile_key)
|
||||||
|
and not _profiles.has(_active_profile_key)
|
||||||
|
):
|
||||||
|
var previous_profile: Variant = _profiles[previous_profile_key]
|
||||||
|
if typeof(previous_profile) == TYPE_DICTIONARY:
|
||||||
|
_profiles[_active_profile_key] = (
|
||||||
|
previous_profile as Dictionary
|
||||||
|
).duplicate(true)
|
||||||
|
_profiles.erase(previous_profile_key)
|
||||||
|
_save_profiles()
|
||||||
|
_sample_axis_rest_values(_active_device_id)
|
||||||
|
_apply_active_profile()
|
||||||
|
active_controller_changed.emit(_active_controller_name)
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_axis_rest_values(device_id: int) -> void:
|
||||||
|
var key: String = str(device_id)
|
||||||
|
if _axis_rest_by_device.has(key):
|
||||||
|
return
|
||||||
|
var values: Dictionary = {}
|
||||||
|
for axis: int in JOY_AXIS_MAX:
|
||||||
|
values[str(axis)] = Input.get_joy_axis(device_id, axis)
|
||||||
|
_axis_rest_by_device[key] = values
|
||||||
|
|
||||||
|
|
||||||
|
func _axis_rest_value(device_id: int, axis: int) -> float:
|
||||||
|
_sample_axis_rest_values(device_id)
|
||||||
|
var values: Dictionary = _axis_rest_by_device.get(str(device_id), {})
|
||||||
|
return float(values.get(str(axis), 0.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _capture_project_defaults() -> void:
|
||||||
|
for action: StringName in _all_managed_actions():
|
||||||
|
var joy_events: Array[InputEvent] = []
|
||||||
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||||
|
joy_events.append(event.duplicate())
|
||||||
|
_default_joy_events[action] = joy_events
|
||||||
|
|
||||||
|
|
||||||
|
func _all_managed_actions() -> Array[StringName]:
|
||||||
|
var result: Array[StringName] = []
|
||||||
|
for source: Dictionary in [
|
||||||
|
BUTTON_ACTION_ROLES,
|
||||||
|
AXIS_ACTION_ROLES,
|
||||||
|
UI_DPAD_ACTION_ROLES,
|
||||||
|
]:
|
||||||
|
for action_value: Variant in source.keys():
|
||||||
|
var action: StringName = StringName(action_value)
|
||||||
|
if action not in result:
|
||||||
|
result.append(action)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_managed_joy_events() -> void:
|
||||||
|
for action: StringName in _all_managed_actions():
|
||||||
|
if not InputMap.has_action(action):
|
||||||
|
continue
|
||||||
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||||
|
InputMap.action_erase_event(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
func _restore_project_defaults() -> void:
|
||||||
|
_remove_managed_joy_events()
|
||||||
|
for action_value: Variant in _default_joy_events:
|
||||||
|
var action: StringName = StringName(action_value)
|
||||||
|
for event: InputEvent in _default_joy_events[action]:
|
||||||
|
InputMap.action_add_event(action, event.duplicate())
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_active_profile() -> void:
|
||||||
|
if not has_custom_mapping():
|
||||||
|
_restore_project_defaults()
|
||||||
|
return
|
||||||
|
_remove_managed_joy_events()
|
||||||
|
var bindings: Dictionary = get_active_bindings()
|
||||||
|
for action_value: Variant in BUTTON_ACTION_ROLES:
|
||||||
|
var action: StringName = StringName(action_value)
|
||||||
|
_add_button_action_binding(
|
||||||
|
action,
|
||||||
|
bindings.get(str(BUTTON_ACTION_ROLES[action]), {}),
|
||||||
|
)
|
||||||
|
for action_value: Variant in AXIS_ACTION_ROLES:
|
||||||
|
var action: StringName = StringName(action_value)
|
||||||
|
var role_data: Array = AXIS_ACTION_ROLES[action]
|
||||||
|
_add_axis_action_binding(
|
||||||
|
action,
|
||||||
|
bindings.get(str(role_data[0]), {}),
|
||||||
|
float(role_data[1]),
|
||||||
|
)
|
||||||
|
for action_value: Variant in UI_DPAD_ACTION_ROLES:
|
||||||
|
var action: StringName = StringName(action_value)
|
||||||
|
_add_button_action_binding(
|
||||||
|
action,
|
||||||
|
bindings.get(str(UI_DPAD_ACTION_ROLES[action]), {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_button_action_binding(action: StringName, value: Variant) -> void:
|
||||||
|
if not InputMap.has_action(action) or typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var binding: Dictionary = value as Dictionary
|
||||||
|
if str(binding.get("kind", "")) != "button":
|
||||||
|
return
|
||||||
|
var event := InputEventJoypadButton.new()
|
||||||
|
event.device = -1
|
||||||
|
event.button_index = int(binding.get("button", -1))
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_axis_action_binding(
|
||||||
|
action: StringName,
|
||||||
|
value: Variant,
|
||||||
|
logical_direction: float,
|
||||||
|
) -> void:
|
||||||
|
if not InputMap.has_action(action) or typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var binding: Dictionary = value as Dictionary
|
||||||
|
if str(binding.get("kind", "")) != "axis":
|
||||||
|
return
|
||||||
|
var captured_negative_direction: float = signf(
|
||||||
|
float(binding.get("direction", -1.0))
|
||||||
|
)
|
||||||
|
var event := InputEventJoypadMotion.new()
|
||||||
|
event.device = -1
|
||||||
|
event.axis = int(binding.get("axis", -1))
|
||||||
|
event.axis_value = logical_direction * -captured_negative_direction
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
|
||||||
|
func _save_profiles() -> bool:
|
||||||
|
var file := FileAccess.open(PROFILE_TEMP_PATH, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
return false
|
||||||
|
file.store_string(JSON.stringify({
|
||||||
|
"format_version": FORMAT_VERSION,
|
||||||
|
"profiles": _profiles,
|
||||||
|
}, "\t"))
|
||||||
|
file.flush()
|
||||||
|
var write_error: Error = file.get_error()
|
||||||
|
file.close()
|
||||||
|
if write_error != OK:
|
||||||
|
_remove_if_present(PROFILE_TEMP_PATH)
|
||||||
|
return false
|
||||||
|
_remove_if_present(PROFILE_BACKUP_PATH)
|
||||||
|
var had_primary: bool = FileAccess.file_exists(PROFILE_PATH)
|
||||||
|
if had_primary and not _rename_file(PROFILE_PATH, PROFILE_BACKUP_PATH):
|
||||||
|
_remove_if_present(PROFILE_TEMP_PATH)
|
||||||
|
return false
|
||||||
|
if not _rename_file(PROFILE_TEMP_PATH, PROFILE_PATH):
|
||||||
|
if had_primary:
|
||||||
|
_rename_file(PROFILE_BACKUP_PATH, PROFILE_PATH)
|
||||||
|
return false
|
||||||
|
_remove_if_present(PROFILE_BACKUP_PATH)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _recover_interrupted_write() -> void:
|
||||||
|
if FileAccess.file_exists(PROFILE_PATH):
|
||||||
|
_remove_if_present(PROFILE_TEMP_PATH)
|
||||||
|
_remove_if_present(PROFILE_BACKUP_PATH)
|
||||||
|
return
|
||||||
|
if FileAccess.file_exists(PROFILE_BACKUP_PATH):
|
||||||
|
_rename_file(PROFILE_BACKUP_PATH, PROFILE_PATH)
|
||||||
|
_remove_if_present(PROFILE_TEMP_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func _rename_file(from_path: String, to_path: String) -> bool:
|
||||||
|
return DirAccess.rename_absolute(
|
||||||
|
ProjectSettings.globalize_path(from_path),
|
||||||
|
ProjectSettings.globalize_path(to_path),
|
||||||
|
) == OK
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_if_present(path: String) -> void:
|
||||||
|
if FileAccess.file_exists(path):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||||
1
settings/controller_mapping_manager.gd.uid
Normal file
1
settings/controller_mapping_manager.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c80x8jkdnx0xy
|
||||||
|
|
@ -30,6 +30,7 @@ const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
|
||||||
@export var chat_draft: String = ""
|
@export var chat_draft: String = ""
|
||||||
@export var chat_collapsed: bool = false
|
@export var chat_collapsed: bool = false
|
||||||
@export var chat_dock_right: bool = false
|
@export var chat_dock_right: bool = false
|
||||||
|
@export var chat_mobile_mode: bool = false
|
||||||
@export var paint_dock_right: bool = true
|
@export var paint_dock_right: bool = true
|
||||||
@export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE
|
@export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE
|
||||||
@export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
|
@export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
|
||||||
|
|
@ -65,6 +66,7 @@ func copy() -> PlayerSettings:
|
||||||
result.chat_draft = chat_draft
|
result.chat_draft = chat_draft
|
||||||
result.chat_collapsed = chat_collapsed
|
result.chat_collapsed = chat_collapsed
|
||||||
result.chat_dock_right = chat_dock_right
|
result.chat_dock_right = chat_dock_right
|
||||||
|
result.chat_mobile_mode = chat_mobile_mode
|
||||||
result.paint_dock_right = paint_dock_right
|
result.paint_dock_right = paint_dock_right
|
||||||
result.world_pixel_size = world_pixel_size
|
result.world_pixel_size = world_pixel_size
|
||||||
result.ui_pixel_size = ui_pixel_size
|
result.ui_pixel_size = ui_pixel_size
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,10 @@ func load_settings() -> bool:
|
||||||
presentation.has("chat_dock_right")
|
presentation.has("chat_dock_right")
|
||||||
and typeof(presentation["chat_dock_right"]) != TYPE_BOOL
|
and typeof(presentation["chat_dock_right"]) != TYPE_BOOL
|
||||||
)
|
)
|
||||||
|
or (
|
||||||
|
presentation.has("chat_mobile_mode")
|
||||||
|
and typeof(presentation["chat_mobile_mode"]) != TYPE_BOOL
|
||||||
|
)
|
||||||
or (
|
or (
|
||||||
presentation.has("paint_dock_right")
|
presentation.has("paint_dock_right")
|
||||||
and typeof(presentation["paint_dock_right"]) != TYPE_BOOL
|
and typeof(presentation["paint_dock_right"]) != TYPE_BOOL
|
||||||
|
|
@ -107,6 +111,9 @@ func load_settings() -> bool:
|
||||||
)
|
)
|
||||||
loaded.chat_collapsed = bool(presentation.get("chat_collapsed", false))
|
loaded.chat_collapsed = bool(presentation.get("chat_collapsed", false))
|
||||||
loaded.chat_dock_right = bool(presentation.get("chat_dock_right", false))
|
loaded.chat_dock_right = bool(presentation.get("chat_dock_right", false))
|
||||||
|
loaded.chat_mobile_mode = bool(
|
||||||
|
presentation.get("chat_mobile_mode", false)
|
||||||
|
)
|
||||||
loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true))
|
loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true))
|
||||||
if not loaded.is_valid():
|
if not loaded.is_valid():
|
||||||
return _use_defaults_after_corruption("Player settings values are invalid.")
|
return _use_defaults_after_corruption("Player settings values are invalid.")
|
||||||
|
|
@ -181,6 +188,7 @@ func save_now() -> bool:
|
||||||
"chat_draft": current_settings.chat_draft,
|
"chat_draft": current_settings.chat_draft,
|
||||||
"chat_collapsed": current_settings.chat_collapsed,
|
"chat_collapsed": current_settings.chat_collapsed,
|
||||||
"chat_dock_right": current_settings.chat_dock_right,
|
"chat_dock_right": current_settings.chat_dock_right,
|
||||||
|
"chat_mobile_mode": current_settings.chat_mobile_mode,
|
||||||
"paint_dock_right": current_settings.paint_dock_right,
|
"paint_dock_right": current_settings.paint_dock_right,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
|
const GameUIType = preload("res://ui/game_ui.gd")
|
||||||
|
const PlayerType = preload("res://player/player.gd")
|
||||||
|
|
||||||
|
|
||||||
func _init() -> void:
|
func _init() -> void:
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -34,11 +37,50 @@ func _init() -> void:
|
||||||
))
|
))
|
||||||
assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT))
|
assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT))
|
||||||
assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT))
|
assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT))
|
||||||
assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_BACK))
|
assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_X))
|
||||||
assert(_has_joypad_button(&"open_system_menu", JOY_BUTTON_START))
|
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_emotes", JOY_BUTTON_DPAD_UP))
|
||||||
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
|
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
|
||||||
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_LEFT_SHOULDER))
|
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_BACK))
|
||||||
|
assert(_has_joypad_button(&"focus_gameplay", JOY_BUTTON_LEFT_SHOULDER))
|
||||||
|
assert(
|
||||||
|
GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS == JOY_AXIS_TRIGGER_RIGHT
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
GameUIType.VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS
|
||||||
|
== JOY_AXIS_TRIGGER_LEFT
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
GameUIType.VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS
|
||||||
|
== JOY_AXIS_TRIGGER_LEFT
|
||||||
|
)
|
||||||
|
assert(PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS == JOY_AXIS_TRIGGER_LEFT)
|
||||||
|
assert(is_zero_approx(
|
||||||
|
GameUIType.normalized_trigger_strength(-1.0, -1.0)
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
GameUIType.normalized_trigger_strength(1.0, -1.0),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
GameUIType.normalized_trigger_strength(-1.0, 0.0),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
GameUIType.normalized_trigger_strength(-1.0, 1.0),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
GameUIType.directional_trigger_strength(-1.0, 0.0, -1.0),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
|
assert(is_zero_approx(
|
||||||
|
GameUIType.directional_trigger_strength(1.0, 0.0, -1.0)
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
GameUIType.directional_trigger_strength(1.0, 0.0, 1.0),
|
||||||
|
1.0,
|
||||||
|
))
|
||||||
print("android readiness validation passed")
|
print("android readiness validation passed")
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,33 @@ func _run() -> void:
|
||||||
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
||||||
assert(player != null and service != null and toolbar != null)
|
assert(player != null and service != null and toolbar != null)
|
||||||
assert(chat_ui != null)
|
assert(chat_ui != null)
|
||||||
|
assert(bool(game_ui.call("_can_start_virtual_mouse")))
|
||||||
|
game_ui.call("_begin_virtual_mouse", 0)
|
||||||
|
var virtual_cursor := game_ui.get_node(
|
||||||
|
"%ControllerVirtualCursor"
|
||||||
|
) as ControllerVirtualCursor
|
||||||
|
assert(virtual_cursor.visible)
|
||||||
|
assert(not player.is_camera_input_enabled())
|
||||||
|
game_ui.call("_end_virtual_mouse")
|
||||||
|
assert(not virtual_cursor.visible)
|
||||||
|
assert(player.is_camera_input_enabled())
|
||||||
var settings_panel := game_ui.get(
|
var settings_panel := game_ui.get(
|
||||||
"_pause_settings_panel"
|
"_pause_settings_panel"
|
||||||
) as SettingsPanel
|
) as SettingsPanel
|
||||||
var chat_dock_button := settings_panel.get_node("%ChatDock") as BubbleButton
|
var chat_dock_button := settings_panel.get_node("%ChatDock") as BubbleButton
|
||||||
|
var chat_mode_button := settings_panel.get_node("%ChatMode") as BubbleButton
|
||||||
var paint_dock_button := settings_panel.get_node("%PaintDock") as BubbleButton
|
var paint_dock_button := settings_panel.get_node("%PaintDock") as BubbleButton
|
||||||
assert(chat_dock_button.neutral_size.x == chat_dock_button.neutral_size.y)
|
assert(chat_dock_button.neutral_size.x == chat_dock_button.neutral_size.y)
|
||||||
|
assert(chat_mode_button.neutral_size.x == chat_mode_button.neutral_size.y)
|
||||||
assert(paint_dock_button.neutral_size.x == paint_dock_button.neutral_size.y)
|
assert(paint_dock_button.neutral_size.x == paint_dock_button.neutral_size.y)
|
||||||
assert(
|
assert(
|
||||||
chat_dock_button.compact_minimum_size.x
|
chat_dock_button.compact_minimum_size.x
|
||||||
== chat_dock_button.compact_minimum_size.y
|
== chat_dock_button.compact_minimum_size.y
|
||||||
)
|
)
|
||||||
|
assert(
|
||||||
|
chat_mode_button.compact_minimum_size.x
|
||||||
|
== chat_mode_button.compact_minimum_size.y
|
||||||
|
)
|
||||||
assert(
|
assert(
|
||||||
paint_dock_button.compact_minimum_size.x
|
paint_dock_button.compact_minimum_size.x
|
||||||
== paint_dock_button.compact_minimum_size.y
|
== paint_dock_button.compact_minimum_size.y
|
||||||
|
|
@ -60,24 +76,85 @@ func _run() -> void:
|
||||||
assert(settings_manager != null)
|
assert(settings_manager != null)
|
||||||
var changed_docks: PlayerSettings = settings_manager.current_settings.copy()
|
var changed_docks: PlayerSettings = settings_manager.current_settings.copy()
|
||||||
changed_docks.chat_dock_right = true
|
changed_docks.chat_dock_right = true
|
||||||
|
changed_docks.chat_mobile_mode = true
|
||||||
changed_docks.paint_dock_right = false
|
changed_docks.paint_dock_right = false
|
||||||
assert(settings_manager.apply_settings(changed_docks))
|
assert(settings_manager.apply_settings(changed_docks))
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(chat_ui.is_docked_right())
|
assert(chat_ui.is_docked_right())
|
||||||
|
assert(chat_ui.is_mobile_mode())
|
||||||
assert(not toolbar.is_docked_right())
|
assert(not toolbar.is_docked_right())
|
||||||
|
var chat_panel := chat_ui.get_node("ChatPanel") as PanelContainer
|
||||||
|
var chat_height_button := chat_ui.get_node("ChatHeightButton") as Button
|
||||||
|
assert(is_zero_approx(chat_panel.position.y))
|
||||||
|
assert(is_equal_approx(chat_panel.size.x, ChatUI.MOBILE_COMPACT_WIDTH))
|
||||||
|
assert(not chat_height_button.visible)
|
||||||
var reloaded_settings := PlayerSettingsManager.new()
|
var reloaded_settings := PlayerSettingsManager.new()
|
||||||
root.add_child(reloaded_settings)
|
root.add_child(reloaded_settings)
|
||||||
assert(reloaded_settings.load_settings())
|
assert(reloaded_settings.load_settings())
|
||||||
assert(reloaded_settings.current_settings.chat_dock_right)
|
assert(reloaded_settings.current_settings.chat_dock_right)
|
||||||
|
assert(reloaded_settings.current_settings.chat_mobile_mode)
|
||||||
assert(not reloaded_settings.current_settings.paint_dock_right)
|
assert(not reloaded_settings.current_settings.paint_dock_right)
|
||||||
reloaded_settings.queue_free()
|
reloaded_settings.queue_free()
|
||||||
var default_docks: PlayerSettings = settings_manager.current_settings.copy()
|
var default_docks: PlayerSettings = settings_manager.current_settings.copy()
|
||||||
default_docks.chat_dock_right = false
|
default_docks.chat_dock_right = false
|
||||||
|
default_docks.chat_mobile_mode = false
|
||||||
default_docks.paint_dock_right = true
|
default_docks.paint_dock_right = true
|
||||||
assert(settings_manager.apply_settings(default_docks))
|
assert(settings_manager.apply_settings(default_docks))
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not chat_ui.is_docked_right())
|
assert(not chat_ui.is_docked_right())
|
||||||
|
assert(not chat_ui.is_mobile_mode())
|
||||||
assert(toolbar.is_docked_right())
|
assert(toolbar.is_docked_right())
|
||||||
|
assert(chat_height_button.visible)
|
||||||
|
var select_button := InputEventJoypadButton.new()
|
||||||
|
select_button.button_index = JOY_BUTTON_BACK
|
||||||
|
select_button.pressed = true
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
||||||
|
assert(chat_ui.is_open())
|
||||||
|
assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_STOP)
|
||||||
|
var typed_chat_entry := chat_ui.find_child(
|
||||||
|
"ChatEntry", true, false
|
||||||
|
) as LineEdit
|
||||||
|
assert(typed_chat_entry != null)
|
||||||
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
||||||
|
var accept_button := InputEventJoypadButton.new()
|
||||||
|
accept_button.button_index = JOY_BUTTON_A
|
||||||
|
accept_button.pressed = true
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", accept_button)))
|
||||||
|
assert(typed_chat_entry.virtual_keyboard_enabled)
|
||||||
|
var left_bumper := InputEventJoypadButton.new()
|
||||||
|
left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
|
||||||
|
left_bumper.pressed = true
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
|
||||||
|
assert(not chat_ui.is_open())
|
||||||
|
assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_IGNORE)
|
||||||
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
|
||||||
|
assert(chat_ui.is_open())
|
||||||
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
|
||||||
|
assert(not chat_ui.is_open())
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
||||||
|
assert(not chat_ui.is_open())
|
||||||
|
assert(chat_ui.is_collapsed())
|
||||||
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
||||||
|
assert(chat_ui.is_open())
|
||||||
|
chat_ui.refocus_gameplay()
|
||||||
|
var cancel_button := InputEventJoypadButton.new()
|
||||||
|
cancel_button.button_index = JOY_BUTTON_B
|
||||||
|
cancel_button.pressed = true
|
||||||
|
assert(not bool(main.call("_is_pause_open_request", cancel_button)))
|
||||||
|
var start_button := InputEventJoypadButton.new()
|
||||||
|
start_button.button_index = JOY_BUTTON_START
|
||||||
|
start_button.pressed = true
|
||||||
|
assert(bool(main.call("_is_pause_open_request", start_button)))
|
||||||
|
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||||
|
player_menu.open_section(PlayerMenu.Section.PROFILE)
|
||||||
|
for _frame: int in 12:
|
||||||
|
await process_frame
|
||||||
|
assert(root.gui_get_focus_owner() is not LineEdit)
|
||||||
|
player_menu.close_menu()
|
||||||
|
for _frame: int in 12:
|
||||||
|
await process_frame
|
||||||
assert(not service.can_activate())
|
assert(not service.can_activate())
|
||||||
assert(not service.is_active() and not toolbar.visible)
|
assert(not service.is_active() and not toolbar.visible)
|
||||||
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
|
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
|
||||||
|
|
|
||||||
157
tests/controller_mapping_validation.gd
Normal file
157
tests/controller_mapping_validation.gd
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
const ControllerMappingPanelType = preload(
|
||||||
|
"res://ui/controller_mapping_panel.gd"
|
||||||
|
)
|
||||||
|
const GameUIType = preload("res://ui/game_ui.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := ControllerMappingManagerType.new()
|
||||||
|
root.add_child(manager)
|
||||||
|
await process_frame
|
||||||
|
var failure: String = _validate_manager(manager)
|
||||||
|
if failure.is_empty():
|
||||||
|
failure = _validate_auto_map(manager)
|
||||||
|
if failure.is_empty():
|
||||||
|
failure = _validate_virtual_mouse_bounds()
|
||||||
|
if failure.is_empty():
|
||||||
|
print("controller mapping validation passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
push_error(failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_manager(manager: ControllerMappingManagerType) -> String:
|
||||||
|
var defaults: Dictionary = manager.get_active_bindings()
|
||||||
|
if defaults.size() != ControllerMappingManagerType.ROLE_ORDER.size():
|
||||||
|
return "default mapping covers %d of %d controller roles: %s" % [
|
||||||
|
defaults.size(),
|
||||||
|
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||||
|
str(defaults.keys()),
|
||||||
|
]
|
||||||
|
var trigger_button := InputEventJoypadButton.new()
|
||||||
|
trigger_button.button_index = JOY_BUTTON_MISC1
|
||||||
|
trigger_button.pressed = true
|
||||||
|
var trigger_binding: Dictionary = manager.binding_from_event(
|
||||||
|
ControllerMappingManagerType.ROLE_LT,
|
||||||
|
trigger_button,
|
||||||
|
)
|
||||||
|
if str(trigger_binding.get("kind", "")) != "button":
|
||||||
|
return "trigger role did not accept a button-backed handheld trigger"
|
||||||
|
var trigger_motion := InputEventJoypadMotion.new()
|
||||||
|
trigger_motion.axis = JOY_AXIS_TRIGGER_RIGHT
|
||||||
|
trigger_motion.axis_value = 1.0
|
||||||
|
trigger_binding = manager.binding_from_event(
|
||||||
|
ControllerMappingManagerType.ROLE_LT,
|
||||||
|
trigger_motion,
|
||||||
|
)
|
||||||
|
if str(trigger_binding.get("kind", "")) != "axis":
|
||||||
|
return "trigger role did not accept an axis-backed controller trigger"
|
||||||
|
var stick_button := manager.binding_from_event(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X,
|
||||||
|
trigger_button,
|
||||||
|
)
|
||||||
|
if not stick_button.is_empty():
|
||||||
|
return "stick axis role accepted a button"
|
||||||
|
var keyboard_events_before: int = _keyboard_event_count(&"jump")
|
||||||
|
var custom: Dictionary = defaults.duplicate(true)
|
||||||
|
custom[str(ControllerMappingManagerType.ROLE_A)] = {
|
||||||
|
"kind": "button",
|
||||||
|
"button": int(JOY_BUTTON_X),
|
||||||
|
}
|
||||||
|
custom[str(ControllerMappingManagerType.ROLE_LT)] = trigger_binding
|
||||||
|
if not manager.replace_active_bindings(custom):
|
||||||
|
return "valid custom mapping could not be saved"
|
||||||
|
if not manager.has_custom_mapping():
|
||||||
|
return "saved custom mapping did not become active"
|
||||||
|
if _keyboard_event_count(&"jump") != keyboard_events_before:
|
||||||
|
return "controller remapping changed keyboard bindings"
|
||||||
|
if not FileAccess.file_exists(
|
||||||
|
ControllerMappingManagerType.PROFILE_PATH
|
||||||
|
):
|
||||||
|
return "controller mapping was not stored device-locally"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
||||||
|
var panel := ControllerMappingPanelType.new()
|
||||||
|
root.add_child(panel)
|
||||||
|
panel.setup(manager)
|
||||||
|
panel.open_panel()
|
||||||
|
panel._begin_auto_map()
|
||||||
|
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||||
|
var binding := (
|
||||||
|
ControllerMappingManagerType.default_bindings()[str(role)]
|
||||||
|
as Dictionary
|
||||||
|
)
|
||||||
|
if role == ControllerMappingManagerType.ROLE_B:
|
||||||
|
binding = (
|
||||||
|
ControllerMappingManagerType.default_bindings()[
|
||||||
|
str(ControllerMappingManagerType.ROLE_A)
|
||||||
|
] as Dictionary
|
||||||
|
)
|
||||||
|
if str(binding.get("kind", "")) == "button":
|
||||||
|
var button := InputEventJoypadButton.new()
|
||||||
|
button.button_index = int(binding.get("button", -1))
|
||||||
|
button.pressed = true
|
||||||
|
panel._input(button)
|
||||||
|
else:
|
||||||
|
var motion := InputEventJoypadMotion.new()
|
||||||
|
motion.axis = int(binding.get("axis", -1))
|
||||||
|
motion.axis_value = float(binding.get("direction", -1.0))
|
||||||
|
panel._input(motion)
|
||||||
|
if panel._auto_map_active:
|
||||||
|
return "auto-map did not complete after every requested input"
|
||||||
|
if not manager.has_custom_mapping():
|
||||||
|
return "completed auto-map did not activate its profile"
|
||||||
|
if panel._binding_buttons.size() != (
|
||||||
|
ControllerMappingManagerType.ROLE_ORDER.size()
|
||||||
|
):
|
||||||
|
return "manual override list does not expose every mapped role"
|
||||||
|
var conflict_button := panel._binding_buttons.get(
|
||||||
|
ControllerMappingManagerType.ROLE_A
|
||||||
|
) as Button
|
||||||
|
var conflict_style := conflict_button.get_theme_stylebox(
|
||||||
|
&"normal"
|
||||||
|
) as StyleBoxFlat
|
||||||
|
if (
|
||||||
|
conflict_style == null
|
||||||
|
or conflict_style.bg_color
|
||||||
|
!= UtilityPageStyle.OCEAN_DANGER
|
||||||
|
):
|
||||||
|
return "duplicate controller bindings were not marked in red"
|
||||||
|
panel._begin_manual_capture(ControllerMappingManagerType.ROLE_LT)
|
||||||
|
if "left trigger" in panel._progress_label.text.to_lower():
|
||||||
|
return "manual remapping still dictates a specific physical input"
|
||||||
|
if "any button" not in panel._progress_label.text.to_lower():
|
||||||
|
return "manual remapping does not request a generic controller input"
|
||||||
|
panel._cancel_capture()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_virtual_mouse_bounds() -> String:
|
||||||
|
var window_size := Vector2(3840.0, 2160.0)
|
||||||
|
var bounds: Rect2 = GameUIType.get_virtual_mouse_window_bounds(window_size)
|
||||||
|
var expected_margin := Vector2(42.0, 42.0)
|
||||||
|
if not bounds.position.is_equal_approx(expected_margin):
|
||||||
|
return "4K virtual cursor minimum bounds are incorrect: %s" % bounds
|
||||||
|
if not bounds.end.is_equal_approx(window_size - expected_margin):
|
||||||
|
return "4K virtual cursor maximum bounds are incorrect: %s" % bounds
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _keyboard_event_count(action: StringName) -> int:
|
||||||
|
var count: int = 0
|
||||||
|
for event: InputEvent in InputMap.action_get_events(action):
|
||||||
|
if event is InputEventKey or event is InputEventMouseButton:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
1
tests/controller_mapping_validation.gd.uid
Normal file
1
tests/controller_mapping_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dhwkjojmpjc13
|
||||||
244
ui/chat_ui.gd
244
ui/chat_ui.gd
|
|
@ -20,6 +20,11 @@ const HANDLE_SIZE := Vector2(34, 42)
|
||||||
const HANDLE_GAP: float = 6.0
|
const HANDLE_GAP: float = 6.0
|
||||||
const COLLAPSED_REVEAL_WIDTH: float = 8.0
|
const COLLAPSED_REVEAL_WIDTH: float = 8.0
|
||||||
const HINT_EDGE_MARGIN: float = 4.0
|
const HINT_EDGE_MARGIN: float = 4.0
|
||||||
|
const MOBILE_COMPACT_WIDTH: float = 620.0
|
||||||
|
const MOBILE_EXPANDED_WIDTH: float = 820.0
|
||||||
|
const MOBILE_COMPACT_HEIGHT: float = 220.0
|
||||||
|
const MOBILE_EXPANDED_HEIGHT: float = 390.0
|
||||||
|
const MOBILE_EDGE_MARGIN: float = 12.0
|
||||||
const CLOCK_SIZE := Vector2(116.0, 34.0)
|
const CLOCK_SIZE := Vector2(116.0, 34.0)
|
||||||
const CLOCK_EDGE_MARGIN: float = 10.0
|
const CLOCK_EDGE_MARGIN: float = 10.0
|
||||||
const WEATHER_ICON_SIZE := Vector2(34.0, 34.0)
|
const WEATHER_ICON_SIZE := Vector2(34.0, 34.0)
|
||||||
|
|
@ -72,8 +77,10 @@ var _send_pending: bool = false
|
||||||
var _pending_send_body: String = ""
|
var _pending_send_body: String = ""
|
||||||
var _prior_movement: bool = true
|
var _prior_movement: bool = true
|
||||||
var _prior_camera: bool = true
|
var _prior_camera: bool = true
|
||||||
|
var _controller_refocused: bool = false
|
||||||
var _output_scale: float = 1.0
|
var _output_scale: float = 1.0
|
||||||
var _dock_right: bool = false
|
var _dock_right: bool = false
|
||||||
|
var _mobile_mode: bool = false
|
||||||
var _world_time: WorldTimeServiceType
|
var _world_time: WorldTimeServiceType
|
||||||
var _world_weather: WorldWeatherServiceType
|
var _world_weather: WorldWeatherServiceType
|
||||||
|
|
||||||
|
|
@ -125,6 +132,7 @@ func setup(
|
||||||
_world_weather.get_weather(),
|
_world_weather.get_weather(),
|
||||||
_world_weather.get_seconds_remaining(),
|
_world_weather.get_seconds_remaining(),
|
||||||
)
|
)
|
||||||
|
set_mobile_mode(_settings.current_settings.chat_mobile_mode)
|
||||||
set_dock_right(_settings.current_settings.chat_dock_right)
|
set_dock_right(_settings.current_settings.chat_dock_right)
|
||||||
_service.message_received.connect(_on_message)
|
_service.message_received.connect(_on_message)
|
||||||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||||
|
|
@ -151,6 +159,7 @@ func open_chat() -> void:
|
||||||
return
|
return
|
||||||
if _presentation_state == PresentationState.COLLAPSED:
|
if _presentation_state == PresentationState.COLLAPSED:
|
||||||
_set_presentation_state(_visible_state_before_collapse, true, true)
|
_set_presentation_state(_visible_state_before_collapse, true, true)
|
||||||
|
_controller_refocused = false
|
||||||
_opened = true
|
_opened = true
|
||||||
text_entry_ownership_changed.emit(true)
|
text_entry_ownership_changed.emit(true)
|
||||||
_prior_movement = _player.is_movement_enabled()
|
_prior_movement = _player.is_movement_enabled()
|
||||||
|
|
@ -162,8 +171,10 @@ func open_chat() -> void:
|
||||||
# prediction would let a remote host continue the last held movement.
|
# prediction would let a remote host continue the last held movement.
|
||||||
_session.submit_neutral_local_movement()
|
_session.submit_neutral_local_movement()
|
||||||
_entry.show()
|
_entry.show()
|
||||||
|
_entry.virtual_keyboard_enabled = false
|
||||||
_entry.grab_focus()
|
_entry.grab_focus()
|
||||||
_hint.hide()
|
_hint.hide()
|
||||||
|
_refresh_input_ownership()
|
||||||
_update_panel_opacity(true)
|
_update_panel_opacity(true)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
@ -184,6 +195,7 @@ func close_chat() -> void:
|
||||||
return
|
return
|
||||||
_opened = false
|
_opened = false
|
||||||
text_entry_ownership_changed.emit(false)
|
text_entry_ownership_changed.emit(false)
|
||||||
|
_entry.virtual_keyboard_enabled = false
|
||||||
_entry.release_focus()
|
_entry.release_focus()
|
||||||
_entry.hide()
|
_entry.hide()
|
||||||
_player.set_movement_enabled(_prior_movement)
|
_player.set_movement_enabled(_prior_movement)
|
||||||
|
|
@ -191,14 +203,62 @@ func close_chat() -> void:
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||||
_flush_draft()
|
_flush_draft()
|
||||||
_refresh_visibility()
|
_refresh_visibility()
|
||||||
|
_refresh_input_ownership()
|
||||||
|
|
||||||
|
|
||||||
|
func toggle_chat() -> void:
|
||||||
|
if _presentation_state == PresentationState.COLLAPSED:
|
||||||
|
open_chat()
|
||||||
|
return
|
||||||
|
if _opened:
|
||||||
|
close_chat()
|
||||||
|
_set_presentation_state(PresentationState.COLLAPSED, true, true)
|
||||||
|
return
|
||||||
|
if _controller_refocused:
|
||||||
|
_controller_refocused = false
|
||||||
|
_set_presentation_state(PresentationState.COLLAPSED, true, true)
|
||||||
|
return
|
||||||
|
open_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func refocus_gameplay() -> void:
|
||||||
|
close_chat()
|
||||||
|
_controller_refocused = true
|
||||||
|
get_viewport().gui_release_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func toggle_focus() -> void:
|
||||||
|
if _presentation_state == PresentationState.COLLAPSED:
|
||||||
|
return
|
||||||
|
if _opened:
|
||||||
|
refocus_gameplay()
|
||||||
|
else:
|
||||||
|
open_chat()
|
||||||
|
|
||||||
|
|
||||||
|
func request_virtual_keyboard() -> bool:
|
||||||
|
if not _opened or not _entry.has_focus():
|
||||||
|
return false
|
||||||
|
_entry.virtual_keyboard_enabled = true
|
||||||
|
# Re-entering focus after the controller accept event is the explicit
|
||||||
|
# request Android uses to display its keyboard. Merely focusing Chat keeps
|
||||||
|
# the keyboard disabled so the world remains readable.
|
||||||
|
_entry.release_focus()
|
||||||
|
_entry.call_deferred("grab_focus")
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func is_open() -> bool:
|
||||||
|
return _opened
|
||||||
|
|
||||||
|
|
||||||
|
func is_collapsed() -> bool:
|
||||||
|
return _presentation_state == PresentationState.COLLAPSED
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("open_chat"):
|
if event.is_action_pressed("open_chat"):
|
||||||
if _opened:
|
toggle_chat()
|
||||||
close_chat()
|
|
||||||
elif _available:
|
|
||||||
open_chat()
|
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if event is InputEventKey and event.pressed and not event.echo:
|
if event is InputEventKey and event.pressed and not event.echo:
|
||||||
|
|
@ -404,14 +464,21 @@ func _build_ui() -> void:
|
||||||
_unread_indicator.hide()
|
_unread_indicator.hide()
|
||||||
_hint.hide()
|
_hint.hide()
|
||||||
_layout_presentation(false)
|
_layout_presentation(false)
|
||||||
|
_refresh_input_ownership()
|
||||||
|
|
||||||
|
|
||||||
func _chat_panel_style() -> StyleBoxFlat:
|
func _chat_panel_style() -> StyleBoxFlat:
|
||||||
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||||
style.corner_radius_top_left = 10 if _dock_right else 0
|
if _mobile_mode:
|
||||||
style.corner_radius_bottom_left = 10 if _dock_right else 0
|
style.corner_radius_top_left = 0
|
||||||
style.corner_radius_top_right = 0 if _dock_right else 10
|
style.corner_radius_top_right = 0
|
||||||
style.corner_radius_bottom_right = 0 if _dock_right else 10
|
style.corner_radius_bottom_left = 10
|
||||||
|
style.corner_radius_bottom_right = 10
|
||||||
|
else:
|
||||||
|
style.corner_radius_top_left = 10 if _dock_right else 0
|
||||||
|
style.corner_radius_bottom_left = 10 if _dock_right else 0
|
||||||
|
style.corner_radius_top_right = 0 if _dock_right else 10
|
||||||
|
style.corner_radius_bottom_right = 0 if _dock_right else 10
|
||||||
style.content_margin_left = 12
|
style.content_margin_left = 12
|
||||||
style.content_margin_right = 12
|
style.content_margin_right = 12
|
||||||
style.content_margin_top = 10
|
style.content_margin_top = 10
|
||||||
|
|
@ -419,6 +486,18 @@ func _chat_panel_style() -> StyleBoxFlat:
|
||||||
return style
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_input_ownership() -> void:
|
||||||
|
if _panel == null or _history == null:
|
||||||
|
return
|
||||||
|
var filter: Control.MouseFilter = (
|
||||||
|
Control.MOUSE_FILTER_STOP
|
||||||
|
if _opened
|
||||||
|
else Control.MOUSE_FILTER_IGNORE
|
||||||
|
)
|
||||||
|
_panel.mouse_filter = filter
|
||||||
|
_history.mouse_filter = filter
|
||||||
|
|
||||||
|
|
||||||
func _clock_panel_style() -> StyleBoxFlat:
|
func _clock_panel_style() -> StyleBoxFlat:
|
||||||
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||||
style.set_corner_radius_all(12)
|
style.set_corner_radius_all(12)
|
||||||
|
|
@ -620,7 +699,10 @@ func _refresh_visibility() -> void:
|
||||||
_collapse_button.show()
|
_collapse_button.show()
|
||||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||||
_panel.show()
|
_panel.show()
|
||||||
_height_button.show()
|
_height_button.visible = not _mobile_mode
|
||||||
|
_height_button.focus_mode = (
|
||||||
|
Control.FOCUS_NONE if _mobile_mode else Control.FOCUS_ALL
|
||||||
|
)
|
||||||
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
_unread_indicator.visible = collapsed and _collapsed_has_unread
|
||||||
_hint.visible = not _opened
|
_hint.visible = not _opened
|
||||||
|
|
||||||
|
|
@ -670,10 +752,31 @@ func is_docked_right() -> bool:
|
||||||
return _dock_right
|
return _dock_right
|
||||||
|
|
||||||
|
|
||||||
|
func set_mobile_mode(enabled: bool) -> void:
|
||||||
|
_mobile_mode = enabled
|
||||||
|
if not is_node_ready() or _panel == null:
|
||||||
|
return
|
||||||
|
if _mobile_mode:
|
||||||
|
if _presentation_state == PresentationState.EXPANDED:
|
||||||
|
_presentation_state = PresentationState.COMPACT
|
||||||
|
if _visible_state_before_collapse == PresentationState.EXPANDED:
|
||||||
|
_visible_state_before_collapse = PresentationState.COMPACT
|
||||||
|
_panel.add_theme_stylebox_override("panel", _chat_panel_style())
|
||||||
|
_refresh_handle_labels(_presentation_state)
|
||||||
|
_refresh_visibility()
|
||||||
|
_layout_presentation(false)
|
||||||
|
|
||||||
|
|
||||||
|
func is_mobile_mode() -> bool:
|
||||||
|
return _mobile_mode
|
||||||
|
|
||||||
|
|
||||||
func _refresh_handle_labels(state: PresentationState) -> void:
|
func _refresh_handle_labels(state: PresentationState) -> void:
|
||||||
var collapsed := state == PresentationState.COLLAPSED
|
var collapsed := state == PresentationState.COLLAPSED
|
||||||
var height_state := _visible_state_before_collapse if collapsed else state
|
var height_state := _visible_state_before_collapse if collapsed else state
|
||||||
if _dock_right:
|
if _mobile_mode:
|
||||||
|
_collapse_button.text = "v" if collapsed else "^"
|
||||||
|
elif _dock_right:
|
||||||
_collapse_button.text = "<" if collapsed else ">"
|
_collapse_button.text = "<" if collapsed else ">"
|
||||||
else:
|
else:
|
||||||
_collapse_button.text = ">" if collapsed else "<"
|
_collapse_button.text = ">" if collapsed else "<"
|
||||||
|
|
@ -798,32 +901,101 @@ func _layout_presentation(animate: bool) -> void:
|
||||||
var viewport_height := size.y
|
var viewport_height := size.y
|
||||||
if viewport_height <= 0.0:
|
if viewport_height <= 0.0:
|
||||||
viewport_height = 720.0
|
viewport_height = 720.0
|
||||||
|
var layout_state := (
|
||||||
|
_visible_state_before_collapse
|
||||||
|
if _presentation_state == PresentationState.COLLAPSED
|
||||||
|
else _presentation_state
|
||||||
|
)
|
||||||
|
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||||
var bottom_margin := minf(
|
var bottom_margin := minf(
|
||||||
BOTTOM_MARGIN,
|
BOTTOM_MARGIN,
|
||||||
maxf(MIN_TOP_MARGIN, (viewport_height - MIN_HISTORY_HEIGHT) * 0.25),
|
maxf(MIN_TOP_MARGIN, (viewport_height - MIN_HISTORY_HEIGHT) * 0.25),
|
||||||
)
|
)
|
||||||
var bottom := viewport_height - bottom_margin
|
var bottom := viewport_height - bottom_margin
|
||||||
var height := COMPACT_HEIGHT
|
var height := COMPACT_HEIGHT
|
||||||
var layout_state := (
|
var panel_width := PANEL_WIDTH
|
||||||
_visible_state_before_collapse
|
var panel_x: float = 0.0
|
||||||
if _presentation_state == PresentationState.COLLAPSED
|
var panel_y: float = 0.0
|
||||||
else _presentation_state
|
var collapse_position := Vector2.ZERO
|
||||||
)
|
var height_position := Vector2.ZERO
|
||||||
if layout_state == PresentationState.EXPANDED:
|
var unread_position := Vector2.ZERO
|
||||||
height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin)
|
if _mobile_mode:
|
||||||
height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN))
|
panel_width = (
|
||||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
MOBILE_EXPANDED_WIDTH
|
||||||
var panel_x: float
|
if layout_state == PresentationState.EXPANDED
|
||||||
if _dock_right:
|
else MOBILE_COMPACT_WIDTH
|
||||||
panel_x = (
|
|
||||||
viewport_width - COLLAPSED_REVEAL_WIDTH
|
|
||||||
if collapsed
|
|
||||||
else viewport_width - PANEL_WIDTH
|
|
||||||
)
|
)
|
||||||
|
panel_width = minf(
|
||||||
|
panel_width,
|
||||||
|
maxf(1.0, viewport_width - MOBILE_EDGE_MARGIN * 2.0),
|
||||||
|
)
|
||||||
|
height = (
|
||||||
|
MOBILE_EXPANDED_HEIGHT
|
||||||
|
if layout_state == PresentationState.EXPANDED
|
||||||
|
else MOBILE_COMPACT_HEIGHT
|
||||||
|
)
|
||||||
|
height = minf(
|
||||||
|
height,
|
||||||
|
maxf(MIN_HISTORY_HEIGHT, viewport_height - HANDLE_SIZE.y),
|
||||||
|
)
|
||||||
|
panel_x = (viewport_width - panel_width) * 0.5
|
||||||
|
panel_y = -(height - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
|
||||||
|
var mobile_handle_y: float = (
|
||||||
|
COLLAPSED_REVEAL_WIDTH if collapsed else height
|
||||||
|
)
|
||||||
|
collapse_position = Vector2(
|
||||||
|
panel_x + panel_width - HANDLE_SIZE.x,
|
||||||
|
mobile_handle_y,
|
||||||
|
)
|
||||||
|
height_position = Vector2(
|
||||||
|
collapse_position.x - HANDLE_SIZE.x - HANDLE_GAP,
|
||||||
|
mobile_handle_y,
|
||||||
|
)
|
||||||
|
unread_position = collapse_position + Vector2(6.0, -18.0)
|
||||||
else:
|
else:
|
||||||
panel_x = -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
|
if layout_state == PresentationState.EXPANDED:
|
||||||
var target_position := Vector2(panel_x, bottom - height)
|
height = maxf(
|
||||||
var target_size := Vector2(PANEL_WIDTH, height)
|
MIN_HISTORY_HEIGHT,
|
||||||
|
viewport_height - 2.0 * bottom_margin,
|
||||||
|
)
|
||||||
|
height = minf(
|
||||||
|
height,
|
||||||
|
maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN),
|
||||||
|
)
|
||||||
|
if _dock_right:
|
||||||
|
panel_x = (
|
||||||
|
viewport_width - COLLAPSED_REVEAL_WIDTH
|
||||||
|
if collapsed
|
||||||
|
else viewport_width - PANEL_WIDTH
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
panel_x = (
|
||||||
|
-(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH)
|
||||||
|
if collapsed
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
panel_y = bottom - height
|
||||||
|
var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP
|
||||||
|
var handle_stack_top := (
|
||||||
|
bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5
|
||||||
|
)
|
||||||
|
var handle_x: float = (
|
||||||
|
panel_x - HANDLE_SIZE.x
|
||||||
|
if _dock_right
|
||||||
|
else COLLAPSED_REVEAL_WIDTH if collapsed else PANEL_WIDTH
|
||||||
|
)
|
||||||
|
collapse_position = Vector2(
|
||||||
|
handle_x,
|
||||||
|
handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP,
|
||||||
|
)
|
||||||
|
height_position = Vector2(handle_x, handle_stack_top)
|
||||||
|
var unread_offset_x := -18.0 if _dock_right else 6.0
|
||||||
|
unread_position = collapse_position + Vector2(
|
||||||
|
unread_offset_x,
|
||||||
|
-18.0,
|
||||||
|
)
|
||||||
|
var target_position := Vector2(panel_x, panel_y)
|
||||||
|
var target_size := Vector2(panel_width, height)
|
||||||
var clock_x: float = (
|
var clock_x: float = (
|
||||||
viewport_width - CLOCK_SIZE.x - CLOCK_EDGE_MARGIN
|
viewport_width - CLOCK_SIZE.x - CLOCK_EDGE_MARGIN
|
||||||
if _dock_right else CLOCK_EDGE_MARGIN
|
if _dock_right else CLOCK_EDGE_MARGIN
|
||||||
|
|
@ -840,22 +1012,6 @@ func _layout_presentation(animate: bool) -> void:
|
||||||
var weather_icon_position := Vector2(
|
var weather_icon_position := Vector2(
|
||||||
weather_icon_x, clock_position.y
|
weather_icon_x, clock_position.y
|
||||||
)
|
)
|
||||||
var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP
|
|
||||||
var handle_stack_top := (
|
|
||||||
bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5
|
|
||||||
)
|
|
||||||
var handle_x: float
|
|
||||||
if _dock_right:
|
|
||||||
handle_x = panel_x - HANDLE_SIZE.x
|
|
||||||
else:
|
|
||||||
handle_x = COLLAPSED_REVEAL_WIDTH if collapsed else PANEL_WIDTH
|
|
||||||
var collapse_position := Vector2(
|
|
||||||
handle_x,
|
|
||||||
handle_stack_top + HANDLE_SIZE.y + HANDLE_GAP,
|
|
||||||
)
|
|
||||||
var height_position := Vector2(handle_x, handle_stack_top)
|
|
||||||
var unread_offset_x := -18.0 if _dock_right else 6.0
|
|
||||||
var unread_position := collapse_position + Vector2(unread_offset_x, -18.0)
|
|
||||||
var hint_position := Vector2(
|
var hint_position := Vector2(
|
||||||
(
|
(
|
||||||
viewport_width - _hint.size.x - HINT_EDGE_MARGIN
|
viewport_width - _hint.size.x - HINT_EDGE_MARGIN
|
||||||
|
|
|
||||||
450
ui/controller_mapping_panel.gd
Normal file
450
ui/controller_mapping_panel.gd
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
class_name ControllerMappingPanel
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||||
|
|
||||||
|
var _mapping_manager: ControllerMappingManagerType
|
||||||
|
var _binding_buttons: Dictionary = {}
|
||||||
|
var _controller_label: Label
|
||||||
|
var _instruction_label: Label
|
||||||
|
var _progress_label: Label
|
||||||
|
var _auto_map_button: Button
|
||||||
|
var _reset_button: Button
|
||||||
|
var _close_button: Button
|
||||||
|
var _capturing_role: StringName = &""
|
||||||
|
var _auto_map_active: bool = false
|
||||||
|
var _auto_map_index: int = -1
|
||||||
|
var _auto_map_draft: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_process_input(true)
|
||||||
|
_build_interface()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(mapping_manager: ControllerMappingManagerType) -> void:
|
||||||
|
_mapping_manager = mapping_manager
|
||||||
|
if not _mapping_manager.active_controller_changed.is_connected(
|
||||||
|
_on_active_controller_changed
|
||||||
|
):
|
||||||
|
_mapping_manager.active_controller_changed.connect(
|
||||||
|
_on_active_controller_changed
|
||||||
|
)
|
||||||
|
if not _mapping_manager.active_profile_changed.is_connected(
|
||||||
|
_refresh_bindings
|
||||||
|
):
|
||||||
|
_mapping_manager.active_profile_changed.connect(_refresh_bindings)
|
||||||
|
_refresh_bindings()
|
||||||
|
|
||||||
|
|
||||||
|
func open_panel() -> void:
|
||||||
|
if _mapping_manager == null:
|
||||||
|
return
|
||||||
|
_cancel_capture()
|
||||||
|
show()
|
||||||
|
_refresh_bindings()
|
||||||
|
_auto_map_button.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func is_open() -> bool:
|
||||||
|
return visible
|
||||||
|
|
||||||
|
|
||||||
|
func is_capturing() -> bool:
|
||||||
|
return visible and not _capturing_role.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func request_back() -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
if not _capturing_role.is_empty():
|
||||||
|
_cancel_capture()
|
||||||
|
return
|
||||||
|
close_panel()
|
||||||
|
|
||||||
|
|
||||||
|
func close_panel() -> void:
|
||||||
|
_cancel_capture()
|
||||||
|
hide()
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not visible or _mapping_manager == null:
|
||||||
|
return
|
||||||
|
if _capturing_role.is_empty():
|
||||||
|
return
|
||||||
|
var key_event := event as InputEventKey
|
||||||
|
if (
|
||||||
|
key_event != null
|
||||||
|
and key_event.pressed
|
||||||
|
and not key_event.echo
|
||||||
|
and key_event.keycode == KEY_ESCAPE
|
||||||
|
):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
_cancel_capture()
|
||||||
|
_progress_label.text = "controller mapping cancelled"
|
||||||
|
return
|
||||||
|
if not (
|
||||||
|
event is InputEventJoypadButton
|
||||||
|
or event is InputEventJoypadMotion
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var binding: Dictionary = _mapping_manager.binding_from_event(
|
||||||
|
_capturing_role,
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
if binding.is_empty():
|
||||||
|
return
|
||||||
|
if not _mapping_manager.validate_binding(_capturing_role, binding):
|
||||||
|
return
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
_accept_captured_binding(binding)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_interface() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
z_index = 100
|
||||||
|
UtilityPageStyleType.apply_page(self)
|
||||||
|
|
||||||
|
var frame := PanelContainer.new()
|
||||||
|
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
frame.offset_left = 18.0
|
||||||
|
frame.offset_top = 18.0
|
||||||
|
frame.offset_right = -18.0
|
||||||
|
frame.offset_bottom = -18.0
|
||||||
|
frame.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyleType.rounded_style(
|
||||||
|
UtilityPageStyleType.OCEAN_PANEL_DEEP,
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
add_child(frame)
|
||||||
|
|
||||||
|
var outer_margin := MarginContainer.new()
|
||||||
|
outer_margin.add_theme_constant_override("margin_left", 24)
|
||||||
|
outer_margin.add_theme_constant_override("margin_top", 20)
|
||||||
|
outer_margin.add_theme_constant_override("margin_right", 24)
|
||||||
|
outer_margin.add_theme_constant_override("margin_bottom", 20)
|
||||||
|
frame.add_child(outer_margin)
|
||||||
|
|
||||||
|
var page := VBoxContainer.new()
|
||||||
|
page.add_theme_constant_override("separation", 12)
|
||||||
|
outer_margin.add_child(page)
|
||||||
|
|
||||||
|
var heading_row := HBoxContainer.new()
|
||||||
|
heading_row.add_theme_constant_override("separation", 12)
|
||||||
|
page.add_child(heading_row)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "controller mapping"
|
||||||
|
title.add_theme_font_size_override("font_size", 28)
|
||||||
|
title.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
heading_row.add_child(title)
|
||||||
|
|
||||||
|
_controller_label = Label.new()
|
||||||
|
_controller_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_controller_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_controller_label.add_theme_font_size_override("font_size", 15)
|
||||||
|
_controller_label.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
heading_row.add_child(_controller_label)
|
||||||
|
|
||||||
|
_instruction_label = Label.new()
|
||||||
|
_instruction_label.text = (
|
||||||
|
"auto-map walks through a standard xbox-style controller. "
|
||||||
|
+ "select any row afterward to override it."
|
||||||
|
)
|
||||||
|
_instruction_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_instruction_label.add_theme_font_size_override("font_size", 15)
|
||||||
|
_instruction_label.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
page.add_child(_instruction_label)
|
||||||
|
|
||||||
|
_progress_label = Label.new()
|
||||||
|
_progress_label.custom_minimum_size.y = 34.0
|
||||||
|
_progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_progress_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
_progress_label.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
page.add_child(_progress_label)
|
||||||
|
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
page.add_child(scroll)
|
||||||
|
|
||||||
|
var role_grid := GridContainer.new()
|
||||||
|
role_grid.columns = 2
|
||||||
|
role_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
role_grid.add_theme_constant_override("h_separation", 12)
|
||||||
|
role_grid.add_theme_constant_override("v_separation", 7)
|
||||||
|
scroll.add_child(role_grid)
|
||||||
|
|
||||||
|
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||||
|
var role_label := Label.new()
|
||||||
|
role_label.text = ControllerMappingManagerType.ROLE_LABELS.get(
|
||||||
|
role,
|
||||||
|
str(role),
|
||||||
|
)
|
||||||
|
role_label.custom_minimum_size = Vector2(330.0, 42.0)
|
||||||
|
role_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
role_label.add_theme_font_size_override("font_size", 16)
|
||||||
|
role_label.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyleType.OCEAN_TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
role_grid.add_child(role_label)
|
||||||
|
|
||||||
|
var binding_button := Button.new()
|
||||||
|
binding_button.custom_minimum_size = Vector2(210.0, 42.0)
|
||||||
|
binding_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
binding_button.focus_mode = Control.FOCUS_ALL
|
||||||
|
binding_button.tooltip_text = (
|
||||||
|
"change " + str(ControllerMappingManagerType.ROLE_LABELS.get(
|
||||||
|
role,
|
||||||
|
str(role),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
UtilityPageStyleType.apply_compact_ocean_button(binding_button)
|
||||||
|
binding_button.pressed.connect(_begin_manual_capture.bind(role))
|
||||||
|
role_grid.add_child(binding_button)
|
||||||
|
_binding_buttons[role] = binding_button
|
||||||
|
|
||||||
|
var footer := HBoxContainer.new()
|
||||||
|
footer.alignment = BoxContainer.ALIGNMENT_END
|
||||||
|
footer.add_theme_constant_override("separation", 10)
|
||||||
|
page.add_child(footer)
|
||||||
|
|
||||||
|
_auto_map_button = Button.new()
|
||||||
|
_auto_map_button.text = "auto-map controller"
|
||||||
|
_auto_map_button.tooltip_text = (
|
||||||
|
"walk through every input on a standard xbox-style controller"
|
||||||
|
)
|
||||||
|
UtilityPageStyleType.apply_ocean_button(_auto_map_button)
|
||||||
|
_auto_map_button.pressed.connect(_begin_auto_map)
|
||||||
|
footer.add_child(_auto_map_button)
|
||||||
|
|
||||||
|
_reset_button = Button.new()
|
||||||
|
_reset_button.text = "restore defaults"
|
||||||
|
_reset_button.tooltip_text = "remove this controller's custom mapping"
|
||||||
|
UtilityPageStyleType.apply_ocean_button(_reset_button)
|
||||||
|
_reset_button.pressed.connect(_reset_mapping)
|
||||||
|
footer.add_child(_reset_button)
|
||||||
|
|
||||||
|
_close_button = Button.new()
|
||||||
|
_close_button.text = "done"
|
||||||
|
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||||||
|
_close_button.pressed.connect(close_panel)
|
||||||
|
footer.add_child(_close_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _begin_auto_map() -> void:
|
||||||
|
if _mapping_manager == null:
|
||||||
|
return
|
||||||
|
_auto_map_active = true
|
||||||
|
_auto_map_index = 0
|
||||||
|
_auto_map_draft = {}
|
||||||
|
_set_capture_role(ControllerMappingManagerType.ROLE_ORDER[_auto_map_index])
|
||||||
|
|
||||||
|
|
||||||
|
func _begin_manual_capture(role: StringName) -> void:
|
||||||
|
_auto_map_active = false
|
||||||
|
_auto_map_index = -1
|
||||||
|
_auto_map_draft.clear()
|
||||||
|
_set_capture_role(role)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_capture_role(role: StringName) -> void:
|
||||||
|
_capturing_role = role
|
||||||
|
_set_action_buttons_disabled(true)
|
||||||
|
if _auto_map_active:
|
||||||
|
_progress_label.text = "step %d of %d: %s" % [
|
||||||
|
_auto_map_index + 1,
|
||||||
|
ControllerMappingManagerType.ROLE_ORDER.size(),
|
||||||
|
_mapping_manager.get_role_prompt(role),
|
||||||
|
]
|
||||||
|
return
|
||||||
|
var input_instruction: String = "press any controller button"
|
||||||
|
if _mapping_manager.role_expects_axis(role):
|
||||||
|
input_instruction = "move any controller axis"
|
||||||
|
elif _mapping_manager.role_accepts_axis(role):
|
||||||
|
input_instruction = "press any button or move any controller axis"
|
||||||
|
_progress_label.text = "%s for %s" % [
|
||||||
|
input_instruction,
|
||||||
|
_mapping_manager.get_role_label(role),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _accept_captured_binding(binding: Dictionary) -> void:
|
||||||
|
if _auto_map_active:
|
||||||
|
_auto_map_draft[str(_capturing_role)] = binding.duplicate(true)
|
||||||
|
_auto_map_index += 1
|
||||||
|
if _auto_map_index < ControllerMappingManagerType.ROLE_ORDER.size():
|
||||||
|
_set_capture_role(
|
||||||
|
ControllerMappingManagerType.ROLE_ORDER[_auto_map_index]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var saved: bool = _mapping_manager.replace_active_bindings(
|
||||||
|
_auto_map_draft
|
||||||
|
)
|
||||||
|
_cancel_capture()
|
||||||
|
_progress_label.text = (
|
||||||
|
"controller mapped successfully"
|
||||||
|
if saved else "could not save the controller mapping"
|
||||||
|
)
|
||||||
|
_refresh_bindings()
|
||||||
|
return
|
||||||
|
var role: StringName = _capturing_role
|
||||||
|
var saved: bool = _mapping_manager.set_binding(role, binding)
|
||||||
|
_cancel_capture()
|
||||||
|
_progress_label.text = (
|
||||||
|
"updated " + _mapping_manager.get_role_label(role)
|
||||||
|
if saved else "could not save that controller input"
|
||||||
|
)
|
||||||
|
_refresh_bindings()
|
||||||
|
|
||||||
|
func _cancel_capture() -> void:
|
||||||
|
_capturing_role = &""
|
||||||
|
_auto_map_active = false
|
||||||
|
_auto_map_index = -1
|
||||||
|
_auto_map_draft.clear()
|
||||||
|
_set_action_buttons_disabled(false)
|
||||||
|
if _progress_label != null:
|
||||||
|
_progress_label.text = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _set_action_buttons_disabled(disabled: bool) -> void:
|
||||||
|
if _auto_map_button != null:
|
||||||
|
_auto_map_button.disabled = disabled
|
||||||
|
if _reset_button != null:
|
||||||
|
_reset_button.disabled = disabled
|
||||||
|
for button_value: Variant in _binding_buttons.values():
|
||||||
|
var button := button_value as Button
|
||||||
|
if button != null:
|
||||||
|
button.disabled = disabled
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_mapping() -> void:
|
||||||
|
if _mapping_manager == null:
|
||||||
|
return
|
||||||
|
var reset: bool = _mapping_manager.reset_active_profile()
|
||||||
|
_progress_label.text = (
|
||||||
|
"restored the project controller defaults"
|
||||||
|
if reset else "could not restore controller defaults"
|
||||||
|
)
|
||||||
|
_refresh_bindings()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_bindings() -> void:
|
||||||
|
if _mapping_manager == null or _controller_label == null:
|
||||||
|
return
|
||||||
|
_controller_label.text = _mapping_manager.get_active_controller_name()
|
||||||
|
var bindings: Dictionary = _mapping_manager.get_active_bindings()
|
||||||
|
for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||||
|
var button := _binding_buttons.get(role) as Button
|
||||||
|
if button == null:
|
||||||
|
continue
|
||||||
|
var raw_binding: Variant = bindings.get(str(role), {})
|
||||||
|
button.text = (
|
||||||
|
_mapping_manager.binding_label(raw_binding as Dictionary)
|
||||||
|
if typeof(raw_binding) == TYPE_DICTIONARY else "unmapped"
|
||||||
|
)
|
||||||
|
var conflict_role: StringName = _find_binding_conflict(
|
||||||
|
role,
|
||||||
|
raw_binding as Dictionary,
|
||||||
|
bindings,
|
||||||
|
)
|
||||||
|
_apply_binding_button_style(button, not conflict_role.is_empty())
|
||||||
|
button.tooltip_text = (
|
||||||
|
"warning: also assigned to "
|
||||||
|
+ _mapping_manager.get_role_label(conflict_role)
|
||||||
|
if not conflict_role.is_empty()
|
||||||
|
else "change " + _mapping_manager.get_role_label(role)
|
||||||
|
)
|
||||||
|
_reset_button.disabled = not _mapping_manager.has_custom_mapping()
|
||||||
|
|
||||||
|
|
||||||
|
func _find_binding_conflict(
|
||||||
|
role: StringName,
|
||||||
|
binding: Dictionary,
|
||||||
|
bindings: Dictionary,
|
||||||
|
) -> StringName:
|
||||||
|
if binding.is_empty():
|
||||||
|
return &""
|
||||||
|
for other_role: StringName in ControllerMappingManagerType.ROLE_ORDER:
|
||||||
|
if other_role == role:
|
||||||
|
continue
|
||||||
|
var raw_other: Variant = bindings.get(str(other_role), {})
|
||||||
|
if typeof(raw_other) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var other := raw_other as Dictionary
|
||||||
|
if _bindings_conflict(role, binding, other_role, other):
|
||||||
|
return other_role
|
||||||
|
return &""
|
||||||
|
|
||||||
|
|
||||||
|
func _bindings_conflict(
|
||||||
|
first_role: StringName,
|
||||||
|
first: Dictionary,
|
||||||
|
second_role: StringName,
|
||||||
|
second: Dictionary,
|
||||||
|
) -> bool:
|
||||||
|
var binding_kind: String = str(first.get("kind", ""))
|
||||||
|
if binding_kind != str(second.get("kind", "")):
|
||||||
|
return false
|
||||||
|
if binding_kind == "button":
|
||||||
|
return int(first.get("button", -1)) == int(
|
||||||
|
second.get("button", -1)
|
||||||
|
)
|
||||||
|
if binding_kind != "axis":
|
||||||
|
return false
|
||||||
|
if int(first.get("axis", -1)) != int(second.get("axis", -1)):
|
||||||
|
return false
|
||||||
|
var opposite_trigger_halves: bool = (
|
||||||
|
first_role in ControllerMappingManagerType.TRIGGER_ROLES
|
||||||
|
and second_role in ControllerMappingManagerType.TRIGGER_ROLES
|
||||||
|
and signf(float(first.get("direction", 0.0)))
|
||||||
|
!= signf(float(second.get("direction", 0.0)))
|
||||||
|
)
|
||||||
|
return not opposite_trigger_halves
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_binding_button_style(button: Button, has_conflict: bool) -> void:
|
||||||
|
UtilityPageStyleType.apply_compact_ocean_button(button)
|
||||||
|
if not has_conflict:
|
||||||
|
return
|
||||||
|
var colors: Dictionary = {
|
||||||
|
&"normal": UtilityPageStyleType.OCEAN_DANGER,
|
||||||
|
&"hover": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||||||
|
&"pressed": UtilityPageStyleType.OCEAN_DANGER.darkened(0.12),
|
||||||
|
&"focus": UtilityPageStyleType.OCEAN_DANGER.lightened(0.12),
|
||||||
|
}
|
||||||
|
for state: StringName in colors:
|
||||||
|
var style := UtilityPageStyleType.ocean_button_style(
|
||||||
|
colors[state] as Color
|
||||||
|
)
|
||||||
|
style.content_margin_left = 10.0
|
||||||
|
style.content_margin_right = 10.0
|
||||||
|
style.content_margin_top = 4.0
|
||||||
|
style.content_margin_bottom = 4.0
|
||||||
|
button.add_theme_stylebox_override(state, style)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_active_controller_changed(_controller_name: String) -> void:
|
||||||
|
if not _capturing_role.is_empty():
|
||||||
|
_cancel_capture()
|
||||||
|
_refresh_bindings()
|
||||||
1
ui/controller_mapping_panel.gd.uid
Normal file
1
ui/controller_mapping_panel.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b0rl6lgpc0h85
|
||||||
|
|
@ -14,11 +14,21 @@ const SIT_SECTOR: int = 0
|
||||||
const RING_RADIUS: float = 172.0
|
const RING_RADIUS: float = 172.0
|
||||||
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
||||||
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
|
||||||
var _buttons: Array[BubbleButton] = []
|
var _buttons: Array[BubbleButton] = []
|
||||||
var _is_open: bool = false
|
var _is_open: bool = false
|
||||||
var _selected_sector: int = SIT_SECTOR
|
var _selected_sector: int = SIT_SECTOR
|
||||||
var _controller_selection_mode: bool = false
|
var _controller_selection_mode: bool = false
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -92,10 +102,7 @@ func _layout_bubbles() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _update_selection() -> void:
|
func _update_selection() -> void:
|
||||||
var stick: Vector2 = Vector2(
|
var stick: Vector2 = _get_selection_stick()
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
|
||||||
)
|
|
||||||
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
||||||
_select_sector_from_offset(stick)
|
_select_sector_from_offset(stick)
|
||||||
return
|
return
|
||||||
|
|
@ -104,6 +111,25 @@ func _update_selection() -> void:
|
||||||
_update_mouse_selection()
|
_update_mouse_selection()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_selection_stick() -> Vector2:
|
||||||
|
if (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
):
|
||||||
|
return Vector2(
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||||
|
),
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Vector2(
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _update_mouse_selection() -> void:
|
func _update_mouse_selection() -> void:
|
||||||
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
||||||
if offset.length() < 24.0:
|
if offset.length() < 24.0:
|
||||||
|
|
|
||||||
480
ui/game_ui.gd
480
ui/game_ui.gd
|
|
@ -44,6 +44,9 @@ const SurfaceDrawingToolbarType = preload(
|
||||||
const PlayerSettingsManagerType = preload(
|
const PlayerSettingsManagerType = preload(
|
||||||
"res://settings/player_settings_manager.gd"
|
"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 WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
const WorldWeatherServiceType = preload(
|
const WorldWeatherServiceType = preload(
|
||||||
"res://world/world_weather_service.gd"
|
"res://world/world_weather_service.gd"
|
||||||
|
|
@ -64,8 +67,15 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
||||||
|
|
||||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||||
const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55
|
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_STICK_DEADZONE: float = 0.18
|
||||||
const VIRTUAL_MOUSE_SPEED: float = 720.0
|
const VIRTUAL_MOUSE_SPEED: float = 720.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
|
||||||
|
|
||||||
@onready var _status_label: Label = %StatusLabel
|
@onready var _status_label: Label = %StatusLabel
|
||||||
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
|
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
|
||||||
|
|
@ -137,7 +147,12 @@ var _virtual_mouse_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
||||||
var _virtual_mouse_window_position: Vector2 = Vector2.ZERO
|
var _virtual_mouse_window_position: Vector2 = Vector2.ZERO
|
||||||
var _virtual_mouse_button_mask: int = 0
|
var _virtual_mouse_button_mask: int = 0
|
||||||
var _virtual_mouse_right_pressed: bool = false
|
var _virtual_mouse_right_pressed: bool = false
|
||||||
var _injecting_virtual_mouse_event: 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
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -294,21 +309,17 @@ func setup(
|
||||||
set_edge_docks(
|
set_edge_docks(
|
||||||
settings_manager.current_settings.chat_dock_right,
|
settings_manager.current_settings.chat_dock_right,
|
||||||
settings_manager.current_settings.paint_dock_right,
|
settings_manager.current_settings.paint_dock_right,
|
||||||
|
settings_manager.current_settings.chat_mobile_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
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):
|
if _handle_virtual_mouse_input(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
if _handle_controller_chat_controls(event):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
_surface_drawing_toolbar != null
|
_surface_drawing_toolbar != null
|
||||||
and _surface_drawing_toolbar.owns_pointer_event(event)
|
and _surface_drawing_toolbar.owns_pointer_event(event)
|
||||||
|
|
@ -378,6 +389,55 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
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
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
)
|
||||||
|
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.toggle_focus()
|
||||||
|
return true
|
||||||
|
if accept_pressed:
|
||||||
|
return _chat_ui.request_virtual_keyboard()
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _on_emote_selected(emote_id: StringName) -> void:
|
func _on_emote_selected(emote_id: StringName) -> void:
|
||||||
if emote_id == &"sit" and _player != null:
|
if emote_id == &"sit" and _player != null:
|
||||||
_player.toggle_sitting()
|
_player.toggle_sitting()
|
||||||
|
|
@ -404,24 +464,59 @@ func _on_quick_action_selected(action_id: StringName) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _handle_virtual_mouse_input(event: InputEvent) -> bool:
|
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
|
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||||
if motion_event != null:
|
if motion_event != null:
|
||||||
if motion_event.axis == JOY_AXIS_TRIGGER_LEFT:
|
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
|
var was_active: bool = _virtual_mouse_active
|
||||||
if motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
|
if (
|
||||||
|
_virtual_mouse_trigger_strength
|
||||||
|
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
||||||
|
):
|
||||||
if not _virtual_mouse_active and _can_start_virtual_mouse():
|
if not _virtual_mouse_active and _can_start_virtual_mouse():
|
||||||
_begin_virtual_mouse()
|
_begin_virtual_mouse(motion_event.device)
|
||||||
elif _virtual_mouse_active:
|
elif (
|
||||||
|
_virtual_mouse_active
|
||||||
|
and _virtual_mouse_trigger_strength
|
||||||
|
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
|
||||||
|
):
|
||||||
_end_virtual_mouse()
|
_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
|
return was_active or _virtual_mouse_active
|
||||||
if (
|
if (
|
||||||
_virtual_mouse_active
|
_virtual_mouse_active
|
||||||
and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT
|
and motion_event.device == _virtual_mouse_device_id
|
||||||
|
and motion_event.axis in [
|
||||||
|
JOY_AXIS_RIGHT_X,
|
||||||
|
JOY_AXIS_RIGHT_Y,
|
||||||
|
]
|
||||||
):
|
):
|
||||||
_set_virtual_mouse_button(
|
if motion_event.axis == JOY_AXIS_RIGHT_X:
|
||||||
MOUSE_BUTTON_RIGHT,
|
_virtual_mouse_stick.x = motion_event.axis_value
|
||||||
motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
|
else:
|
||||||
)
|
_virtual_mouse_stick.y = motion_event.axis_value
|
||||||
return true
|
return true
|
||||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||||
if (
|
if (
|
||||||
|
|
@ -429,34 +524,124 @@ func _handle_virtual_mouse_input(event: InputEvent) -> bool:
|
||||||
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||||
and (
|
and (
|
||||||
_virtual_mouse_active
|
_virtual_mouse_active
|
||||||
or Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
|
or _virtual_mouse_trigger_strength
|
||||||
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
if not _virtual_mouse_active and _can_start_virtual_mouse():
|
if not _virtual_mouse_active and _can_start_virtual_mouse():
|
||||||
_begin_virtual_mouse()
|
_begin_virtual_mouse(button_event.device)
|
||||||
if _virtual_mouse_active:
|
if _virtual_mouse_active:
|
||||||
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
|
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_mapped_virtual_mouse_input(event: InputEvent) -> bool:
|
||||||
|
var uses_activation: bool = _controller_mapping_manager.event_uses_role(
|
||||||
|
event,
|
||||||
|
ControllerMappingManagerType.ROLE_LT,
|
||||||
|
)
|
||||||
|
if uses_activation:
|
||||||
|
var was_active: bool = _virtual_mouse_active
|
||||||
|
_virtual_mouse_trigger_strength = (
|
||||||
|
_controller_mapping_manager.get_role_strength(
|
||||||
|
ControllerMappingManagerType.ROLE_LT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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_RT,
|
||||||
|
):
|
||||||
|
_set_virtual_mouse_button(
|
||||||
|
MOUSE_BUTTON_RIGHT,
|
||||||
|
_controller_mapping_manager.get_role_strength(
|
||||||
|
ControllerMappingManagerType.ROLE_RT
|
||||||
|
) >= 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:
|
func _can_start_virtual_mouse() -> bool:
|
||||||
return (
|
return (
|
||||||
_gameplay_ui_enabled
|
_gameplay_ui_enabled
|
||||||
and not _showcase_active
|
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 _player != null
|
||||||
and _fishing_spot != null
|
and _fishing_spot != null
|
||||||
and _fishing_spot.can_use_surface_drawing()
|
and _fishing_spot.can_open_system_menu()
|
||||||
and not _emote_radial_menu.is_open()
|
and not _emote_radial_menu.is_open()
|
||||||
and not _quick_radial_menu.is_open()
|
and not _quick_radial_menu.is_open()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _begin_virtual_mouse() -> void:
|
func _begin_virtual_mouse(device_id: int) -> void:
|
||||||
if _virtual_mouse_active:
|
if _virtual_mouse_active:
|
||||||
return
|
return
|
||||||
_virtual_mouse_active = true
|
_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 = (
|
_virtual_mouse_prior_camera_input_enabled = (
|
||||||
_player.is_camera_input_enabled()
|
_player.is_camera_input_enabled()
|
||||||
)
|
)
|
||||||
|
|
@ -470,8 +655,11 @@ func _begin_virtual_mouse() -> void:
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
|
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
|
||||||
_virtual_mouse_button_mask = 0
|
_virtual_mouse_button_mask = 0
|
||||||
_virtual_mouse_right_pressed = false
|
_virtual_mouse_right_pressed = false
|
||||||
_virtual_mouse_window_position = Vector2(get_window().size) * 0.5
|
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
|
||||||
|
Vector2(get_window().size) * 0.5
|
||||||
|
)
|
||||||
_controller_virtual_cursor.visible = true
|
_controller_virtual_cursor.visible = true
|
||||||
|
_update_virtual_cursor_position(_virtual_mouse_window_position)
|
||||||
_emit_virtual_mouse_motion(Vector2.ZERO)
|
_emit_virtual_mouse_motion(Vector2.ZERO)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -483,6 +671,8 @@ func _end_virtual_mouse() -> void:
|
||||||
if _virtual_mouse_right_pressed:
|
if _virtual_mouse_right_pressed:
|
||||||
_set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false)
|
_set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false)
|
||||||
_virtual_mouse_active = false
|
_virtual_mouse_active = false
|
||||||
|
_virtual_mouse_trigger_strength = 0.0
|
||||||
|
_virtual_mouse_stick = Vector2.ZERO
|
||||||
_controller_virtual_cursor.visible = false
|
_controller_virtual_cursor.visible = false
|
||||||
if _fishing_spot != null:
|
if _fishing_spot != null:
|
||||||
_fishing_spot.set_local_menu_input_suppressed(
|
_fishing_spot.set_local_menu_input_suppressed(
|
||||||
|
|
@ -500,15 +690,12 @@ func _update_virtual_mouse(delta: float) -> void:
|
||||||
if not _virtual_mouse_active:
|
if not _virtual_mouse_active:
|
||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
|
_virtual_mouse_trigger_strength
|
||||||
< VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
|
||||||
):
|
):
|
||||||
_end_virtual_mouse()
|
_end_virtual_mouse()
|
||||||
return
|
return
|
||||||
var stick: Vector2 = Vector2(
|
var stick: Vector2 = _virtual_mouse_stick
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
|
||||||
)
|
|
||||||
var stick_length: float = stick.length()
|
var stick_length: float = stick.length()
|
||||||
if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE:
|
if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE:
|
||||||
return
|
return
|
||||||
|
|
@ -526,22 +713,166 @@ func _update_virtual_mouse(delta: float) -> void:
|
||||||
* display_scale
|
* display_scale
|
||||||
* delta
|
* delta
|
||||||
)
|
)
|
||||||
var window_size: Vector2 = Vector2(get_window().size)
|
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
|
||||||
_virtual_mouse_window_position = Vector2(
|
_virtual_mouse_window_position + relative_motion
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
_update_virtual_cursor_position(_virtual_mouse_window_position)
|
||||||
_emit_virtual_mouse_motion(relative_motion)
|
_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_LT
|
||||||
|
)
|
||||||
|
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_RT
|
||||||
|
) >= 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:
|
func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void:
|
||||||
var mask: int = (
|
var mask: int = (
|
||||||
MOUSE_BUTTON_MASK_LEFT
|
MOUSE_BUTTON_MASK_LEFT
|
||||||
|
|
@ -577,16 +908,40 @@ func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _parse_virtual_mouse_event(event: InputEventMouse) -> void:
|
func _parse_virtual_mouse_event(event: InputEventMouse) -> void:
|
||||||
_injecting_virtual_mouse_event = true
|
|
||||||
Input.parse_input_event(event)
|
Input.parse_input_event(event)
|
||||||
_injecting_virtual_mouse_event = false
|
|
||||||
|
|
||||||
|
|
||||||
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
|
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
|
||||||
var root_transform: Transform2D = _ui_root.get_global_transform_with_canvas()
|
var output_scale: float = UIReferencePresentationType.get_scale(
|
||||||
_controller_virtual_cursor.set_pointer_position(
|
Vector2(get_window().size)
|
||||||
root_transform.affine_inverse() * viewport_position
|
|
||||||
)
|
)
|
||||||
|
_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:
|
func _toggle_surface_drawing() -> void:
|
||||||
|
|
@ -619,7 +974,29 @@ func setup_data_and_identity(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
func _process(delta: float) -> void:
|
||||||
|
_poll_virtual_mouse_controller_state()
|
||||||
_update_virtual_mouse(delta)
|
_update_virtual_mouse(delta)
|
||||||
_update_experience_bubble_position()
|
_update_experience_bubble_position()
|
||||||
if _item_effects == null or not _gameplay_ui_enabled:
|
if _item_effects == null or not _gameplay_ui_enabled:
|
||||||
|
|
@ -753,8 +1130,13 @@ func set_effective_ui_pixel_size(pixel_size: int) -> void:
|
||||||
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)
|
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)
|
||||||
|
|
||||||
|
|
||||||
func set_edge_docks(chat_dock_right: bool, paint_dock_right: bool) -> void:
|
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_dock_right(chat_dock_right)
|
||||||
|
_chat_ui.set_mobile_mode(chat_mobile_mode)
|
||||||
_surface_drawing_toolbar.set_dock_right(paint_dock_right)
|
_surface_drawing_toolbar.set_dock_right(paint_dock_right)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1131,6 +1513,8 @@ func _update_experience_bubble_position() -> void:
|
||||||
|
|
||||||
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||||
_player_menu_open = is_open
|
_player_menu_open = is_open
|
||||||
|
if is_open:
|
||||||
|
_end_virtual_mouse()
|
||||||
if is_open and _surface_drawing != null:
|
if is_open and _surface_drawing != null:
|
||||||
_surface_drawing.deactivate()
|
_surface_drawing.deactivate()
|
||||||
_gameplay_transient_hud.visible = _gameplay_ui_enabled and not is_open
|
_gameplay_transient_hud.visible = _gameplay_ui_enabled and not is_open
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ const PlayerCoolerCapacityType = preload(
|
||||||
const PlayerExperienceType = preload(
|
const PlayerExperienceType = preload(
|
||||||
"res://progression/player_experience.gd"
|
"res://progression/player_experience.gd"
|
||||||
)
|
)
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
const FishBatchSelectionType = preload(
|
const FishBatchSelectionType = preload(
|
||||||
"res://ui/fish_batch_selection.gd"
|
"res://ui/fish_batch_selection.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -280,6 +283,7 @@ var _network_mail_service: NetworkMailService
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
var _network_profile_service: NetworkProfileService
|
var _network_profile_service: NetworkProfileService
|
||||||
var _network_player_list: NetworkPlayerListService
|
var _network_player_list: NetworkPlayerListService
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
var _default_buyer: FishBuyerProfileType
|
var _default_buyer: FishBuyerProfileType
|
||||||
var _sale_buyer_override: FishBuyerProfileType
|
var _sale_buyer_override: FishBuyerProfileType
|
||||||
var _shop_cooler_context_active: bool = false
|
var _shop_cooler_context_active: bool = false
|
||||||
|
|
@ -564,9 +568,19 @@ func setup(
|
||||||
_refresh_all()
|
_refresh_all()
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
_profile_page.setup_controller_mapping(_controller_mapping_manager)
|
||||||
|
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.echo:
|
if event is InputEventKey and event.echo:
|
||||||
return
|
return
|
||||||
|
if _handle_controller_page_switch(event):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if _handle_direct_page_shortcut(event):
|
if _handle_direct_page_shortcut(event):
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -585,6 +599,71 @@ func _input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_controller_page_switch(event: InputEvent) -> bool:
|
||||||
|
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||||
|
var use_mapping: bool = (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
)
|
||||||
|
var uses_left_bumper: bool = (
|
||||||
|
_controller_mapping_manager.event_uses_role(
|
||||||
|
event, ControllerMappingManagerType.ROLE_LB
|
||||||
|
)
|
||||||
|
if use_mapping
|
||||||
|
else (
|
||||||
|
button_event != null
|
||||||
|
and button_event.button_index == JOY_BUTTON_LEFT_SHOULDER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var uses_right_bumper: bool = (
|
||||||
|
_controller_mapping_manager.event_uses_role(
|
||||||
|
event, ControllerMappingManagerType.ROLE_RB
|
||||||
|
)
|
||||||
|
if use_mapping
|
||||||
|
else (
|
||||||
|
button_event != null
|
||||||
|
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
button_event == null
|
||||||
|
or not (uses_left_bumper or uses_right_bumper)
|
||||||
|
or not visible
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if not button_event.pressed:
|
||||||
|
return true
|
||||||
|
# Shoulder input belongs to the Player Menu while it is visible, even when
|
||||||
|
# a transition or modal temporarily prevents changing pages. This keeps LB
|
||||||
|
# from opening Chat and RB from leaking into gameplay behind the menu.
|
||||||
|
if (
|
||||||
|
_transitioning
|
||||||
|
or _page_transitioning
|
||||||
|
or _sale_confirmation.visible
|
||||||
|
or get_viewport().gui_is_dragging()
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
var sections: Array[Section] = [
|
||||||
|
_last_inventory_section,
|
||||||
|
Section.LOGBOOK,
|
||||||
|
Section.NET,
|
||||||
|
Section.MAIL,
|
||||||
|
Section.PROFILE,
|
||||||
|
Section.PLAYERS,
|
||||||
|
]
|
||||||
|
var current_index: int = 0
|
||||||
|
if not _is_inventory_section(_current_section):
|
||||||
|
current_index = sections.find(_current_section)
|
||||||
|
if current_index < 0:
|
||||||
|
current_index = 0
|
||||||
|
var direction: int = (
|
||||||
|
-1 if uses_left_bumper else 1
|
||||||
|
)
|
||||||
|
var next_index: int = wrapi(current_index + direction, 0, sections.size())
|
||||||
|
_show_section(sections[next_index])
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
func _handle_direct_page_shortcut(event: InputEvent) -> bool:
|
||||||
var key_event := event as InputEventKey
|
var key_event := event as InputEventKey
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ class_name ProfilePage
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
|
||||||
var _service: NetworkProfileService
|
var _service: NetworkProfileService
|
||||||
var _experience: PlayerExperience
|
var _experience: PlayerExperience
|
||||||
|
|
@ -70,12 +73,20 @@ func setup(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
if _preview != null:
|
||||||
|
_preview.setup_controller_mapping(mapping_manager)
|
||||||
|
|
||||||
|
|
||||||
func activate() -> void:
|
func activate() -> void:
|
||||||
visible = true
|
visible = true
|
||||||
UtilityPageStyle.animate_in(self)
|
UtilityPageStyle.animate_in(self)
|
||||||
if _service != null:
|
if _service != null:
|
||||||
_load_persisted()
|
_load_persisted()
|
||||||
_name_edit.grab_focus()
|
# Keep controller page switches on the Profile navigation bubble. Focusing
|
||||||
|
# the name field here summons the Android keyboard during LB/RB traversal.
|
||||||
|
|
||||||
|
|
||||||
func deactivate() -> void:
|
func deactivate() -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
class_name ProfilePreview
|
class_name ProfilePreview
|
||||||
extends SubViewportContainer
|
extends SubViewportContainer
|
||||||
|
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
|
||||||
@export_range(0.1, 2.0, 0.05) var drag_sensitivity: float = 0.012
|
@export_range(0.1, 2.0, 0.05) var drag_sensitivity: float = 0.012
|
||||||
@export_range(0.1, 4.0, 0.1) var keyboard_speed: float = 1.8
|
@export_range(0.1, 4.0, 0.1) var keyboard_speed: float = 1.8
|
||||||
|
|
||||||
|
|
@ -8,6 +12,13 @@ extends SubViewportContainer
|
||||||
|
|
||||||
var _dragging: bool = false
|
var _dragging: bool = false
|
||||||
var _visuals: Node3D
|
var _visuals: Node3D
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -36,7 +47,16 @@ func _process(delta: float) -> void:
|
||||||
if not has_focus():
|
if not has_focus():
|
||||||
return
|
return
|
||||||
var axis := Input.get_axis("ui_left", "ui_right")
|
var axis := Input.get_axis("ui_left", "ui_right")
|
||||||
var right_stick := Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
|
var right_stick: float = (
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
)
|
||||||
|
else Input.get_joy_axis(0, JOY_AXIS_RIGHT_X)
|
||||||
|
)
|
||||||
if absf(right_stick) > 0.2:
|
if absf(right_stick) > 0.2:
|
||||||
axis = right_stick
|
axis = right_stick
|
||||||
if absf(axis) > 0.1:
|
if absf(axis) > 0.1:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ const SECTOR_COUNT: int = 8
|
||||||
const RING_RADIUS: float = 172.0
|
const RING_RADIUS: float = 172.0
|
||||||
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
||||||
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
const ACTIONS: Array[StringName] = [
|
const ACTIONS: Array[StringName] = [
|
||||||
&"stuff",
|
&"stuff",
|
||||||
&"logbook",
|
&"logbook",
|
||||||
|
|
@ -38,6 +41,13 @@ var _buttons: Array[BubbleButton] = []
|
||||||
var _is_open: bool = false
|
var _is_open: bool = false
|
||||||
var _selected_sector: int = 0
|
var _selected_sector: int = 0
|
||||||
var _controller_selection_mode: bool = false
|
var _controller_selection_mode: bool = false
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -114,10 +124,7 @@ func _layout_bubbles() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _update_selection() -> void:
|
func _update_selection() -> void:
|
||||||
var stick: Vector2 = Vector2(
|
var stick: Vector2 = _get_selection_stick()
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
|
||||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
|
||||||
)
|
|
||||||
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
||||||
_select_sector_from_offset(stick)
|
_select_sector_from_offset(stick)
|
||||||
return
|
return
|
||||||
|
|
@ -126,6 +133,25 @@ func _update_selection() -> void:
|
||||||
_update_mouse_selection()
|
_update_mouse_selection()
|
||||||
|
|
||||||
|
|
||||||
|
func _get_selection_stick() -> Vector2:
|
||||||
|
if (
|
||||||
|
_controller_mapping_manager != null
|
||||||
|
and _controller_mapping_manager.has_custom_mapping()
|
||||||
|
):
|
||||||
|
return Vector2(
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_X
|
||||||
|
),
|
||||||
|
_controller_mapping_manager.get_role_axis(
|
||||||
|
ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Vector2(
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||||
|
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _update_mouse_selection() -> void:
|
func _update_mouse_selection() -> void:
|
||||||
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
||||||
if offset.length() < 24.0:
|
if offset.length() < 24.0:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ const PIXELATION_NAMES: PackedStringArray = [
|
||||||
const SettingsManagerType = preload(
|
const SettingsManagerType = preload(
|
||||||
"res://settings/player_settings_manager.gd"
|
"res://settings/player_settings_manager.gd"
|
||||||
)
|
)
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
|
const ControllerMappingPanelType = preload(
|
||||||
|
"res://ui/controller_mapping_panel.gd"
|
||||||
|
)
|
||||||
|
|
||||||
signal applied
|
signal applied
|
||||||
signal closed
|
signal closed
|
||||||
|
|
@ -42,6 +48,7 @@ enum PresentationMode {
|
||||||
@onready var _world_value: BubbleButton = %WorldValue
|
@onready var _world_value: BubbleButton = %WorldValue
|
||||||
@onready var _ui_value: BubbleButton = %UIValue
|
@onready var _ui_value: BubbleButton = %UIValue
|
||||||
@onready var _chat_dock: BubbleButton = %ChatDock
|
@onready var _chat_dock: BubbleButton = %ChatDock
|
||||||
|
@onready var _chat_mode: BubbleButton = %ChatMode
|
||||||
@onready var _paint_dock: BubbleButton = %PaintDock
|
@onready var _paint_dock: BubbleButton = %PaintDock
|
||||||
@onready var _mouse_value: BubbleButton = %MouseValue
|
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||||
|
|
@ -84,6 +91,8 @@ var _identity_backups: IdentityBackupService
|
||||||
var _player_identity: PlayerIdentityStore
|
var _player_identity: PlayerIdentityStore
|
||||||
var _host_identity: HostIdentityStore
|
var _host_identity: HostIdentityStore
|
||||||
var _interface_fonts: InterfaceFontController
|
var _interface_fonts: InterfaceFontController
|
||||||
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
|
var _controller_mapping_panel: ControllerMappingPanelType
|
||||||
var _data_folder_dialog: FileDialog
|
var _data_folder_dialog: FileDialog
|
||||||
var _backup_file_dialog: FileDialog
|
var _backup_file_dialog: FileDialog
|
||||||
var _export_file_dialog: FileDialog
|
var _export_file_dialog: FileDialog
|
||||||
|
|
@ -114,6 +123,7 @@ func _ready() -> void:
|
||||||
%RootBackButton.pressed.connect(close_panel)
|
%RootBackButton.pressed.connect(close_panel)
|
||||||
%DisplayBackButton.pressed.connect(handle_back)
|
%DisplayBackButton.pressed.connect(handle_back)
|
||||||
%ControlsBackButton.pressed.connect(handle_back)
|
%ControlsBackButton.pressed.connect(handle_back)
|
||||||
|
%ControllerMapping.pressed.connect(_open_controller_mapping)
|
||||||
%AccessibilityBackButton.pressed.connect(handle_back)
|
%AccessibilityBackButton.pressed.connect(handle_back)
|
||||||
%DataBackButton.pressed.connect(handle_back)
|
%DataBackButton.pressed.connect(handle_back)
|
||||||
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||||
|
|
@ -136,6 +146,7 @@ func _ready() -> void:
|
||||||
_set_ui_pixelation.bind(index + 1)
|
_set_ui_pixelation.bind(index + 1)
|
||||||
)
|
)
|
||||||
_chat_dock.pressed.connect(_toggle_chat_dock)
|
_chat_dock.pressed.connect(_toggle_chat_dock)
|
||||||
|
_chat_mode.pressed.connect(_toggle_chat_mode)
|
||||||
_paint_dock.pressed.connect(_toggle_paint_dock)
|
_paint_dock.pressed.connect(_toggle_paint_dock)
|
||||||
%MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1))
|
%MouseDecrease.pressed.connect(_adjust_mouse_sensitivity.bind(-1))
|
||||||
%MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1))
|
%MouseIncrease.pressed.connect(_adjust_mouse_sensitivity.bind(1))
|
||||||
|
|
@ -178,6 +189,11 @@ func _ready() -> void:
|
||||||
parent_control.resized.connect(_refresh_panel_size)
|
parent_control.resized.connect(_refresh_panel_size)
|
||||||
for page: SettingsBubblePage in _pages.values():
|
for page: SettingsBubblePage in _pages.values():
|
||||||
page.hide_page()
|
page.hide_page()
|
||||||
|
_controller_mapping_panel = ControllerMappingPanelType.new()
|
||||||
|
add_child(_controller_mapping_panel)
|
||||||
|
_controller_mapping_panel.closed.connect(
|
||||||
|
_on_controller_mapping_panel_closed
|
||||||
|
)
|
||||||
_style_data_page()
|
_style_data_page()
|
||||||
call_deferred("_refresh_panel_size")
|
call_deferred("_refresh_panel_size")
|
||||||
|
|
||||||
|
|
@ -190,6 +206,13 @@ func setup_network_profile(
|
||||||
_network_session = session
|
_network_session = session
|
||||||
|
|
||||||
|
|
||||||
|
func setup_controller_mapping(
|
||||||
|
mapping_manager: ControllerMappingManagerType,
|
||||||
|
) -> void:
|
||||||
|
_controller_mapping_manager = mapping_manager
|
||||||
|
_controller_mapping_panel.setup(_controller_mapping_manager)
|
||||||
|
|
||||||
|
|
||||||
func setup_data_and_identity(
|
func setup_data_and_identity(
|
||||||
data_root: PlayerDataRoot,
|
data_root: PlayerDataRoot,
|
||||||
identity_backups: IdentityBackupService,
|
identity_backups: IdentityBackupService,
|
||||||
|
|
@ -263,6 +286,11 @@ func close_panel(immediate: bool = false) -> void:
|
||||||
|
|
||||||
func _finish_panel_close(applied_result: bool) -> void:
|
func _finish_panel_close(applied_result: bool) -> void:
|
||||||
_cancel_page_transition()
|
_cancel_page_transition()
|
||||||
|
if (
|
||||||
|
_controller_mapping_panel != null
|
||||||
|
and _controller_mapping_panel.is_open()
|
||||||
|
):
|
||||||
|
_controller_mapping_panel.close_panel()
|
||||||
for page: SettingsBubblePage in _pages.values():
|
for page: SettingsBubblePage in _pages.values():
|
||||||
page.hide_page()
|
page.hide_page()
|
||||||
_page_stack.clear()
|
_page_stack.clear()
|
||||||
|
|
@ -276,6 +304,12 @@ func _finish_panel_close(applied_result: bool) -> void:
|
||||||
|
|
||||||
|
|
||||||
func handle_back() -> void:
|
func handle_back() -> void:
|
||||||
|
if (
|
||||||
|
_controller_mapping_panel != null
|
||||||
|
and _controller_mapping_panel.is_open()
|
||||||
|
):
|
||||||
|
_controller_mapping_panel.request_back()
|
||||||
|
return
|
||||||
if _page_transition_active:
|
if _page_transition_active:
|
||||||
return
|
return
|
||||||
if _page_stack.size() > 1:
|
if _page_stack.size() > 1:
|
||||||
|
|
@ -284,10 +318,28 @@ func handle_back() -> void:
|
||||||
close_panel()
|
close_panel()
|
||||||
|
|
||||||
|
|
||||||
|
func _open_controller_mapping() -> void:
|
||||||
|
if _controller_mapping_manager == null:
|
||||||
|
_feedback.text = "no controller mapping service is available"
|
||||||
|
return
|
||||||
|
_controller_mapping_panel.open_panel()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_controller_mapping_panel_closed() -> void:
|
||||||
|
%ControllerMapping.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
func get_active_page_id() -> StringName:
|
func get_active_page_id() -> StringName:
|
||||||
return _page_stack.back() if not _page_stack.is_empty() else StringName()
|
return _page_stack.back() if not _page_stack.is_empty() else StringName()
|
||||||
|
|
||||||
|
|
||||||
|
func is_controller_mapping_capturing() -> bool:
|
||||||
|
return (
|
||||||
|
_controller_mapping_panel != null
|
||||||
|
and _controller_mapping_panel.is_capturing()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _push_page(page_id: StringName) -> void:
|
func _push_page(page_id: StringName) -> void:
|
||||||
if (
|
if (
|
||||||
_page_transition_active
|
_page_transition_active
|
||||||
|
|
@ -689,6 +741,9 @@ func _refresh_value_labels() -> void:
|
||||||
+ _pixel_size_label(settings.ui_pixel_size)
|
+ _pixel_size_label(settings.ui_pixel_size)
|
||||||
)
|
)
|
||||||
_chat_dock.text = "chat dock\n" + ("right" if settings.chat_dock_right else "left")
|
_chat_dock.text = "chat dock\n" + ("right" if settings.chat_dock_right else "left")
|
||||||
|
_chat_mode.text = (
|
||||||
|
"chat mode\n" + ("mobile" if settings.chat_mobile_mode else "desktop")
|
||||||
|
)
|
||||||
_paint_dock.text = (
|
_paint_dock.text = (
|
||||||
"paint dock\n" + ("right" if settings.paint_dock_right else "left")
|
"paint dock\n" + ("right" if settings.paint_dock_right else "left")
|
||||||
)
|
)
|
||||||
|
|
@ -774,6 +829,17 @@ func _toggle_chat_dock() -> void:
|
||||||
_refresh_value_labels()
|
_refresh_value_labels()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_chat_mode() -> void:
|
||||||
|
if _settings_manager == null:
|
||||||
|
return
|
||||||
|
var edited: PlayerSettings = _settings_manager.current_settings.copy()
|
||||||
|
edited.chat_mobile_mode = not edited.chat_mobile_mode
|
||||||
|
if not _settings_manager.apply_settings(edited):
|
||||||
|
_feedback.text = "failed to save chat mode setting."
|
||||||
|
return
|
||||||
|
_refresh_value_labels()
|
||||||
|
|
||||||
|
|
||||||
func _toggle_paint_dock() -> void:
|
func _toggle_paint_dock() -> void:
|
||||||
if _settings_manager == null:
|
if _settings_manager == null:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -157,8 +157,8 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("3_page")
|
script = ExtResource("3_page")
|
||||||
page_id = &"display"
|
page_id = &"display"
|
||||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
|
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/WorldValue"), NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UIValue"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
|
||||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
|
focus_paths = Array[NodePath]([NodePath("BubbleCluster/WorldLegible"), NodePath("BubbleCluster/WorldCute"), NodePath("BubbleCluster/WorldRetro"), NodePath("BubbleCluster/WorldHardcore"), NodePath("BubbleCluster/WorldWtf"), NodePath("BubbleCluster/UILegible"), NodePath("BubbleCluster/UICute"), NodePath("BubbleCluster/UIRetro"), NodePath("BubbleCluster/UIHardcore"), NodePath("BubbleCluster/UIWtf"), NodePath("BubbleCluster/ChatDock"), NodePath("BubbleCluster/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
|
||||||
initial_focus_path = NodePath("BubbleCluster/WorldRetro")
|
initial_focus_path = NodePath("BubbleCluster/WorldRetro")
|
||||||
back_focus_path = NodePath("BubbleCluster/DisplayBackButton")
|
back_focus_path = NodePath("BubbleCluster/DisplayBackButton")
|
||||||
compact_maximum_layout_size = Vector2(544, 400)
|
compact_maximum_layout_size = Vector2(544, 400)
|
||||||
|
|
@ -337,36 +337,48 @@ unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "chat dock\nleft"
|
text = "chat dock\nleft"
|
||||||
neutral_size = Vector2(96, 96)
|
neutral_size = Vector2(96, 96)
|
||||||
desktop_anchor = Vector2(190, 435)
|
desktop_anchor = Vector2(105, 435)
|
||||||
compact_anchor = Vector2(160, 378)
|
compact_anchor = Vector2(76, 378)
|
||||||
compact_minimum_size = Vector2(82, 82)
|
compact_minimum_size = Vector2(82, 82)
|
||||||
minimum_font_size = 12
|
minimum_font_size = 12
|
||||||
maximum_font_size = 16
|
maximum_font_size = 16
|
||||||
motion_phase = 1.1
|
motion_phase = 1.1
|
||||||
|
|
||||||
|
[node name="ChatMode" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 0)
|
||||||
|
text = "chat mode\ndesktop"
|
||||||
|
neutral_size = Vector2(96, 96)
|
||||||
|
desktop_anchor = Vector2(275, 435)
|
||||||
|
compact_anchor = Vector2(225, 378)
|
||||||
|
compact_minimum_size = Vector2(82, 82)
|
||||||
|
minimum_font_size = 12
|
||||||
|
maximum_font_size = 16
|
||||||
|
motion_phase = 1.4
|
||||||
|
|
||||||
[node name="PaintDock" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
[node name="PaintDock" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "paint dock\nright"
|
text = "paint dock\nright"
|
||||||
neutral_size = Vector2(96, 96)
|
neutral_size = Vector2(96, 96)
|
||||||
desktop_anchor = Vector2(530, 435)
|
desktop_anchor = Vector2(445, 435)
|
||||||
compact_anchor = Vector2(448, 378)
|
compact_anchor = Vector2(380, 378)
|
||||||
compact_minimum_size = Vector2(82, 82)
|
compact_minimum_size = Vector2(82, 82)
|
||||||
minimum_font_size = 12
|
minimum_font_size = 12
|
||||||
maximum_font_size = 16
|
maximum_font_size = 16
|
||||||
motion_phase = 1.65
|
motion_phase = 1.7
|
||||||
|
|
||||||
[node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
[node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "back"
|
text = "back"
|
||||||
neutral_size = Vector2(96, 90)
|
neutral_size = Vector2(96, 90)
|
||||||
desktop_anchor = Vector2(350, 445)
|
desktop_anchor = Vector2(615, 435)
|
||||||
compact_anchor = Vector2(315, 390)
|
compact_anchor = Vector2(530, 378)
|
||||||
compact_minimum_size = Vector2(82, 78)
|
compact_minimum_size = Vector2(82, 78)
|
||||||
minimum_font_size = 13
|
minimum_font_size = 13
|
||||||
maximum_font_size = 16
|
maximum_font_size = 16
|
||||||
motion_phase = 2.2
|
motion_phase = 2.0
|
||||||
|
|
||||||
[node name="ControlsPage" type="Control" parent="."]
|
[node name="ControlsPage" type="Control" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
@ -379,8 +391,8 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("3_page")
|
script = ExtResource("3_page")
|
||||||
page_id = &"controls"
|
page_id = &"controls"
|
||||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")])
|
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControlsBackButton")])
|
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||||
|
|
||||||
|
|
@ -470,24 +482,36 @@ unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "invert\nvertical\ncamera\noff"
|
text = "invert\nvertical\ncamera\noff"
|
||||||
neutral_size = Vector2(176, 170)
|
neutral_size = Vector2(176, 170)
|
||||||
desktop_anchor = Vector2(525, 230)
|
|
||||||
compact_anchor = Vector2(485, 225)
|
|
||||||
compact_minimum_size = Vector2(170, 164)
|
compact_minimum_size = Vector2(170, 164)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 24
|
maximum_font_size = 24
|
||||||
|
desktop_anchor = Vector2(525, 145)
|
||||||
|
compact_anchor = Vector2(485, 135)
|
||||||
motion_phase = 4.5
|
motion_phase = 4.5
|
||||||
|
|
||||||
|
[node name="ControllerMapping" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 0)
|
||||||
|
text = "map\ncontroller"
|
||||||
|
neutral_size = Vector2(160, 150)
|
||||||
|
desktop_anchor = Vector2(525, 330)
|
||||||
|
compact_anchor = Vector2(485, 300)
|
||||||
|
compact_minimum_size = Vector2(148, 138)
|
||||||
|
minimum_font_size = 14
|
||||||
|
maximum_font_size = 23
|
||||||
|
motion_phase = 4.9
|
||||||
|
|
||||||
[node name="ControlsBackButton" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
[node name="ControlsBackButton" parent="ControlsPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 0)
|
custom_minimum_size = Vector2(0, 0)
|
||||||
text = "back"
|
text = "back"
|
||||||
neutral_size = Vector2(100, 94)
|
neutral_size = Vector2(100, 94)
|
||||||
desktop_anchor = Vector2(525, 425)
|
desktop_anchor = Vector2(635, 445)
|
||||||
compact_anchor = Vector2(540, 395)
|
compact_anchor = Vector2(555, 405)
|
||||||
compact_minimum_size = Vector2(84, 80)
|
compact_minimum_size = Vector2(84, 80)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 14
|
||||||
maximum_font_size = 17
|
maximum_font_size = 17
|
||||||
motion_phase = 5.2
|
motion_phase = 5.5
|
||||||
|
|
||||||
[node name="AccessibilityPage" type="Control" parent="."]
|
[node name="AccessibilityPage" type="Control" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
|
||||||
|
|
@ -490,6 +490,11 @@ func _process(delta: float) -> void:
|
||||||
func _input(event: InputEvent) -> void:
|
func _input(event: InputEvent) -> void:
|
||||||
if not visible:
|
if not visible:
|
||||||
return
|
return
|
||||||
|
if (
|
||||||
|
_settings_panel.visible
|
||||||
|
and _settings_panel.is_controller_mapping_capturing()
|
||||||
|
):
|
||||||
|
return
|
||||||
if _join_game_page.visible:
|
if _join_game_page.visible:
|
||||||
if event.is_action_pressed("ui_cancel"):
|
if event.is_action_pressed("ui_cancel"):
|
||||||
_close_join_game()
|
_close_join_game()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue