## 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)