## Manages tetromino selection, ghosting, and repositioning. ## ## Allows players to: ## 1. Click an already-placed tetromino to select it ## 2. Tetromino becomes "ghost" (semi-transparent, movable) ## 3. Move and rotate the ghost tetromino ## 4. Click to place it in the new position class_name TetrominoSelector extends Node3D # Signals signal tetromino_selected(tetromino: Tetromino) signal tetromino_deselected(tetromino: Tetromino) signal selection_placed(tetromino: Tetromino) # References @onready var board_manager: BoardManager3D = get_parent() @onready var camera: Camera3D = get_viewport().get_camera_3d() # Selection state var _selected_tetromino: Tetromino = null var _ghost_position: Vector3i = Vector3i.ZERO var _ghost_rotation: Quaternion = Quaternion.IDENTITY var _original_position: Vector3i = Vector3i.ZERO var _original_rotation: Quaternion = Quaternion.IDENTITY # Rotation speed (radians per frame) var rotation_speed: float = PI / 4.0 # 45 degrees func _ready() -> void: # Input is handled in _input() pass #func _input(event: InputEvent) -> void: #if event is InputEventMouseButton: #if event.pressed and event.button_index == MOUSE_BUTTON_LEFT: #_handle_left_click() #elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: #_handle_right_click() # #if _selected_tetromino and event is InputEventMouseMotion: #_update_ghost_position() # #if _selected_tetromino: #if event.is_action_pressed("ui_select"): # Spacebar #_rotate_ghost() #elif event.is_action_pressed("ui_cancel"): # ESC #_cancel_selection() ### Handles left mouse click for selection or placement. #func _handle_left_click() -> void: #if _selected_tetromino: ## Try to place the ghost tetromino #_place_selection() #return # ## Try to select a tetromino #var tetromino = _raycast_tetromino() #if tetromino: #_select_tetromino(tetromino) ## Handles right mouse click to deselect. func _handle_right_click() -> void: if _selected_tetromino: _cancel_selection() ### Raycasts from mouse position to find a tetromino. #func _raycast_tetromino() -> Tetromino: #if not camera: #return null # #var mouse_pos = get_viewport().get_mouse_position() #var from = camera.project_ray_origin(mouse_pos) #var to = from + camera.project_ray_normal(mouse_pos) * 10000.0 # #var space_state = get_world_3d().direct_space_state #var collision_mask = 0xFFFFFFFF # Hit all layers #var query = PhysicsRayQueryParameters3D.create(from, to, collision_mask) #query.collide_with_areas = true; # #var result = space_state.intersect_ray(query) # #if result and result.get("collider"): #var collider = result["collider"] ## Walk up the node tree to find a Tetromino #var node = collider #while node: #if node is Tetromino: #return node #node = node.get_parent() #else: #print_debug("Raycast hit nothing") # #return null ## Selects a tetromino and converts it to ghost mode. func _select_tetromino(tetromino: Tetromino) -> void: if _selected_tetromino: _cancel_selection() _selected_tetromino = tetromino _original_position = tetromino.grid_position _original_rotation = tetromino.rotation_quat _ghost_position = _original_position _ghost_rotation = _original_rotation # Enable ghost mode tetromino.set_ghost_mode(true) tetromino_selected.emit(tetromino) get_tree().root.set_input_as_handled() ## Updates ghost position based on mouse position. func _update_ghost_position() -> void: if not _selected_tetromino or not camera: return var mouse_pos = get_viewport().get_mouse_position() var ray_origin = camera.project_ray_origin(mouse_pos) var ray_direction = camera.project_ray_normal(mouse_pos) # Cast ray to find ground position (Y = 0 plane) var t = -ray_origin.y / ray_direction.y if ray_direction.y != 0 else 0.0 var world_hit = ray_origin + ray_direction * t # Convert to grid position _ghost_position = board_manager.world_to_grid(world_hit) # Clamp to valid grid bounds _ghost_position = Vector3i( clampi(_ghost_position.x, 0, board_manager.grid_size.x - 1), clampi(_ghost_position.y, 0, board_manager.grid_size.y - 1), clampi(_ghost_position.z, 0, board_manager.grid_size.z - 1) ) # Update visual position of ghost _selected_tetromino.grid_position = _ghost_position _selected_tetromino.global_position = board_manager.grid_to_world(_ghost_position) ## Rotates the ghost tetromino. func _rotate_ghost() -> void: if not _selected_tetromino: return _ghost_rotation = _ghost_rotation * Quaternion.from_euler(Vector3(0, rotation_speed, 0)) _selected_tetromino.rotation_quat = _ghost_rotation get_tree().root.set_input_as_handled() ## Places the ghost tetromino at its current position. func _place_selection() -> void: if not _selected_tetromino: return var tetromino = _selected_tetromino # Validate placement if not board_manager._can_place_tetromino(tetromino, _ghost_position, _ghost_rotation): # Placement invalid, snap back to original position tetromino.grid_position = _original_position tetromino.rotation_quat = _original_rotation tetromino.world_position = board_manager.grid_to_world(_original_position) return # Update board manager state # Remove from old position var old_cells = tetromino.get_grid_cells(_original_position) for cell_pos in old_cells: board_manager._occupied_cells.erase(cell_pos) # Place at new position tetromino.grid_position = _ghost_position tetromino.rotation_quat = _ghost_rotation tetromino.global_position = board_manager.grid_to_world(_ghost_position) var new_cells = tetromino.get_grid_cells(_ghost_position) for cell_pos in new_cells: board_manager._occupied_cells[cell_pos] = tetromino # Disable ghost mode tetromino.set_ghost_mode(false) _selected_tetromino = null selection_placed.emit(tetromino) get_tree().root.set_input_as_handled() ## Cancels current selection and reverts tetromino to original state. func _cancel_selection() -> void: if not _selected_tetromino: return var tetromino = _selected_tetromino # Restore original position and rotation tetromino.grid_position = _original_position tetromino.rotation_quat = _original_rotation tetromino.world_position = board_manager.grid_to_world(_original_position) # Disable ghost mode tetromino.set_ghost_mode(false) tetromino_deselected.emit(tetromino) _selected_tetromino = null get_tree().root.set_input_as_handled() ## Returns the currently selected tetromino, if any. func get_selected_tetromino() -> Tetromino: return _selected_tetromino