# 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