Files
trenchlock/scripts/tetromino_selector.gd

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