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"
|
||||
)
|
||||
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 PauseMenuType = preload("res://ui/pause_menu.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 _save_manager: PlayerSaveManagerType = %PlayerSaveManager
|
||||
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
|
||||
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
|
||||
%ControllerMappingManager
|
||||
)
|
||||
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
|
||||
@onready var _title_music: AudioStreamPlayer = %TitleMusic
|
||||
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
|
||||
|
|
@ -534,6 +540,7 @@ func _initialize_after_data_root() -> void:
|
|||
_network_session,
|
||||
_interface_fonts,
|
||||
)
|
||||
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||
_data_root.conflict_detected.connect(_on_portable_conflict)
|
||||
_data_root.status_changed.connect(_on_data_root_status)
|
||||
if (
|
||||
|
|
@ -927,6 +934,8 @@ func _on_peer_identity_observed(_peer_id: int, status: String) -> void:
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _game_ui.is_controller_mapping_capturing():
|
||||
return
|
||||
if (
|
||||
not (
|
||||
event.is_action_pressed("ui_cancel")
|
||||
|
|
@ -935,6 +944,7 @@ func _input(event: InputEvent) -> void:
|
|||
or (event is InputEventKey and event.echo)
|
||||
):
|
||||
return
|
||||
var pause_open_requested: bool = _is_pause_open_request(event)
|
||||
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
||||
if title_screen.visible or not _gameplay_started:
|
||||
return
|
||||
|
|
@ -953,7 +963,8 @@ func _input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
not _water_recovery.is_recovery_active()
|
||||
pause_open_requested
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_system_menu()
|
||||
):
|
||||
_game_ui.close_player_menu_for_game_menu()
|
||||
|
|
@ -961,6 +972,13 @@ func _input(event: InputEvent) -> void:
|
|||
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:
|
||||
if (
|
||||
not _gameplay_started
|
||||
|
|
@ -1002,6 +1020,7 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
|||
_game_ui.set_edge_docks(
|
||||
settings.chat_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://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
|
||||
[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"]
|
||||
shader = ExtResource("40_title_water")
|
||||
|
|
@ -273,6 +274,10 @@ script = ExtResource("9_save")
|
|||
unique_name_in_owner = true
|
||||
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]
|
||||
unique_name_in_owner = true
|
||||
stream = ExtResource("13_title_music")
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ const PlayerArtUnlocksType = preload(
|
|||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const FishingRodAttachmentScene = preload(
|
||||
"res://player/fishing_rod_attachment.tscn"
|
||||
)
|
||||
|
|
@ -32,6 +35,10 @@ const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
|||
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
||||
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
||||
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 = (
|
||||
CharacterCustomizationCatalog.default_snapshot()
|
||||
|
|
@ -160,6 +167,7 @@ var _character_animation_name: StringName = &""
|
|||
var _sitting: bool = false
|
||||
var _fishing_rod: Node3D
|
||||
var _fishing_rod_tip: Marker3D
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -169,6 +177,12 @@ func _ready() -> void:
|
|||
_camera.current = local_control_enabled
|
||||
|
||||
|
||||
func set_controller_mapping_manager(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func _initialize_fishing_rod() -> void:
|
||||
var skeleton := get_node_or_null(
|
||||
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||
|
|
@ -287,13 +301,10 @@ func _process(delta: float) -> void:
|
|||
_camera_dragging = false
|
||||
|
||||
if _camera_input_enabled:
|
||||
var stick: Vector2 = Vector2(
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
|
||||
)
|
||||
var stick: Vector2 = _get_controller_camera_stick()
|
||||
if stick.length() > controller_camera_deadzone:
|
||||
if (
|
||||
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_RIGHT)
|
||||
_get_controller_zoom_strength()
|
||||
>= controller_trigger_threshold
|
||||
):
|
||||
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:
|
||||
if _character_animation_player == null:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ hotbar_next={
|
|||
open_backpack={
|
||||
"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)
|
||||
, 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={
|
||||
|
|
@ -183,6 +183,11 @@ open_quick_actions={
|
|||
}
|
||||
open_chat={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
|
|
|
|||
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_collapsed: bool = false
|
||||
@export var chat_dock_right: bool = false
|
||||
@export var chat_mobile_mode: bool = false
|
||||
@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 ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
|
||||
|
|
@ -65,6 +66,7 @@ func copy() -> PlayerSettings:
|
|||
result.chat_draft = chat_draft
|
||||
result.chat_collapsed = chat_collapsed
|
||||
result.chat_dock_right = chat_dock_right
|
||||
result.chat_mobile_mode = chat_mobile_mode
|
||||
result.paint_dock_right = paint_dock_right
|
||||
result.world_pixel_size = world_pixel_size
|
||||
result.ui_pixel_size = ui_pixel_size
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ func load_settings() -> bool:
|
|||
presentation.has("chat_dock_right")
|
||||
and typeof(presentation["chat_dock_right"]) != TYPE_BOOL
|
||||
)
|
||||
or (
|
||||
presentation.has("chat_mobile_mode")
|
||||
and typeof(presentation["chat_mobile_mode"]) != TYPE_BOOL
|
||||
)
|
||||
or (
|
||||
presentation.has("paint_dock_right")
|
||||
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_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))
|
||||
if not loaded.is_valid():
|
||||
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_collapsed": current_settings.chat_collapsed,
|
||||
"chat_dock_right": current_settings.chat_dock_right,
|
||||
"chat_mobile_mode": current_settings.chat_mobile_mode,
|
||||
"paint_dock_right": current_settings.paint_dock_right,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
extends SceneTree
|
||||
|
||||
const GameUIType = preload("res://ui/game_ui.gd")
|
||||
const PlayerType = preload("res://player/player.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
assert(
|
||||
|
|
@ -34,11 +37,50 @@ func _init() -> void:
|
|||
))
|
||||
assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT))
|
||||
assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT))
|
||||
assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_BACK))
|
||||
assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_X))
|
||||
assert(_has_joypad_button(&"open_system_menu", JOY_BUTTON_START))
|
||||
assert(_has_joypad_button(&"open_emotes", JOY_BUTTON_DPAD_UP))
|
||||
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
|
||||
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_LEFT_SHOULDER))
|
||||
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")
|
||||
quit()
|
||||
|
||||
|
|
|
|||
|
|
@ -37,17 +37,33 @@ func _run() -> void:
|
|||
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
||||
assert(player != null and service != null and toolbar != 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(
|
||||
"_pause_settings_panel"
|
||||
) as SettingsPanel
|
||||
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
|
||||
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(
|
||||
chat_dock_button.compact_minimum_size.x
|
||||
== chat_dock_button.compact_minimum_size.y
|
||||
)
|
||||
assert(
|
||||
chat_mode_button.compact_minimum_size.x
|
||||
== chat_mode_button.compact_minimum_size.y
|
||||
)
|
||||
assert(
|
||||
paint_dock_button.compact_minimum_size.x
|
||||
== paint_dock_button.compact_minimum_size.y
|
||||
|
|
@ -60,24 +76,85 @@ func _run() -> void:
|
|||
assert(settings_manager != null)
|
||||
var changed_docks: PlayerSettings = settings_manager.current_settings.copy()
|
||||
changed_docks.chat_dock_right = true
|
||||
changed_docks.chat_mobile_mode = true
|
||||
changed_docks.paint_dock_right = false
|
||||
assert(settings_manager.apply_settings(changed_docks))
|
||||
await process_frame
|
||||
assert(chat_ui.is_docked_right())
|
||||
assert(chat_ui.is_mobile_mode())
|
||||
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()
|
||||
root.add_child(reloaded_settings)
|
||||
assert(reloaded_settings.load_settings())
|
||||
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)
|
||||
reloaded_settings.queue_free()
|
||||
var default_docks: PlayerSettings = settings_manager.current_settings.copy()
|
||||
default_docks.chat_dock_right = false
|
||||
default_docks.chat_mobile_mode = false
|
||||
default_docks.paint_dock_right = true
|
||||
assert(settings_manager.apply_settings(default_docks))
|
||||
await process_frame
|
||||
assert(not chat_ui.is_docked_right())
|
||||
assert(not chat_ui.is_mobile_mode())
|
||||
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.is_active() and not toolbar.visible)
|
||||
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 COLLAPSED_REVEAL_WIDTH: float = 8.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_EDGE_MARGIN: float = 10.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 _prior_movement: bool = true
|
||||
var _prior_camera: bool = true
|
||||
var _controller_refocused: bool = false
|
||||
var _output_scale: float = 1.0
|
||||
var _dock_right: bool = false
|
||||
var _mobile_mode: bool = false
|
||||
var _world_time: WorldTimeServiceType
|
||||
var _world_weather: WorldWeatherServiceType
|
||||
|
||||
|
|
@ -125,6 +132,7 @@ func setup(
|
|||
_world_weather.get_weather(),
|
||||
_world_weather.get_seconds_remaining(),
|
||||
)
|
||||
set_mobile_mode(_settings.current_settings.chat_mobile_mode)
|
||||
set_dock_right(_settings.current_settings.chat_dock_right)
|
||||
_service.message_received.connect(_on_message)
|
||||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||
|
|
@ -151,6 +159,7 @@ func open_chat() -> void:
|
|||
return
|
||||
if _presentation_state == PresentationState.COLLAPSED:
|
||||
_set_presentation_state(_visible_state_before_collapse, true, true)
|
||||
_controller_refocused = false
|
||||
_opened = true
|
||||
text_entry_ownership_changed.emit(true)
|
||||
_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.
|
||||
_session.submit_neutral_local_movement()
|
||||
_entry.show()
|
||||
_entry.virtual_keyboard_enabled = false
|
||||
_entry.grab_focus()
|
||||
_hint.hide()
|
||||
_refresh_input_ownership()
|
||||
_update_panel_opacity(true)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
|
|
@ -184,6 +195,7 @@ func close_chat() -> void:
|
|||
return
|
||||
_opened = false
|
||||
text_entry_ownership_changed.emit(false)
|
||||
_entry.virtual_keyboard_enabled = false
|
||||
_entry.release_focus()
|
||||
_entry.hide()
|
||||
_player.set_movement_enabled(_prior_movement)
|
||||
|
|
@ -191,14 +203,62 @@ func close_chat() -> void:
|
|||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||
_flush_draft()
|
||||
_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:
|
||||
if event.is_action_pressed("open_chat"):
|
||||
if _opened:
|
||||
close_chat()
|
||||
elif _available:
|
||||
open_chat()
|
||||
toggle_chat()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
|
|
@ -404,14 +464,21 @@ func _build_ui() -> void:
|
|||
_unread_indicator.hide()
|
||||
_hint.hide()
|
||||
_layout_presentation(false)
|
||||
_refresh_input_ownership()
|
||||
|
||||
|
||||
func _chat_panel_style() -> StyleBoxFlat:
|
||||
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||
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
|
||||
if _mobile_mode:
|
||||
style.corner_radius_top_left = 0
|
||||
style.corner_radius_top_right = 0
|
||||
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_right = 12
|
||||
style.content_margin_top = 10
|
||||
|
|
@ -419,6 +486,18 @@ func _chat_panel_style() -> StyleBoxFlat:
|
|||
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:
|
||||
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||
style.set_corner_radius_all(12)
|
||||
|
|
@ -620,7 +699,10 @@ func _refresh_visibility() -> void:
|
|||
_collapse_button.show()
|
||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||
_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
|
||||
_hint.visible = not _opened
|
||||
|
||||
|
|
@ -670,10 +752,31 @@ func is_docked_right() -> bool:
|
|||
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:
|
||||
var collapsed := state == PresentationState.COLLAPSED
|
||||
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 ">"
|
||||
else:
|
||||
_collapse_button.text = ">" if collapsed else "<"
|
||||
|
|
@ -798,32 +901,101 @@ func _layout_presentation(animate: bool) -> void:
|
|||
var viewport_height := size.y
|
||||
if viewport_height <= 0.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(
|
||||
BOTTOM_MARGIN,
|
||||
maxf(MIN_TOP_MARGIN, (viewport_height - MIN_HISTORY_HEIGHT) * 0.25),
|
||||
)
|
||||
var bottom := viewport_height - bottom_margin
|
||||
var height := COMPACT_HEIGHT
|
||||
var layout_state := (
|
||||
_visible_state_before_collapse
|
||||
if _presentation_state == PresentationState.COLLAPSED
|
||||
else _presentation_state
|
||||
)
|
||||
if layout_state == PresentationState.EXPANDED:
|
||||
height = maxf(MIN_HISTORY_HEIGHT, viewport_height - 2.0 * bottom_margin)
|
||||
height = minf(height, maxf(MIN_HISTORY_HEIGHT, bottom - MIN_TOP_MARGIN))
|
||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||
var panel_x: float
|
||||
if _dock_right:
|
||||
panel_x = (
|
||||
viewport_width - COLLAPSED_REVEAL_WIDTH
|
||||
if collapsed
|
||||
else viewport_width - PANEL_WIDTH
|
||||
var panel_width := PANEL_WIDTH
|
||||
var panel_x: float = 0.0
|
||||
var panel_y: float = 0.0
|
||||
var collapse_position := Vector2.ZERO
|
||||
var height_position := Vector2.ZERO
|
||||
var unread_position := Vector2.ZERO
|
||||
if _mobile_mode:
|
||||
panel_width = (
|
||||
MOBILE_EXPANDED_WIDTH
|
||||
if layout_state == PresentationState.EXPANDED
|
||||
else MOBILE_COMPACT_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:
|
||||
panel_x = -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
|
||||
var target_position := Vector2(panel_x, bottom - height)
|
||||
var target_size := Vector2(PANEL_WIDTH, height)
|
||||
if layout_state == PresentationState.EXPANDED:
|
||||
height = maxf(
|
||||
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 = (
|
||||
viewport_width - CLOCK_SIZE.x - 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(
|
||||
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(
|
||||
(
|
||||
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 BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
||||
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
|
||||
var _buttons: Array[BubbleButton] = []
|
||||
var _is_open: bool = false
|
||||
var _selected_sector: int = SIT_SECTOR
|
||||
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:
|
||||
|
|
@ -92,10 +102,7 @@ func _layout_bubbles() -> void:
|
|||
|
||||
|
||||
func _update_selection() -> void:
|
||||
var stick: Vector2 = Vector2(
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||
)
|
||||
var stick: Vector2 = _get_selection_stick()
|
||||
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
||||
_select_sector_from_offset(stick)
|
||||
return
|
||||
|
|
@ -104,6 +111,25 @@ func _update_selection() -> void:
|
|||
_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:
|
||||
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
||||
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(
|
||||
"res://settings/player_settings_manager.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
|
|
@ -64,8 +67,15 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
|||
|
||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||
const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55
|
||||
const VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD: float = 0.35
|
||||
const VIRTUAL_MOUSE_STICK_DEADZONE: float = 0.18
|
||||
const VIRTUAL_MOUSE_SPEED: float = 720.0
|
||||
# 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 _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_button_mask: int = 0
|
||||
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:
|
||||
|
|
@ -294,21 +309,17 @@ func setup(
|
|||
set_edge_docks(
|
||||
settings_manager.current_settings.chat_dock_right,
|
||||
settings_manager.current_settings.paint_dock_right,
|
||||
settings_manager.current_settings.chat_mobile_mode,
|
||||
)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if (
|
||||
_virtual_mouse_active
|
||||
and _injecting_virtual_mouse_event
|
||||
and event is InputEventMouseMotion
|
||||
):
|
||||
_update_virtual_cursor_position(
|
||||
(event as InputEventMouseMotion).position
|
||||
)
|
||||
if _handle_virtual_mouse_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_controller_chat_controls(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_surface_drawing_toolbar != null
|
||||
and _surface_drawing_toolbar.owns_pointer_event(event)
|
||||
|
|
@ -378,6 +389,55 @@ func _input(event: InputEvent) -> void:
|
|||
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:
|
||||
if emote_id == &"sit" and _player != null:
|
||||
_player.toggle_sitting()
|
||||
|
|
@ -404,24 +464,59 @@ func _on_quick_action_selected(action_id: StringName) -> void:
|
|||
|
||||
|
||||
func _handle_virtual_mouse_input(event: InputEvent) -> bool:
|
||||
if (
|
||||
_controller_mapping_manager != null
|
||||
and _controller_mapping_manager.has_custom_mapping()
|
||||
):
|
||||
return _handle_mapped_virtual_mouse_input(event)
|
||||
var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
|
||||
if motion_event != null:
|
||||
if motion_event.axis == 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
|
||||
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():
|
||||
_begin_virtual_mouse()
|
||||
elif _virtual_mouse_active:
|
||||
_begin_virtual_mouse(motion_event.device)
|
||||
elif (
|
||||
_virtual_mouse_active
|
||||
and _virtual_mouse_trigger_strength
|
||||
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
|
||||
):
|
||||
_end_virtual_mouse()
|
||||
elif _virtual_mouse_active:
|
||||
_set_virtual_mouse_button(
|
||||
MOUSE_BUTTON_RIGHT,
|
||||
_secondary_click_strength_for_device(
|
||||
motion_event.device
|
||||
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
|
||||
)
|
||||
return was_active or _virtual_mouse_active
|
||||
if (
|
||||
_virtual_mouse_active
|
||||
and motion_event.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(
|
||||
MOUSE_BUTTON_RIGHT,
|
||||
motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
|
||||
)
|
||||
if motion_event.axis == JOY_AXIS_RIGHT_X:
|
||||
_virtual_mouse_stick.x = motion_event.axis_value
|
||||
else:
|
||||
_virtual_mouse_stick.y = motion_event.axis_value
|
||||
return true
|
||||
var button_event: InputEventJoypadButton = event as InputEventJoypadButton
|
||||
if (
|
||||
|
|
@ -429,34 +524,124 @@ func _handle_virtual_mouse_input(event: InputEvent) -> bool:
|
|||
and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER
|
||||
and (
|
||||
_virtual_mouse_active
|
||||
or Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
|
||||
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
||||
or _virtual_mouse_trigger_strength
|
||||
>= VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
||||
)
|
||||
):
|
||||
if not _virtual_mouse_active and _can_start_virtual_mouse():
|
||||
_begin_virtual_mouse()
|
||||
_begin_virtual_mouse(button_event.device)
|
||||
if _virtual_mouse_active:
|
||||
_set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _handle_mapped_virtual_mouse_input(event: InputEvent) -> bool:
|
||||
var uses_activation: bool = _controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_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:
|
||||
return (
|
||||
_gameplay_ui_enabled
|
||||
and not _showcase_active
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
and not _chat_input_open
|
||||
and _player != null
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_use_surface_drawing()
|
||||
and _fishing_spot.can_open_system_menu()
|
||||
and not _emote_radial_menu.is_open()
|
||||
and not _quick_radial_menu.is_open()
|
||||
)
|
||||
|
||||
|
||||
func _begin_virtual_mouse() -> void:
|
||||
func _begin_virtual_mouse(device_id: int) -> void:
|
||||
if _virtual_mouse_active:
|
||||
return
|
||||
_virtual_mouse_active = true
|
||||
_virtual_mouse_device_id = maxi(device_id, 0)
|
||||
_virtual_mouse_stick = (
|
||||
_mapped_virtual_mouse_stick()
|
||||
if (
|
||||
_controller_mapping_manager != null
|
||||
and _controller_mapping_manager.has_custom_mapping()
|
||||
)
|
||||
else Vector2(
|
||||
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_X),
|
||||
Input.get_joy_axis(_virtual_mouse_device_id, JOY_AXIS_RIGHT_Y),
|
||||
)
|
||||
)
|
||||
_virtual_mouse_prior_camera_input_enabled = (
|
||||
_player.is_camera_input_enabled()
|
||||
)
|
||||
|
|
@ -470,8 +655,11 @@ func _begin_virtual_mouse() -> void:
|
|||
Input.mouse_mode = Input.MOUSE_MODE_HIDDEN
|
||||
_virtual_mouse_button_mask = 0
|
||||
_virtual_mouse_right_pressed = false
|
||||
_virtual_mouse_window_position = Vector2(get_window().size) * 0.5
|
||||
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
|
||||
Vector2(get_window().size) * 0.5
|
||||
)
|
||||
_controller_virtual_cursor.visible = true
|
||||
_update_virtual_cursor_position(_virtual_mouse_window_position)
|
||||
_emit_virtual_mouse_motion(Vector2.ZERO)
|
||||
|
||||
|
||||
|
|
@ -483,6 +671,8 @@ func _end_virtual_mouse() -> void:
|
|||
if _virtual_mouse_right_pressed:
|
||||
_set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false)
|
||||
_virtual_mouse_active = false
|
||||
_virtual_mouse_trigger_strength = 0.0
|
||||
_virtual_mouse_stick = Vector2.ZERO
|
||||
_controller_virtual_cursor.visible = false
|
||||
if _fishing_spot != null:
|
||||
_fishing_spot.set_local_menu_input_suppressed(
|
||||
|
|
@ -500,15 +690,12 @@ func _update_virtual_mouse(delta: float) -> void:
|
|||
if not _virtual_mouse_active:
|
||||
return
|
||||
if (
|
||||
Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT)
|
||||
< VIRTUAL_MOUSE_TRIGGER_THRESHOLD
|
||||
_virtual_mouse_trigger_strength
|
||||
< VIRTUAL_MOUSE_TRIGGER_RELEASE_THRESHOLD
|
||||
):
|
||||
_end_virtual_mouse()
|
||||
return
|
||||
var stick: Vector2 = Vector2(
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||
)
|
||||
var stick: Vector2 = _virtual_mouse_stick
|
||||
var stick_length: float = stick.length()
|
||||
if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE:
|
||||
return
|
||||
|
|
@ -526,22 +713,166 @@ func _update_virtual_mouse(delta: float) -> void:
|
|||
* display_scale
|
||||
* delta
|
||||
)
|
||||
var window_size: Vector2 = Vector2(get_window().size)
|
||||
_virtual_mouse_window_position = Vector2(
|
||||
clampf(
|
||||
_virtual_mouse_window_position.x + relative_motion.x,
|
||||
0.0,
|
||||
window_size.x,
|
||||
),
|
||||
clampf(
|
||||
_virtual_mouse_window_position.y + relative_motion.y,
|
||||
0.0,
|
||||
window_size.y,
|
||||
),
|
||||
_virtual_mouse_window_position = _clamp_virtual_mouse_window_position(
|
||||
_virtual_mouse_window_position + relative_motion
|
||||
)
|
||||
_update_virtual_cursor_position(_virtual_mouse_window_position)
|
||||
_emit_virtual_mouse_motion(relative_motion)
|
||||
|
||||
|
||||
func _poll_virtual_mouse_controller_state() -> void:
|
||||
if (
|
||||
_controller_mapping_manager != null
|
||||
and _controller_mapping_manager.has_custom_mapping()
|
||||
):
|
||||
_poll_mapped_virtual_mouse_controller_state()
|
||||
return
|
||||
var device_ids: Array[int] = Input.get_connected_joypads()
|
||||
# Some Android controller backends deliver device-0 axes without including
|
||||
# that device in get_connected_joypads(). Player camera input already uses
|
||||
# this same primary-device fallback.
|
||||
if device_ids.is_empty():
|
||||
device_ids.append(0)
|
||||
for device_id: int in device_ids:
|
||||
_sample_trigger_rest_values(device_id)
|
||||
if _virtual_mouse_active:
|
||||
if not device_ids.has(_virtual_mouse_device_id):
|
||||
_end_virtual_mouse()
|
||||
return
|
||||
_virtual_mouse_trigger_strength = _virtual_mouse_strength_for_device(
|
||||
_virtual_mouse_device_id
|
||||
)
|
||||
_set_virtual_mouse_button(
|
||||
MOUSE_BUTTON_RIGHT,
|
||||
_secondary_click_strength_for_device(
|
||||
_virtual_mouse_device_id
|
||||
) >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD,
|
||||
)
|
||||
_virtual_mouse_stick = Vector2(
|
||||
Input.get_joy_axis(
|
||||
_virtual_mouse_device_id,
|
||||
JOY_AXIS_RIGHT_X,
|
||||
),
|
||||
Input.get_joy_axis(
|
||||
_virtual_mouse_device_id,
|
||||
JOY_AXIS_RIGHT_Y,
|
||||
),
|
||||
)
|
||||
return
|
||||
if not _can_start_virtual_mouse():
|
||||
return
|
||||
for device_id: int in device_ids:
|
||||
var trigger_strength: float = _virtual_mouse_strength_for_device(
|
||||
device_id
|
||||
)
|
||||
if trigger_strength < VIRTUAL_MOUSE_TRIGGER_THRESHOLD:
|
||||
continue
|
||||
_virtual_mouse_trigger_strength = trigger_strength
|
||||
_begin_virtual_mouse(device_id)
|
||||
return
|
||||
|
||||
|
||||
func _poll_mapped_virtual_mouse_controller_state() -> void:
|
||||
var trigger_strength: float = _controller_mapping_manager.get_role_strength(
|
||||
ControllerMappingManagerType.ROLE_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:
|
||||
var mask: int = (
|
||||
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:
|
||||
_injecting_virtual_mouse_event = true
|
||||
Input.parse_input_event(event)
|
||||
_injecting_virtual_mouse_event = false
|
||||
|
||||
|
||||
func _update_virtual_cursor_position(viewport_position: Vector2) -> void:
|
||||
var root_transform: Transform2D = _ui_root.get_global_transform_with_canvas()
|
||||
_controller_virtual_cursor.set_pointer_position(
|
||||
root_transform.affine_inverse() * viewport_position
|
||||
var output_scale: float = UIReferencePresentationType.get_scale(
|
||||
Vector2(get_window().size)
|
||||
)
|
||||
_controller_virtual_cursor.set_pointer_position(
|
||||
viewport_position / output_scale
|
||||
)
|
||||
|
||||
|
||||
func _clamp_virtual_mouse_window_position(
|
||||
window_position: Vector2,
|
||||
) -> Vector2:
|
||||
var bounds: Rect2 = get_virtual_mouse_window_bounds(
|
||||
Vector2(get_window().size)
|
||||
)
|
||||
return Vector2(
|
||||
clampf(window_position.x, bounds.position.x, bounds.end.x),
|
||||
clampf(window_position.y, bounds.position.y, bounds.end.y),
|
||||
)
|
||||
|
||||
|
||||
static func get_virtual_mouse_window_bounds(window_size: Vector2) -> Rect2:
|
||||
var output_scale: float = UIReferencePresentationType.get_scale(window_size)
|
||||
var cursor_margin: Vector2 = (
|
||||
ControllerVirtualCursorType.CURSOR_SIZE * output_scale * 0.5
|
||||
)
|
||||
var bounds_size: Vector2 = Vector2(
|
||||
maxf(window_size.x - cursor_margin.x * 2.0, 0.0),
|
||||
maxf(window_size.y - cursor_margin.y * 2.0, 0.0),
|
||||
)
|
||||
return Rect2(cursor_margin, bounds_size)
|
||||
|
||||
|
||||
func _toggle_surface_drawing() -> void:
|
||||
|
|
@ -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:
|
||||
_poll_virtual_mouse_controller_state()
|
||||
_update_virtual_mouse(delta)
|
||||
_update_experience_bubble_position()
|
||||
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)
|
||||
|
||||
|
||||
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_mobile_mode(chat_mobile_mode)
|
||||
_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:
|
||||
_player_menu_open = is_open
|
||||
if is_open:
|
||||
_end_virtual_mouse()
|
||||
if is_open and _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_gameplay_transient_hud.visible = _gameplay_ui_enabled and not is_open
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const PlayerCoolerCapacityType = preload(
|
|||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const FishBatchSelectionType = preload(
|
||||
"res://ui/fish_batch_selection.gd"
|
||||
)
|
||||
|
|
@ -280,6 +283,7 @@ var _network_mail_service: NetworkMailService
|
|||
var _reservations: PlayerAssetReservationService
|
||||
var _network_profile_service: NetworkProfileService
|
||||
var _network_player_list: NetworkPlayerListService
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _default_buyer: FishBuyerProfileType
|
||||
var _sale_buyer_override: FishBuyerProfileType
|
||||
var _shop_cooler_context_active: bool = false
|
||||
|
|
@ -564,9 +568,19 @@ func setup(
|
|||
_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:
|
||||
if event is InputEventKey and event.echo:
|
||||
return
|
||||
if _handle_controller_page_switch(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _handle_direct_page_shortcut(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -585,6 +599,71 @@ func _input(event: InputEvent) -> void:
|
|||
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:
|
||||
var key_event := event as InputEventKey
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ class_name ProfilePage
|
|||
extends Control
|
||||
|
||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
|
||||
var _service: NetworkProfileService
|
||||
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:
|
||||
visible = true
|
||||
UtilityPageStyle.animate_in(self)
|
||||
if _service != null:
|
||||
_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:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
class_name ProfilePreview
|
||||
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, 4.0, 0.1) var keyboard_speed: float = 1.8
|
||||
|
||||
|
|
@ -8,6 +12,13 @@ extends SubViewportContainer
|
|||
|
||||
var _dragging: bool = false
|
||||
var _visuals: Node3D
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -36,7 +47,16 @@ func _process(delta: float) -> void:
|
|||
if not has_focus():
|
||||
return
|
||||
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:
|
||||
axis = right_stick
|
||||
if absf(axis) > 0.1:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ const SECTOR_COUNT: int = 8
|
|||
const RING_RADIUS: float = 172.0
|
||||
const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0)
|
||||
const CONTROLLER_SELECTION_DEADZONE: float = 0.35
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const ACTIONS: Array[StringName] = [
|
||||
&"stuff",
|
||||
&"logbook",
|
||||
|
|
@ -38,6 +41,13 @@ var _buttons: Array[BubbleButton] = []
|
|||
var _is_open: bool = false
|
||||
var _selected_sector: int = 0
|
||||
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:
|
||||
|
|
@ -114,10 +124,7 @@ func _layout_bubbles() -> void:
|
|||
|
||||
|
||||
func _update_selection() -> void:
|
||||
var stick: Vector2 = Vector2(
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
|
||||
Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
|
||||
)
|
||||
var stick: Vector2 = _get_selection_stick()
|
||||
if stick.length() >= CONTROLLER_SELECTION_DEADZONE:
|
||||
_select_sector_from_offset(stick)
|
||||
return
|
||||
|
|
@ -126,6 +133,25 @@ func _update_selection() -> void:
|
|||
_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:
|
||||
var offset: Vector2 = get_local_mouse_position() - size * 0.5
|
||||
if offset.length() < 24.0:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ const PIXELATION_NAMES: PackedStringArray = [
|
|||
const SettingsManagerType = preload(
|
||||
"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 closed
|
||||
|
|
@ -42,6 +48,7 @@ enum PresentationMode {
|
|||
@onready var _world_value: BubbleButton = %WorldValue
|
||||
@onready var _ui_value: BubbleButton = %UIValue
|
||||
@onready var _chat_dock: BubbleButton = %ChatDock
|
||||
@onready var _chat_mode: BubbleButton = %ChatMode
|
||||
@onready var _paint_dock: BubbleButton = %PaintDock
|
||||
@onready var _mouse_value: BubbleButton = %MouseValue
|
||||
@onready var _controller_value: BubbleButton = %ControllerValue
|
||||
|
|
@ -84,6 +91,8 @@ var _identity_backups: IdentityBackupService
|
|||
var _player_identity: PlayerIdentityStore
|
||||
var _host_identity: HostIdentityStore
|
||||
var _interface_fonts: InterfaceFontController
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _controller_mapping_panel: ControllerMappingPanelType
|
||||
var _data_folder_dialog: FileDialog
|
||||
var _backup_file_dialog: FileDialog
|
||||
var _export_file_dialog: FileDialog
|
||||
|
|
@ -114,6 +123,7 @@ func _ready() -> void:
|
|||
%RootBackButton.pressed.connect(close_panel)
|
||||
%DisplayBackButton.pressed.connect(handle_back)
|
||||
%ControlsBackButton.pressed.connect(handle_back)
|
||||
%ControllerMapping.pressed.connect(_open_controller_mapping)
|
||||
%AccessibilityBackButton.pressed.connect(handle_back)
|
||||
%DataBackButton.pressed.connect(handle_back)
|
||||
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
|
|
@ -136,6 +146,7 @@ func _ready() -> void:
|
|||
_set_ui_pixelation.bind(index + 1)
|
||||
)
|
||||
_chat_dock.pressed.connect(_toggle_chat_dock)
|
||||
_chat_mode.pressed.connect(_toggle_chat_mode)
|
||||
_paint_dock.pressed.connect(_toggle_paint_dock)
|
||||
%MouseDecrease.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)
|
||||
for page: SettingsBubblePage in _pages.values():
|
||||
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()
|
||||
call_deferred("_refresh_panel_size")
|
||||
|
||||
|
|
@ -190,6 +206,13 @@ func setup_network_profile(
|
|||
_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(
|
||||
data_root: PlayerDataRoot,
|
||||
identity_backups: IdentityBackupService,
|
||||
|
|
@ -263,6 +286,11 @@ func close_panel(immediate: bool = false) -> void:
|
|||
|
||||
func _finish_panel_close(applied_result: bool) -> void:
|
||||
_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():
|
||||
page.hide_page()
|
||||
_page_stack.clear()
|
||||
|
|
@ -276,6 +304,12 @@ func _finish_panel_close(applied_result: bool) -> 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:
|
||||
return
|
||||
if _page_stack.size() > 1:
|
||||
|
|
@ -284,10 +318,28 @@ func handle_back() -> void:
|
|||
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:
|
||||
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:
|
||||
if (
|
||||
_page_transition_active
|
||||
|
|
@ -689,6 +741,9 @@ func _refresh_value_labels() -> void:
|
|||
+ _pixel_size_label(settings.ui_pixel_size)
|
||||
)
|
||||
_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\n" + ("right" if settings.paint_dock_right else "left")
|
||||
)
|
||||
|
|
@ -774,6 +829,17 @@ func _toggle_chat_dock() -> void:
|
|||
_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:
|
||||
if _settings_manager == null:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -157,8 +157,8 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
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")])
|
||||
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")])
|
||||
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/ChatMode"), NodePath("BubbleCluster/PaintDock"), NodePath("BubbleCluster/DisplayBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/WorldRetro")
|
||||
back_focus_path = NodePath("BubbleCluster/DisplayBackButton")
|
||||
compact_maximum_layout_size = Vector2(544, 400)
|
||||
|
|
@ -337,36 +337,48 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "chat dock\nleft"
|
||||
neutral_size = Vector2(96, 96)
|
||||
desktop_anchor = Vector2(190, 435)
|
||||
compact_anchor = Vector2(160, 378)
|
||||
desktop_anchor = Vector2(105, 435)
|
||||
compact_anchor = Vector2(76, 378)
|
||||
compact_minimum_size = Vector2(82, 82)
|
||||
minimum_font_size = 12
|
||||
maximum_font_size = 16
|
||||
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")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "paint dock\nright"
|
||||
neutral_size = Vector2(96, 96)
|
||||
desktop_anchor = Vector2(530, 435)
|
||||
compact_anchor = Vector2(448, 378)
|
||||
desktop_anchor = Vector2(445, 435)
|
||||
compact_anchor = Vector2(380, 378)
|
||||
compact_minimum_size = Vector2(82, 82)
|
||||
minimum_font_size = 12
|
||||
maximum_font_size = 16
|
||||
motion_phase = 1.65
|
||||
motion_phase = 1.7
|
||||
|
||||
[node name="DisplayBackButton" parent="DisplayPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "back"
|
||||
neutral_size = Vector2(96, 90)
|
||||
desktop_anchor = Vector2(350, 445)
|
||||
compact_anchor = Vector2(315, 390)
|
||||
desktop_anchor = Vector2(615, 435)
|
||||
compact_anchor = Vector2(530, 378)
|
||||
compact_minimum_size = Vector2(82, 78)
|
||||
minimum_font_size = 13
|
||||
maximum_font_size = 16
|
||||
motion_phase = 2.2
|
||||
motion_phase = 2.0
|
||||
|
||||
[node name="ControlsPage" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
|
@ -379,8 +391,8 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
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")])
|
||||
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")])
|
||||
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/ControllerMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -470,24 +482,36 @@ unique_name_in_owner = true
|
|||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "invert\nvertical\ncamera\noff"
|
||||
neutral_size = Vector2(176, 170)
|
||||
desktop_anchor = Vector2(525, 230)
|
||||
compact_anchor = Vector2(485, 225)
|
||||
compact_minimum_size = Vector2(170, 164)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 24
|
||||
desktop_anchor = Vector2(525, 145)
|
||||
compact_anchor = Vector2(485, 135)
|
||||
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")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "back"
|
||||
neutral_size = Vector2(100, 94)
|
||||
desktop_anchor = Vector2(525, 425)
|
||||
compact_anchor = Vector2(540, 395)
|
||||
desktop_anchor = Vector2(635, 445)
|
||||
compact_anchor = Vector2(555, 405)
|
||||
compact_minimum_size = Vector2(84, 80)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 17
|
||||
motion_phase = 5.2
|
||||
motion_phase = 5.5
|
||||
|
||||
[node name="AccessibilityPage" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
|
|
|||
|
|
@ -490,6 +490,11 @@ func _process(delta: float) -> void:
|
|||
func _input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if (
|
||||
_settings_panel.visible
|
||||
and _settings_panel.is_controller_mapping_capturing()
|
||||
):
|
||||
return
|
||||
if _join_game_page.visible:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_close_join_game()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue