saves and score are added

This commit is contained in:
2025-09-25 22:47:59 +04:00
parent ea8c85d7ad
commit f858f9b633
13 changed files with 1190 additions and 118 deletions

View File

@@ -6,3 +6,13 @@ music_volume;"Music Volume";"Громкость музыки"
sfx_volume;"SFX Volume";"Громкость эффектов" sfx_volume;"SFX Volume";"Громкость эффектов"
language;"Language";"Язык" language;"Language";"Язык"
back;"Back";"Назад" back;"Back";"Назад"
reset_progress;"Reset All Progress";"Сбросить весь прогресс"
confirm_reset_title;"Confirm Progress Reset";"Подтвердите сброс прогресса"
confirm_reset_message;"Are you sure you want to delete ALL your progress?\n\nThis will permanently remove:\n• All scores and high scores\n• Game statistics\n• Saved game states\n\nThis action cannot be undone!";"Вы уверены, что хотите удалить ВЕСЬ свой прогресс?\n\nЭто навсегда удалит:\n• Все очки и рекорды\n• Игровую статистику\n• Сохранённые игровые состояния\n\nЭто действие нельзя отменить!"
reset_confirm;"Yes, Reset Everything";"Да, сбросить всё"
cancel;"Cancel";"Отмена"
reset_success_title;"Progress Reset";"Прогресс сброшен"
reset_success_message;"All progress has been successfully deleted.\nYour game has been reset to the beginning.";"Весь прогресс был успешно удалён.\nВаша игра была сброшена к началу."
reset_error_title;"Reset Failed";"Ошибка сброса"
reset_error_message;"There was an error resetting your progress.\nPlease try again.";"Произошла ошибка при сбросе вашего прогресса.\nПопробуйте ещё раз."
ok;"OK";"ОК"
1 keys en ru
6 sfx_volume SFX Volume Громкость эффектов
7 language Language Язык
8 back Back Назад
9 reset_progress Reset All Progress Сбросить весь прогресс
10 confirm_reset_title Confirm Progress Reset Подтвердите сброс прогресса
11 confirm_reset_message Are you sure you want to delete ALL your progress?\n\nThis will permanently remove:\n• All scores and high scores\n• Game statistics\n• Saved game states\n\nThis action cannot be undone! Вы уверены, что хотите удалить ВЕСЬ свой прогресс?\n\nЭто навсегда удалит:\n• Все очки и рекорды\n• Игровую статистику\n• Сохранённые игровые состояния\n\nЭто действие нельзя отменить!
12 reset_confirm Yes, Reset Everything Да, сбросить всё
13 cancel Cancel Отмена
14 reset_success_title Progress Reset Прогресс сброшен
15 reset_success_message All progress has been successfully deleted.\nYour game has been reset to the beginning. Весь прогресс был успешно удалён.\nВаша игра была сброшена к началу.
16 reset_error_title Reset Failed Ошибка сброса
17 reset_error_message There was an error resetting your progress.\nPlease try again. Произошла ошибка при сбросе вашего прогресса.\nПопробуйте ещё раз.
18 ok OK ОК

View File

@@ -26,6 +26,7 @@ AudioManager="*res://src/autoloads/AudioManager.gd"
GameManager="*res://src/autoloads/GameManager.gd" GameManager="*res://src/autoloads/GameManager.gd"
LocalizationManager="*res://src/autoloads/LocalizationManager.gd" LocalizationManager="*res://src/autoloads/LocalizationManager.gd"
DebugManager="*res://src/autoloads/DebugManager.gd" DebugManager="*res://src/autoloads/DebugManager.gd"
SaveManager="*res://src/autoloads/SaveManager.gd"
[input] [input]

View File

@@ -16,20 +16,29 @@ func _ready() -> void:
if not back_button.pressed.is_connected(_on_back_button_pressed): if not back_button.pressed.is_connected(_on_back_button_pressed):
back_button.pressed.connect(_on_back_button_pressed) back_button.pressed.connect(_on_back_button_pressed)
# Default to match3 for now # GameManager will set the gameplay mode, don't set default here
set_gameplay_mode("match3") DebugManager.log_debug("Game _ready() completed, waiting for GameManager to set gameplay mode", "Game")
func set_gameplay_mode(mode: String) -> void: func set_gameplay_mode(mode: String) -> void:
DebugManager.log_info("set_gameplay_mode called with mode: %s" % mode, "Game")
current_gameplay_mode = mode 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: func load_gameplay(mode: String) -> void:
DebugManager.log_debug("Loading gameplay mode: %s" % mode, "Game") DebugManager.log_debug("Loading gameplay mode: %s" % mode, "Game")
# Clear existing gameplay # Clear existing gameplay and wait for removal
for child in gameplay_container.get_children(): var existing_children = gameplay_container.get_children()
DebugManager.log_debug("Removing existing child: %s" % child.name, "Game") if existing_children.size() > 0:
child.queue_free() 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 # Load new gameplay
if GAMEPLAY_SCENES.has(mode): if GAMEPLAY_SCENES.has(mode):
@@ -54,11 +63,32 @@ func set_global_score(value: int) -> void:
func _on_score_changed(points: int) -> void: func _on_score_changed(points: int) -> void:
self.global_score += points 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: func _on_back_button_pressed() -> void:
DebugManager.log_debug("Back button pressed in game scene", "Game") DebugManager.log_debug("Back button pressed in game scene", "Game")
AudioManager.play_ui_click() 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() GameManager.exit_to_main_menu()
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:

View File

@@ -1,6 +1,7 @@
extends Node2D extends Node2D
signal score_changed(points: int) signal score_changed(points: int)
signal grid_state_loaded(grid_size: Vector2i, tile_types: int)
enum GameState { enum GameState {
WAITING, # Waiting for player input WAITING, # Waiting for player input
@@ -13,6 +14,24 @@ var GRID_SIZE := Vector2i(8, 8)
var TILE_TYPES := 5 var TILE_TYPES := 5
const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn") 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 grid := []
var tile_size: float = 48.0 var tile_size: float = 48.0
var grid_offset: Vector2 var grid_offset: Vector2
@@ -20,35 +39,54 @@ var current_state: GameState = GameState.WAITING
var selected_tile: Node2D = null var selected_tile: Node2D = null
var cursor_position: Vector2i = Vector2i(0, 0) var cursor_position: Vector2i = Vector2i(0, 0)
var keyboard_navigation_enabled: bool = false var keyboard_navigation_enabled: bool = false
var grid_initialized: bool = false
var instance_id: String
func _ready(): 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() _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") 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 # Debug: Check scene tree structure immediately
call_deferred("_debug_scene_structure") call_deferred("_debug_scene_structure")
func _calculate_grid_layout(): func _calculate_grid_layout():
var viewport_size = get_viewport().get_visible_rect().size var viewport_size = get_viewport().get_visible_rect().size
var available_width = viewport_size.x * 0.8 # Use 80% of screen width var available_width = viewport_size.x * SCREEN_WIDTH_USAGE
var available_height = viewport_size.y * 0.7 # Use 70% of screen height var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE
# Calculate tile size based on available space # Calculate tile size based on available space
var max_tile_width = available_width / GRID_SIZE.x var max_tile_width = available_width / GRID_SIZE.x
var max_tile_height = available_height / GRID_SIZE.y var max_tile_height = available_height / GRID_SIZE.y
tile_size = min(max_tile_width, max_tile_height) 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 var total_grid_height = tile_size * GRID_SIZE.y
grid_offset = Vector2( grid_offset = Vector2(
50, # Left margin GRID_LEFT_MARGIN,
(viewport_size.y - total_grid_height) / 2 + 50 # Vertically centered with top margin (viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN
) )
func _initialize_grid(): func _initialize_grid():
@@ -79,10 +117,21 @@ func _initialize_grid():
grid[y].append(tile) grid[y].append(tile)
func _has_match_at(pos: Vector2i) -> bool: func _has_match_at(pos: Vector2i) -> bool:
# Fixed: Add bounds and null checks # Comprehensive bounds and null checks
if pos.x < 0 or pos.y < 0 or pos.x >= GRID_SIZE.x or pos.y >= GRID_SIZE.y: if not _is_valid_grid_position(pos):
return false 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 return false
var matches_horizontal = _get_match_line(pos, Vector2i(1, 0)) var matches_horizontal = _get_match_line(pos, Vector2i(1, 0))
@@ -101,57 +150,145 @@ func _check_for_matches() -> bool:
return false return false
func _get_match_line(start: Vector2i, dir: Vector2i) -> Array: func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
var line = [grid[start.y][start.x]] # Validate input parameters
var type = grid[start.y][start.x].tile_type 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]: for offset in [1, -1]:
var current = start + dir * offset 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 steps = 0
var neighbor = grid[current.y][current.x] while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current):
# Fixed: Add null check to prevent crashes if current.y >= grid.size() or current.x >= grid[current.y].size():
if not neighbor or neighbor.tile_type != type:
break 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) line.append(neighbor)
current += dir * offset current += dir * offset
steps += 1
return line return line
func _clear_matches(): 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): for y in range(GRID_SIZE.y):
if y >= grid.size():
continue
for x in range(GRID_SIZE.x): for x in range(GRID_SIZE.x):
# Fixed: Add null check before processing if x >= grid[y].size():
if not grid[y][x]:
continue 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 pos = Vector2i(x, y)
var horiz = _get_match_line(pos, Vector2i(1, 0)) 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)) 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 := {} var unique_dict := {}
for tile in to_clear: for match_group in match_groups:
unique_dict[tile] = true for tile in match_group:
unique_dict[tile] = true
to_clear = unique_dict.keys() to_clear = unique_dict.keys()
# Fixed: Only proceed if there are matches to clear # Fixed: Only proceed if there are matches to clear
if to_clear.size() == 0: if to_clear.size() == 0:
return 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: 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() tile.queue_free()
# Wait one frame for nodes to be queued properly # Wait one frame for nodes to be queued properly
await get_tree().process_frame await get_tree().process_frame
_drop_tiles() _drop_tiles()
await get_tree().create_timer(0.2).timeout await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout
_fill_empty_cells() _fill_empty_cells()
func _drop_tiles(): func _drop_tiles():
@@ -173,83 +310,173 @@ func _drop_tiles():
moved = true moved = true
func _fill_empty_cells(): 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 # Create gem pool for current tile types
var gem_indices = [] var gem_indices = []
for i in range(TILE_TYPES): for i in range(TILE_TYPES):
gem_indices.append(i) gem_indices.append(i)
var tiles_created = 0
for y in range(GRID_SIZE.y): 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): 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]: if not grid[y][x]:
var tile = TILE_SCENE.instantiate() 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.grid_position = Vector2i(x, y)
tile.position = grid_offset + Vector2(x, y) * tile_size tile.position = grid_offset + Vector2(x, y) * tile_size
add_child(tile) add_child(tile)
# Set gem types for this tile # Set gem types for this tile with error handling
tile.set_active_gem_types(gem_indices) 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 # Set random tile type with bounds checking
tile.tile_type = randi() % TILE_TYPES 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 grid[y][x] = tile
# Connect tile signals for new tiles # Connect tile signals for new tiles with error handling
tile.tile_selected.connect(_on_tile_selected) 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 tiles_created += 1
await get_tree().create_timer(0.1).timeout
var max_iterations = 10 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 var iteration = 0
while _check_for_matches() and iteration < max_iterations: while _check_for_matches() and iteration < MAX_CASCADE_ITERATIONS:
_clear_matches() _clear_matches()
await get_tree().create_timer(0.1).timeout await get_tree().create_timer(CASCADE_WAIT_TIME).timeout
iteration += 1 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(): 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 # Use time-based seed to ensure different patterns each time
var new_seed = Time.get_ticks_msec() var new_seed = Time.get_ticks_msec()
seed(new_seed) seed(new_seed)
DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3") DebugManager.log_debug("Regenerating grid with seed: " + str(new_seed), "Match3")
# Fixed: Clear ALL tile children, not just ones in grid array # Safe tile cleanup with improved error handling
# This handles any orphaned tiles that might not be tracked in the grid
var children_to_remove = [] var children_to_remove = []
var removed_count = 0
for child in get_children(): 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(): if child.has_method("get_script") and child.get_script():
var script_path = child.get_script().resource_path var script_path = child.get_script().resource_path
if script_path == "res://scenes/game/gameplays/tile.gd": if script_path == "res://scenes/game/gameplays/tile.gd":
children_to_remove.append(child) 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 # First clear grid array references to prevent access to nodes being freed
for y in range(grid.size()): 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()): 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() grid.clear()
# Remove all found tile children using queue_free for safe cleanup # Remove all found tile children using queue_free for safe cleanup
for child in children_to_remove: 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 # Wait for nodes to be properly freed from the scene tree
await get_tree().process_frame 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() _calculate_grid_layout()
# Regenerate the grid (gem pool is updated in _initialize_grid) # Regenerate the grid with safety checks
_initialize_grid() _initialize_grid()
func set_tile_types(new_count: int): 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 TILE_TYPES = new_count
# Regenerate grid with new tile types (gem pool is updated in regenerate_grid) # Regenerate grid with new tile types (gem pool is updated in regenerate_grid)
await regenerate_grid() await regenerate_grid()
func set_grid_size(new_size: Vector2i): 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 GRID_SIZE = new_size
# Regenerate grid with new size # Regenerate grid with new size
await regenerate_grid() await regenerate_grid()
@@ -336,6 +563,11 @@ func _move_cursor(direction: Vector2i) -> void:
DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3") DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3")
return 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 old_pos = cursor_position
var new_pos = cursor_position + direction 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) new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1)
if new_pos != cursor_position: if new_pos != cursor_position:
# Validate old position before accessing grid # Safe access to old tile
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 = _safe_grid_access(old_pos)
var old_tile = grid[cursor_position.y][cursor_position.x] if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile:
if old_tile and not old_tile.is_selected: if not old_tile.is_selected:
old_tile.is_highlighted = false 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") 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 cursor_position = new_pos
# Validate new position before accessing grid # Safe access to new tile
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 = _safe_grid_access(cursor_position)
var new_tile = grid[cursor_position.y][cursor_position.x] if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile:
if new_tile and not new_tile.is_selected: if not new_tile.is_selected:
new_tile.is_highlighted = true new_tile.is_highlighted = true
func _select_tile_at_cursor() -> void: 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: # Validate cursor position and grid integrity
var tile = grid[cursor_position.y][cursor_position.x] if not _is_valid_grid_position(cursor_position):
if tile: DebugManager.log_warn("Invalid cursor position for selection: (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") return
_on_tile_selected(tile)
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: func _on_tile_selected(tile: Node2D) -> void:
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: 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") DebugManager.log_debug("Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3")
func _deselect_tile() -> void: func _deselect_tile() -> void:
if selected_tile: if selected_tile and is_instance_valid(selected_tile):
DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3") # 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 # Safe property access for selection state
selected_tile.is_selected = false if "is_selected" in selected_tile:
selected_tile.is_selected = false
# Handle cursor highlighting for keyboard navigation # Handle cursor highlighting for keyboard navigation
if keyboard_navigation_enabled: if keyboard_navigation_enabled:
# Clear all highlighting first to avoid conflicts # Clear highlighting on selected tile
selected_tile.is_highlighted = false if "is_highlighted" in selected_tile:
selected_tile.is_highlighted = false
# Re-highlight cursor position if it's different from the deselected tile # Re-highlight cursor position if it's different from the deselected tile
var cursor_tile = grid[cursor_position.y][cursor_position.x] var cursor_tile = _safe_grid_access(cursor_position)
if cursor_tile: if cursor_tile and "is_highlighted" in cursor_tile:
cursor_tile.is_highlighted = true cursor_tile.is_highlighted = true
DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3") DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
else: else:
# For mouse navigation, just clear highlighting # 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 selected_tile = null
current_state = GameState.WAITING current_state = GameState.WAITING
@@ -478,8 +723,218 @@ func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
var tween = create_tween() var tween = create_tween()
tween.set_parallel(true) tween.set_parallel(true)
tween.tween_property(tile1, "position", target_pos1, 0.3) tween.tween_property(tile1, "position", target_pos1, SWAP_ANIMATION_TIME)
tween.tween_property(tile2, "position", target_pos2, 0.3) tween.tween_property(tile2, "position", target_pos2, SWAP_ANIMATION_TIME)
await tween.finished await tween.finished
DebugManager.log_trace("Tile swap animation completed", "Match3") 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)

