486 lines
17 KiB
GDScript
486 lines
17 KiB
GDScript
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")
|
|
|
|
# Gem pool will be set individually on each tile during creation
|
|
|
|
_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():
|
|
# Create gem pool for current tile types
|
|
var gem_indices = []
|
|
for i in range(TILE_TYPES):
|
|
gem_indices.append(i)
|
|
|
|
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 gem types for this tile
|
|
tile.set_active_gem_types(gem_indices)
|
|
|
|
# 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
|
|
|
|
# Clear grid references first, then queue nodes for removal
|
|
for tile in to_clear:
|
|
grid[tile.grid_position.y][tile.grid_position.x] = null
|
|
tile.queue_free()
|
|
|
|
# Wait one frame for nodes to be queued properly
|
|
await get_tree().process_frame
|
|
_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():
|
|
# Create gem pool for current tile types
|
|
var gem_indices = []
|
|
for i in range(TILE_TYPES):
|
|
gem_indices.append(i)
|
|
|
|
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.position = grid_offset + Vector2(x, y) * tile_size
|
|
add_child(tile)
|
|
|
|
# Set gem types for this tile
|
|
tile.set_active_gem_types(gem_indices)
|
|
|
|
# Set random tile type
|
|
tile.tile_type = randi() % TILE_TYPES
|
|
|
|
grid[y][x] = tile
|
|
# Connect tile signals for new tiles
|
|
tile.tile_selected.connect(_on_tile_selected)
|
|
|
|
# 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)
|
|
|
|
# First clear grid array references to prevent access to nodes being freed
|
|
for y in range(grid.size()):
|
|
if grid[y] and grid[y] is Array:
|
|
for x in range(grid[y].size()):
|
|
grid[y][x] = null
|
|
|
|
# Clear the grid array
|
|
grid.clear()
|
|
|
|
# Remove all found tile children using queue_free for safe cleanup
|
|
for child in children_to_remove:
|
|
child.queue_free()
|
|
|
|
# Wait for nodes to be properly freed from the scene tree
|
|
await get_tree().process_frame
|
|
|
|
# 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("action_east") 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("action_south"):
|
|
if keyboard_navigation_enabled:
|
|
_select_tile_at_cursor()
|
|
|
|
|
|
func _move_cursor(direction: Vector2i) -> void:
|
|
# Input validation for direction vector
|
|
if abs(direction.x) > 1 or abs(direction.y) > 1:
|
|
DebugManager.log_error("Invalid cursor direction vector: " + str(direction), "Match3")
|
|
return
|
|
|
|
if direction.x != 0 and direction.y != 0:
|
|
DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3")
|
|
return
|
|
|
|
var old_pos = cursor_position
|
|
var new_pos = cursor_position + direction
|
|
|
|
# Bounds checking
|
|
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:
|
|
# Validate old position before accessing grid
|
|
if old_pos.x >= 0 and old_pos.y >= 0 and old_pos.x < GRID_SIZE.x and old_pos.y < GRID_SIZE.y:
|
|
var old_tile = grid[cursor_position.y][cursor_position.x]
|
|
if old_tile and not old_tile.is_selected:
|
|
old_tile.is_highlighted = false
|
|
|
|
DebugManager.log_debug("Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], "Match3")
|
|
cursor_position = new_pos
|
|
|
|
# Validate new position before accessing grid
|
|
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 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")
|