From 6982ded2a15d479e70ccb4be336d0079726dafbb Mon Sep 17 00:00:00 2001 From: Michele Rossi Date: Tue, 13 Jan 2026 22:43:51 +0100 Subject: [PATCH] Select/hover tetramino with Area3D signal --- TETROMINO_SELECTOR.md | 21 ++--- assets/models/MSH_1x3.res | Bin 6622 -> 2195 bytes assets/models/MSH_T_Corta.res | Bin 0 -> 7490 bytes resources/data/tetrominos/i_piece.tres | 4 +- resources/data/tetrominos/t_piece.tres | 9 +++ scenes/board/board.tscn | 4 +- scenes/board/tetromino.tscn | 14 ++-- scripts/tetromino.gd | 44 +++++++--- scripts/tetromino_selector.gd | 108 ++++++++++++------------- 9 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 assets/models/MSH_T_Corta.res create mode 100644 resources/data/tetrominos/t_piece.tres diff --git a/TETROMINO_SELECTOR.md b/TETROMINO_SELECTOR.md index 92832a5..b3db608 100644 --- a/TETROMINO_SELECTOR.md +++ b/TETROMINO_SELECTOR.md @@ -1,19 +1,22 @@ # Tetromino Selector System ## Overview -Adds interactive tetromino selection and repositioning during gameplay. Players can select already-placed tetrominoes, move them, rotate them, and place them in new positions. +Adds interactive tetromino selection and repositioning during gameplay. Players can select already- +placed tetrominoes, move them, rotate them, and place them in new positions. ## Components ### 1. TetrominoSelector (scripts/tetromino_selector.gd) + Main controller for tetromino selection logic. **Features:** -- Raycast-based tetromino detection on left-click -- Ghost mode visualization (semi-transparent) +- Raycast-based tetromino detection on left-click from camera to world - Real-time position tracking with mouse movement -- Rotation with spacebar (45° increments) + +- Ghost mode visualization (semi-transparent) - Placement validation using board manager +- Rotation with spacebar (45° increments) - Cancellation with right-click or ESC key **Signals:** @@ -78,12 +81,12 @@ Added collision detection for raycast selection. - Player left-clicks to place - Selector validates placement using BoardManager3D._can_place_tetromino() - If valid: - - Updates board manager occupied cells - - Tetromino exits ghost mode - - Signals emit `selection_placed` + - Updates board manager occupied cells + - Tetromino exits ghost mode + - Signals emit `selection_placed` - If invalid: - - Tetromino snaps back to original position - - Reverts to original rotation + - Tetromino snaps back to original position + - Reverts to original rotation 5. **Cancellation** - Player right-clicks or presses ESC diff --git a/assets/models/MSH_1x3.res b/assets/models/MSH_1x3.res index e704101328c5931514cd1615108ae0c7093836a5..9367ae6a64821ab4b01a526b0b34ef9077580d28 100644 GIT binary patch literal 2195 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 diff --git a/assets/models/MSH_T_Corta.res b/assets/models/MSH_T_Corta.res new file mode 100644 index 0000000000000000000000000000000000000000..7099e050b97dbc0e6ac37aa999092b909dc888a2 GIT binary patch literal 7490 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 q|L&!v|E_9&ThZT1_}c`(u!0}_8TC7~;x8@Kz=u void: - #_initialize_material() - #_set_mesh_representation() + _initialize_material() + _set_mesh_representation() pass ## Initializes the material for visual representation. func _initialize_material() -> void: _material = StandardMaterial3D.new() - _material.albedo_color = mesh_color + _material.albedo_color = Color.RED _material.metallic = 0.3 _material.roughness = 0.7 @@ -44,15 +49,9 @@ func _initialize_material() -> void: ## Creates a simple mesh representation based on shape type. func _set_mesh_representation() -> void: _mesh_instance.mesh = resource.mesh - #_mesh_instance.set_surface_override_material(0, _material) + _mesh_instance.set_surface_override_material(0, _material) + $SelectionArea/CollisionShape3D.shape = _mesh_instance.mesh.create_trimesh_shape() _grid_cells = resource.grid_cells - - -## Creates a simple box mesh for tetromino visualization. -func _create_box_mesh(width: float, height: float, depth: float) -> BoxMesh: - var box = BoxMesh.new() - box.size = Vector3(width, height, depth) - return box ## Gets the grid cells occupied by this tetromino at its current position. @@ -139,3 +138,26 @@ func _to_string() -> String: grid_position, _grid_cells.size() ] + +## Callback used to handle tetromino selection and deselection +func _on_selection_area_input_event(camera: Node, event: InputEvent, event_position: Vector3, normal: Vector3, shape_idx: int) -> void: + if event is InputEventMouseButton: + if event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + print("Tetromino selected") + selected.emit(self, grid_position) + elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: + print("Tetromino released") + deselected.emit(self) + + +## Callback used to handle tetromino hovering +func _on_selection_area_mouse_entered() -> void: + print("Tetromino hovered") + set_highlighted(true) + hovered.emit(self) + +## Callback used to handle tetromino unhovering +func _on_selection_area_mouse_exited() -> void: + print("Tetromino unhovered") + set_highlighted(false) + unhovered.emit(self) diff --git a/scripts/tetromino_selector.gd b/scripts/tetromino_selector.gd index 75ccb92..e5dac41 100644 --- a/scripts/tetromino_selector.gd +++ b/scripts/tetromino_selector.gd @@ -33,34 +33,34 @@ func _ready() -> void: 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() +#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 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. @@ -69,34 +69,34 @@ func _handle_right_click() -> void: _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 +### 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.