extends Control # TouchControlsManager - Manages mobile touch controls including virtual joystick and action buttons signal grab_pressed signal put_pressed signal attack_mode_pressed # Touch control nodes var virtual_joystick: Control var power_bar_container: Control # Renamed from actions_container var interaction_container: Control # New container for Interaction var grab_button: Button var put_button: Button var attack_mode_button: Button # Renamed from special_button var spawn_boost_button: Button var settings_button: Button var tekton_grab_button: Button @onready var SettingsManager = get_node_or_null("/root/SettingsManager") # 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_size: float = 60.0 var joystick_enabled: bool = true var touch_buttons_enabled: bool = true # Master toggle for action buttons 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), "attack_mode": Vector2(-200, -80), # Renamed "spawn_boost": Vector2(-120, -80) } var button_scale: float = 1.0 # Reference to main scene and player var main_scene: Node var local_player: Node func initialize(p_main: Node): main_scene = p_main _create_touch_ui() _load_settings() # Connect to remapping signals if SettingsManager: if not SettingsManager.control_remapped.is_connected(_on_control_remapped): SettingsManager.control_remapped.connect(_on_control_remapped) # Also refresh when settings_applied fires (e.g. use_controller toggle) if not SettingsManager.settings_applied.is_connected(_on_settings_applied_refresh): SettingsManager.settings_applied.connect(_on_settings_applied_refresh) func _on_control_remapped(_action: String, _key: int): print("[TouchControls] Control remapped: %s. Refreshing labels." % _action) _refresh_all_shortcut_labels() func _on_settings_applied_refresh(): """Called when SettingsManager.settings_applied fires (e.g. use_controller toggled).""" _refresh_all_shortcut_labels() func _refresh_all_shortcut_labels(): """Re-run _ensure_shortcut_label on all known buttons to reflect current binding mode.""" if grab_button: _ensure_shortcut_label(grab_button, "Grab") if put_button: _ensure_shortcut_label(put_button, "Put") if attack_mode_button: _ensure_shortcut_label(attack_mode_button, "AttackMode") if spawn_boost_button: _ensure_shortcut_label(spawn_boost_button, "SpawnBoost") if tekton_grab_button: _ensure_shortcut_label(tekton_grab_button, "TektonGrab") # Also walk containers for any dynamically added buttons if power_bar_container: for child in power_bar_container.get_children(): if child is Button: var b_name = child.name.replace("Btn", "") _ensure_shortcut_label(child, b_name) if interaction_container: for child in interaction_container.get_children(): if child is Button: var b_name = child.name.replace("Btn", "") _ensure_shortcut_label(child, b_name) func set_player(p_player: Node): local_player = p_player # Connect to Tekton status updates if not local_player.tekton_carried_changed.is_connected(_on_tekton_carried_changed): local_player.tekton_carried_changed.connect(_on_tekton_carried_changed) # Connect to PowerUpManager if it exists (for boost updates) var powerup_mgr = local_player.get_node_or_null("PowerUpManager") if powerup_mgr: if not powerup_mgr.points_changed.is_connected(_on_boost_points_changed): powerup_mgr.points_changed.connect(_on_boost_points_changed) # Initialize state _on_boost_points_changed(powerup_mgr.current_boost, powerup_mgr.MAX_BOOST) else: # Retry connection if manager appears later? if not local_player.child_entered_tree.is_connected(_on_player_child_to_find_powerup): local_player.child_entered_tree.connect(_on_player_child_to_find_powerup) func _on_player_child_to_find_powerup(node): if node.name == "PowerUpManager": var powerup_mgr = node if not powerup_mgr.points_changed.is_connected(_on_boost_points_changed): powerup_mgr.points_changed.connect(_on_boost_points_changed) _on_boost_points_changed(powerup_mgr.current_boost, powerup_mgr.MAX_BOOST) if local_player.child_entered_tree.is_connected(_on_player_child_to_find_powerup): local_player.child_entered_tree.disconnect(_on_player_child_to_find_powerup) func _create_touch_ui(): print("[TouchControls] Creating/Finding touch UI...") # Use layer 10 - above regular UI but below pause menu # Check if container already exists (added in scene) var container = self virtual_joystick = container.get_node_or_null("VirtualJoystick") if virtual_joystick: print("[TouchControls] Found local VirtualJoystick node") if not virtual_joystick.direction_changed.is_connected(_on_joystick_direction): virtual_joystick.direction_changed.connect(_on_joystick_direction) else: push_error("[TouchControls] VirtualJoystick node missing!") # --- Actions Containers --- power_bar_container = container.get_node_or_null("PowerBarBtn") interaction_container = container.get_node_or_null("InteractionBtn") # Create containers if missing (runtime dynamic creation) #if not power_bar_container: #power_bar_container = Control.new() #power_bar_container.name = "PowerBarBtn" #power_bar_container.set_anchors_preset(Control.PRESET_FULL_RECT) #power_bar_container.mouse_filter = Control.MOUSE_FILTER_PASS #container.add_child(power_bar_container) #else: #print("[TouchControls] Found existing PowerBarBtn container") # #if not interaction_container: #interaction_container = Control.new() #interaction_container.name = "InteractionBtn" #interaction_container.set_anchors_preset(Control.PRESET_FULL_RECT) #interaction_container.mouse_filter = Control.MOUSE_FILTER_PASS #container.add_child(interaction_container) #else: #print("[TouchControls] Found existing InteractionBtn container") # Style/Align Containers if power_bar_container is BoxContainer: power_bar_container.alignment = BoxContainer.ALIGNMENT_CENTER if interaction_container is BoxContainer: interaction_container.alignment = BoxContainer.ALIGNMENT_CENTER # Create action buttons attack_mode_button = _find_or_create_action_button(power_bar_container, "AttackMode", "⚡", button_positions.attack_mode) tekton_grab_button = _find_or_create_action_button(power_bar_container, "TektonGrab", "👋", Vector2(-280, -80)) grab_button = _find_or_create_action_button(interaction_container, "Grab", "👋", button_positions.grab) put_button = _find_or_create_action_button(interaction_container, "Put", "📦", button_positions.put) # Order: AttackMode, SpawnBoost, Grab, TektonGrab # Order / Icons if attack_mode_button: attack_mode_button.icon = load("res://assets/graphics/touch_control/attack_mode.png") attack_mode_button.expand_icon = true if grab_button: grab_button.icon = load("res://assets/graphics/touch_control/take_tile.png") grab_button.expand_icon = true if tekton_grab_button: tekton_grab_button.icon = load("res://assets/graphics/touch_control/grab_tekton.png") tekton_grab_button.expand_icon = true # Hide Put Button if put_button: put_button.visible = false # SettingsBtn signal is handled by main.gd (_toggle_pause_menu). # We only grab the reference here in case touch_controls needs it in future, # but we do NOT connect a second handler to avoid double-toggle. settings_button = main_scene.get_node_or_null("TopMenuUI/SettingsBtn") if not settings_button: settings_button = main_scene.get_node_or_null("SettingsBtn") if not settings_button: settings_button = container.get_node_or_null("SettingsBtn") if settings_button: print("[TouchControls] Found existing SettingsBtn on main scene") settings_button.visible = true # Note: do NOT connect pressed here — main.gd already connects _toggle_pause_menu # 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)) _ensure_shortcut_label(btn, 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)) # Helper to ensure label exists _ensure_shortcut_label(btn, button_name) 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) # Prevent buttons from stealing focus (fixes Spacebar activation) btn.focus_mode = Control.FOCUS_NONE # Fix "Floating" issue: don't expand button to fill whole container height # This keeps the button (and its labels) centered near the icon/text btn.size_flags_vertical = Control.SIZE_SHRINK_CENTER btn.custom_minimum_size.y = 70 # Consistent height func _ensure_shortcut_label(btn: Button, button_name: String): if btn.has_node("ShortcutLabel"): var existing_lbl = btn.get_node("ShortcutLabel") if not SettingsManager: return match button_name: "Grab": existing_lbl.text = SettingsManager.get_action_display("grab") "Put": existing_lbl.text = SettingsManager.get_action_display("put") "AttackMode": existing_lbl.text = SettingsManager.get_action_display("attack_mode") "TektonGrab": existing_lbl.text = SettingsManager.get_action_display("tekton_grab") print("[TouchControls] Updated %s shortcut label to: %s" % [button_name, existing_lbl.text]) # Ensure correct placement (Top Right) existing_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT existing_lbl.vertical_alignment = VERTICAL_ALIGNMENT_TOP existing_lbl.offset_top = -8 # Lowered slightly from -18 existing_lbl.offset_right = 0 # Aligned with right edge # Ensure Outline existing_lbl.add_theme_color_override("font_outline_color", Color.BLACK) existing_lbl.add_theme_constant_override("outline_size", 4) return # Add Keyboard Shortcut Label var shortcut_lbl = Label.new() shortcut_lbl.name = "ShortcutLabel" shortcut_lbl.set_anchors_preset(Control.PRESET_FULL_RECT) shortcut_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT shortcut_lbl.vertical_alignment = VERTICAL_ALIGNMENT_TOP shortcut_lbl.offset_top = -8 # Lowered slightly from -18 shortcut_lbl.offset_right = 0 # Aligned with right edge shortcut_lbl.add_theme_font_size_override("font_size", 16) shortcut_lbl.add_theme_color_override("font_outline_color", Color.BLACK) shortcut_lbl.add_theme_constant_override("outline_size", 4) match button_name: "Grab": shortcut_lbl.text = SettingsManager.get_action_display("grab") if SettingsManager else "Space" "Put": shortcut_lbl.text = SettingsManager.get_action_display("put") if SettingsManager else "R" "AttackMode": shortcut_lbl.text = SettingsManager.get_action_display("attack_mode") if SettingsManager else "Q" "TektonGrab": shortcut_lbl.text = SettingsManager.get_action_display("tekton_grab") if SettingsManager else "G" btn.add_child(shortcut_lbl) func _on_joystick_direction(direction: Vector2i): if local_player and local_player.movement_manager: 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 "AttackMode": btn = attack_mode_button "SpawnBoost": btn = spawn_boost_button "TektonGrab": btn = tekton_grab_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() "AttackMode": emit_signal("attack_mode_pressed") 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] AttackMode Pressed. Boost: %s, CanUse: %s" % [boost_val, can_use]) if can_use: powerup_mgr.use_special_effect() # Sets is_attack_mode=true else: pass else: print("[TouchControls] PowerUpManager missing on player") # "SpawnBoost": # if local_player and local_player.is_carrying_tekton: # if local_player.powerup_manager and local_player.powerup_manager.has_method("spawn_boost_reward"): # local_player.powerup_manager.spawn_boost_reward() "TektonGrab": if not local_player.is_carrying_tekton and local_player.has_method("grab_tekton"): local_player.grab_tekton() func _on_button_released(button_name: String): var btn: Button match button_name: "Grab": btn = grab_button "Put": btn = put_button "AttackMode": btn = attack_mode_button "SpawnBoost": btn = spawn_boost_button "TektonGrab": btn = tekton_grab_button if btn: var tween = create_tween() tween.tween_property(btn, "scale", Vector2(1.0, 1.0), 0.1) func _on_settings_pressed(): # Toggle pause menu on main scene if main_scene and main_scene.has_method("_toggle_pause_menu"): main_scene._toggle_pause_menu() print("[TouchControls] Toggling pause menu") else: print("[TouchControls] Main scene _toggle_pause_menu method not found") 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") _apply_settings() # Apply default settings immediately 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) joystick_size = config.get_value("touch_controls", "joystick_size", 60.0) 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 attack_mode_pos = config.get_value("touch_controls", "attack_mode_position", Vector2(-200, -80)) # Changed key from special if config.has_section_key("touch_controls", "special_position"): # Migrate legacy attack_mode_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, "attack_mode": attack_mode_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", "joystick_size", joystick_size) 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", "attack_mode_position", button_positions.attack_mode) # Renamed 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 and _is_touch_device() if virtual_joystick.has_method("set_radius"): virtual_joystick.set_radius(joystick_size) # Apply touch buttons visibility - FORCED ON per request to "just show them" # Apply touch buttons visibility if power_bar_container: power_bar_container.visible = true if interaction_container: interaction_container.visible = true var buttons_visible = true print("[TouchControls] Applying settings: ButtonsVisible=", buttons_visible) if grab_button: grab_button.visible = true #grab_button.vertical_icon_alignment = VERTICAL_ALIGNMENT_TOP #grab_button.scale = Vector2(button_scale, button_scale) #grab_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) #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 = false if attack_mode_button: attack_mode_button.visible = true attack_mode_button.scale = Vector2(button_scale, button_scale) attack_mode_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) attack_mode_button.offset_left = button_positions.attack_mode.x attack_mode_button.offset_top = button_positions.attack_mode.y attack_mode_button.offset_right = button_positions.attack_mode.x + button_size attack_mode_button.offset_bottom = button_positions.attack_mode.y + button_size if spawn_boost_button: # spawn_boost_button.visible = true # spawn_boost_button.scale = Vector2(button_scale, button_scale) # spawn_boost_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) # 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 spawn_boost_button.visible = false if tekton_grab_button: tekton_grab_button.visible = true tekton_grab_button.scale = Vector2(button_scale, button_scale) tekton_grab_button.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT) tekton_grab_button.offset_left = -280 tekton_grab_button.offset_top = -80 tekton_grab_button.offset_right = -280 + button_size tekton_grab_button.offset_bottom = -80 + 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, attack_mode).""" 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 # Logic to update offsets removed per request (Scene Controlled) # Only saving the data for persistence if user eventually wants it back 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 func _on_boost_points_changed(current_points: int, max_points: int): # User Request: Disable Special & SpawnBoost if < 100% var is_full = current_points >= (max_points - 1) # Tolerance # Attack Mode (⚡) is only enabled if full AND not carrying a Tekton var can_attack = is_full and not (local_player and local_player.is_carrying_tekton) _update_boost_button_state(attack_mode_button, can_attack) # SpawnBoost depends on carrying a Tekton, not boost points # var can_spawn = local_player and local_player.is_carrying_tekton # _update_boost_button_state(spawn_boost_button, can_spawn) # Tekton Grab is only enabled if full AND not already carrying one var can_grab = not (local_player and local_player.is_carrying_tekton) and (is_full) _update_boost_button_state(tekton_grab_button, can_grab) func _on_tekton_carried_changed(_is_carrying: bool): # Refresh button states var powerup_mgr = local_player.get_node_or_null("PowerUpManager") if powerup_mgr: _on_boost_points_changed(powerup_mgr.current_boost, powerup_mgr.MAX_BOOST) func _update_boost_button_state(btn: Button, is_enabled: bool): if not btn: return btn.disabled = !is_enabled # Visual feedback if is_enabled: btn.modulate = Color.WHITE # Optional: Pulse effect? else: btn.modulate = Color(0.5, 0.5, 0.5, 0.5) # Dimmed