feature/saves-and-score (#8)
Reviewed-on: #8 Co-authored-by: Vladimir nett00n Budylnikov <git@nett00n.org> Co-committed-by: Vladimir nett00n Budylnikov <git@nett00n.org>
This commit is contained in:
@@ -16,20 +16,29 @@ func _ready() -> void:
|
||||
if not back_button.pressed.is_connected(_on_back_button_pressed):
|
||||
back_button.pressed.connect(_on_back_button_pressed)
|
||||
|
||||
# Default to match3 for now
|
||||
set_gameplay_mode("match3")
|
||||
# GameManager will set the gameplay mode, don't set default here
|
||||
DebugManager.log_debug("Game _ready() completed, waiting for GameManager to set gameplay mode", "Game")
|
||||
|
||||
func set_gameplay_mode(mode: String) -> void:
|
||||
DebugManager.log_info("set_gameplay_mode called with mode: %s" % mode, "Game")
|
||||
current_gameplay_mode = mode
|
||||
load_gameplay(mode)
|
||||
await load_gameplay(mode)
|
||||
DebugManager.log_info("set_gameplay_mode completed for mode: %s" % mode, "Game")
|
||||
|
||||
func load_gameplay(mode: String) -> void:
|
||||
DebugManager.log_debug("Loading gameplay mode: %s" % mode, "Game")
|
||||
|
||||
# Clear existing gameplay
|
||||
for child in gameplay_container.get_children():
|
||||
DebugManager.log_debug("Removing existing child: %s" % child.name, "Game")
|
||||
child.queue_free()
|
||||
# Clear existing gameplay and wait for removal
|
||||
var existing_children = gameplay_container.get_children()
|
||||
if existing_children.size() > 0:
|
||||
DebugManager.log_debug("Removing %d existing children" % existing_children.size(), "Game")
|
||||
for child in existing_children:
|
||||
DebugManager.log_debug("Removing existing child: %s" % child.name, "Game")
|
||||
child.queue_free()
|
||||
|
||||
# Wait for children to be properly removed from scene tree
|
||||
await get_tree().process_frame
|
||||
DebugManager.log_debug("Children removal complete, container count: %d" % gameplay_container.get_child_count(), "Game")
|
||||
|
||||
# Load new gameplay
|
||||
if GAMEPLAY_SCENES.has(mode):
|
||||
@@ -54,11 +63,32 @@ func set_global_score(value: int) -> void:
|
||||
|
||||
func _on_score_changed(points: int) -> void:
|
||||
self.global_score += points
|
||||
SaveManager.update_current_score(self.global_score)
|
||||
|
||||
func get_global_score() -> int:
|
||||
return global_score
|
||||
|
||||
func _get_current_gameplay_instance() -> Node:
|
||||
if gameplay_container.get_child_count() > 0:
|
||||
return gameplay_container.get_child(0)
|
||||
return null
|
||||
|
||||
func _on_back_button_pressed() -> void:
|
||||
DebugManager.log_debug("Back button pressed in game scene", "Game")
|
||||
AudioManager.play_ui_click()
|
||||
GameManager.save_game()
|
||||
|
||||
# Save current grid state if we have an active match3 gameplay
|
||||
var gameplay_instance = _get_current_gameplay_instance()
|
||||
if gameplay_instance and gameplay_instance.has_method("save_current_state"):
|
||||
DebugManager.log_info("Saving grid state before exit", "Game")
|
||||
# Make sure the gameplay instance is still valid and not being destroyed
|
||||
if is_instance_valid(gameplay_instance) and gameplay_instance.is_inside_tree():
|
||||
gameplay_instance.save_current_state()
|
||||
else:
|
||||
DebugManager.log_warn("Gameplay instance invalid, skipping grid save on exit", "Game")
|
||||
|
||||
# Save the current score immediately before exiting
|
||||
SaveManager.finish_game(global_score)
|
||||
GameManager.exit_to_main_menu()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
extends Node2D
|
||||
|
||||
signal score_changed(points: int)
|
||||
signal grid_state_loaded(grid_size: Vector2i, tile_types: int)
|
||||
|
||||
enum GameState {
|
||||
WAITING, # Waiting for player input
|
||||
@@ -13,6 +14,24 @@ var GRID_SIZE := Vector2i(8, 8)
|
||||
var TILE_TYPES := 5
|
||||
const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn")
|
||||
|
||||
# Safety constants
|
||||
const MAX_GRID_SIZE := 15
|
||||
const MAX_TILE_TYPES := 10
|
||||
const MAX_CASCADE_ITERATIONS := 20
|
||||
const MIN_GRID_SIZE := 3
|
||||
const MIN_TILE_TYPES := 3
|
||||
|
||||
# Layout constants (replacing magic numbers)
|
||||
const SCREEN_WIDTH_USAGE := 0.8 # Use 80% of screen width
|
||||
const SCREEN_HEIGHT_USAGE := 0.7 # Use 70% of screen height
|
||||
const GRID_LEFT_MARGIN := 50.0
|
||||
const GRID_TOP_MARGIN := 50.0
|
||||
|
||||
# Timing constants
|
||||
const CASCADE_WAIT_TIME := 0.1
|
||||
const SWAP_ANIMATION_TIME := 0.3
|
||||
const TILE_DROP_WAIT_TIME := 0.2
|
||||
|
||||
var grid := []
|
||||
var tile_size: float = 48.0
|
||||
var grid_offset: Vector2
|
||||
@@ -20,35 +39,54 @@ var current_state: GameState = GameState.WAITING
|
||||
var selected_tile: Node2D = null
|
||||
var cursor_position: Vector2i = Vector2i(0, 0)
|
||||
var keyboard_navigation_enabled: bool = false
|
||||
var grid_initialized: bool = false
|
||||
var instance_id: String
|
||||
|
||||
func _ready():
|
||||
DebugManager.log_debug("Match3 _ready() started", "Match3")
|
||||
# Generate unique instance ID for debugging
|
||||
instance_id = "Match3_%d" % get_instance_id()
|
||||
|
||||
# Gem pool will be set individually on each tile during creation
|
||||
if grid_initialized:
|
||||
DebugManager.log_warn("[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id, "Match3")
|
||||
return
|
||||
|
||||
DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3")
|
||||
grid_initialized = true
|
||||
|
||||
# Always calculate grid layout first
|
||||
_calculate_grid_layout()
|
||||
_initialize_grid()
|
||||
|
||||
# Try to load saved state first, otherwise use default initialization
|
||||
var loaded_saved_state = await load_saved_state()
|
||||
if not loaded_saved_state:
|
||||
DebugManager.log_info("No saved state found, using default grid initialization", "Match3")
|
||||
_initialize_grid()
|
||||
else:
|
||||
DebugManager.log_info("Successfully loaded saved grid state", "Match3")
|
||||
|
||||
DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3")
|
||||
|
||||
# Emit signal to notify UI components (like debug menu) that grid state is fully loaded
|
||||
grid_state_loaded.emit(GRID_SIZE, TILE_TYPES)
|
||||
|
||||
# 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
|
||||
var available_width = viewport_size.x * SCREEN_WIDTH_USAGE
|
||||
var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE
|
||||
|
||||
# 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
|
||||
# Align grid to left side with configurable margins
|
||||
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
|
||||
GRID_LEFT_MARGIN,
|
||||
(viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN
|
||||
)
|
||||
|
||||
func _initialize_grid():
|
||||
@@ -79,10 +117,21 @@ func _initialize_grid():
|
||||
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:
|
||||
# Comprehensive bounds and null checks
|
||||
if not _is_valid_grid_position(pos):
|
||||
return false
|
||||
if not grid[pos.y][pos.x]:
|
||||
|
||||
if pos.y >= grid.size() or pos.x >= grid[pos.y].size():
|
||||
DebugManager.log_error("Grid array bounds exceeded at (%d,%d)" % [pos.x, pos.y], "Match3")
|
||||
return false
|
||||
|
||||
var tile = grid[pos.y][pos.x]
|
||||
if not tile or not is_instance_valid(tile):
|
||||
return false
|
||||
|
||||
# Check if tile has required properties
|
||||
if not "tile_type" in tile:
|
||||
DebugManager.log_warn("Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3")
|
||||
return false
|
||||
|
||||
var matches_horizontal = _get_match_line(pos, Vector2i(1, 0))
|
||||
@@ -101,57 +150,145 @@ func _check_for_matches() -> bool:
|
||||
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
|
||||
# Validate input parameters
|
||||
if not _is_valid_grid_position(start):
|
||||
DebugManager.log_error("Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3")
|
||||
return []
|
||||
|
||||
# Fixed: Check in both directions separately to avoid infinite loops
|
||||
if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0):
|
||||
DebugManager.log_error("Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3")
|
||||
return []
|
||||
|
||||
# Check grid bounds and tile validity
|
||||
if start.y >= grid.size() or start.x >= grid[start.y].size():
|
||||
return []
|
||||
|
||||
var start_tile = grid[start.y][start.x]
|
||||
if not start_tile or not is_instance_valid(start_tile):
|
||||
return []
|
||||
|
||||
if not "tile_type" in start_tile:
|
||||
return []
|
||||
|
||||
var line = [start_tile]
|
||||
var type = start_tile.tile_type
|
||||
|
||||
# Check in both directions separately with safety limits
|
||||
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:
|
||||
var steps = 0
|
||||
while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current):
|
||||
if current.y >= grid.size() or current.x >= grid[current.y].size():
|
||||
break
|
||||
|
||||
var neighbor = grid[current.y][current.x]
|
||||
if not neighbor or not is_instance_valid(neighbor):
|
||||
break
|
||||
|
||||
if not "tile_type" in neighbor or neighbor.tile_type != type:
|
||||
break
|
||||
|
||||
line.append(neighbor)
|
||||
current += dir * offset
|
||||
steps += 1
|
||||
|
||||
return line
|
||||
|
||||
func _clear_matches():
|
||||
var to_clear := []
|
||||
# Safety check for grid integrity
|
||||
if not _validate_grid_integrity():
|
||||
DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3")
|
||||
return
|
||||
|
||||
var match_groups := []
|
||||
var processed_tiles := {}
|
||||
|
||||
for y in range(GRID_SIZE.y):
|
||||
if y >= grid.size():
|
||||
continue
|
||||
|
||||
for x in range(GRID_SIZE.x):
|
||||
# Fixed: Add null check before processing
|
||||
if not grid[y][x]:
|
||||
if x >= grid[y].size():
|
||||
continue
|
||||
|
||||
# Safe tile access with validation
|
||||
var tile = grid[y][x]
|
||||
if not tile or not is_instance_valid(tile):
|
||||
continue
|
||||
if processed_tiles.has(tile):
|
||||
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
|
||||
if horiz.size() >= 3:
|
||||
match_groups.append(horiz)
|
||||
for match_tile in horiz:
|
||||
if is_instance_valid(match_tile):
|
||||
processed_tiles[match_tile] = true
|
||||
|
||||
if vert.size() >= 3:
|
||||
match_groups.append(vert)
|
||||
for match_tile in vert:
|
||||
if is_instance_valid(match_tile):
|
||||
processed_tiles[match_tile] = true
|
||||
|
||||
# Calculate total score from all matches
|
||||
# Formula: 3 gems = 3 points, 4 gems = 6 points, 5 gems = 8 points, etc.
|
||||
# Pattern: n gems = n + (n-2) points for n >= 3, but 3 gems = exactly 3 points
|
||||
var total_score := 0
|
||||
for match_group in match_groups:
|
||||
var match_size = match_group.size()
|
||||
var match_score: int
|
||||
if match_size == 3:
|
||||
match_score = 3 # Exactly 3 points for 3 gems
|
||||
else:
|
||||
match_score = match_size + max(0, match_size - 2) # n + (n-2) for n >= 4
|
||||
total_score += match_score
|
||||
print("Debug: Match of ", match_size, " gems = ", match_score, " points")
|
||||
|
||||
# Remove duplicates from all matches combined
|
||||
var to_clear := []
|
||||
var unique_dict := {}
|
||||
for tile in to_clear:
|
||||
unique_dict[tile] = true
|
||||
for match_group in match_groups:
|
||||
for tile in match_group:
|
||||
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
|
||||
# Emit score before clearing tiles
|
||||
if total_score > 0:
|
||||
print("Debug: Total score for this turn: ", total_score)
|
||||
score_changed.emit(total_score)
|
||||
|
||||
# Safe tile removal with validation
|
||||
for tile in to_clear:
|
||||
grid[tile.grid_position.y][tile.grid_position.x] = null
|
||||
if not is_instance_valid(tile):
|
||||
continue
|
||||
|
||||
# Validate tile has grid_position property
|
||||
if not "grid_position" in tile:
|
||||
DebugManager.log_warn("Tile missing grid_position during removal", "Match3")
|
||||
tile.queue_free()
|
||||
continue
|
||||
|
||||
var tile_pos = tile.grid_position
|
||||
# Validate grid position before clearing reference
|
||||
if _is_valid_grid_position(tile_pos) and tile_pos.y < grid.size() and tile_pos.x < grid[tile_pos.y].size():
|
||||
grid[tile_pos.y][tile_pos.x] = null
|
||||
else:
|
||||
DebugManager.log_warn("Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y], "Match3")
|
||||
|
||||
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
|
||||
await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout
|
||||
_fill_empty_cells()
|
||||
|
||||
func _drop_tiles():
|
||||
@@ -173,83 +310,173 @@ func _drop_tiles():
|
||||
moved = true
|
||||
|
||||
func _fill_empty_cells():
|
||||
# Safety check for grid integrity
|
||||
if not _validate_grid_integrity():
|
||||
DebugManager.log_error("Grid integrity check failed in _fill_empty_cells", "Match3")
|
||||
return
|
||||
|
||||
# Create gem pool for current tile types
|
||||
var gem_indices = []
|
||||
for i in range(TILE_TYPES):
|
||||
gem_indices.append(i)
|
||||
|
||||
var tiles_created = 0
|
||||
for y in range(GRID_SIZE.y):
|
||||
if y >= grid.size():
|
||||
DebugManager.log_error("Grid row %d does not exist" % y, "Match3")
|
||||
continue
|
||||
|
||||
for x in range(GRID_SIZE.x):
|
||||
if x >= grid[y].size():
|
||||
DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3")
|
||||
continue
|
||||
|
||||
if not grid[y][x]:
|
||||
var tile = TILE_SCENE.instantiate()
|
||||
if not tile:
|
||||
DebugManager.log_error("Failed to instantiate tile at (%d,%d)" % [x, y], "Match3")
|
||||
continue
|
||||
|
||||
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 gem types for this tile with error handling
|
||||
if tile.has_method("set_active_gem_types"):
|
||||
tile.set_active_gem_types(gem_indices)
|
||||
else:
|
||||
DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3")
|
||||
|
||||
# Set random tile type
|
||||
tile.tile_type = randi() % TILE_TYPES
|
||||
# Set random tile type with bounds checking
|
||||
if TILE_TYPES > 0:
|
||||
tile.tile_type = randi() % TILE_TYPES
|
||||
else:
|
||||
DebugManager.log_error("TILE_TYPES is 0, cannot set tile type", "Match3")
|
||||
tile.queue_free()
|
||||
continue
|
||||
|
||||
grid[y][x] = tile
|
||||
# Connect tile signals for new tiles
|
||||
tile.tile_selected.connect(_on_tile_selected)
|
||||
# Connect tile signals for new tiles with error handling
|
||||
if tile.has_signal("tile_selected"):
|
||||
tile.tile_selected.connect(_on_tile_selected)
|
||||
else:
|
||||
DebugManager.log_warn("Tile missing tile_selected signal", "Match3")
|
||||
|
||||
# Fixed: Add recursion protection to prevent stack overflow
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
var max_iterations = 10
|
||||
tiles_created += 1
|
||||
|
||||
DebugManager.log_debug("Created %d new tiles" % tiles_created, "Match3")
|
||||
|
||||
# Enhanced cascade protection with better limits
|
||||
await get_tree().create_timer(CASCADE_WAIT_TIME).timeout
|
||||
var iteration = 0
|
||||
while _check_for_matches() and iteration < max_iterations:
|
||||
while _check_for_matches() and iteration < MAX_CASCADE_ITERATIONS:
|
||||
_clear_matches()
|
||||
await get_tree().create_timer(0.1).timeout
|
||||
await get_tree().create_timer(CASCADE_WAIT_TIME).timeout
|
||||
iteration += 1
|
||||
|
||||
if iteration >= MAX_CASCADE_ITERATIONS:
|
||||
DebugManager.log_warn("Maximum cascade iterations reached (%d), stopping to prevent infinite loop" % MAX_CASCADE_ITERATIONS, "Match3")
|
||||
|
||||
# Save grid state after cascades complete
|
||||
save_current_state()
|
||||
|
||||
func regenerate_grid():
|
||||
# Validate grid size before regeneration
|
||||
if GRID_SIZE.x < MIN_GRID_SIZE or GRID_SIZE.y < MIN_GRID_SIZE or GRID_SIZE.x > MAX_GRID_SIZE or GRID_SIZE.y > MAX_GRID_SIZE:
|
||||
DebugManager.log_error("Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3")
|
||||
return
|
||||
|
||||
if TILE_TYPES < 3 or TILE_TYPES > MAX_TILE_TYPES:
|
||||
DebugManager.log_error("Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3")
|
||||
return
|
||||
|
||||
# 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
|
||||
# Safe tile cleanup with improved error handling
|
||||
var children_to_remove = []
|
||||
var removed_count = 0
|
||||
|
||||
for child in get_children():
|
||||
if not is_instance_valid(child):
|
||||
continue
|
||||
|
||||
# More robust tile detection
|
||||
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)
|
||||
removed_count += 1
|
||||
elif "grid_position" in child: # Fallback detection
|
||||
children_to_remove.append(child)
|
||||
removed_count += 1
|
||||
|
||||
DebugManager.log_debug("Found %d tile children to remove" % removed_count, "Match3")
|
||||
|
||||
# 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:
|
||||
if y < grid.size() and grid[y] and grid[y] is Array:
|
||||
for x in range(grid[y].size()):
|
||||
grid[y][x] = null
|
||||
if x < grid[y].size():
|
||||
grid[y][x] = null
|
||||
|
||||
# Clear the grid array
|
||||
# Clear the grid array safely
|
||||
grid.clear()
|
||||
|
||||
# Remove all found tile children using queue_free for safe cleanup
|
||||
for child in children_to_remove:
|
||||
child.queue_free()
|
||||
if is_instance_valid(child):
|
||||
child.queue_free()
|
||||
|
||||
# Wait for nodes to be properly freed from the scene tree
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame # Extra frame for complex hierarchies
|
||||
|
||||
# Fixed: Recalculate grid layout before regenerating tiles
|
||||
# Recalculate grid layout before regenerating tiles
|
||||
_calculate_grid_layout()
|
||||
|
||||
# Regenerate the grid (gem pool is updated in _initialize_grid)
|
||||
# Regenerate the grid with safety checks
|
||||
_initialize_grid()
|
||||
|
||||
func set_tile_types(new_count: int):
|
||||
# Input validation
|
||||
if new_count < 3:
|
||||
DebugManager.log_error("Tile types count too low: %d (minimum 3)" % new_count, "Match3")
|
||||
return
|
||||
|
||||
if new_count > MAX_TILE_TYPES:
|
||||
DebugManager.log_error("Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3")
|
||||
return
|
||||
|
||||
if new_count == TILE_TYPES:
|
||||
DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3")
|
||||
return
|
||||
|
||||
DebugManager.log_debug("Changing tile types from %d to %d" % [TILE_TYPES, new_count], "Match3")
|
||||
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")
|
||||
# Comprehensive input validation
|
||||
if new_size.x < MIN_GRID_SIZE or new_size.y < MIN_GRID_SIZE:
|
||||
DebugManager.log_error("Grid size too small: %dx%d (minimum %dx%d)" % [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE], "Match3")
|
||||
return
|
||||
|
||||
if new_size.x > MAX_GRID_SIZE or new_size.y > MAX_GRID_SIZE:
|
||||
DebugManager.log_error("Grid size too large: %dx%d (maximum %dx%d)" % [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "Match3")
|
||||
return
|
||||
|
||||
if new_size == GRID_SIZE:
|
||||
DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3")
|
||||
return
|
||||
|
||||
DebugManager.log_debug("Changing grid size from %s to %s" % [GRID_SIZE, new_size], "Match3")
|
||||
GRID_SIZE = new_size
|
||||
|
||||
# Regenerate grid with new size
|
||||
await regenerate_grid()
|
||||
|
||||
@@ -336,6 +563,11 @@ func _move_cursor(direction: Vector2i) -> void:
|
||||
DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3")
|
||||
return
|
||||
|
||||
# Validate grid integrity before cursor operations
|
||||
if not _validate_grid_integrity():
|
||||
DebugManager.log_error("Grid integrity check failed in cursor movement", "Match3")
|
||||
return
|
||||
|
||||
var old_pos = cursor_position
|
||||
var new_pos = cursor_position + direction
|
||||
|
||||
@@ -344,27 +576,33 @@ func _move_cursor(direction: Vector2i) -> void:
|
||||
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:
|
||||
# Safe access to old tile
|
||||
var old_tile = _safe_grid_access(old_pos)
|
||||
if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile:
|
||||
if 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:
|
||||
# Safe access to new tile
|
||||
var new_tile = _safe_grid_access(cursor_position)
|
||||
if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile:
|
||||
if 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)
|
||||
# Validate cursor position and grid integrity
|
||||
if not _is_valid_grid_position(cursor_position):
|
||||
DebugManager.log_warn("Invalid cursor position for selection: (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
|
||||
return
|
||||
|
||||
var tile = _safe_grid_access(cursor_position)
|
||||
if tile:
|
||||
DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
|
||||
_on_tile_selected(tile)
|
||||
else:
|
||||
DebugManager.log_warn("No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
|
||||
|
||||
func _on_tile_selected(tile: Node2D) -> void:
|
||||
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING:
|
||||
@@ -394,25 +632,32 @@ func _select_tile(tile: Node2D) -> void:
|
||||
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")
|
||||
if selected_tile and is_instance_valid(selected_tile):
|
||||
# Safe access to tile properties
|
||||
if "grid_position" in selected_tile:
|
||||
DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3")
|
||||
else:
|
||||
DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3")
|
||||
|
||||
# Clear selection state
|
||||
selected_tile.is_selected = false
|
||||
# Safe property access for selection state
|
||||
if "is_selected" in selected_tile:
|
||||
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
|
||||
# Clear highlighting on selected tile
|
||||
if "is_highlighted" in selected_tile:
|
||||
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:
|
||||
var cursor_tile = _safe_grid_access(cursor_position)
|
||||
if cursor_tile and "is_highlighted" in 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
|
||||
if "is_highlighted" in selected_tile:
|
||||
selected_tile.is_highlighted = false
|
||||
|
||||
selected_tile = null
|
||||
current_state = GameState.WAITING
|
||||
@@ -478,8 +723,218 @@ func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
|
||||
|
||||
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)
|
||||
tween.tween_property(tile1, "position", target_pos1, SWAP_ANIMATION_TIME)
|
||||
tween.tween_property(tile2, "position", target_pos2, SWAP_ANIMATION_TIME)
|
||||
|
||||
await tween.finished
|
||||
DebugManager.log_trace("Tile swap animation completed", "Match3")
|
||||
|
||||
func serialize_grid_state() -> Array:
|
||||
# Convert the current grid to a serializable 2D array
|
||||
DebugManager.log_info("Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)" % [grid.size(), GRID_SIZE.x, GRID_SIZE.y], "Match3")
|
||||
|
||||
if grid.size() == 0:
|
||||
DebugManager.log_error("Grid array is empty during serialization!", "Match3")
|
||||
return []
|
||||
|
||||
var serialized_grid = []
|
||||
var valid_tiles = 0
|
||||
var null_tiles = 0
|
||||
|
||||
for y in range(GRID_SIZE.y):
|
||||
var row = []
|
||||
for x in range(GRID_SIZE.x):
|
||||
if y < grid.size() and x < grid[y].size() and grid[y][x]:
|
||||
row.append(grid[y][x].tile_type)
|
||||
valid_tiles += 1
|
||||
# Only log first few for brevity
|
||||
if valid_tiles <= 5:
|
||||
DebugManager.log_debug("Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3")
|
||||
else:
|
||||
row.append(-1) # Invalid/empty tile
|
||||
null_tiles += 1
|
||||
# Only log first few nulls for brevity
|
||||
if null_tiles <= 5:
|
||||
DebugManager.log_debug("Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3")
|
||||
serialized_grid.append(row)
|
||||
|
||||
DebugManager.log_info("Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles" % [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles], "Match3")
|
||||
return serialized_grid
|
||||
|
||||
func get_active_gem_types() -> Array:
|
||||
# Get active gem types from the first available tile
|
||||
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
|
||||
return grid[0][0].active_gem_types.duplicate()
|
||||
else:
|
||||
# Fallback to default
|
||||
var default_types = []
|
||||
for i in range(TILE_TYPES):
|
||||
default_types.append(i)
|
||||
return default_types
|
||||
|
||||
func save_current_state():
|
||||
# Save complete game state
|
||||
var grid_layout = serialize_grid_state()
|
||||
var active_gems = get_active_gem_types()
|
||||
|
||||
DebugManager.log_info("Saving match3 state: size(%d,%d), %d tile types, %d active gems" % [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()], "Match3")
|
||||
|
||||
SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout)
|
||||
|
||||
func load_saved_state() -> bool:
|
||||
# Check if there's a saved grid state
|
||||
if not SaveManager.has_saved_grid():
|
||||
DebugManager.log_info("No saved grid state found, using default generation", "Match3")
|
||||
return false
|
||||
|
||||
var saved_state = SaveManager.get_saved_grid_state()
|
||||
|
||||
# Restore grid settings
|
||||
var saved_size = Vector2i(saved_state.grid_size.x, saved_state.grid_size.y)
|
||||
TILE_TYPES = saved_state.tile_types_count
|
||||
var saved_gems = saved_state.active_gem_types
|
||||
var saved_layout = saved_state.grid_layout
|
||||
|
||||
DebugManager.log_info("[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d" % [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()], "Match3")
|
||||
|
||||
# Debug: Print first few rows of loaded layout
|
||||
for y in range(min(3, saved_layout.size())):
|
||||
var row_str = ""
|
||||
for x in range(min(8, saved_layout[y].size())):
|
||||
row_str += str(saved_layout[y][x]) + " "
|
||||
DebugManager.log_debug("Loading row %d: %s" % [y, row_str], "Match3")
|
||||
|
||||
# Validate saved data
|
||||
if saved_layout.size() != saved_size.y:
|
||||
DebugManager.log_error("Saved grid layout height mismatch: expected %d, got %d" % [saved_size.y, saved_layout.size()], "Match3")
|
||||
return false
|
||||
|
||||
if saved_layout.size() > 0 and saved_layout[0].size() != saved_size.x:
|
||||
DebugManager.log_error("Saved grid layout width mismatch: expected %d, got %d" % [saved_size.x, saved_layout[0].size()], "Match3")
|
||||
return false
|
||||
|
||||
# Apply the saved settings
|
||||
var old_size = GRID_SIZE
|
||||
GRID_SIZE = saved_size
|
||||
|
||||
# Recalculate layout if size changed
|
||||
if old_size != saved_size:
|
||||
DebugManager.log_info("Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size], "Match3")
|
||||
_calculate_grid_layout()
|
||||
|
||||
await _restore_grid_from_layout(saved_layout, saved_gems)
|
||||
|
||||
DebugManager.log_info("Successfully loaded saved grid state", "Match3")
|
||||
return true
|
||||
|
||||
func _restore_grid_from_layout(grid_layout: Array, active_gems: Array):
|
||||
DebugManager.log_info("[%s] Starting grid restoration: layout_size=%d, active_gems=%s" % [instance_id, grid_layout.size(), active_gems], "Match3")
|
||||
|
||||
# Clear ALL existing tile children, not just ones in grid array
|
||||
# This ensures no duplicate layers are created
|
||||
var all_tile_children = []
|
||||
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":
|
||||
all_tile_children.append(child)
|
||||
|
||||
DebugManager.log_debug("Found %d existing tile children to remove" % all_tile_children.size(), "Match3")
|
||||
|
||||
# Remove all found tile children
|
||||
for child in all_tile_children:
|
||||
child.queue_free()
|
||||
|
||||
# Clear existing grid array
|
||||
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 grid array
|
||||
grid.clear()
|
||||
|
||||
# Wait for nodes to be freed
|
||||
await get_tree().process_frame
|
||||
|
||||
# Restore grid from saved layout
|
||||
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(active_gems)
|
||||
|
||||
# Set the saved tile type
|
||||
var saved_tile_type = grid_layout[y][x]
|
||||
DebugManager.log_debug("Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d" % [x, y, saved_tile_type, TILE_TYPES], "Match3")
|
||||
|
||||
if saved_tile_type >= 0 and saved_tile_type < TILE_TYPES:
|
||||
tile.tile_type = saved_tile_type
|
||||
DebugManager.log_debug("✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3")
|
||||
else:
|
||||
# Fallback for invalid tile types
|
||||
tile.tile_type = randi() % TILE_TYPES
|
||||
DebugManager.log_error("✗ Invalid saved tile type %d at (%d,%d), using random %d" % [saved_tile_type, x, y, tile.tile_type], "Match3")
|
||||
|
||||
# Connect tile signals
|
||||
tile.tile_selected.connect(_on_tile_selected)
|
||||
grid[y].append(tile)
|
||||
|
||||
DebugManager.log_info("Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3")
|
||||
|
||||
# Safety and validation helper functions
|
||||
func _is_valid_grid_position(pos: Vector2i) -> bool:
|
||||
return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y
|
||||
|
||||
func _validate_grid_integrity() -> bool:
|
||||
# Check if grid array structure is valid
|
||||
if not grid is Array:
|
||||
DebugManager.log_error("Grid is not an array", "Match3")
|
||||
return false
|
||||
|
||||
if grid.size() != GRID_SIZE.y:
|
||||
DebugManager.log_error("Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3")
|
||||
return false
|
||||
|
||||
for y in range(grid.size()):
|
||||
if not grid[y] is Array:
|
||||
DebugManager.log_error("Grid row %d is not an array" % y, "Match3")
|
||||
return false
|
||||
|
||||
if grid[y].size() != GRID_SIZE.x:
|
||||
DebugManager.log_error("Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3")
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
func _safe_grid_access(pos: Vector2i) -> Node2D:
|
||||
# Safe grid access with comprehensive bounds checking
|
||||
if not _is_valid_grid_position(pos):
|
||||
return null
|
||||
|
||||
if pos.y >= grid.size() or pos.x >= grid[pos.y].size():
|
||||
DebugManager.log_warn("Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3")
|
||||
return null
|
||||
|
||||
var tile = grid[pos.y][pos.x]
|
||||
if not tile or not is_instance_valid(tile):
|
||||
return null
|
||||
|
||||
return tile
|
||||
|
||||
func _safe_tile_access(tile: Node2D, property: String):
|
||||
# Safe property access on tiles
|
||||
if not tile or not is_instance_valid(tile):
|
||||
return null
|
||||
|
||||
if not property in tile:
|
||||
DebugManager.log_warn("Tile missing property: %s" % property, "Match3")
|
||||
return null
|
||||
|
||||
return tile.get(property)
|
||||
|
||||
Reference in New Issue
Block a user