extends CanvasLayer ## Controlli on-screen per browser mobile e dispositivi touchscreen. ## ## Un solo Control a schermo intero smista il multi-touch per indice di tocco: ## - meta' sinistra -> joystick di movimento flottante (alimenta le azioni move_*) ## - meta' destra -> area di puntamento (trascina per ruotare la visuale) ## - cerchi in basso a destra -> fuoco (tenere premuto), granata, ricarica, salto ## ## Non dipende dall'emulazione mouse<->touch: l'emulazione copre un solo ## puntatore e romperebbe il multi-touch (es. fuoco + movimento insieme). ## Vedi docs/TOUCH-CONTROLS.md. const DEAD_ZONE := 10.0 # px: sotto questa soglia il vettore e' nullo const JOY_RADIUS := 92.0 const KNOB_RADIUS := 40.0 const BTN_RADIUS := 46.0 const BTN_FIRE_RADIUS := 62.0 const LOOK_SENSITIVITY := 1.7 # moltiplicatore del trascinamento nell'area di puntamento const EDGE := 24.0 const COLOR_BASE := Color(1, 1, 1, 0.09) const COLOR_RING := Color(1, 1, 1, 0.34) const COLOR_KNOB := Color(0.82, 0.88, 1.0, 0.42) const COLOR_BTN := Color(1, 1, 1, 0.13) const COLOR_ACTIVE := Color(1, 0.55, 0.15, 0.55) const COLOR_TEXT := Color(1, 1, 1, 0.88) ## Giocatore controllato: riceve i delta di visuale. Impostato da main.gd. var player: Node ## Modalita' mouse: trattare i drag del mouse come tocchi (solo test su desktop). var mouse_fallback := false ## Finche' e' false i tocchi non vengono consumati: serve al gate di avvio Web, ## dove il primo tocco deve arrivare a main.gd per sbloccare audio e partita. var enabled := true enum Role { JOY, LOOK, FIRE, GRENADE, RELOAD, JUMP } const MOUSE_INDEX := -1 const MOVE_ACTIONS := ["move_left", "move_right", "move_forward", "move_back"] var move_vector := Vector2.ZERO var _touches := {} # index -> Role var _active := {} # Role -> bool (per il disegno) var _joy_origin := Vector2.ZERO var _joy_knob := Vector2.ZERO var _buttons := {} # Role -> {center: Vector2, radius: float, label: String} var _size := Vector2(1280, 720) func _ready() -> void: layer = 3 # I tocchi non devono essere tradotti in eventi mouse: la posizione di fuoco # e' gestita solo dall'area di puntamento, altrimenti la visuale si muove due volte. Input.set_emulate_mouse_from_touch(false) var root := Control.new() root.name = "TouchRoot" root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) # IGNORE, non STOP: un Control a schermo intero che cattura i click # impedirebbe al gate di avvio Web (in _unhandled_input) di vedere il primo # tocco. Node._input riceve comunque tutti gli eventi e il consumo avviene # esplicitamente con set_input_as_handled() sugli eventi gestiti. root.mouse_filter = Control.MOUSE_FILTER_IGNORE # I metodi draw_* valgono solo durante il disegno: il segnale draw e' il punto giusto. root.draw.connect(_on_draw) add_child(root) _root = root var vp := get_viewport().get_visible_rect().size _size = root.size if root.size.x > 1.0 else vp root.resized.connect(_on_resized) _layout() root.queue_redraw() var _root: Control func _on_resized() -> void: _size = _root.size if _root.size.x > 1.0 else get_viewport().get_visible_rect().size _layout() _root.queue_redraw() func _on_draw() -> void: var font: Font = _root.get_theme_default_font() var fs := 15 # joystick var base := _joy_base() _root.draw_circle(base, JOY_RADIUS, COLOR_BASE) _root.draw_arc(base, JOY_RADIUS, 0, TAU, 64, COLOR_RING, 2.0) _root.draw_circle(base + _joy_knob, KNOB_RADIUS, COLOR_KNOB) _root.draw_arc(base + _joy_knob, KNOB_RADIUS, 0, TAU, 48, COLOR_RING, 1.5) _draw_label(font, fs, "MUOVI", base + Vector2(-JOY_RADIUS + 6, JOY_RADIUS + 22), COLOR_TEXT) # pulsanti for role in _buttons: var b: Dictionary = _buttons[role] var c: Vector2 = b.center var r: float = b.radius _root.draw_circle(c, r, COLOR_ACTIVE if _active.get(role, false) else COLOR_BTN) _root.draw_arc(c, r, 0, TAU, 48, COLOR_RING, 2.0) var text: String = b.label var w := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs).x _draw_label(font, fs, text, c - Vector2(w * 0.5, -fs * 0.35), COLOR_TEXT) func _draw_label(font: Font, fs: int, text: String, pos: Vector2, color: Color) -> void: _root.draw_string(font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, fs, color) ## Riposiziona pulsanti e base del joystick in base alla dimensione della finestra. func _layout() -> void: var w := _size.x var h := _size.y var fire := Vector2(w - EDGE - BTN_FIRE_RADIUS, h - EDGE - BTN_FIRE_RADIUS) var gap := BTN_FIRE_RADIUS + BTN_RADIUS - 6.0 _buttons = { Role.FIRE: {"center": fire, "radius": BTN_FIRE_RADIUS, "label": "FUOCO"}, Role.GRENADE: {"center": fire + Vector2(0, -gap), "radius": BTN_RADIUS, "label": "GRANATA"}, Role.RELOAD: {"center": fire + Vector2(-gap, 0), "radius": BTN_RADIUS, "label": "Ricarica"}, Role.JUMP: {"center": fire + Vector2(-gap, -gap), "radius": BTN_RADIUS, "label": "Salto"}, } func _joy_base() -> Vector2: if _touches.values().has(Role.JOY): return _joy_origin return Vector2(EDGE + JOY_RADIUS, _size.y - EDGE - JOY_RADIUS) # ---------------------------------------------------------------- input func _input(event: InputEvent) -> void: if not enabled: return var index := -2 var pressed := false var pos := Vector2.ZERO var relative := Vector2.ZERO var is_press := false var is_release := false var is_drag := false if event is InputEventScreenTouch: index = event.index pos = event.position is_press = event.pressed is_release = not event.pressed elif event is InputEventScreenDrag: index = event.index pos = event.position relative = event.relative is_drag = true elif mouse_fallback and event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: index = MOUSE_INDEX pos = event.position is_press = event.pressed is_release = not event.pressed elif mouse_fallback and event is InputEventMouseMotion and _touches.has(MOUSE_INDEX): index = MOUSE_INDEX pos = event.position relative = event.relative is_drag = true else: return if is_press: _press(index, pos) get_viewport().set_input_as_handled() elif is_release: _release(index) get_viewport().set_input_as_handled() elif is_drag: _drag(index, pos, relative) get_viewport().set_input_as_handled() func _press(index: int, pos: Vector2) -> void: if _touches.has(index): return var role := _hit_test(pos) if role != Role.JOY and _touches.values().has(role): return # un solo dito per ruolo if role == Role.JOY and not _in_left_half(pos): return _touches[index] = role _active[role] = true match role: Role.JOY: _joy_origin = pos _joy_knob = Vector2.ZERO _apply_move() Role.LOOK: pass Role.FIRE: Input.action_press("shoot") Role.GRENADE: _send_action("grenade") Role.RELOAD: _send_action("reload") Role.JUMP: _send_action("jump") _root.queue_redraw() func _release(index: int) -> void: if not _touches.has(index): return var role: Role = _touches[index] _touches.erase(index) _active.erase(role) match role: Role.JOY: _joy_knob = Vector2.ZERO move_vector = Vector2.ZERO for action in MOVE_ACTIONS: Input.action_release(action) Role.FIRE: Input.action_release("shoot") _root.queue_redraw() func _drag(index: int, pos: Vector2, relative: Vector2) -> void: if not _touches.has(index): # un drag iniziato fuori dai Control (raro su Web) viene adottato come visuale if not _in_left_half(pos): _touches[index] = Role.LOOK _active[Role.LOOK] = true else: return var role: Role = _touches[index] match role: Role.JOY: _joy_knob = (pos - _joy_origin).limit_length(JOY_RADIUS) _apply_move() _root.queue_redraw() Role.LOOK: if player and player.has_method("apply_look"): player.apply_look(relative * LOOK_SENSITIVITY) ## Traduce il vettore del joystick nelle azioni di movimento (analogiche). func _apply_move() -> void: var v := _joy_knob / JOY_RADIUS if v.length() * JOY_RADIUS < DEAD_ZONE: v = Vector2.ZERO move_vector = v.limit_length(1.0) _set_action("move_left", -move_vector.x) _set_action("move_right", move_vector.x) _set_action("move_forward", -move_vector.y) _set_action("move_back", move_vector.y) func _set_action(action: String, strength: float) -> void: if strength > 0.01: Input.action_press(action, minf(1.0, strength)) else: Input.action_release(action) ## Azioni a pressione singola (granata, ricarica, salto). Il giocatore le legge ## a polling: premere lo stato dell'azione e rilasciarlo qualche frame dopo ## evita sia l'evento sintetico perso sia il vedere premuto e rilasciato ## nello stesso frame (is_action_just_pressed). func _send_action(action: String) -> void: Input.action_press(action) get_tree().create_timer(0.08).timeout.connect(func(): Input.action_release(action)) func _in_left_half(pos: Vector2) -> bool: return pos.x < _size.x * 0.5 ## Ordine di priorita': pulsanti, poi meta' sinistra (movimento), poi visuale. func _hit_test(pos: Vector2) -> Role: var best := Role.LOOK var best_dist := INF for role in _buttons: var b: Dictionary = _buttons[role] var d: float = pos.distance_to(b.center) if d <= b.radius + 6.0 and d < best_dist: best = role best_dist = d if best != Role.LOOK: return best if _in_left_half(pos): return Role.JOY return Role.LOOK # ------------------------------------------------- rilevamento del contesto ## True se il gioco gira su un dispositivo che merita i controlli on-screen. ## Documentazione: in un export Web OS.has_feature("mobile") e' false, valgono ## i tag web_android / web_ios. static func should_auto_enable() -> bool: if OS.has_feature("web_android") or OS.has_feature("web_ios"): return true if DisplayServer.is_touchscreen_available(): return true return false ## Forzatura per test: 1 = attivo, -1 = disattivo, 0 = automatico. ## Riga di comando: --touch / --no-touch. Sul Web anche ?touch=1 / ?touch=0. static func force_mode() -> int: var args := OS.get_cmdline_user_args() if "--touch" in args: return 1 if "--no-touch" in args: return -1 if OS.has_feature("web"): var query := str(JavaScriptBridge.eval("window.location.search", true)) if query.contains("touch=1"): return 1 if query.contains("touch=0"): return -1 return 0