223 lines
7.4 KiB
GDScript
223 lines
7.4 KiB
GDScript
# MobileInputOverlay —— touch controls layered over the world.
|
|
#
|
|
# Child controls consume their own touch events, so PlayerController and
|
|
# GameCamera continue receiving unhandled world touches for tap-to-move,
|
|
# camera orbit and pinch zoom.
|
|
extends Control
|
|
|
|
const Joystick := preload("res://ui/mobile/virtual_joystick.gd")
|
|
const TouchButton := preload("res://ui/mobile/mobile_touch_button.gd")
|
|
|
|
var player_controller: Node
|
|
var net_play: Node
|
|
var quickbar: Node
|
|
var ground_items: Node
|
|
var _joystick: Joystick
|
|
var _attack: TouchButton
|
|
var _potion: TouchButton
|
|
var _skills: Array[TouchButton] = []
|
|
var _ui_touch_indices := {}
|
|
var _aim_slot := -1
|
|
var _aim_direction := Vector2.ZERO
|
|
var _skip_skill_release := {}
|
|
var _aim_hint: Label
|
|
|
|
func setup() -> void:
|
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_build_joystick()
|
|
_build_combat_pad()
|
|
_aim_hint = Label.new()
|
|
_aim_hint.name = "SkillAimHint"
|
|
_aim_hint.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
|
_aim_hint.position = Vector2(-170, 70)
|
|
_aim_hint.size = Vector2(340, 30)
|
|
_aim_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
_aim_hint.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
_aim_hint.add_theme_font_size_override("font_size", 12)
|
|
_aim_hint.add_theme_color_override("font_color", Color(1.0, 0.86, 0.52))
|
|
_aim_hint.add_theme_color_override("font_outline_color", Color(0.02, 0.03, 0.05, 0.95))
|
|
_aim_hint.add_theme_constant_override("outline_size", 4)
|
|
_aim_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
_aim_hint.visible = false
|
|
add_child(_aim_hint)
|
|
set_process_input(true)
|
|
set_process_unhandled_input(true)
|
|
|
|
func bind_controls(pc: Node, np: Node, qb: Node, ground: Node = null) -> void:
|
|
player_controller = pc
|
|
net_play = np
|
|
quickbar = qb
|
|
ground_items = ground
|
|
if _joystick:
|
|
if not _joystick.axis_changed.is_connected(_on_axis):
|
|
_joystick.axis_changed.connect(_on_axis)
|
|
if _attack:
|
|
if not _attack.press_state.is_connected(_on_attack_state):
|
|
_attack.press_state.connect(_on_attack_state)
|
|
|
|
func _build_joystick() -> void:
|
|
_joystick = Joystick.new()
|
|
_joystick.setup(172.0)
|
|
_joystick.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
|
_joystick.position = Vector2(20, -192)
|
|
_joystick.size = Vector2(172, 172)
|
|
add_child(_joystick)
|
|
|
|
func _build_combat_pad() -> void:
|
|
_attack = TouchButton.new()
|
|
_attack.setup("普攻", Color(1.0, 0.42, 0.25, 0.98))
|
|
_attack.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
|
_attack.position = Vector2(-126, -168)
|
|
_attack.size = Vector2(112, 112)
|
|
add_child(_attack)
|
|
|
|
_potion = TouchButton.new()
|
|
_potion.setup("药", Color(0.38, 0.88, 0.58, 0.96))
|
|
_potion.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
|
_potion.position = Vector2(-388, -72)
|
|
_potion.size = Vector2(52, 52)
|
|
_potion.pressed.connect(func():
|
|
# The first page's slot 5 is the conventional consumable slot in the
|
|
# existing quickbar. Quickbar still validates item availability/server state.
|
|
if quickbar and quickbar.has_method("activate"):
|
|
quickbar.activate(4))
|
|
add_child(_potion)
|
|
|
|
for slot in 4:
|
|
var skill := TouchButton.new()
|
|
skill.setup("技%d" % (slot + 1), Color(0.34, 0.65, 0.98, 0.96))
|
|
skill.set_gesture_enabled(true)
|
|
skill.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
|
skill.position = Vector2(-330 + slot * 58, -72)
|
|
skill.size = Vector2(52, 52)
|
|
var captured := slot
|
|
skill.long_pressed.connect(func(): _begin_skill_aim(captured, skill))
|
|
skill.drag_changed.connect(func(_delta: Vector2): _update_skill_aim(captured, skill))
|
|
skill.drag_released.connect(func(_delta: Vector2): _release_skill_aim(captured, skill))
|
|
skill.released.connect(func(): _finish_short_skill_press(captured, skill))
|
|
add_child(skill)
|
|
_skills.append(skill)
|
|
|
|
func _on_axis(axis: Vector2) -> void:
|
|
if player_controller and player_controller.has_method("set_mobile_axis"):
|
|
player_controller.set_mobile_axis(axis)
|
|
|
|
func _on_attack_state(down: bool) -> void:
|
|
if net_play and net_play.has_method("set_attack_key"):
|
|
net_play.set_attack_key(down)
|
|
|
|
func _begin_skill_aim(slot: int, button: TouchButton) -> void:
|
|
if _aim_slot >= 0 and _aim_slot != slot:
|
|
return
|
|
_aim_slot = slot
|
|
_aim_direction = Vector2.ZERO
|
|
button.set_aiming(true)
|
|
if _aim_hint:
|
|
_aim_hint.text = "拖动调整方向,松手释放"
|
|
_aim_hint.visible = true
|
|
|
|
func _update_skill_aim(slot: int, button: TouchButton) -> void:
|
|
if _aim_slot != slot:
|
|
return
|
|
var total := button.drag_total()
|
|
if total.length() >= TouchButton.DRAG_THRESHOLD:
|
|
# Screen Y grows downwards; keep the conventional joystick direction
|
|
# (up = forward) for Quickbar.activate_aimed().
|
|
_aim_direction = Vector2(total.x, total.y).normalized()
|
|
if _aim_hint:
|
|
_aim_hint.text = "方向 %d° · 松手释放" % int(round(rad_to_deg(atan2(-_aim_direction.x, -_aim_direction.y))))
|
|
|
|
func _release_skill_aim(slot: int, button: TouchButton) -> void:
|
|
if _aim_slot != slot:
|
|
return
|
|
_skip_skill_release[slot] = true
|
|
if quickbar:
|
|
if quickbar.has_method("activate_aimed"):
|
|
quickbar.activate_aimed(slot, _aim_direction)
|
|
elif quickbar.has_method("activate"):
|
|
quickbar.activate(slot)
|
|
_end_skill_aim(button)
|
|
|
|
func _finish_short_skill_press(slot: int, button: TouchButton) -> void:
|
|
if bool(_skip_skill_release.get(slot, false)):
|
|
_skip_skill_release.erase(slot)
|
|
return
|
|
if _aim_slot == slot:
|
|
_end_skill_aim(button)
|
|
return
|
|
if quickbar and quickbar.has_method("activate"):
|
|
quickbar.activate(slot)
|
|
|
|
func _end_skill_aim(button: TouchButton = null) -> void:
|
|
if button:
|
|
button.set_aiming(false)
|
|
else:
|
|
for skill in _skills:
|
|
if skill:
|
|
skill.set_aiming(false)
|
|
_aim_slot = -1
|
|
_aim_direction = Vector2.ZERO
|
|
if _aim_hint:
|
|
_aim_hint.visible = false
|
|
|
|
func cancel_all() -> void:
|
|
if _joystick:
|
|
_joystick.cancel_press()
|
|
if _attack:
|
|
_attack.cancel_press()
|
|
if _potion:
|
|
_potion.cancel_press()
|
|
for skill in _skills:
|
|
if skill:
|
|
skill.cancel_press()
|
|
_end_skill_aim()
|
|
_skip_skill_release.clear()
|
|
for index in _ui_touch_indices.keys():
|
|
if player_controller and player_controller.has_method("set_mobile_ui_touch"):
|
|
player_controller.set_mobile_ui_touch(int(index), false)
|
|
_ui_touch_indices.clear()
|
|
if player_controller:
|
|
if player_controller.has_method("clear_mobile_input"):
|
|
player_controller.clear_mobile_input()
|
|
elif player_controller.has_method("set_mobile_axis"):
|
|
player_controller.set_mobile_axis(Vector2.ZERO)
|
|
if net_play and net_play.has_method("set_attack_key"):
|
|
net_play.set_attack_key(false)
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_APPLICATION_FOCUS_OUT \
|
|
or what == NOTIFICATION_APPLICATION_PAUSED \
|
|
or what == NOTIFICATION_WM_WINDOW_FOCUS_OUT:
|
|
cancel_all()
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if not player_controller or not player_controller.has_method("set_mobile_ui_touch"):
|
|
return
|
|
if event is InputEventScreenTouch:
|
|
var touch := event as InputEventScreenTouch
|
|
if touch.pressed:
|
|
var owns := _control_at(touch.position) != null
|
|
if owns:
|
|
_ui_touch_indices[touch.index] = true
|
|
player_controller.set_mobile_ui_touch(touch.index, true)
|
|
elif _ui_touch_indices.has(touch.index):
|
|
_ui_touch_indices.erase(touch.index)
|
|
player_controller.set_mobile_ui_touch(touch.index, false)
|
|
|
|
func _control_at(screen_position: Vector2) -> Control:
|
|
var controls: Array[Control] = []
|
|
if _joystick:
|
|
controls.append(_joystick)
|
|
if _attack:
|
|
controls.append(_attack)
|
|
if _potion:
|
|
controls.append(_potion)
|
|
for skill in _skills:
|
|
if skill:
|
|
controls.append(skill)
|
|
for control in controls:
|
|
if control.visible and control.get_global_rect().has_point(screen_position):
|
|
return control
|
|
return null
|