Start encounter feature
This commit is contained in:
219
scripts/tetromino_selector.gd
Normal file
219
scripts/tetromino_selector.gd
Normal file
@@ -0,0 +1,219 @@
|
||||
## 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
|
||||
Reference in New Issue
Block a user