Add tetromino movement

This commit is contained in:
2026-01-14 13:52:57 +01:00
parent 6982ded2a1
commit e9689ac3cd
4 changed files with 80 additions and 42 deletions

View File

@@ -2,14 +2,13 @@
[ext_resource type="Script" uid="uid://c8041v2usigk4" path="res://scripts/tetromino.gd" id="1_hprdj"] [ext_resource type="Script" uid="uid://c8041v2usigk4" path="res://scripts/tetromino.gd" id="1_hprdj"]
[ext_resource type="Resource" uid="uid://curn7voye0ewx" path="res://resources/data/tetrominos/i_piece.tres" id="2_f3wyc"] [ext_resource type="Resource" uid="uid://curn7voye0ewx" path="res://resources/data/tetrominos/i_piece.tres" id="2_f3wyc"]
[ext_resource type="ArrayMesh" uid="uid://5jb2cluw5746" path="res://assets/models/MSH_T_Corta.res" id="3_f3wyc"] [ext_resource type="ArrayMesh" uid="uid://cp721yqfdga1n" path="res://assets/models/MSH_T_Corta.res" id="3_f3wyc"]
[sub_resource type="BoxShape3D" id="BoxShape3D_f3wyc"] [sub_resource type="BoxShape3D" id="BoxShape3D_f3wyc"]
[node name="Tetromino" type="Node3D"] [node name="Tetromino" type="Node3D"]
script = ExtResource("1_hprdj") script = ExtResource("1_hprdj")
resource = ExtResource("2_f3wyc") resource = ExtResource("2_f3wyc")
mesh_color = Color(1, 0, 0, 1)
[node name="Visuals" type="Node3D" parent="."] [node name="Visuals" type="Node3D" parent="."]

View File

