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