extends CanvasLayer # TouchControlsManager - Manages mobile touch controls including virtual joystick and action buttons signal grab_pressed signal put_pressed signal special_pressed # Touch control nodes var virtual_joystick: Control var grab_button: Button var put_button: Button var special_button: Button var spawn_boost_button: Button var settings_button: Button # Settings - persisted to config file const CONFIG_PATH = "user://touch_controls_settings.cfg" var button_size: float = 70.0 var button_opacity: float = 0.7 var joystick_enabled: bool = true var touch_buttons_enabled: bool = true # Master toggle for action buttons (grab, put, special) var joystick_position: Vector2 = Vector2(120, -120) # Relative to bottom-left var button_positions: Dictionary = { "grab": Vector2(-200, -240), # Relative to bottom-right "put": Vector2(-120, -160), "special": Vector2(-200, -80), "spawn_boost": Vector2(-120, -80) } var button_scale: float = 1.0 # Reference to main scene and player var main_scene: Node3D var local_player: Node3D func initialize(p_main: Node3D): main_scene = p_main _create_touch_ui() _load_settings() func set_player(p_player: Node3D): local_player = p_player func _create_touch_ui(): print("[TouchControls] Creating/Finding touch UI...") # Use layer 10 - above regular UI but below pause menu layer = 10 # Check if container already exists (added in scene) var container = get_node_or_null("TouchControls") if not container: # Create main container if missing container = Control.new() container.name = "TouchControls" container.set_anchors_preset(Control.PRESET_FULL_RECT) container.mouse_filter = Control.MOUSE_FILTER_PASS # Pass input to children add_child(container) else: print("[TouchControls] Found existing TouchControls container") # Helper to find or create control var find_or_create_joystick = func(): var joy = container.get_node_or_null("VirtualJoystick") if joy: print("[TouchControls] Found existing VirtualJoystick") return joy var joystick_script = load("res://scripts/ui/virtual_joystick.gd") joy = Control.new() joy.set_script(joystick_script) joy.name = "VirtualJoystick" joy.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) # Use standard size from joystick script defaults (radius 60 -> size 160) var joy_size = Vector2(160, 160) joy.custom_minimum_size = joy_size joy.size = joy_size joy.offset_left = 120 joy.offset_top = -280 joy.offset_right = 280 joy.offset_bottom = -120 container.add_child(joy) return joy virtual_joystick = find_or_create_joystick.call() if not virtual_joystick.direction_changed.is_connected(_on_joystick_direction): virtual_joystick.direction_changed.connect(_on_joystick_direction) # Helper to find or create button logic moved to function _find_or_create_action_button # Create action buttons (bottom-right) grab_button = _find_or_create_action_button(container, "Grab", "👋", button_positions.grab) put_button = _find_or_create_action_button(container, "Put", "📦", button_positions.put) special_button = _find_or_create_action_button(container, "Special", "⚡", button_positions.special) spawn_boost_button = _find_or_create_action_button(container, "SpawnBoost", "🚀", button_positions.spawn_boost) # Create settings button (top-right corner) settings_button = container.get_node_or_null("SettingsBtn") if not settings_button: settings_button = Button.new() settings_button.name = "SettingsBtn" settings_button.text = "⚙" settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT) settings_button.offset_left = -70 settings_button.offset_right = -20 settings_button.offset_top = 70 settings_button.offset_bottom = 120 settings_button.custom_minimum_size = Vector2(50, 50) settings_button.mouse_filter = Control.MOUSE_FILTER_STOP _style_button(settings_button, 0.5) container.add_child(settings_button) if not settings_button.pressed.is_connected(_on_settings_pressed): settings_button.pressed.connect(_on_settings_pressed) # Always visible now - controlled by settings toggle # Can be hidden via settings if user doesn't want touch controls on desktop visible = true func _find_or_create_action_button(container: Control, button_name: String, icon: String, pos: Vector2) -> Button: var btn = container.get_node_or_null(button_name + "Btn") if btn: print("[TouchControls] Found existing %s button" % button_name) # Style it and connect _style_button(btn, button_opacity) # Avoid duplicate signal connections if not btn.button_down.is_connected(_on_button_pressed): # Wait, cannot check lambda easily # Disconnect all to be safe if previously connected for conn in btn.button_down.get_connections(): if conn["callable"].get_object() == self: btn.button_down.disconnect(conn["callable"]) for conn in btn.button_up.get_connections(): if conn["callable"].get_object() == self: btn.button_up.disconnect(conn["callable"]) btn.button_down.connect(func(): _on_button_pressed(button_name)) btn.button_up.connect(func(): _on_button_released(button_name)) return btn # Create new var new_btn = _create_action_button(button_name, icon, pos) container.add_child(new_btn) return new_btn func _create_action_button(button_name: String, icon: String, pos: Vector2) -> Button: var btn = Button.new() btn.name = button_name + "Btn" btn.text = icon btn.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) # Use offsets strictly for anchored positioning # pos.x and pos.y are negative offsets from bottom-right (e.g. -200, -240) btn.offset_left = pos.x btn.offset_top = pos.y btn.offset_right = pos.x + button_size btn.offset_bottom = pos.y + button_size btn.custom_minimum_size = Vector2(button_size, button_size) btn.pivot_offset = Vector2(button_size / 2, button_size / 2) # Center pivot for scaling # Connect signals btn.button_down.connect(func(): _on_button_pressed(button_name)) btn.button_up.connect(func(): _on_button_released(button_name)) _style_button(btn, button_opacity) return btn func _style_button(btn: Button, opacity: float): var style = StyleBoxFlat.new() style.bg_color = Color(0.2, 0.2, 0.25, opacity) style.border_width_left = 2 style.border_width_top = 2 style.border_width_right = 2 style.border_width_bottom = 2 style.border_color = Color(0.647, 0.996, 0.224, 0.8) style.corner_radius_top_left = 15 style.corner_radius_top_right = 15 style.corner_radius_bottom_right = 15 style.corner_radius_bottom_left = 15 btn.add_theme_stylebox_override("normal", style) var pressed_style = style.duplicate() pressed_style.bg_color = Color(0.4, 0.8, 0.4, opacity + 0.2) btn.add_theme_stylebox_override("pressed", pressed_style) var hover_style = style.duplicate() hover_style.bg_color = Color(0.3, 0.3, 0.35, opacity) btn.add_theme_stylebox_override("hover", hover_style) btn.add_theme_font_size_override("font_size", 28) func _on_joystick_direction(direction: Vector2i): if local_player and local_player.has_method("simple_move_to"): var target_pos = local_player.current_position + direction local_player.movement_manager.simple_move_to(target_pos) func _on_button_pressed(button_name: String): if not local_player: return # Visual feedback - scale up var btn: Button match button_name: "Grab": btn = grab_button "Put": btn = put_button "Special": btn = special_button "SpawnBoost": btn = spawn_boost_button if btn: var tween = create_tween() tween.tween_property(btn, "scale", Vector2(1.15, 1.15), 0.1) # Trigger action match button_name: "Grab": emit_signal("grab_pressed") if local_player.has_method("grab_item"): local_player.grab_item(local_player.current_position) "Put": emit_signal("put_pressed") if local_player.has_method("auto_put_item"): local_player.auto_put_item() "Special": emit_signal("special_pressed") var powerup_mgr = local_player.get_node_or_null("PowerUpManager") if powerup_mgr: # Require Full Boost to Activate (User Request: "Connect to boost bar") var can_use = powerup_mgr.can_use_special() var boost_val = powerup_mgr.current_boost print("[TouchControls] Special Pressed. Boost: %s, CanUse: %s" % [boost_val, can_use]) if can_use: powerup_mgr.use_special_effect() # Sets is_attack_mode=true, Does NOT consume boost yet else: # Optional feedback for not ready? pass else: print("[TouchControls] PowerUpManager missing on player") "SpawnBoost": var powerup_mgr = local_player.get_node_or_null("PowerUpManager") if powerup_mgr: var can_use = powerup_mgr.can_use_special() var boost_val = powerup_mgr.current_boost print("[TouchControls] SpawnBoost Pressed. Boost: %s, CanUse: %s" % [boost_val, can_use]) if can_use: # Check if boost is full if local_player.special_tiles_manager and local_player.special_tiles_manager.has_method("spawn_powerups_around"): local_player.special_tiles_manager.spawn_powerups_around(local_player.current_position) powerup_mgr.reset_boost() # Consume the boost manually since we bypassed use_special_effect else: print("[TouchControls] PowerUpManager missing on player") func _on_button_released(button_name: String): var btn: Button match button_name: "Grab": btn = grab_button "Put": btn = put_button "Special": btn = special_button "SpawnBoost": btn = spawn_boost_button if btn: var tween = create_tween() tween.tween_property(btn, "scale", Vector2(1.0, 1.0), 0.1) func _on_settings_pressed(): # Open settings panel in main scene if main_scene: var settings_panel = main_scene.get_node_or_null("SettingsPanel") if settings_panel: settings_panel.visible = true print("[TouchControls] Opening settings panel") else: print("[TouchControls] SettingsPanel not found in main scene") func _is_touch_device() -> bool: # Check if running on mobile return OS.has_feature("android") or OS.has_feature("ios") or OS.has_feature("web_android") or OS.has_feature("web_ios") func _load_settings(): """Load touch control settings from config file.""" var config = ConfigFile.new() var err = config.load(CONFIG_PATH) if err != OK: print("[TouchControls] No saved settings found, using defaults") return # Load settings values button_size = config.get_value("touch_controls", "button_size", 70.0) button_opacity = config.get_value("touch_controls", "button_opacity", 0.7) button_scale = config.get_value("touch_controls", "button_scale", 1.0) joystick_enabled = config.get_value("touch_controls", "joystick_enabled", true) touch_buttons_enabled = config.get_value("touch_controls", "touch_buttons_enabled", true) # Load button positions var grab_pos = config.get_value("touch_controls", "grab_position", Vector2(-200, -240)) var put_pos = config.get_value("touch_controls", "put_position", Vector2(-120, -160)) var special_pos = config.get_value("touch_controls", "special_position", Vector2(-200, -80)) var spawn_boost_pos = config.get_value("touch_controls", "spawn_boost_position", Vector2(-120, -80)) button_positions = {"grab": grab_pos, "put": put_pos, "special": special_pos, "spawn_boost": spawn_boost_pos} # Apply loaded settings _apply_settings() print("[TouchControls] Settings loaded") func _save_settings(): """Save touch control settings to config file.""" var config = ConfigFile.new() config.set_value("touch_controls", "button_size", button_size) config.set_value("touch_controls", "button_opacity", button_opacity) config.set_value("touch_controls", "button_scale", button_scale) config.set_value("touch_controls", "joystick_enabled", joystick_enabled) config.set_value("touch_controls", "touch_buttons_enabled", touch_buttons_enabled) config.set_value("touch_controls", "grab_position", button_positions.grab) config.set_value("touch_controls", "put_position", button_positions.put) config.set_value("touch_controls", "special_position", button_positions.special) config.set_value("touch_controls", "spawn_boost_position", button_positions.spawn_boost) var err = config.save(CONFIG_PATH) if err != OK: push_error("[TouchControls] Failed to save settings: %s" % err) else: print("[TouchControls] Settings saved") func _apply_settings(): """Apply current settings to UI elements.""" # Apply joystick visibility if virtual_joystick: virtual_joystick.visible = joystick_enabled # Apply touch buttons visibility - dependent on master joystick_enabled switch # If joystick is disabled, ALL touch controls are hidden # Note: We ignore touch_buttons_enabled here to ensure "Enable Virtual Joystick" shows EVERYTHING as requested var buttons_visible = joystick_enabled print("[TouchControls] Applying settings: JoystickEnabled=", joystick_enabled, " ButtonsVisible=", buttons_visible) if grab_button: grab_button.visible = buttons_visible grab_button.scale = Vector2(button_scale, button_scale) # Use offsets for anchored controls, not position grab_button.offset_left = button_positions.grab.x grab_button.offset_top = button_positions.grab.y grab_button.offset_right = button_positions.grab.x + button_size grab_button.offset_bottom = button_positions.grab.y + button_size if put_button: put_button.visible = buttons_visible put_button.scale = Vector2(button_scale, button_scale) put_button.offset_left = button_positions.put.x put_button.offset_top = button_positions.put.y put_button.offset_right = button_positions.put.x + button_size put_button.offset_bottom = button_positions.put.y + button_size if special_button: special_button.visible = buttons_visible special_button.scale = Vector2(button_scale, button_scale) special_button.offset_left = button_positions.special.x special_button.offset_top = button_positions.special.y special_button.offset_right = button_positions.special.x + button_size special_button.offset_bottom = button_positions.special.y + button_size if spawn_boost_button: spawn_boost_button.visible = buttons_visible spawn_boost_button.scale = Vector2(button_scale, button_scale) spawn_boost_button.offset_left = button_positions.spawn_boost.x spawn_boost_button.offset_top = button_positions.spawn_boost.y spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size # Force layer update visible = true # ============================================================================= # Public Settings API # ============================================================================= func set_touch_buttons_enabled(enabled: bool): """Enable or disable all action buttons (grab, put, special).""" touch_buttons_enabled = enabled _apply_settings() func set_joystick_enabled(enabled: bool): """Enable or disable the virtual joystick (and all touch controls).""" joystick_enabled = enabled _apply_settings() func set_button_scale(p_scale: float): """Set scale for all action buttons.""" button_scale = p_scale _apply_settings() func set_button_position(button_name: String, new_position: Vector2): """Update position of a specific button.""" button_positions[button_name] = new_position match button_name: "grab": if grab_button: grab_button.offset_left = new_position.x grab_button.offset_top = new_position.y grab_button.offset_right = new_position.x + button_size grab_button.offset_bottom = new_position.y + button_size "put": if put_button: put_button.offset_left = new_position.x put_button.offset_top = new_position.y put_button.offset_right = new_position.x + button_size put_button.offset_bottom = new_position.y + button_size "special": if special_button: special_button.offset_left = new_position.x special_button.offset_top = new_position.y special_button.offset_right = new_position.x + button_size special_button.offset_bottom = new_position.y + button_size "spawn_boost": if spawn_boost_button: spawn_boost_button.offset_left = new_position.x spawn_boost_button.offset_top = new_position.y spawn_boost_button.offset_right = new_position.x + button_size spawn_boost_button.offset_bottom = new_position.y + button_size func get_button_positions() -> Dictionary: """Get current button positions for settings UI.""" return button_positions.duplicate() func get_settings() -> Dictionary: """Get all current settings as a dictionary.""" return { "button_size": button_size, "button_opacity": button_opacity, "button_scale": button_scale, "joystick_enabled": joystick_enabled, "touch_buttons_enabled": touch_buttons_enabled, "button_positions": button_positions.duplicate() } func show_controls(): visible = true func hide_controls(): visible = false