@@ -20,6 +20,7 @@ signal placement_invalid(reason: String)
var _grid: Dictionary = {} # Key: Vector3i, Value: GridCell3D var _grid: Dictionary = {} # Key: Vector3i, Value: GridCell3D
var _tetrominoes: Array[Tetromino] = [] var _tetrominoes: Array[Tetromino] = []
var _occupied_cells: Dictionary = {} # Key: Vector3i, Value: Tetromino3D reference var _occupied_cells: Dictionary = {} # Key: Vector3i, Value: Tetromino3D reference
var selected_tetromino: Tetromino
# Ray casting for placement validation # Ray casting for placement validation
var _space_state: PhysicsDirectSpaceState3D var _space_state: PhysicsDirectSpaceState3D
@@ -28,6 +29,20 @@ var _space_state: PhysicsDirectSpaceState3D
func _ready() -> void: func _ready() -> void:
_space_state = get_world_3d().direct_space_state _space_state = get_world_3d().direct_space_state
_initialize_grid() _initialize_grid()
# Connect to every tetrominos already present on board
var curr_tetrominos = $TetrominoContainer.get_children();
for t in curr_tetrominos:
t.selected.connect(_on_tetromino_selected)
t.deselected.connect(_on_tetromino_deselected)
func _on_tetromino_selected(tetromino: Tetromino):
print("Tetromino selected")
selected_tetromino = tetromino
func _on_tetromino_deselected(tetromino: Tetromino):
print("Tetromino deselected")
selected_tetromino = null
## Initializes the 3D grid structure with empty cells. ## Initializes the 3D grid structure with empty cells.
@@ -72,16 +87,16 @@ func is_cell_empty(grid_pos: Vector3i) -> bool:
## Attempts to place a tetromino at the given grid position with optional rotation. ## Attempts to place a tetromino at the given grid position with optional rotation.
func place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rotation: Quaternion = Quaternion.IDENTITY) -> bool: func place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rot: Quaternion = Quaternion.IDENTITY) -> bool:
# Validate placement # Validate placement
if not _can_place_tetromino(tetromino, grid_position, rotation): if not _can_place_tetromino(tetromino, grid_position, rot):
placement_invalid.emit("Invalid placement: overlapping or out of bounds") placement_invalid.emit("Invalid placement: overlapping or out of bounds")
return false return false
# Update tetromino state # Update tetromino state
tetromino.grid_position = grid_position tetromino.grid_position = grid_position
tetromino.rotation_quat = rotation tetromino.rotation_quat = rot
tetromino.world_position = grid_to_world(grid_position) tetromino.global_position = grid_to_world(grid_position)
# Mark cells as occupied # Mark cells as occupied
var cells = tetromino.get_grid_cells(grid_position) var cells = tetromino.get_grid_cells(grid_position)
@@ -97,8 +112,8 @@ func place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rotation: Qu
## Moves a tetromino to a new position (used during combat). ## Moves a tetromino to a new position (used during combat).
func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool: func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool:
if tetromino not in _tetrominoes: #if tetromino not in _tetrominoes:
return false #return false
# Clear old occupation # Clear old occupation
var old_cells = tetromino.get_grid_cells(tetromino.grid_position) var old_cells = tetromino.get_grid_cells(tetromino.grid_position)
@@ -107,20 +122,20 @@ func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool:
if cell_pos in _grid: if cell_pos in _grid:
_grid[cell_pos].owner = null _grid[cell_pos].owner = null
# Validate new position ## Validate new position
if not _can_place_tetromino(tetromino, new_grid_position, tetromino.rotation_quat): #if not _can_place_tetromino(tetromino, new_grid_position, tetromino.rotation_quat):
# Restore old occupation ## Restore old occupation
for cell_pos in old_cells: #for cell_pos in old_cells:
_occupied_cells[cell_pos] = tetromino #_occupied_cells[cell_pos] = tetromino
if cell_pos in _grid: #if cell_pos in _grid:
_grid[cell_pos].owner = tetromino #_grid[cell_pos].owner = tetromino
placement_invalid.emit("Cannot move to target position") #placement_invalid.emit("Cannot move to target position")
return false #return false
# Update position # Update position
var old_position = tetromino.grid_position var old_position = tetromino.grid_position
tetromino.grid_position = new_grid_position tetromino.grid_position = new_grid_position
tetromino.world_position = grid_to_world(new_grid_position) tetromino.global_position = grid_to_world(new_grid_position)
# Mark cells as occupied # Mark cells as occupied
var new_cells = tetromino.get_grid_cells(new_grid_position) var new_cells = tetromino.get_grid_cells(new_grid_position)
@@ -129,7 +144,7 @@ func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool:
if cell_pos in _grid: if cell_pos in _grid:
_grid[cell_pos].owner = tetromino _grid[cell_pos].owner = tetromino
tetromino_moved.emit(tetromino, grid_to_world(old_position), tetromino.world_position) tetromino_moved.emit(tetromino, grid_to_world(old_position), tetromino.global_position)
return true return true
@@ -194,13 +209,13 @@ func get_tetromino_at(grid_pos: Vector3i) -> Tetromino:
## Gets all adjacent tetrominoes within synergy radius. ## Gets all adjacent tetrominoes within synergy radius.
func get_adjacent_tetrominoes(tetromino: Tetromino, synergy_radius: float = 2.5) -> Array[Tetromino]: func get_adjacent_tetrominoes(tetromino: Tetromino, synergy_radius: float = 2.5) -> Array[Tetromino]:
var adjacent: Array[Tetromino] = [] var adjacent: Array[Tetromino] = []
var world_pos = tetromino.world_position var world_pos = tetromino.global_position
for other in _tetrominoes: for other in _tetrominoes:
if other == tetromino: if other == tetromino:
continue continue
var distance = world_pos.distance_to(other.world_position) var distance = world_pos.distance_to(other.global_position)
if distance <= synergy_radius: if distance <= synergy_radius:
adjacent.append(other) adjacent.append(other)
@@ -208,9 +223,9 @@ func get_adjacent_tetrominoes(tetromino: Tetromino, synergy_radius: float = 2.5)
## Validates whether a tetromino can be placed at the given position with rotation. ## Validates whether a tetromino can be placed at the given position with rotation.
func _can_place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rotation: Quaternion) -> bool: func _can_place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rot: Quaternion) -> bool:
# Get the cells this tetromino would occupy # Get the cells this tetromino would occupy
var cells = tetromino.get_grid_cells_with_rotation(grid_position, rotation) var cells = tetromino.get_grid_cells_with_rotation(grid_position, rot)
# Check all cells are within bounds and empty # Check all cells are within bounds and empty
for cell_pos in cells: for cell_pos in cells:
@@ -222,7 +237,7 @@ func _can_place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rotatio
return false return false
# Raycast check for physical obstacles # Raycast check for physical obstacles
if not _validate_placement_raycast(tetromino, grid_position, rotation): if not _validate_placement_raycast(tetromino, grid_position):
return false return false
return true return true
@@ -230,7 +245,7 @@ func _can_place_tetromino(tetromino: Tetromino, grid_position: Vector3i, rotatio
## Performs raycast validation for tetromino placement. ## Performs raycast validation for tetromino placement.
## Checks for physical obstacles using raycasting. ## Checks for physical obstacles using raycasting.
func _validate_placement_raycast(tetromino: Tetromino, grid_position: Vector3i, rotation: Quaternion) -> bool: func _validate_placement_raycast(tetromino: Tetromino, grid_position: Vector3i) -> bool:
if not _space_state: if not _space_state:
return true return true

View File

@@ -32,6 +32,9 @@ signal health_changed(amount: int)
@export var starting_health: int = 100 @export var starting_health: int = 100
@export var starting_gold: int = 500 @export var starting_gold: int = 500
#
@onready var camera: Camera3D = $Camera3D
# State management # State management
var _current_state: State = State.DRAFT var _current_state: State = State.DRAFT
var _previous_state: State var _previous_state: State
@@ -71,11 +74,11 @@ func _ready() -> void:
_event_bus = get_node("/root/EventBus") if "/root/EventBus" in get_tree().root else null _event_bus = get_node("/root/EventBus") if "/root/EventBus" in get_tree().root else null
# Find board manager in scene # Find board manager in scene
_board_manager = get_parent().get_node_or_null("Board") _board_manager = get_node_or_null("Board")
_combat_system = get_parent().get_node_or_null("CombatSystem") #_combat_system = get_node_or_null("CombatSystem")
_synergy_system = get_parent().get_node_or_null("SynergySystem") #_synergy_system = get_node_or_null("SynergySystem")
_enemy_spawner = get_parent().get_node_or_null("EnemySpawner") #_enemy_spawner = get_node_or_null("EnemySpawner")
_ui_manager = get_parent().get_node_or_null("HUD") #_ui_manager = get_node_or_null("HUD")
# Connect to signal events if event bus exists # Connect to signal events if event bus exists
if _event_bus: if _event_bus:
@@ -88,6 +91,28 @@ func _ready() -> void:
_current_wave = 0 _current_wave = 0
_transition_to_state(State.DRAFT) _transition_to_state(State.DRAFT)
func _input(event: InputEvent) -> void:
if _board_manager.selected_tetromino and event is InputEventMouseMotion:
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
var new_grid_pos = _board_manager.world_to_grid(world_hit)
_board_manager.move_tetromino(_board_manager.selected_tetromino, new_grid_pos)
func _on_tetromino_selected(tetromino: Tetromino):
print("Tetromino selected")
_board_manager.selected_tetromino = tetromino
func _on_tetromino_deselected(tetromino: Tetromino):
print("Tetromino deselected")
_board_manager.selected_tetromino = null
func _process(delta: float) -> void: func _process(delta: float) -> void:
_state_timer += delta _state_timer += delta

View File

@@ -5,16 +5,17 @@
class_name Tetromino class_name Tetromino
extends Node3D extends Node3D
signal hovered(tetromino) signal hovered(tetromino: Tetromino)
signal unhovered(tetromino) signal unhovered(tetromino: Tetromino)
signal selected(tetromino, grid_pos) signal selected(tetromino: Tetromino)
signal deselected(tetromino) signal deselected(tetromino: Tetromino)
# Data # Data
@export var resource: TetrominoDefinition @export var resource: TetrominoDefinition
# Visual # Visual
@export var mesh_color: Color = Color.WHITE var _mesh_color: Color = Color.WHITE
var _material: StandardMaterial3D var _material: StandardMaterial3D
var _is_ghost: bool = false var _is_ghost: bool = false
var _original_color: Color = Color.WHITE var _original_color: Color = Color.WHITE
@@ -63,12 +64,12 @@ func get_grid_cells(at_position: Vector3i = grid_position) -> PackedVector3Array
## Gets grid cells with a specific rotation applied. ## Gets grid cells with a specific rotation applied.
func get_grid_cells_with_rotation(at_position: Vector3i, rotation: Quaternion) -> PackedVector3Array: func get_grid_cells_with_rotation(at_position: Vector3i, rot: Quaternion) -> PackedVector3Array:
var result = PackedVector3Array() var result = PackedVector3Array()
# Apply rotation to relative grid cells # Apply rotation to relative grid cells
for cell in _grid_cells: for cell in _grid_cells:
var rotated = rotation * Vector3(cell) var rotated = rot * Vector3(cell)
var rotated_int = Vector3i(rotated.round()) var rotated_int = Vector3i(rotated.round())
result.append(at_position + rotated_int) result.append(at_position + rotated_int)
@@ -92,7 +93,7 @@ func get_bounds() -> AABB:
## Sets the color/material of the tetromino. ## Sets the color/material of the tetromino.
func set_color(color: Color) -> void: func set_color(color: Color) -> void:
mesh_color = color _mesh_color = color
_original_color = color _original_color = color
if _material: if _material:
_material.albedo_color = color _material.albedo_color = color
@@ -125,7 +126,7 @@ func set_highlighted(enabled: bool) -> void:
if _material: if _material:
_material.emission_enabled = enabled _material.emission_enabled = enabled
if enabled: if enabled:
_material.emission = mesh_color _material.emission = _mesh_color
_material.emission_energy_multiplier = 2.0 _material.emission_energy_multiplier = 2.0
else: else:
_material.emission_energy_multiplier = 0.0 _material.emission_energy_multiplier = 0.0
@@ -143,10 +144,8 @@ func _to_string() -> String:
func _on_selection_area_input_event(camera: Node, event: InputEvent, event_position: Vector3, normal: Vector3, shape_idx: int) -> void: 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 is InputEventMouseButton:
if event.pressed and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
print("Tetromino selected") selected.emit(self)
selected.emit(self, grid_position)
elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT: elif event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
print("Tetromino released")
deselected.emit(self) deselected.emit(self)