Files
mtgodot-poc/project/ui/mobile/mobile_input_overlay.gd

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