View File

@@ -12,8 +12,16 @@ extends Control
@export var target_script_path: String = "res://scenes/game/gameplays/match3_gameplay.gd" @export var target_script_path: String = "res://scenes/game/gameplays/match3_gameplay.gd"
@export var log_category: String = "DebugMenu" @export var log_category: String = "DebugMenu"
# Safety constants matching match3_gameplay.gd
const MAX_GRID_SIZE := 15
const MAX_TILE_TYPES := 10
const MIN_GRID_SIZE := 3
const MIN_TILE_TYPES := 3
var match3_scene: Node2D var match3_scene: Node2D
var search_timer: Timer var search_timer: Timer
var last_scene_search_time: float = 0.0
const SCENE_SEARCH_COOLDOWN := 0.5 # Prevent excessive scene searching
func _exit_tree(): func _exit_tree():
if search_timer: if search_timer:
@@ -43,27 +51,27 @@ func _ready():
_find_target_scene() _find_target_scene()
func _initialize_spinboxes(): func _initialize_spinboxes():
# Initialize gem types spinbox # Initialize gem types spinbox with safety limits
gem_types_spinbox.min_value = 3 gem_types_spinbox.min_value = MIN_TILE_TYPES
gem_types_spinbox.max_value = 8 gem_types_spinbox.max_value = MAX_TILE_TYPES
gem_types_spinbox.step = 1 gem_types_spinbox.step = 1
gem_types_spinbox.value = 5 # Default value gem_types_spinbox.value = 5 # Default value
# Initialize grid size spinboxes # Initialize grid size spinboxes with safety limits
grid_width_spinbox.min_value = 4 grid_width_spinbox.min_value = MIN_GRID_SIZE
grid_width_spinbox.max_value = 12 grid_width_spinbox.max_value = MAX_GRID_SIZE
grid_width_spinbox.step = 1 grid_width_spinbox.step = 1
grid_width_spinbox.value = 8 # Default value grid_width_spinbox.value = 8 # Default value
grid_height_spinbox.min_value = 4 grid_height_spinbox.min_value = MIN_GRID_SIZE
grid_height_spinbox.max_value = 12 grid_height_spinbox.max_value = MAX_GRID_SIZE
grid_height_spinbox.step = 1 grid_height_spinbox.step = 1
grid_height_spinbox.value = 8 # Default value grid_height_spinbox.value = 8 # Default value
func _setup_scene_finding(): func _setup_scene_finding():
# Create timer for periodic scene search # Create timer for periodic scene search with longer intervals to reduce CPU usage
search_timer = Timer.new() search_timer = Timer.new()
search_timer.wait_time = 0.1 search_timer.wait_time = 0.5 # Reduced frequency from 0.1 to 0.5 seconds
search_timer.timeout.connect(_find_target_scene) search_timer.timeout.connect(_find_target_scene)
add_child(search_timer) add_child(search_timer)
@@ -92,6 +100,11 @@ func _update_ui_from_scene():
if not match3_scene: if not match3_scene:
return return
# Connect to grid state loaded signal if not already connected
if match3_scene.has_signal("grid_state_loaded") and not match3_scene.grid_state_loaded.is_connected(_on_grid_state_loaded):
match3_scene.grid_state_loaded.connect(_on_grid_state_loaded)
DebugManager.log_debug("Connected to grid_state_loaded signal", log_category)
# Update gem types display # Update gem types display
if match3_scene.has_method("get") and "TILE_TYPES" in match3_scene: if match3_scene.has_method("get") and "TILE_TYPES" in match3_scene:
gem_types_spinbox.value = match3_scene.TILE_TYPES gem_types_spinbox.value = match3_scene.TILE_TYPES
@@ -105,6 +118,18 @@ func _update_ui_from_scene():
grid_width_label.text = "Width: " + str(grid_size.x) grid_width_label.text = "Width: " + str(grid_size.x)
grid_height_label.text = "Height: " + str(grid_size.y) grid_height_label.text = "Height: " + str(grid_size.y)
func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int):
DebugManager.log_debug("Grid state loaded signal received: size=%s, types=%d" % [grid_size, tile_types], log_category)
# Update the UI with the actual loaded values
gem_types_spinbox.value = tile_types
gem_types_label.text = "Gem Types: " + str(tile_types)
grid_width_spinbox.value = grid_size.x
grid_height_spinbox.value = grid_size.y
grid_width_label.text = "Width: " + str(grid_size.x)
grid_height_label.text = "Height: " + str(grid_size.y)
func _stop_search_timer(): func _stop_search_timer():
if search_timer and search_timer.timeout.is_connected(_find_target_scene): if search_timer and search_timer.timeout.is_connected(_find_target_scene):
search_timer.stop() search_timer.stop()
@@ -122,6 +147,14 @@ func _on_debug_toggled(enabled: bool):
if not match3_scene: if not match3_scene:
_find_target_scene() _find_target_scene()
_update_ui_from_scene() _update_ui_from_scene()
# Force refresh the values in case they changed while debug was hidden
_refresh_current_values()
func _refresh_current_values():
# Refresh UI with current values from the scene
if match3_scene:
DebugManager.log_debug("Refreshing debug menu values from current scene state", log_category)
_update_ui_from_scene()
func _on_regenerate_pressed(): func _on_regenerate_pressed():
if not match3_scene: if not match3_scene:
@@ -138,17 +171,25 @@ func _on_regenerate_pressed():
DebugManager.log_error("Target scene does not have regenerate_grid method", log_category) DebugManager.log_error("Target scene does not have regenerate_grid method", log_category)
func _on_gem_types_changed(value: float): func _on_gem_types_changed(value: float):
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
if not match3_scene: if not match3_scene:
_find_target_scene() _find_target_scene()
last_scene_search_time = current_time
if not match3_scene: if not match3_scene:
DebugManager.log_error("Could not find target scene for gem types change", log_category) DebugManager.log_error("Could not find target scene for gem types change", log_category)
return return
var new_value = int(value) var new_value = int(value)
# Input validation # Enhanced input validation with safety constants
if new_value < gem_types_spinbox.min_value or new_value > gem_types_spinbox.max_value: if new_value < MIN_TILE_TYPES or new_value > MAX_TILE_TYPES:
DebugManager.log_error("Invalid gem types value: %d (range: %d-%d)" % [new_value, gem_types_spinbox.min_value, gem_types_spinbox.max_value], log_category) DebugManager.log_error("Invalid gem types value: %d (range: %d-%d)" % [new_value, MIN_TILE_TYPES, MAX_TILE_TYPES], log_category)
# Reset to valid value
gem_types_spinbox.value = clamp(new_value, MIN_TILE_TYPES, MAX_TILE_TYPES)
return return
if match3_scene.has_method("set_tile_types"): if match3_scene.has_method("set_tile_types"):
@@ -163,17 +204,25 @@ func _on_gem_types_changed(value: float):
gem_types_label.text = "Gem Types: " + str(new_value) gem_types_label.text = "Gem Types: " + str(new_value)
func _on_grid_width_changed(value: float): func _on_grid_width_changed(value: float):
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
if not match3_scene: if not match3_scene:
_find_target_scene() _find_target_scene()
last_scene_search_time = current_time
if not match3_scene: if not match3_scene:
DebugManager.log_error("Could not find target scene for grid width change", log_category) DebugManager.log_error("Could not find target scene for grid width change", log_category)
return return
var new_width = int(value) var new_width = int(value)
# Input validation # Enhanced input validation with safety constants
if new_width < grid_width_spinbox.min_value or new_width > grid_width_spinbox.max_value: if new_width < MIN_GRID_SIZE or new_width > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid width value: %d (range: %d-%d)" % [new_width, grid_width_spinbox.min_value, grid_width_spinbox.max_value], log_category) DebugManager.log_error("Invalid grid width value: %d (range: %d-%d)" % [new_width, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category)
# Reset to valid value
grid_width_spinbox.value = clamp(new_width, MIN_GRID_SIZE, MAX_GRID_SIZE)
return return
grid_width_label.text = "Width: " + str(new_width) grid_width_label.text = "Width: " + str(new_width)
@@ -188,17 +237,25 @@ func _on_grid_width_changed(value: float):
DebugManager.log_error("Target scene does not have set_grid_size method", log_category) DebugManager.log_error("Target scene does not have set_grid_size method", log_category)
func _on_grid_height_changed(value: float): func _on_grid_height_changed(value: float):
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
if not match3_scene: if not match3_scene:
_find_target_scene() _find_target_scene()
last_scene_search_time = current_time
if not match3_scene: if not match3_scene:
DebugManager.log_error("Could not find target scene for grid height change", log_category) DebugManager.log_error("Could not find target scene for grid height change", log_category)
return return
var new_height = int(value) var new_height = int(value)
# Input validation # Enhanced input validation with safety constants
if new_height < grid_height_spinbox.min_value or new_height > grid_height_spinbox.max_value: if new_height < MIN_GRID_SIZE or new_height > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid height value: %d (range: %d-%d)" % [new_height, grid_height_spinbox.min_value, grid_height_spinbox.max_value], log_category) DebugManager.log_error("Invalid grid height value: %d (range: %d-%d)" % [new_height, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category)
# Reset to valid value
grid_height_spinbox.value = clamp(new_height, MIN_GRID_SIZE, MAX_GRID_SIZE)
return return
grid_height_label.text = "Height: " + str(new_height) grid_height_label.text = "Height: " + str(new_height)

