Files
trenchlock/scripts/board_manager.gd
2026-01-13 08:31:42 +01:00

271 lines
8.5 KiB
GDScript

## Manages the 3D game board state, tetromino placement, and grid operations.
##
## Provides 3D grid representation, tetromino placement, collision detection,
## rotation logic, and real-time re positioning during combat.
class_name BoardManager3D
extends Node3D
# Signals
signal tetromino_placed(tetromino: Tetromino)
signal tetromino_moved(tetromino: Tetromino, old_position: Vector3, new_position: Vector3)
signal tetromino_rotated(tetromino: Tetromino, rotation: Quaternion)
signal placement_invalid(reason: String)
# Grid configuration
@export var grid_size: Vector3i = Vector3i(8, 5, 8) # Width (X), Height (Y), Depth (Z)
@export var cell_size: float = 1.0
@export var placement_plane_y: float = 0.0 # Y-position where tetrominoes are placed
# Internal state
var _grid: Dictionary = {} # Key: Vector3i, Value: GridCell3D
var _tetrominoes: Array[Tetromino] = []
var _occupied_cells: Dictionary = {} # Key: Vector3i, Value: Tetromino3D reference
# Ray casting for placement validation
var _space_state: PhysicsDirectSpaceState3D
func _ready() -> void:
_space_state = get_world_3d().direct_space_state
_initialize_grid()
## Initializes the 3D grid structure with empty cells.
func _initialize_grid() -> void:
for x in range(grid_size.x):
for y in range(grid_size.y):
for z in range(grid_size.z):
var cell_pos = Vector3i(x, y, z)
var cell = GridCell.new()
cell.position = cell_pos
_grid[cell_pos] = cell
## Converts world position to grid coordinates.
func world_to_grid(world_pos: Vector3) -> Vector3i:
var local_pos = world_pos - (Vector3(grid_size) * cell_size / 2.0)
return Vector3i(
int(local_pos.x / cell_size),
int(local_pos.y / cell_size),
int(local_pos.z / cell_size)
)
## Converts grid coordinates to world position.
func grid_to_world(grid_pos: Vector3i) -> Vector3:
var center_offset = Vector3(grid_size) * cell_size / 2.0
return Vector3(grid_pos) * cell_size + center_offset
## Checks if a grid position is within bounds.
func is_within_bounds(grid_pos: Vector3i) -> bool:
return (grid_pos.x >= 0 and grid_pos.x < grid_size.x and
grid_pos.y >= 0 and grid_pos.y < grid_size.y and
grid_pos.z >= 0 and grid_pos.z < grid_size.z)
## Checks if a grid cell is empty and unoccupied.
func is_cell_empty(grid_pos: Vector3i) -> bool:
if not is_within_bounds(grid_pos):
return false
return grid_pos not in _occupied_cells
## 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:
# Validate placement
if not _can_place_tetromino(tetromino, grid_position, rotation):
placement_invalid.emit("Invalid placement: overlapping or out of bounds")
return false
# Update tetromino state
tetromino.grid_position = grid_position
tetromino.rotation_quat = rotation
tetromino.world_position = grid_to_world(grid_position)
# Mark cells as occupied
var cells = tetromino.get_grid_cells(grid_position)
for cell_pos in cells:
_occupied_cells[cell_pos] = tetromino
if cell_pos in _grid:
_grid[cell_pos].owner = tetromino
_tetrominoes.append(tetromino)
tetromino_placed.emit(tetromino)
return true
## Moves a tetromino to a new position (used during combat).
func move_tetromino(tetromino: Tetromino, new_grid_position: Vector3i) -> bool:
if tetromino not in _tetrominoes:
return false
# Clear old occupation
var old_cells = tetromino.get_grid_cells(tetromino.grid_position)
for cell_pos in old_cells:
_occupied_cells.erase(cell_pos)
if cell_pos in _grid:
_grid[cell_pos].owner = null
# Validate new position
if not _can_place_tetromino(tetromino, new_grid_position, tetromino.rotation_quat):
# Restore old occupation
for cell_pos in old_cells:
_occupied_cells[cell_pos] = tetromino
if cell_pos in _grid:
_grid[cell_pos].owner = tetromino
placement_invalid.emit("Cannot move to target position")
return false
# Update position
var old_position = tetromino.grid_position
tetromino.grid_position = new_grid_position
tetromino.world_position = grid_to_world(new_grid_position)
# Mark cells as occupied
var new_cells = tetromino.get_grid_cells(new_grid_position)
for cell_pos in new_cells:
_occupied_cells[cell_pos] = tetromino
if cell_pos in _grid:
_grid[cell_pos].owner = tetromino
tetromino_moved.emit(tetromino, grid_to_world(old_position), tetromino.world_position)
return true
## Rotates a tetromino around the Y-axis (XZ plane rotation).
func rotate_tetromino_y(tetromino: Tetromino, angle: float) -> bool:
if tetromino not in _tetrominoes:
return false
var new_rotation = tetromino.rotation_quat * Quaternion.from_euler(Vector3(0, angle, 0))
# Validate rotation doesn't cause overlap
if not _can_place_tetromino(tetromino, tetromino.grid_position, new_rotation):
placement_invalid.emit("Cannot rotate: would overlap or go out of bounds")
return false
# Clear old occupation
var old_cells = tetromino.get_grid_cells(tetromino.grid_position)
for cell_pos in old_cells:
_occupied_cells.erase(cell_pos)
if cell_pos in _grid:
_grid[cell_pos].owner = null
# Update rotation
tetromino.rotation_quat = new_rotation
# Mark cells as occupied with new rotation
var new_cells = tetromino.get_grid_cells(tetromino.grid_position)
for cell_pos in new_cells:
_occupied_cells[cell_pos] = tetromino
if cell_pos in _grid:
_grid[cell_pos].owner = tetromino
tetromino_rotated.emit(tetromino, new_rotation)
return true
## Removes a tetromino from the board (e.g., destroyed during combat).
func remove_tetromino(tetromino: Tetromino) -> void:
if tetromino not in _tetrominoes:
return
# Clear occupied cells
var cells = tetromino.get_grid_cells(tetromino.grid_position)
for cell_pos in cells:
_occupied_cells.erase(cell_pos)
if cell_pos in _grid:
_grid[cell_pos].owner = null
_tetrominoes.erase(tetromino)
## Gets all placed tetrominoes.
func get_tetrominoes() -> Array[Tetromino]:
return _tetrominoes.duplicate()
## Gets the tetromino at a specific grid position, if any.
func get_tetromino_at(grid_pos: Vector3i) -> Tetromino:
return _occupied_cells.get(grid_pos)
## Gets all adjacent tetrominoes within synergy radius.
func get_adjacent_tetrominoes(tetromino: Tetromino, synergy_radius: float = 2.5) -> Array[Tetromino]:
var adjacent: Array[Tetromino] = []
var world_pos = tetromino.world_position
for other in _tetrominoes:
if other == tetromino:
continue
var distance = world_pos.distance_to(other.world_position)
if distance <= synergy_radius:
adjacent.append(other)
return adjacent
## 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:
# Get the cells this tetromino would occupy
var cells = tetromino.get_grid_cells_with_rotation(grid_position, rotation)
# Check all cells are within bounds and empty
for cell_pos in cells:
if not is_within_bounds(cell_pos):
return false
# Check if occupied by another tetromino
if cell_pos in _occupied_cells and _occupied_cells[cell_pos] != tetromino:
return false
# Raycast check for physical obstacles
if not _validate_placement_raycast(tetromino, grid_position, rotation):
return false
return true
## Performs raycast validation for tetromino placement.
## Checks for physical obstacles using raycasting.
func _validate_placement_raycast(tetromino: Tetromino, grid_position: Vector3i, rotation: Quaternion) -> bool:
if not _space_state:
return true
var world_pos = grid_to_world(grid_position)
# Cast a ray downward from above the placement position
var query = PhysicsRayQueryParameters3D.create(
world_pos + Vector3.UP * 10.0,
world_pos
)
query.exclude = [tetromino]
var result = _space_state.intersect_ray(query)
# If ray hits something other than the grid plane, placement is invalid
if result and result.get("collider") and result.collider != self:
return false
return true
## Gets a string representation of the grid for debugging.
func get_grid_state() -> String:
var state = "Grid State (X x Y x Z = %d x %d x %d):\n" % [grid_size.x, grid_size.y, grid_size.z]
state += "Occupied cells: %d\n" % _occupied_cells.size()
state += "Placed tetrominoes: %d\n" % _tetrominoes.size()
for tetromino in _tetrominoes:
state += " - %s at %v\n" % [tetromino.shape_type, tetromino.grid_position]
return state
## Clears the board (removes all tetrominoes).
func clear_board() -> void:
for tetromino in _tetrominoes.duplicate():
remove_tetromino(tetromino)