class_name TouchJoystick extends Control # Touch joystick for steering the fleet. Lives in the bottom-left corner. # # Behaves like a "floating" stick within its own rect: the base re-centres # on wherever the finger first lands, so the player doesn't have to hit an # exact spot on a phone screen they can't feel. The knob then follows the # finger out to max_radius, and `direction` reports the offset normalised # against that radius (so magnitude is 0..1, usable for throttle later). # # Releasing puts BOTH the base and the knob back in the middle of the rect, so # an idle stick always looks the same and always looks idle. # # Only claims touches that start inside its own rect - anything else falls # through untouched, which is what keeps pinch-to-zoom working everywhere # else on screen. signal direction_changed(direction: Vector2) signal released # How far the knob travels from centre, as a fraction of the control's # shorter side. Derived rather than a fixed number of pixels so that # enlarging the stick's rect enlarges the ring the player actually sees - # otherwise the touch area grows and the visible control stays small. const RADIUS_RATIO: float = 0.28 @export var max_radius: float = 90.0 @export var deadzone: float = 0.12 @export var glow_color: Color = Color(0.25, 0.85, 1.0) @export var core_color: Color = Color(1.0, 1.0, 1.0) var direction: Vector2 = Vector2.ZERO var active_touch_index: int = -1 var base_pos: Vector2 = Vector2.ZERO var knob_pos: Vector2 = Vector2.ZERO func _ready() -> void: resized.connect(_relayout) _relayout() # Where the stick sits when nobody is touching it: the middle of its own rect. # The base only moves away from here while a finger is down (see handle_touch), # and returns the moment it lifts. func home_pos() -> Vector2: return size / 2.0 func _relayout() -> void: base_pos = home_pos() knob_pos = base_pos max_radius = minf(size.x, size.y) * RADIUS_RATIO queue_redraw() func is_active() -> bool: return active_touch_index != -1 # Returns true if the event was consumed, so the caller knows not to treat # it as a world gesture (a fleet move, or one finger of a pinch). func handle_touch(event: InputEventScreenTouch) -> bool: var local: Vector2 = event.position - global_position if event.pressed: if active_touch_index != -1: return false if not Rect2(Vector2.ZERO, size).has_point(local): return false active_touch_index = event.index base_pos = local knob_pos = local _update_direction() return true if event.index != active_touch_index: return false active_touch_index = -1 # Back to the middle of the rect, BASE included. This used to reset the # knob onto base_pos and leave the base wherever the finger had landed, so # a released stick stayed parked off-centre - which reads as a control # still in use, and left the ring somewhere different every time. base_pos = home_pos() knob_pos = base_pos direction = Vector2.ZERO queue_redraw() direction_changed.emit(direction) released.emit() return true func handle_drag(event: InputEventScreenDrag) -> bool: if event.index != active_touch_index: return false knob_pos = event.position - global_position _update_direction() return true func _update_direction() -> void: var offset: Vector2 = knob_pos - base_pos if offset.length() > max_radius: offset = offset.normalized() * max_radius knob_pos = base_pos + offset var raw: Vector2 = offset / max_radius direction = Vector2.ZERO if raw.length() < deadzone else raw queue_redraw() direction_changed.emit(direction) func _draw() -> void: var ring: PackedVector2Array = PackedVector2Array() var segments: int = 28 for i in range(segments): var a: float = (float(i) / segments) * TAU ring.append(base_pos + Vector2(cos(a), sin(a)) * max_radius) NeonDraw.draw_glowing_polygon(self, ring, glow_color, 0.55, core_color) var knob: PackedVector2Array = PackedVector2Array() var knob_radius: float = max_radius * 0.34 for i in range(segments): var a: float = (float(i) / segments) * TAU knob.append(knob_pos + Vector2(cos(a), sin(a)) * knob_radius) NeonDraw.draw_glowing_polygon(self, knob, glow_color, 1.0 if is_active() else 0.75, core_color)