View File

@@ -9,11 +9,17 @@ var original_button_scales: Array[Vector2] = []
func _ready(): func _ready():
DebugManager.log_info("MainMenu ready", "MainMenu") DebugManager.log_info("MainMenu ready", "MainMenu")
_setup_menu_navigation() _setup_menu_navigation()
_update_new_game_button()
func _on_new_game_button_pressed(): func _on_new_game_button_pressed():
AudioManager.play_ui_click() AudioManager.play_ui_click()
DebugManager.log_info("New Game pressed", "MainMenu") var button_text = $MenuContainer/NewGameButton.text
GameManager.start_new_game() if button_text == "Continue":
DebugManager.log_info("Continue pressed", "MainMenu")
GameManager.continue_game()
else:
DebugManager.log_info("New Game pressed", "MainMenu")
GameManager.start_new_game()
func _on_settings_button_pressed(): func _on_settings_button_pressed():
AudioManager.play_ui_click() AudioManager.play_ui_click()
@@ -77,3 +83,17 @@ func _update_visual_selection():
else: else:
button.scale = original_button_scales[i] button.scale = original_button_scales[i]
button.modulate = Color.WHITE button.modulate = Color.WHITE
func _update_new_game_button():
# Check if there's an existing save with progress
var current_score = SaveManager.get_current_score()
var games_played = SaveManager.get_games_played()
var has_saved_grid = SaveManager.has_saved_grid()
var new_game_button = $MenuContainer/NewGameButton
if current_score > 0 or games_played > 0 or has_saved_grid:
new_game_button.text = "Continue"
DebugManager.log_info("Updated button to Continue (score: %d, games: %d, grid: %s)" % [current_score, games_played, has_saved_grid], "MainMenu")
else:
new_game_button.text = "New Game"
DebugManager.log_info("Updated button to New Game", "MainMenu")

