extends Node2D signal score_changed(points: int) enum GameState { WAITING, # Waiting for player input SELECTING, # First tile selected SWAPPING, # Animating tile swap PROCESSING # Processing matches and cascades } var GRID_SIZE := Vector2i(8, 8) var TILE_TYPES := 5 const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn") var grid := [] var tile_size: float = 48.0 var grid_offset: Vector2 var current_state: GameState = GameState.WAITING var selected_tile: Node2D = null var cursor_position: Vector2i = Vector2i(0, 0) var keyboard_navigation_enabled: bool = false func _ready(): DebugManager.log_debug("Match3 _ready() started", "Match3") # Set up initial gem pool var gem_indices = [] for i in range(TILE_TYPES): gem_indices.append(i) const TileScript = preload("res://scenes/game/gameplays/tile.gd") TileScript.set_active_gem_pool(gem_indices) _calculate_grid_layout() _initialize_grid() DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3") # Debug: Check scene tree structure immediately call_deferred("_debug_scene_structure") func _calculate_grid_layout(): var viewport_size = get_viewport().get_visible_rect().size var available_width = viewport_size.x * 0.8 # Use 80% of screen width var available_height = viewport_size.y * 0.7 # Use 70% of screen height # Calculate tile size based on available space var max_tile_width = available_width / GRID_SIZE.x var max_tile_height = available_height / GRID_SIZE.y tile_size = min(max_tile_width, max_tile_height) # Align grid to left side with some margin var total_grid_height = tile_size * GRID_SIZE.y grid_offset = Vector2( 50, # Left margin (viewport_size.y - total_grid_height) / 2 + 50 # Vertically centered with top margin ) func _initialize_grid(): # Update gem pool BEFORE creating any tiles var gem_indices = [] for i in range(TILE_TYPES): gem_indices.append(i) const TileScript = preload("res://scenes/game/gameplays/tile.gd") TileScript.set_active_gem_pool(gem_indices) for y in range(GRID_SIZE.y): grid.append([]) for x in range(GRID_SIZE.x): var tile = TILE_SCENE.instantiate() var tile_position = grid_offset + Vector2(x, y) * tile_size tile.position = tile_position tile.grid_position = Vector2i(x, y) add_child(tile) # Set tile type after adding to scene tree so sprite reference is available var new_type = randi() % TILE_TYPES tile.tile_type = new_type # Connect tile signals tile.tile_selected.connect(_on_tile_selected) DebugManager.log_debug("Created tile at grid(%d,%d) world_pos(%s) with type %d" % [x, y, tile_position, new_type], "Match3") grid[y].append(tile) func _has_match_at(pos: Vector2i) -> bool: # Fixed: Add bounds and null checks if pos.x < 0 or pos.y < 0 or pos.x >= GRID_SIZE.x or pos.y >= GRID_SIZE.y: return false if not grid[pos.y][pos.x]: return false var matches_horizontal = _get_match_line(pos, Vector2i(1, 0)) if matches_horizontal.size() >= 3: return true var matches_vertical = _get_match_line(pos, Vector2i(0, 1)) return matches_vertical.size() >= 3 # Fixed: Add missing function to check for any matches on the board func _check_for_matches() -> bool: for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if _has_match_at(Vector2i(x, y)): return true return false func _get_match_line(start: Vector2i, dir: Vector2i) -> Array: var line = [grid[start.y][start.x]] var type = grid[start.y][start.x].tile_type # Fixed: Check in both directions separately to avoid infinite loops for offset in [1, -1]: var current = start + dir * offset while current.x >= 0 and current.y >= 0 and current.x < GRID_SIZE.x and current.y < GRID_SIZE.y: var neighbor = grid[current.y][current.x] # Fixed: Add null check to prevent crashes if not neighbor or neighbor.tile_type != type: break line.append(neighbor) current += dir * offset return line func _clear_matches(): var to_clear := [] for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): # Fixed: Add null check before processing if not grid[y][x]: continue var pos = Vector2i(x, y) var horiz = _get_match_line(pos, Vector2i(1, 0)) if horiz.size() >= 3: to_clear.append_array(horiz) var vert = _get_match_line(pos, Vector2i(0, 1)) if vert.size() >= 3: to_clear.append_array(vert) # Remove duplicates using dictionary keys trick var unique_dict := {} for tile in to_clear: unique_dict[tile] = true to_clear = unique_dict.keys() # Fixed: Only proceed if there are matches to clear if to_clear.size() == 0: return for tile in to_clear: grid[tile.grid_position.y][tile.grid_position.x] = null tile.queue_free() _drop_tiles() await get_tree().create_timer(0.2).timeout _fill_empty_cells() func _drop_tiles(): var moved = true while moved: moved = false for x in range(GRID_SIZE.x): # Fixed: Start from GRID_SIZE.y - 1 to avoid out of bounds for y in range(GRID_SIZE.y - 1, -1, -1): var tile = grid[y][x] # Fixed: Check bounds before accessing y + 1 if tile and y + 1 < GRID_SIZE.y and not grid[y + 1][x]: grid[y + 1][x] = tile grid[y][x] = null tile.grid_position = Vector2i(x, y + 1) # You can animate position here using Tween for smooth drop: # tween.interpolate_property(tile, "position", tile.position, grid_offset + Vector2(x, y + 1) * tile_size, 0.2) tile.position = grid_offset + Vector2(x, y + 1) * tile_size moved = true func _fill_empty_cells(): for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if not grid[y][x]: var tile = TILE_SCENE.instantiate() tile.grid_position = Vector2i(x, y) tile.tile_type = randi() % TILE_TYPES tile.position = grid_offset + Vector2(x, y) * tile_size grid[y][x] = tile add_child(tile) # Fixed: Add recursion protection to prevent stack overflow await get_tree().create_timer(0.1).timeout var max_iterations = 10 var iteration = 0 while _check_for_matches() and iteration < max_iterations: _clear_matches() await get_tree().create_timer(0.1).timeout iteration += 1 func regenerate_grid(): # Use time-based seed to ensure different patterns each time var new_seed = Time.get_ticks_msec() seed(new_seed) DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3") # Fixed: Clear ALL tile children, not just ones in grid array # This handles any orphaned tiles that might not be tracked in the grid var children_to_remove = [] for child in get_children(): if child.has_method("get_script") and child.get_script(): var script_path = child.get_script().resource_path if script_path == "res://scenes/game/gameplays/tile.gd": children_to_remove.append(child) # Remove all found tile children for child in children_to_remove: child.free() # Also clear grid array references for y in range(grid.size()): if grid[y] and grid[y] is Array: for x in range(grid[y].size()): # Set to null since we already freed the nodes above grid[y][x] = null # Clear the grid array grid.clear() # No need to wait for nodes to be freed since we used free() # Fixed: Recalculate grid layout before regenerating tiles _calculate_grid_layout() # Regenerate the grid (gem pool is updated in _initialize_grid) _initialize_grid() func set_tile_types(new_count: int): TILE_TYPES = new_count # Regenerate grid with new tile types (gem pool is updated in regenerate_grid) await regenerate_grid() func set_grid_size(new_size: Vector2i): DebugManager.log_debug("Changing grid size from " + str(GRID_SIZE) + " to " + str(new_size), "Match3") GRID_SIZE = new_size # Regenerate grid with new size await regenerate_grid() func reset_all_visual_states() -> void: # Debug function to reset all tile visual states DebugManager.log_debug("Resetting all tile visual states", "Match3") for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if grid[y][x] and grid[y][x].has_method("force_reset_visual_state"): grid[y][x].force_reset_visual_state() # Reset game state selected_tile = null current_state = GameState.WAITING keyboard_navigation_enabled = false func _debug_scene_structure() -> void: DebugManager.log_debug("=== Scene Structure Debug ===", "Match3") DebugManager.log_debug("Match3 node children count: %d" % get_child_count(), "Match3") DebugManager.log_debug("Match3 global position: %s" % global_position, "Match3") DebugManager.log_debug("Match3 scale: %s" % scale, "Match3") # Check if grid is properly initialized if not grid or grid.size() == 0: DebugManager.log_error("Grid not initialized when debug structure called", "Match3") return # Check tiles var tile_count = 0 for y in range(GRID_SIZE.y): for x in range(GRID_SIZE.x): if y < grid.size() and x < grid[y].size() and grid[y][x]: tile_count += 1 DebugManager.log_debug("Created %d tiles out of %d expected" % [tile_count, GRID_SIZE.x * GRID_SIZE.y], "Match3") # Check first tile in detail if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: var first_tile = grid[0][0] DebugManager.log_debug("First tile global position: %s" % first_tile.global_position, "Match3") DebugManager.log_debug("First tile local position: %s" % first_tile.position, "Match3") # Check parent chain var current_node = self var depth = 0 while current_node and depth < 10: DebugManager.log_debug("Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()], "Match3") current_node = current_node.get_parent() depth += 1 func _input(event: InputEvent) -> void: # Debug key to reset all visual states if event.is_action_pressed("ui_menu_toggle") and DebugManager.is_debug_enabled(): reset_all_visual_states() return if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: return # Handle keyboard/gamepad navigation if event.is_action_pressed("move_up"): _move_cursor(Vector2i.UP) keyboard_navigation_enabled = true elif event.is_action_pressed("move_down"): _move_cursor(Vector2i.DOWN) keyboard_navigation_enabled = true elif event.is_action_pressed("move_left"): _move_cursor(Vector2i.LEFT) keyboard_navigation_enabled = true elif event.is_action_pressed("move_right"): _move_cursor(Vector2i.RIGHT) keyboard_navigation_enabled = true elif event.is_action_pressed("select_gem"): if keyboard_navigation_enabled: _select_tile_at_cursor() func _move_cursor(direction: Vector2i) -> void: var old_pos = cursor_position var new_pos = cursor_position + direction new_pos.x = clamp(new_pos.x, 0, GRID_SIZE.x - 1) new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1) if new_pos != cursor_position: DebugManager.log_debug("Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], "Match3") # Clear highlighting from old cursor position (only if not selected) var old_tile = grid[cursor_position.y][cursor_position.x] if old_tile and not old_tile.is_selected: old_tile.is_highlighted = false cursor_position = new_pos # Highlight new cursor position (only if not selected) var new_tile = grid[cursor_position.y][cursor_position.x] if new_tile and not new_tile.is_selected: new_tile.is_highlighted = true func _select_tile_at_cursor() -> void: if cursor_position.x >= 0 and cursor_position.y >= 0 and cursor_position.x < GRID_SIZE.x and cursor_position.y < GRID_SIZE.y: var tile = grid[cursor_position.y][cursor_position.x] if tile: DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") _on_tile_selected(tile) func _on_tile_selected(tile: Node2D) -> void: if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: DebugManager.log_debug("Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]], "Match3") return DebugManager.log_debug("Tile selected at (%d,%d), gem type: %d" % [tile.grid_position.x, tile.grid_position.y, tile.tile_type], "Match3") if current_state == GameState.WAITING: # First tile selection _select_tile(tile) elif current_state == GameState.SELECTING: if tile == selected_tile: # Deselect current tile DebugManager.log_debug("Same tile clicked - deselecting", "Match3") _deselect_tile() else: # Attempt to swap with selected tile DebugManager.log_debug("Attempting swap between (%d,%d) and (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y, tile.grid_position.x, tile.grid_position.y], "Match3") _attempt_swap(selected_tile, tile) func _select_tile(tile: Node2D) -> void: selected_tile = tile tile.is_selected = true current_state = GameState.SELECTING DebugManager.log_debug("Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3") func _deselect_tile() -> void: if selected_tile: DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3") # Clear selection state selected_tile.is_selected = false # Handle cursor highlighting for keyboard navigation if keyboard_navigation_enabled: # Clear all highlighting first to avoid conflicts selected_tile.is_highlighted = false # Re-highlight cursor position if it's different from the deselected tile var cursor_tile = grid[cursor_position.y][cursor_position.x] if cursor_tile: cursor_tile.is_highlighted = true DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") else: # For mouse navigation, just clear highlighting selected_tile.is_highlighted = false selected_tile = null current_state = GameState.WAITING func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool: if not tile1 or not tile2: return false var pos1 = tile1.grid_position var pos2 = tile2.grid_position var diff = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y) return diff == 1 func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void: if not _are_adjacent(tile1, tile2): DebugManager.log_debug("Tiles are not adjacent, cannot swap", "Match3") return DebugManager.log_debug("Starting swap animation: (%d,%d)[type:%d] <-> (%d,%d)[type:%d]" % [tile1.grid_position.x, tile1.grid_position.y, tile1.tile_type, tile2.grid_position.x, tile2.grid_position.y, tile2.tile_type], "Match3") current_state = GameState.SWAPPING await _swap_tiles(tile1, tile2) # Check if swap creates matches if _has_match_at(tile1.grid_position) or _has_match_at(tile2.grid_position): DebugManager.log_info("Valid swap created matches at (%d,%d) or (%d,%d)" % [tile1.grid_position.x, tile1.grid_position.y, tile2.grid_position.x, tile2.grid_position.y], "Match3") _deselect_tile() current_state = GameState.PROCESSING _clear_matches() await get_tree().create_timer(0.3).timeout current_state = GameState.WAITING else: # Invalid swap - revert DebugManager.log_debug("No matches created, reverting swap", "Match3") await _swap_tiles(tile1, tile2) # Swap back DebugManager.log_debug("Swap reverted successfully", "Match3") _deselect_tile() current_state = GameState.WAITING func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void: if not tile1 or not tile2: DebugManager.log_error("Cannot swap tiles - one or both tiles are null", "Match3") return # Update grid positions var pos1 = tile1.grid_position var pos2 = tile2.grid_position DebugManager.log_debug("Swapping tile positions: (%d,%d) -> (%d,%d), (%d,%d) -> (%d,%d)" % [pos1.x, pos1.y, pos2.x, pos2.y, pos2.x, pos2.y, pos1.x, pos1.y], "Match3") tile1.grid_position = pos2 tile2.grid_position = pos1 # Update grid array grid[pos1.y][pos1.x] = tile2 grid[pos2.y][pos2.x] = tile1 # Animate position swap var target_pos1 = grid_offset + Vector2(pos2.x, pos2.y) * tile_size var target_pos2 = grid_offset + Vector2(pos1.x, pos1.y) * tile_size DebugManager.log_trace("Animating tile movement over 0.3 seconds", "Match3") var tween = create_tween() tween.set_parallel(true) tween.tween_property(tile1, "position", target_pos1, 0.3) tween.tween_property(tile2, "position", target_pos2, 0.3) await tween.finished DebugManager.log_trace("Tile swap animation completed", "Match3")