@tool class_name EnhancedGridMap extends GridMap signal mesh_library_changed signal grid_updated @export var columns: int = 10: set = set_columns @export var rows: int = 10: set = set_rows @export var floors: int = 3: set = set_floors @export var auto_generate: bool = false: set = set_auto_generate @export var auto_randomize: bool = false # If true, randomizes grid on start @export var normal_items: Array[int] = [0] @export var non_walkable_items: Array[int] = [4] @export var hover_item: int = 1 @export var start_item: int = -1 @export var end_item: int = -1 @export var immutable_items: Array[int] = [1, 2, 3, 4, 16] # Items that cannot be randomized/reset (Start, Safe, Finish, Wall, Safe Zone Wall) var current_mesh_library: MeshLibrary var grid_data: Array = [] # 3D array [floor][row][column] # A* Pathfinding variables (per floor) var astar_by_floor = {} # Dictionary of AStar2D instances per floor var path = [] # Direction and movement systems enum Direction { NORTHWEST, NORTH, NORTHEAST, WEST, CENTER, EAST, SOUTHWEST, SOUTH, SOUTHEAST } var diagonal_movement: bool = false class NeighborInfo: var position: Vector2i var direction: Direction var is_walkable: bool func _init(pos: Vector2i, dir: Direction, walkable: bool): position = pos direction = dir is_walkable = walkable func _ready(): mesh_library_changed.connect(_on_mesh_library_changed) if not Engine.is_editor_hint(): if auto_generate: generate_grid() if auto_randomize: # Safety check: Don't auto-randomize if game mode manages its own arena if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \ and get_node_or_null("/root/LobbyManager") \ and (get_node("/root/LobbyManager").game_mode == "Stop n Go" \ or get_node("/root/LobbyManager").game_mode == "Tekton Doors")): randomize_grid() validate_item_indices() # Core grid management functions func set_columns(value: int): columns = value if auto_generate: generate_grid() else: update_grid_data() func set_rows(value: int): rows = value if auto_generate: generate_grid() else: update_grid_data() func set_floors(value: int): floors = value if auto_generate: generate_grid() else: update_grid_data() func set_auto_generate(value: bool): auto_generate = value if auto_generate: generate_grid() # Item validation func validate_item_indices(): if not mesh_library: print("Warning: No MeshLibrary assigned to GridMap") return var item_list = mesh_library.get_item_list() var max_index = item_list.size() - 1 normal_items = normal_items.filter(func(item): return item >= 0 and item <= max_index) hover_item = clamp(hover_item, 0, max_index) start_item = clamp(start_item, 0, max_index) end_item = clamp(end_item, 0, max_index) non_walkable_items = non_walkable_items.filter(func(item): return item >= 0 and item <= max_index) if normal_items.is_empty(): normal_items = [0] if non_walkable_items.is_empty(): non_walkable_items = [max_index] # Grid generation and management func generate_grid(floor_index: int = -1): if floor_index == -1: clear() # Generate Floor 0 using Modular Generator var map_gen = load("res://scripts/generators/modular_map_generator.gd") if map_gen: var generator = map_gen.new() # Pick random shape var rng = RandomNumberGenerator.new() rng.randomize() var shape = rng.randi() % 4 # 0-3 (Rect, Islands, Maze, Rooms) generator.generate_map(self , columns, rows, shape) else: generate_floor(0) # Generate upper floors (just Items, initially empty or randomized) for y in range(1, floors): # Just clear them initially or fill based on generator? # Usually Floor 1 is items on top of Floor 0. # Let's clean them first. clear_floor(y) else: if floor_index == 0: var map_gen = load("res://scripts/generators/modular_map_generator.gd") if map_gen: var generator = map_gen.new() var rng = RandomNumberGenerator.new() rng.randomize() var shape = rng.randi() % 4 generator.generate_map(self , columns, rows, shape) else: clear_floor(floor_index) generate_floor(floor_index) update_grid_data() initialize_astar() update_astar_costs() # After generating map, if we are auto-randomizing, we should randomize items (Floor 1) # ONLY on valid Floor 0 tiles. if auto_randomize: randomize_grid(1) # Floor 1 items # Explicitly ensure Floor 0 is never randomized with items via standard randomize_grid # (randomize_grid(0) would overwrite the map with items) func generate_floor(floor_index: int): # Fallback / Standard generation if not mesh_library: print("Error: No MeshLibrary assigned to GridMap") return validate_item_indices() current_mesh_library = mesh_library var item_list = mesh_library.get_item_list() if floor_index == 0: for x in range(columns): for z in range(rows): set_cell_item(Vector3i(x, floor_index, z), normal_items[0]) else: # Upper floors start empty unless specified pass # Grid operations func clear_floor(floor_index: int): for x in range(columns): for z in range(rows): set_cell_item(Vector3i(x, floor_index, z), -1) update_grid_data() func clear_grid(floor_index: int = -1): if floor_index == -1: clear() else: clear_floor(floor_index) update_grid_data() # ============================================================================= # Data Serialization Helpers (For Networking) # ============================================================================= func get_floor_data(floor_index: int) -> PackedInt32Array: # Returns a flat PackedInt32Array [x, z, item_id, ...] for the specified floor. var data = PackedInt32Array() # Use get_used_cells() as the source of truth from the GridMap itself for cell in get_used_cells(): if cell.y == floor_index: data.append(cell.x) data.append(cell.z) data.append(get_cell_item(cell)) return data func set_floor_data(floor_index: int, data: PackedInt32Array): # Sets the floor items from a flat PackedInt32Array. Clears existing items on that floor first. clear_floor(floor_index) # Iterate by triplets [x, z, item] var count = data.size() if count % 3 != 0: print("[EnhancedGridMap] Error: Malformed grid data array (size %d not divisible by 3)" % count) return for i in range(0, count, 3): var x = data[i] var z = data[i + 1] var item = data[i + 2] # WALL-SAFETY CHECK: Skip if item is a tile (7-20) and Floor 0 is a wall if floor_index == 1 and item >= 7 and item <= 20: var f0 = get_cell_item(Vector3i(x, 0, z)) if f0 != -1 and f0 in non_walkable_items: # Skip illegal placement from synced data continue set_cell_item(Vector3i(x, floor_index, z), item) update_grid_data() func fill_grid(item_index: int, floor_index: int = -1): if not mesh_library: print("No MeshLibrary assigned to GridMap") return if item_index < 0 or item_index >= mesh_library.get_item_list().size(): print("Invalid item index") return if floor_index == -1: for y in range(floors): fill_floor(item_index, y) else: if floor_index >= 0 and floor_index < floors: fill_floor(item_index, floor_index) else: print("Invalid floor index") update_grid_data() initialize_astar() update_astar_costs() func fill_floor(item_index: int, floor_index: int): for x in range(columns): for z in range(rows): var cell_pos = Vector3i(x, floor_index, z) var current_orientation = get_cell_item_orientation(cell_pos) set_cell_item(cell_pos, item_index, current_orientation) # Randomization functions func randomize_grid(floor_index: int = -1, custom_rng_callable: Callable = Callable()): if floor_index == -1: # Default auto-randomize behavior: # Preserve Floor 0 (Ground) # Shuffle Floor 1 (Objects/Items) shuffle_floor_objects(1) else: randomize_floor(floor_index, custom_rng_callable) update_grid_data() initialize_astar() update_astar_costs() func randomize_floor(floor_index: int, custom_rng_callable: Callable = Callable()): if not mesh_library: print("Error: No MeshLibrary assigned to GridMap") return validate_item_indices() var rng = RandomNumberGenerator.new() rng.randomize() for x in range(columns): for z in range(rows): # Check if current item on this floor is immutable var current_item_on_floor = get_cell_item(Vector3i(x, floor_index, z)) if current_item_on_floor in immutable_items: continue # IMPORTANT: Only place items if Floor 0 has a valid ground tile (Walkable, Safe Zone, etc) var floor_0_item = get_cell_item(Vector3i(x, 0, z)) var is_ground = (floor_0_item != -1 and not floor_0_item in non_walkable_items) if not is_ground: set_cell_item(Vector3i(x, floor_index, z), -1) # Clear item if no ground continue # Use custom callable if provided, otherwise default to ScarcityController or internal logic var item_index = -1 if custom_rng_callable.is_valid(): item_index = custom_rng_callable.call() elif ResourceLoader.exists("res://scripts/controllers/scarcity_controller.gd"): # Use ScarcityController by default if available (Project Specific) # We use call() to avoid direct dependency if class_name isn't fully loaded in tool mode sometimes, # but ScarcityController is a class_name so we can try valid access. # To be safe in a tool script, we can check dynamic usage or just use the static method directly if known. # Since this is "tekton-enet" specific codebase, we can directly use it. item_index = ScarcityController.get_random_tile_id() else: # Fallback default behavior var random_value = rng.randi() % 100 if random_value < 80: item_index = normal_items[rng.randi() % normal_items.size()] else: item_index = non_walkable_items[rng.randi() % non_walkable_items.size()] set_cell_item(Vector3i(x, floor_index, z), item_index) func randomize_grid_custom(randomize_states: Array, floor_index: int = -1): if not mesh_library: print("Error: No MeshLibrary assigned to GridMap") return if floor_index == -1: for y in range(floors): randomize_floor_custom(randomize_states, y) else: if floor_index >= 0 and floor_index < floors: randomize_floor_custom(randomize_states, floor_index) else: print("Invalid floor index") update_grid_data() initialize_astar() update_astar_costs() func randomize_floor_custom(randomize_states: Array, floor_index: int): if randomize_states.is_empty(): print("No randomize states provided") return var rng = RandomNumberGenerator.new() rng.randomize() for x in range(columns): for z in range(rows): var cell_pos = Vector3i(x, floor_index, z) var random_value = rng.randf() * 100 var accumulated_percentage = 0 var selected_state = null for state in randomize_states: if state.include_in_randomize: accumulated_percentage += state.randomize_percentage if random_value <= accumulated_percentage: selected_state = state break var current_orientation = get_cell_item_orientation(cell_pos) if selected_state: set_cell_item(cell_pos, selected_state.id, current_orientation) else: var fallback_state = null for state in randomize_states: if state.include_in_randomize: fallback_state = state break if fallback_state: set_cell_item(cell_pos, fallback_state.id, current_orientation) else: set_cell_item(cell_pos, normal_items[0], current_orientation) func shuffle_floor_objects(floor_idx: int): if not mesh_library: print("Error: No MeshLibrary assigned to GridMap") return # 1. Collect all existing items and their positions on this floor var positions: Array[Vector3i] = [] var items: Array[int] = [] for x in range(columns): for z in range(rows): var cell_pos = Vector3i(x, floor_idx, z) var item = get_cell_item(cell_pos) if item != -1: positions.append(cell_pos) items.append(item) if positions.is_empty(): return # 2. Shuffle the items list var rng = RandomNumberGenerator.new() rng.randomize() items.shuffle() # 3. Reassign items to the existing positions # This preserves the location of "something exists here" (layout) # but randomizes "what exists here" (type) for i in range(positions.size()): var pos = positions[i] var new_item = items[i] var orientation = get_cell_item_orientation(pos) # Keep orientation? Or reset? Keeping seems safer. set_cell_item(pos, new_item, orientation) print("Shuffled %d items on Floor %d" % [items.size(), floor_idx]) #func get_neighbors(current_pos: Vector2i, floor_index: int) -> Array[NeighborInfo]: #var neighbors: Array[NeighborInfo] = [] # #var directions = { #Direction.NORTHWEST: Vector2i(-1, -1), #Direction.NORTH: Vector2i(0, -1), #Direction.NORTHEAST: Vector2i(1, -1), #Direction.WEST: Vector2i(-1, 0), #Direction.EAST: Vector2i(1, 0), #Direction.SOUTHWEST: Vector2i(-1, 1), #Direction.SOUTH: Vector2i(0, 1), #Direction.SOUTHEAST: Vector2i(1, 1) #} # #for dir in directions: #var offset = directions[dir] #var neighbor_pos = current_pos + offset # #if is_position_valid(neighbor_pos): #var is_walkable = is_cell_walkable(neighbor_pos, floor_index) # ## Check for obstacles - specifically for orthogonal movement #if not is_diagonal_direction(dir) and is_blocked_by_obstacle(current_pos, neighbor_pos, 3): #is_walkable = false # ## Special handling for diagonal movement #if is_diagonal_direction(dir): #var adjacent1: Vector2i #var adjacent2: Vector2i # #match dir: #Direction.NORTHWEST: #adjacent1 = current_pos + Vector2i(-1, 0) # West #adjacent2 = current_pos + Vector2i(0, -1) # North #Direction.NORTHEAST: #adjacent1 = current_pos + Vector2i(1, 0) # East #adjacent2 = current_pos + Vector2i(0, -1) # North #Direction.SOUTHWEST: #adjacent1 = current_pos + Vector2i(-1, 0) # West #adjacent2 = current_pos + Vector2i(0, 1) # South #Direction.SOUTHEAST: #adjacent1 = current_pos + Vector2i(1, 0) # East #adjacent2 = current_pos + Vector2i(0, 1) # South # ## For diagonal movement, both adjacent cells must be walkable ## AND the movements to those adjacent cells must not be blocked #is_walkable = is_walkable and \ #is_position_valid(adjacent1) and is_cell_walkable(adjacent1, floor_index) and \ #is_position_valid(adjacent2) and is_cell_walkable(adjacent2, floor_index) and \ #not is_blocked_by_obstacle(current_pos, adjacent1, 3) and \ #not is_blocked_by_obstacle(current_pos, adjacent2, 3) # #if diagonal_movement or not is_diagonal_direction(dir): #neighbors.append(NeighborInfo.new(neighbor_pos, dir, is_walkable)) # #return neighbors func get_neighbors(current_pos: Vector2i, floor_index: int) -> Array[NeighborInfo]: var neighbors: Array[NeighborInfo] = [] # Four orthogonal directions var directions = { Direction.NORTH: Vector2i(0, -1), Direction.EAST: Vector2i(1, 0), Direction.SOUTH: Vector2i(0, 1), Direction.WEST: Vector2i(-1, 0) } # Add diagonal directions if enabled if diagonal_movement: directions[Direction.NORTHWEST] = Vector2i(-1, -1) directions[Direction.NORTHEAST] = Vector2i(1, -1) directions[Direction.SOUTHWEST] = Vector2i(-1, 1) directions[Direction.SOUTHEAST] = Vector2i(1, 1) for dir in directions: var offset = directions[dir] var neighbor_pos = current_pos + offset if is_position_valid(neighbor_pos): var is_walkable = is_cell_walkable(neighbor_pos, floor_index) # Check if movement to this neighbor is blocked by obstacles # if not is_diagonal_direction(dir) and is_movement_blocked(current_pos, neighbor_pos, floor_index): # is_walkable = false if is_diagonal_direction(dir): # For diagonal movement, check if both orthogonal paths are blocked var mid1 = Vector2i(neighbor_pos.x, current_pos.y) var mid2 = Vector2i(current_pos.x, neighbor_pos.y) if is_walkable: neighbors.append(NeighborInfo.new(neighbor_pos, dir, is_walkable)) return neighbors # Helper functions for neighbor checking func is_diagonal_direction(direction: Direction) -> bool: return direction in [Direction.NORTHWEST, Direction.NORTHEAST, Direction.SOUTHWEST, Direction.SOUTHEAST] func is_position_valid(pos: Vector2i) -> bool: return pos.x >= 0 and pos.x < columns and pos.y >= 0 and pos.y < rows func is_cell_walkable(pos: Vector2i, floor_index: int) -> bool: var cell_item = get_cell_item(Vector3i(pos.x, floor_index, pos.y)) return cell_item != -1 and not (cell_item in non_walkable_items) # Improved A* pathfinding func initialize_astar(): astar_by_floor.clear() for y in range(floors): var astar = AStar2D.new() # Add all points for x in range(columns): for z in range(rows): var point_id = z * columns + x astar.add_point(point_id, Vector2(x, z)) # Connect points based on neighbors for x in range(columns): for z in range(rows): var current_pos = Vector2i(x, z) var current_point_id = z * columns + x if not is_cell_walkable(current_pos, y): continue var neighbors = get_neighbors(current_pos, y) for neighbor in neighbors: if neighbor.is_walkable: var neighbor_id = neighbor.position.y * columns + neighbor.position.x if not astar.are_points_connected(current_point_id, neighbor_id): var weight = 1.0 if not is_diagonal_direction(neighbor.direction) else 1.4142 # Check if movement is allowed by obstacles if true: # Obstacle check removed astar.connect_points(current_point_id, neighbor_id, true) astar.set_point_weight_scale(neighbor_id, weight) astar_by_floor[y] = astar update_astar_costs() func find_path(start: Vector2, end: Vector2, floor_index: int = 0, clear_path_visual: bool = true, visualize: bool = true) -> Array: var astar = astar_by_floor.get(floor_index) if not astar: return [] var start_point = start.y * columns + start.x var end_point = end.y * columns + end.x if not is_position_valid(Vector2i(int(start.x), int(start.y))) or not is_position_valid(Vector2i(int(end.x), int(end.y))): return [] if not astar.has_point(start_point) or not astar.has_point(end_point): return [] path = astar.get_point_path(start_point, end_point) # TEMPORARY PATH VISUALIZATION - Moved to Layer 2 (Overlay) to protect Floor 0 and Floor 1 if visualize: if clear_path_visual: clear_path_visualization() # Always use Layer 2 for these temporary markers if start_item >= 0: set_cell_item(Vector3i(start.x, 2, start.y), start_item) if end_item >= 0: set_cell_item(Vector3i(end.x, 2, end.y), end_item) for point in path: if Vector2(point.x, point.y) != start and Vector2(point.x, point.y) != end: set_cell_item(Vector3i(point.x, 2, point.y), hover_item) return path # Path visualization - Standardized to Layer 2 func clear_path_visualization(unused_floor_idx: int = 0): # We strictly clear Layer 2 (Overlay) and reset to -1 (Empty) # This ensures we never overwrite Layer 0 (Floor) or Layer 1 (Items) for x in range(columns): for z in range(rows): var cell_item = get_cell_item(Vector3i(x, 2, z)) # Protect Safe Zone (ID 2) from being cleared by path visualization if cell_item == 2: continue if cell_item == hover_item or cell_item == start_item or cell_item == end_item: set_cell_item(Vector3i(x, 2, z), -1) # Cost calculation and updates func get_cell_cost(x: int, z: int, floor_index: int = 0) -> float: var cell_item = get_cell_item(Vector3i(x, floor_index, z)) # Block -1 (Void) and Non-Walkable Items if cell_item == -1 or cell_item in non_walkable_items: return INF elif cell_item == hover_item: return 0.5 elif cell_item == start_item or cell_item == end_item: return 0.0 return 1.0 func update_astar_costs(): for floor_index in range(floors): var astar = astar_by_floor.get(floor_index) if astar: for x in range(columns): for z in range(rows): var point_id = z * columns + x var cost = get_cell_cost(x, z, floor_index) if cost == INF: astar.set_point_disabled(point_id, true) else: astar.set_point_disabled(point_id, false) astar.set_point_weight_scale(point_id, cost) # Grid data management func update_grid_data(): grid_data.clear() for y in range(floors): var floor_data = [] for z in range(rows): var row = [] for x in range(columns): row.append(get_cell_item(Vector3i(x, y, z))) floor_data.append(row) grid_data.append(floor_data) emit_signal("grid_updated") # Orientation helper func get_cell_orientation(pos: Vector3i) -> int: return get_cell_item_orientation(pos) # Cell rotation handling func get_cell_rotation(position: Vector3i) -> int: return get_cell_item_orientation(position) func set_cell_rotation(position: Vector3i, mode: int): var item = get_cell_item(position) if item != -1: set_cell_item(position, item, mode) # Mesh library handling func _on_mesh_library_changed(): validate_item_indices() if auto_generate: generate_grid() _update_cell_option_buttons() func _update_cell_option_buttons(): if not mesh_library: return var item_list = mesh_library.get_item_list() for x in range(columns): for z in range(rows): var position = Vector3i(x, 0, z) var cell_item = get_cell_item(position) if cell_item != -1 and cell_item < item_list.size(): set_cell_item(position, cell_item) else: set_cell_item(position, 0) func _set(property, value): if property.begins_with("data/cells") and Engine.is_editor_hint(): call_deferred("_emit_grid_updated") return false func _emit_grid_updated(): emit_signal("grid_updated") # Toggle diagonal movement func set_diagonal_movement(enable: bool): diagonal_movement = enable initialize_astar()