View File

@@ -6,10 +6,14 @@ signal back_to_main_menu
@onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider @onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider
@onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider @onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider
@onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper @onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper
@onready var reset_progress_button = $ResetSettingsContainer/ResetProgressButton
@export var settings_manager: Node = SettingsManager @export var settings_manager: Node = SettingsManager
@export var localization_manager: Node = LocalizationManager @export var localization_manager: Node = LocalizationManager
# Progress reset confirmation dialog
var confirmation_dialog: AcceptDialog
# Navigation system variables # Navigation system variables
var navigable_controls: Array[Control] = [] var navigable_controls: Array[Control] = []
@@ -39,6 +43,7 @@ func _ready():
_update_controls_from_settings() _update_controls_from_settings()
update_text() update_text()
_setup_navigation_system() _setup_navigation_system()
_setup_confirmation_dialog()
func _update_controls_from_settings(): func _update_controls_from_settings():
master_slider.value = settings_manager.get_setting("master_volume") master_slider.value = settings_manager.get_setting("master_volume")
@@ -112,6 +117,7 @@ func update_text():
$SettingsContainer/SFXVolumeContainer/SFXVolume.text = tr("sfx_volume") $SettingsContainer/SFXVolumeContainer/SFXVolume.text = tr("sfx_volume")
$SettingsContainer/LanguageContainer/LanguageLabel.text = tr("language") $SettingsContainer/LanguageContainer/LanguageLabel.text = tr("language")
$BackButtonContainer/BackButton.text = tr("back") $BackButtonContainer/BackButton.text = tr("back")
reset_progress_button.text = tr("reset_progress")
func _on_reset_setting_button_pressed() -> void: func _on_reset_setting_button_pressed() -> void:
@@ -133,6 +139,7 @@ func _setup_navigation_system():
navigable_controls.append(language_stepper) # Use the ValueStepper component navigable_controls.append(language_stepper) # Use the ValueStepper component
navigable_controls.append($BackButtonContainer/BackButton) navigable_controls.append($BackButtonContainer/BackButton)
navigable_controls.append($ResetSettingsContainer/ResetSettingButton) navigable_controls.append($ResetSettingsContainer/ResetSettingButton)
navigable_controls.append(reset_progress_button)
# Store original visual properties # Store original visual properties
for control in navigable_controls: for control in navigable_controls:
@@ -218,3 +225,73 @@ func _get_control_name(control: Control) -> String:
func _on_language_stepper_value_changed(new_value: String, new_index: int): func _on_language_stepper_value_changed(new_value: String, new_index: int):
DebugManager.log_info("Language changed via ValueStepper: " + new_value + " (index: " + str(new_index) + ")", "Settings") DebugManager.log_info("Language changed via ValueStepper: " + new_value + " (index: " + str(new_index) + ")", "Settings")
func _setup_confirmation_dialog():
"""Create confirmation dialog for progress reset"""
confirmation_dialog = AcceptDialog.new()
confirmation_dialog.title = tr("confirm_reset_title")
confirmation_dialog.dialog_text = tr("confirm_reset_message")
confirmation_dialog.ok_button_text = tr("reset_confirm")
confirmation_dialog.add_cancel_button(tr("cancel"))
# Make dialog modal and centered
confirmation_dialog.set_flag(Window.FLAG_POPUP, true)
confirmation_dialog.popup_window = true
# Connect signals
confirmation_dialog.confirmed.connect(_on_reset_progress_confirmed)
confirmation_dialog.canceled.connect(_on_reset_progress_canceled)
add_child(confirmation_dialog)
func _on_reset_progress_button_pressed():
"""Handle reset progress button press with confirmation"""
AudioManager.play_ui_click()
DebugManager.log_info("Reset progress button pressed", "Settings")
# Update dialog text with current translations
confirmation_dialog.title = tr("confirm_reset_title")
confirmation_dialog.dialog_text = tr("confirm_reset_message")
confirmation_dialog.ok_button_text = tr("reset_confirm")
# Show confirmation dialog
confirmation_dialog.popup_centered()
func _on_reset_progress_confirmed():
"""Actually reset the progress after confirmation"""
AudioManager.play_ui_click()
DebugManager.log_info("Progress reset confirmed by user", "Settings")
# Call SaveManager to reset all progress
if SaveManager.reset_all_progress():
DebugManager.log_info("All progress successfully reset", "Settings")
# Show success message
var success_dialog = AcceptDialog.new()
success_dialog.title = tr("reset_success_title")
success_dialog.dialog_text = tr("reset_success_message")
success_dialog.ok_button_text = tr("ok")
add_child(success_dialog)
success_dialog.popup_centered()
# Auto-close success dialog and remove it after 3 seconds
success_dialog.confirmed.connect(func(): success_dialog.queue_free())
await get_tree().create_timer(3.0).timeout
if is_instance_valid(success_dialog):
success_dialog.queue_free()
else:
DebugManager.log_error("Failed to reset progress", "Settings")
# Show error message
var error_dialog = AcceptDialog.new()
error_dialog.title = tr("reset_error_title")
error_dialog.dialog_text = tr("reset_error_message")
error_dialog.ok_button_text = tr("ok")
add_child(error_dialog)
error_dialog.popup_centered()
error_dialog.confirmed.connect(func(): error_dialog.queue_free())
func _on_reset_progress_canceled():
"""Handle reset progress cancellation"""
AudioManager.play_ui_click()
DebugManager.log_info("Progress reset canceled by user", "Settings")

