## 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