220 lines
6.4 KiB
GDScript
220 lines
6.4 KiB
GDScript
## 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
|