View File

@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dftenhuhwskqa" path="res://scenes/ui/SettingsMenu.gd" id="1_oqkcn"] [ext_resource type="Script" uid="uid://dftenhuhwskqa" path="res://scenes/ui/SettingsMenu.gd" id="1_oqkcn"]
[ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="2_debug"] [ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="2_debug"]
[ext_resource type="PackedScene" path="res://scenes/ui/components/ValueStepper.tscn" id="3_value_stepper"] [ext_resource type="PackedScene" uid="uid://cb6k05r8t7l4l" path="res://scenes/ui/components/ValueStepper.tscn" id="3_value_stepper"]
[node name="SettingsMenu" type="Control" groups=["localizable"]] [node name="SettingsMenu" type="Control" groups=["localizable"]]
layout_mode = 3 layout_mode = 3
@@ -26,6 +26,7 @@ offset_right = 34.5
offset_bottom = 20.0 offset_bottom = 20.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
alignment = 1
[node name="SettingsTitle" type="Label" parent="SettingsContainer"] [node name="SettingsTitle" type="Label" parent="SettingsContainer"]
custom_minimum_size = Vector2(300, 0) custom_minimum_size = Vector2(300, 0)
@@ -105,30 +106,28 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
text = "back" text = "back"
[node name="ResetSettingsContainer" type="Control" parent="."] [node name="ResetSettingsContainer" type="VBoxContainer" parent="."]
layout_mode = 1 layout_mode = 1
anchors_preset = 7 anchors_preset = 7
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 1.0 anchor_top = 1.0
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = -20.0 offset_left = -98.0
offset_top = -40.0 offset_top = -80.0
offset_right = 20.0 offset_right = 98.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
[node name="ResetSettingButton" type="Button" parent="ResetSettingsContainer"] [node name="ResetSettingButton" type="Button" parent="ResetSettingsContainer"]
layout_mode = 1 layout_mode = 2
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -98.0
offset_right = 98.0
offset_bottom = 31.0
grow_horizontal = 2
text = "Reset settings to default" text = "Reset settings to default"
[node name="ResetProgressButton" type="Button" parent="ResetSettingsContainer"]
modulate = Color(1, 0.7, 0.7, 1)
layout_mode = 2
text = "Reset All Progress"
[node name="DebugToggle" parent="." instance=ExtResource("2_debug")] [node name="DebugToggle" parent="." instance=ExtResource("2_debug")]
layout_mode = 1 layout_mode = 1
@@ -138,3 +137,4 @@ layout_mode = 1
[connection signal="value_changed" from="SettingsContainer/LanguageContainer/LanguageStepper" to="." method="_on_language_stepper_value_changed"] [connection signal="value_changed" from="SettingsContainer/LanguageContainer/LanguageStepper" to="." method="_on_language_stepper_value_changed"]
[connection signal="pressed" from="BackButtonContainer/BackButton" to="." method="_on_back_button_pressed"] [connection signal="pressed" from="BackButtonContainer/BackButton" to="." method="_on_back_button_pressed"]
[connection signal="pressed" from="ResetSettingsContainer/ResetSettingButton" to="." method="_on_reset_setting_button_pressed"] [connection signal="pressed" from="ResetSettingsContainer/ResetSettingButton" to="." method="_on_reset_setting_button_pressed"]
[connection signal="pressed" from="ResetSettingsContainer/ResetProgressButton" to="." method="_on_reset_progress_button_pressed"]

View File

@@ -7,12 +7,19 @@ var pending_gameplay_mode: String = "match3"
var is_changing_scene: bool = false var is_changing_scene: bool = false
func start_new_game() -> void: func start_new_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("match3")
func continue_game() -> void:
# Don't reset score - just load the game scene
start_game_with_mode("match3") start_game_with_mode("match3")
func start_match3_game() -> void: func start_match3_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("match3") start_game_with_mode("match3")
func start_clickomania_game() -> void: func start_clickomania_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("clickomania") start_game_with_mode("clickomania")
func start_game_with_mode(gameplay_mode: String) -> void: func start_game_with_mode(gameplay_mode: String) -> void:
@@ -65,13 +72,25 @@ func start_game_with_mode(gameplay_mode: String) -> void:
if get_tree().current_scene.has_method("set_gameplay_mode"): if get_tree().current_scene.has_method("set_gameplay_mode"):
DebugManager.log_info("Setting gameplay mode to: %s" % pending_gameplay_mode, "GameManager") DebugManager.log_info("Setting gameplay mode to: %s" % pending_gameplay_mode, "GameManager")
get_tree().current_scene.set_gameplay_mode(pending_gameplay_mode) get_tree().current_scene.set_gameplay_mode(pending_gameplay_mode)
# Load saved score
if get_tree().current_scene.has_method("set_global_score"):
var saved_score = SaveManager.get_current_score()
DebugManager.log_info("Loading saved score: %d" % saved_score, "GameManager")
get_tree().current_scene.set_global_score(saved_score)
else: else:
DebugManager.log_error("Game scene does not have set_gameplay_mode method", "GameManager") DebugManager.log_error("Game scene does not have set_gameplay_mode method", "GameManager")
is_changing_scene = false is_changing_scene = false
func save_game() -> void: func save_game() -> void:
DebugManager.log_info("Game saved (mock)", "GameManager") # Get current score from the active game scene
var current_score = 0
if get_tree().current_scene and get_tree().current_scene.has_method("get_global_score"):
current_score = get_tree().current_scene.get_global_score()
SaveManager.finish_game(current_score)
DebugManager.log_info("Game saved with score: %d" % current_score, "GameManager")
func exit_to_main_menu() -> void: func exit_to_main_menu() -> void:
# Prevent concurrent scene changes # Prevent concurrent scene changes

View File

@@ -0,0 +1,402 @@
extends Node
const SAVE_FILE_PATH = "user://savegame.save"
const SAVE_FORMAT_VERSION = 1
const MAX_GRID_SIZE = 15
const MAX_TILE_TYPES = 10
const MAX_SCORE = 999999999
const MAX_GAMES_PLAYED = 100000
var game_data = {
"high_score": 0,
"current_score": 0,
"games_played": 0,
"total_score": 0,
"grid_state": {
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
"grid_layout": [] # 2D array of tile types
}
}
func _ready():
load_game()
func save_game():
# Create backup before saving
_create_backup()
# Add version and validation data
var save_data = game_data.duplicate(true)
save_data["_version"] = SAVE_FORMAT_VERSION
save_data["_checksum"] = _calculate_checksum(save_data)
var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if save_file == null:
DebugManager.log_error("Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager")
return false
var json_string = JSON.stringify(save_data)
# Validate JSON was created successfully
if json_string.is_empty():
DebugManager.log_error("Failed to serialize save data to JSON", "SaveManager")
save_file.close()
return false
save_file.store_var(json_string)
save_file.close()
DebugManager.log_info("Game saved successfully. High score: %d, Current score: %d" % [game_data.high_score, game_data.current_score], "SaveManager")
return true
func load_game():
if not FileAccess.file_exists(SAVE_FILE_PATH):
DebugManager.log_info("No save file found, using defaults", "SaveManager")
return
var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
if save_file == null:
DebugManager.log_error("Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager")
return
# Check file size to prevent memory exhaustion
var file_size = save_file.get_length()
if file_size > 1048576: # 1MB limit
DebugManager.log_error("Save file too large: %d bytes (max 1MB)" % file_size, "SaveManager")
save_file.close()
return
var json_string = save_file.get_var()
save_file.close()
if not json_string is String:
DebugManager.log_error("Save file contains invalid data type", "SaveManager")
return
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result != OK:
DebugManager.log_error("Failed to parse save file JSON: %s" % json.error_string, "SaveManager")
_restore_backup_if_exists()
return
var loaded_data = json.data
if not loaded_data is Dictionary:
DebugManager.log_error("Save file root is not a dictionary", "SaveManager")
_restore_backup_if_exists()
return
# Validate and sanitize loaded data
if not _validate_save_data(loaded_data):
DebugManager.log_error("Save file failed validation, using defaults", "SaveManager")
_restore_backup_if_exists()
return
# Safely merge validated data
_merge_validated_data(loaded_data)
DebugManager.log_info("Game loaded successfully. High score: %d, Games played: %d" % [game_data.high_score, game_data.games_played], "SaveManager")
func update_current_score(score: int):
# Input validation
if score < 0:
DebugManager.log_warn("Negative score rejected: %d" % score, "SaveManager")
return
if score > MAX_SCORE:
DebugManager.log_warn("Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager")
score = MAX_SCORE
game_data.current_score = score
if score > game_data.high_score:
game_data.high_score = score
DebugManager.log_info("New high score: %d" % score, "SaveManager")
func start_new_game():
game_data.current_score = 0
game_data.games_played += 1
# Clear any saved grid state for fresh start
game_data.grid_state.grid_layout = []
DebugManager.log_info("Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager")
func finish_game(final_score: int):
# Input validation
if final_score < 0:
DebugManager.log_warn("Negative final score rejected: %d" % final_score, "SaveManager")
return
if final_score > MAX_SCORE:
DebugManager.log_warn("Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager")
final_score = MAX_SCORE
DebugManager.log_info("Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score], "SaveManager")
game_data.current_score = final_score
# Prevent total_score overflow
var new_total = game_data.total_score + final_score
if new_total < game_data.total_score: # Overflow check
DebugManager.log_warn("Total score overflow prevented", "SaveManager")
game_data.total_score = MAX_SCORE
else:
game_data.total_score = new_total
if final_score > game_data.high_score:
game_data.high_score = final_score
DebugManager.log_info("New high score achieved: %d" % final_score, "SaveManager")
save_game()
func get_high_score() -> int:
return game_data.high_score
func get_current_score() -> int:
return game_data.current_score
func get_games_played() -> int:
return game_data.games_played
func get_total_score() -> int:
return game_data.total_score
func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array):
# Comprehensive input validation
if not _validate_grid_parameters(grid_size, tile_types_count, active_gem_types, grid_layout):
DebugManager.log_error("Grid state validation failed, not saving", "SaveManager")
return
DebugManager.log_info("Saving grid state: size(%d,%d), types=%d, layout_rows=%d" % [grid_size.x, grid_size.y, tile_types_count, grid_layout.size()], "SaveManager")
game_data.grid_state.grid_size = {"x": grid_size.x, "y": grid_size.y}
game_data.grid_state.tile_types_count = tile_types_count
game_data.grid_state.active_gem_types = active_gem_types.duplicate()
game_data.grid_state.grid_layout = grid_layout.duplicate(true) # Deep copy
# Debug: Print first few rows of saved layout
for y in range(min(3, grid_layout.size())):
var row_str = ""
for x in range(min(8, grid_layout[y].size())):
row_str += str(grid_layout[y][x]) + " "
DebugManager.log_debug("Saved row %d: %s" % [y, row_str], "SaveManager")
save_game()
func get_saved_grid_state() -> Dictionary:
return game_data.grid_state
func has_saved_grid() -> bool:
return game_data.grid_state.grid_layout.size() > 0
func clear_grid_state():
DebugManager.log_info("Clearing saved grid state", "SaveManager")
game_data.grid_state.grid_layout = []
save_game()
func reset_all_progress():
"""Reset all game progress and delete save files completely"""
DebugManager.log_info("Starting complete progress reset", "SaveManager")
# Reset all game data to initial values
game_data = {
"high_score": 0,
"current_score": 0,
"games_played": 0,
"total_score": 0,
"grid_state": {
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
"grid_layout": [] # 2D array of tile types
}
}
# Delete main save file
if FileAccess.file_exists(SAVE_FILE_PATH):
var error = DirAccess.remove_absolute(SAVE_FILE_PATH)
if error == OK:
DebugManager.log_info("Main save file deleted successfully", "SaveManager")
else:
DebugManager.log_error("Failed to delete main save file: error %d" % error, "SaveManager")
# Delete backup file
var backup_path = SAVE_FILE_PATH + ".backup"
if FileAccess.file_exists(backup_path):
var error = DirAccess.remove_absolute(backup_path)
if error == OK:
DebugManager.log_info("Backup save file deleted successfully", "SaveManager")
else:
DebugManager.log_error("Failed to delete backup save file: error %d" % error, "SaveManager")
DebugManager.log_info("Progress reset completed - all scores and save data cleared", "SaveManager")
return true
# Security and validation helper functions
func _validate_save_data(data: Dictionary) -> bool:
# Check required fields exist and have correct types
var required_fields = ["high_score", "current_score", "games_played", "total_score", "grid_state"]
for field in required_fields:
if not data.has(field):
DebugManager.log_error("Missing required field: %s" % field, "SaveManager")
return false
# Validate numeric fields
if not _is_valid_score(data.get("high_score", 0)):
return false
if not _is_valid_score(data.get("current_score", 0)):
return false
if not _is_valid_score(data.get("total_score", 0)):
return false
var games_played = data.get("games_played", 0)
# Accept both int and float for games_played, convert to int for validation
if not (games_played is int or games_played is float):
DebugManager.log_error("Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)], "SaveManager")
return false
var games_played_int = int(games_played)
if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED:
DebugManager.log_error("Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED], "SaveManager")
return false
# Validate grid state
var grid_state = data.get("grid_state", {})
if not grid_state is Dictionary:
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
return false
return _validate_grid_state(grid_state)
func _validate_grid_state(grid_state: Dictionary) -> bool:
# Check grid size
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary:
DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
return false
var size = grid_state.grid_size
if not size.has("x") or not size.has("y"):
return false
var width = size.x
var height = size.y
if not width is int or not height is int:
return false
if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE:
DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager")
return false
# Check tile types
var tile_types = grid_state.get("tile_types_count", 0)
if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES:
DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager")
return false
# Validate grid layout if present
var layout = grid_state.get("grid_layout", [])
if layout.size() > 0:
return _validate_grid_layout(layout, width, height, tile_types)
return true
func _validate_grid_layout(layout: Array, expected_width: int, expected_height: int, max_tile_type: int) -> bool:
if layout.size() != expected_height:
DebugManager.log_error("Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height], "SaveManager")
return false
for y in range(layout.size()):
var row = layout[y]
if not row is Array:
DebugManager.log_error("Grid layout row %d is not an array" % y, "SaveManager")
return false
if row.size() != expected_width:
DebugManager.log_error("Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width], "SaveManager")
return false
for x in range(row.size()):
var tile_type = row[x]
if not tile_type is int:
DebugManager.log_error("Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)], "SaveManager")
return false
if tile_type < -1 or tile_type >= max_tile_type:
DebugManager.log_error("Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager")
return false
return true
func _validate_grid_parameters(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array) -> bool:
# Validate grid size
if grid_size.x < 3 or grid_size.y < 3 or grid_size.x > MAX_GRID_SIZE or grid_size.y > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid size: %dx%d (min 3x3, max %dx%d)" % [grid_size.x, grid_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "SaveManager")
return false
# Validate tile types count
if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES:
DebugManager.log_error("Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES], "SaveManager")
return false
# Validate active gem types
if active_gem_types.size() != tile_types_count:
DebugManager.log_error("Active gem types size mismatch: %d vs %d" % [active_gem_types.size(), tile_types_count], "SaveManager")
return false
# Validate grid layout
return _validate_grid_layout(grid_layout, grid_size.x, grid_size.y, tile_types_count)
func _is_valid_score(score) -> bool:
# Accept both int and float, but convert to int for validation
if not (score is int or score is float):
DebugManager.log_error("Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager")
return false
var score_int = int(score)
if score_int < 0 or score_int > MAX_SCORE:
DebugManager.log_error("Score out of bounds: %d" % score_int, "SaveManager")
return false
return true
func _merge_validated_data(loaded_data: Dictionary):
# Safely merge only validated fields, converting floats to ints for scores
for key in ["high_score", "current_score", "total_score"]:
if loaded_data.has(key):
var value = loaded_data[key]
# Convert float scores to integers
game_data[key] = int(value) if (value is float or value is int) else 0
# Games played should always be an integer
if loaded_data.has("games_played"):
var games_played = loaded_data["games_played"]
game_data["games_played"] = int(games_played) if (games_played is float or games_played is int) else 0
# Merge grid state carefully
var loaded_grid = loaded_data.get("grid_state", {})
for grid_key in ["grid_size", "tile_types_count", "active_gem_types", "grid_layout"]:
if loaded_grid.has(grid_key):
game_data.grid_state[grid_key] = loaded_grid[grid_key]
func _calculate_checksum(data: Dictionary) -> String:
# Simple checksum for save file integrity
var json_string = JSON.stringify(data)
return str(json_string.hash())
func _create_backup():
# Create backup of current save file
if FileAccess.file_exists(SAVE_FILE_PATH):
var backup_path = SAVE_FILE_PATH + ".backup"
var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
var backup = FileAccess.open(backup_path, FileAccess.WRITE)
if original and backup:
backup.store_var(original.get_var())
backup.close()
if original:
original.close()
func _restore_backup_if_exists():
var backup_path = SAVE_FILE_PATH + ".backup"
if FileAccess.file_exists(backup_path):
DebugManager.log_info("Attempting to restore from backup", "SaveManager")
var backup = FileAccess.open(backup_path, FileAccess.READ)
var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if backup and original:
original.store_var(backup.get_var())
original.close()
DebugManager.log_info("Backup restored successfully", "SaveManager")
load_game() # Try to load the restored backup
if backup:
backup.close()

View File

@@ -0,0 +1 @@
uid://d1fdah5x4rme0