more lint and formatting
Some checks failed
Continuous Integration / Code Formatting (push) Successful in 33s
Continuous Integration / Code Quality Check (push) Successful in 29s
Continuous Integration / Test Execution (push) Failing after 16s
Continuous Integration / CI Summary (push) Failing after 4s

This commit is contained in:
2025-10-01 15:04:40 +04:00
parent 538459f323
commit 3b8da89ad5
31 changed files with 2112 additions and 691 deletions

View File

@@ -1,13 +1,19 @@
# GDFormat configuration file # GDFormat configuration file (YAML format)
# This file configures the gdformat tool for consistent GDScript formatting # This file configures the gdformat tool for consistent GDScript formatting
# Maximum line length (default is 100) # Maximum line length (default is 100)
# Godot's style guide recommends keeping lines under 100 characters # Godot's style guide recommends keeping lines under 100 characters
line_length = 80 line_length: 80
# Whether to use tabs or spaces for indentation # Use spaces instead of tabs (null = use tabs)
# Set to integer for space count, or null for tabs
# Godot uses tabs by default # Godot uses tabs by default
use_tabs = true use_spaces: null
# Number of spaces per tab (when displaying) # Safety checks (null = enabled by default)
tab_width = 4 safety_checks: null
# Directories to exclude from formatting
excluded_directories: !!set
.git: null
.godot: null

View File

@@ -210,9 +210,19 @@ jobs:
- name: Run tests - name: Run tests
id: test id: test
continue-on-error: true
run: | run: |
echo "🧪 Running GDScript tests..." echo "🧪 Running GDScript tests..."
python tools/run_development.py --test --silent --yaml > test_results.yaml python tools/run_development.py --test --yaml > test_results.yaml 2>&1
TEST_EXIT_CODE=$?
# Display test results regardless of success/failure
echo ""
echo "📊 Test Results:"
cat test_results.yaml
# Exit with the original test exit code
exit $TEST_EXIT_CODE
- name: Upload test results - name: Upload test results
if: always() if: always()
@@ -224,6 +234,49 @@ jobs:
retention-days: 7 retention-days: 7
compression-level: 0 compression-level: 0
- name: Check test results and display errors
if: always()
run: |
echo ""
echo "================================================"
echo "📊 TEST RESULTS SUMMARY"
echo "================================================"
# Parse YAML to check if tests failed
if grep -q "success: false" test_results.yaml; then
echo "❌ Status: FAILED"
echo ""
echo "💥 Failed Test Details:"
echo "================================================"
# Extract and display failed test information
grep -A 2 "failed_test_details:" test_results.yaml || echo "No detailed error information available"
# Show statistics
echo ""
echo "📈 Statistics:"
grep "tests_passed:" test_results.yaml || true
grep "tests_failed:" test_results.yaml || true
grep "total_tests_run:" test_results.yaml || true
echo ""
echo "================================================"
echo "⚠️ Please review the test errors above and fix them before merging."
echo "================================================"
exit 1
elif grep -q "success: true" test_results.yaml; then
echo "✅ Status: PASSED"
echo ""
grep "total_tests_run:" test_results.yaml || true
echo "================================================"
exit 0
else
echo "⚠️ Status: UNKNOWN"
echo "Could not determine test status from YAML output"
echo "================================================"
exit 1
fi
summary: summary:
name: CI Summary name: CI Summary
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -11,14 +11,17 @@ var language_stepper: ValueStepper = $VBoxContainer/Examples/LanguageContainer/L
var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContainer/DifficultyStepper var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContainer/DifficultyStepper
@onready @onready
var resolution_stepper: ValueStepper = $VBoxContainer/Examples/ResolutionContainer/ResolutionStepper var resolution_stepper: ValueStepper = $VBoxContainer/Examples/ResolutionContainer/ResolutionStepper
@onready var custom_stepper: ValueStepper = $VBoxContainer/Examples/CustomContainer/CustomStepper @onready
var custom_stepper: ValueStepper = $VBoxContainer/Examples/CustomContainer/CustomStepper
func _ready(): func _ready():
DebugManager.log_info("ValueStepper example ready", "Example") DebugManager.log_info("ValueStepper example ready", "Example")
# Setup navigation array # Setup navigation array
navigable_steppers = [language_stepper, difficulty_stepper, resolution_stepper, custom_stepper] navigable_steppers = [
language_stepper, difficulty_stepper, resolution_stepper, custom_stepper
]
# Connect to value change events # Connect to value change events
for stepper in navigable_steppers: for stepper in navigable_steppers:
@@ -52,15 +55,22 @@ func _input(event: InputEvent):
func _navigate_steppers(direction: int): func _navigate_steppers(direction: int):
current_stepper_index = (current_stepper_index + direction) % navigable_steppers.size() current_stepper_index = (
(current_stepper_index + direction) % navigable_steppers.size()
)
if current_stepper_index < 0: if current_stepper_index < 0:
current_stepper_index = navigable_steppers.size() - 1 current_stepper_index = navigable_steppers.size() - 1
_update_stepper_highlighting() _update_stepper_highlighting()
DebugManager.log_info("Stepper navigation: index " + str(current_stepper_index), "Example") DebugManager.log_info(
"Stepper navigation: index " + str(current_stepper_index), "Example"
)
func _handle_stepper_input(action: String): func _handle_stepper_input(action: String):
if current_stepper_index >= 0 and current_stepper_index < navigable_steppers.size(): if (
current_stepper_index >= 0
and current_stepper_index < navigable_steppers.size()
):
var stepper = navigable_steppers[current_stepper_index] var stepper = navigable_steppers[current_stepper_index]
if stepper.handle_input_action(action): if stepper.handle_input_action(action):
AudioManager.play_ui_click() AudioManager.play_ui_click()
@@ -73,7 +83,14 @@ func _update_stepper_highlighting():
func _on_stepper_value_changed(new_value: String, new_index: int): func _on_stepper_value_changed(new_value: String, new_index: int):
DebugManager.log_info( DebugManager.log_info(
"Stepper value changed to: " + new_value + " (index: " + str(new_index) + ")", "Example" (
"Stepper value changed to: "
+ new_value
+ " (index: "
+ str(new_index)
+ ")"
),
"Example"
) )
# Handle value change in your scene # Handle value change in your scene
# For example: apply settings, save preferences, update UI, etc. # For example: apply settings, save preferences, update UI, etc.

View File

@@ -20,15 +20,20 @@ func _ready() -> void:
# GameManager will set the gameplay mode, don't set default here # GameManager will set the gameplay mode, don't set default here
DebugManager.log_debug( DebugManager.log_debug(
"Game _ready() completed, waiting for GameManager to set gameplay mode", "Game" "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") DebugManager.log_info(
"set_gameplay_mode called with mode: %s" % mode, "Game"
)
current_gameplay_mode = mode current_gameplay_mode = mode
await load_gameplay(mode) await load_gameplay(mode)
DebugManager.log_info("set_gameplay_mode completed for mode: %s" % mode, "Game") DebugManager.log_info(
"set_gameplay_mode completed for mode: %s" % mode, "Game"
)
func load_gameplay(mode: String) -> void: func load_gameplay(mode: String) -> void:
@@ -37,24 +42,35 @@ func load_gameplay(mode: String) -> void:
# Clear existing gameplay and wait for removal # Clear existing gameplay and wait for removal
var existing_children = gameplay_container.get_children() var existing_children = gameplay_container.get_children()
if existing_children.size() > 0: if existing_children.size() > 0:
DebugManager.log_debug("Removing %d existing children" % existing_children.size(), "Game") DebugManager.log_debug(
"Removing %d existing children" % existing_children.size(), "Game"
)
for child in existing_children: for child in existing_children:
DebugManager.log_debug("Removing existing child: %s" % child.name, "Game") DebugManager.log_debug(
"Removing existing child: %s" % child.name, "Game"
)
child.queue_free() child.queue_free()
# Wait for children to be properly removed from scene tree # Wait for children to be properly removed from scene tree
await get_tree().process_frame await get_tree().process_frame
DebugManager.log_debug( DebugManager.log_debug(
"Children removal complete, container count: %d" % gameplay_container.get_child_count(), (
"Children removal complete, container count: %d"
% gameplay_container.get_child_count()
),
"Game" "Game"
) )
# Load new gameplay # Load new gameplay
if GAMEPLAY_SCENES.has(mode): if GAMEPLAY_SCENES.has(mode):
DebugManager.log_debug("Found scene path: %s" % GAMEPLAY_SCENES[mode], "Game") DebugManager.log_debug(
"Found scene path: %s" % GAMEPLAY_SCENES[mode], "Game"
)
var gameplay_scene = load(GAMEPLAY_SCENES[mode]) var gameplay_scene = load(GAMEPLAY_SCENES[mode])
var gameplay_instance = gameplay_scene.instantiate() var gameplay_instance = gameplay_scene.instantiate()
DebugManager.log_debug("Instantiated gameplay: %s" % gameplay_instance.name, "Game") DebugManager.log_debug(
"Instantiated gameplay: %s" % gameplay_instance.name, "Game"
)
gameplay_container.add_child(gameplay_instance) gameplay_container.add_child(gameplay_instance)
DebugManager.log_debug( DebugManager.log_debug(
( (
@@ -69,7 +85,9 @@ func load_gameplay(mode: String) -> void:
gameplay_instance.score_changed.connect(_on_score_changed) gameplay_instance.score_changed.connect(_on_score_changed)
DebugManager.log_debug("Connected score_changed signal", "Game") DebugManager.log_debug("Connected score_changed signal", "Game")
else: else:
DebugManager.log_error("Gameplay mode '%s' not found in GAMEPLAY_SCENES" % mode, "Game") DebugManager.log_error(
"Gameplay mode '%s' not found in GAMEPLAY_SCENES" % mode, "Game"
)
func set_global_score(value: int) -> void: func set_global_score(value: int) -> void:
@@ -102,10 +120,15 @@ func _on_back_button_pressed() -> void:
if gameplay_instance and gameplay_instance.has_method("save_current_state"): if gameplay_instance and gameplay_instance.has_method("save_current_state"):
DebugManager.log_info("Saving grid state before exit", "Game") DebugManager.log_info("Saving grid state before exit", "Game")
# Make sure the gameplay instance is still valid and not being destroyed # Make sure the gameplay instance is still valid and not being destroyed
if is_instance_valid(gameplay_instance) and gameplay_instance.is_inside_tree(): if (
is_instance_valid(gameplay_instance)
and gameplay_instance.is_inside_tree()
):
gameplay_instance.save_current_state() gameplay_instance.save_current_state()
else: else:
DebugManager.log_warn("Gameplay instance invalid, skipping grid save on exit", "Game") DebugManager.log_warn(
"Gameplay instance invalid, skipping grid save on exit", "Game"
)
# Save the current score immediately before exiting # Save the current score immediately before exiting
SaveManager.finish_game(global_score) SaveManager.finish_game(global_score)
@@ -116,7 +139,10 @@ func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_back"): if event.is_action_pressed("ui_back"):
# Handle gamepad/keyboard back action - same as back button # Handle gamepad/keyboard back action - same as back button
_on_back_button_pressed() _on_back_button_pressed()
elif event.is_action_pressed("action_south") and Input.is_action_pressed("action_north"): elif (
event.is_action_pressed("action_south")
and Input.is_action_pressed("action_north")
):
# Debug: Switch to clickomania when primary+secondary actions pressed together # Debug: Switch to clickomania when primary+secondary actions pressed together
if current_gameplay_mode == "match3": if current_gameplay_mode == "match3":
set_gameplay_mode("clickomania") set_gameplay_mode("clickomania")

View File

@@ -49,13 +49,21 @@ func _ready() -> void:
instance_id = "Match3_%d" % get_instance_id() instance_id = "Match3_%d" % get_instance_id()
if grid_initialized: if grid_initialized:
DebugManager.log_warn( (
"[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id, DebugManager
. log_warn(
(
"[%s] Match3 _ready() called multiple times, skipping initialization"
% instance_id
),
"Match3" "Match3"
) )
)
return return
DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3") DebugManager.log_debug(
"[%s] Match3 _ready() started" % instance_id, "Match3"
)
grid_initialized = true grid_initialized = true
# Calculate grid layout # Calculate grid layout
@@ -64,12 +72,16 @@ func _ready() -> void:
# Try to load saved state, otherwise use default # Try to load saved state, otherwise use default
var loaded_saved_state = await load_saved_state() var loaded_saved_state = await load_saved_state()
if not loaded_saved_state: if not loaded_saved_state:
DebugManager.log_info("No saved state found, using default grid initialization", "Match3") DebugManager.log_info(
"No saved state found, using default grid initialization", "Match3"
)
_initialize_grid() _initialize_grid()
else: else:
DebugManager.log_info("Successfully loaded saved grid state", "Match3") 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"
)
# Notify UI that grid state is loaded # Notify UI that grid state is loaded
grid_state_loaded.emit(grid_size, tile_types) grid_state_loaded.emit(grid_size, tile_types)
@@ -91,7 +103,8 @@ func _calculate_grid_layout():
# Align grid to left side with margins # Align grid to left side with 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(
GRID_LEFT_MARGIN, (viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN GRID_LEFT_MARGIN,
(viewport_size.y - total_grid_height) / 2 + GRID_TOP_MARGIN
) )
@@ -136,7 +149,9 @@ func _has_match_at(pos: Vector2i) -> bool:
return false return false
if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): 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") DebugManager.log_error(
"Grid array bounds exceeded at (%d,%d)" % [pos.x, pos.y], "Match3"
)
return false return false
var tile = grid[pos.y][pos.x] var tile = grid[pos.y][pos.x]
@@ -146,7 +161,8 @@ func _has_match_at(pos: Vector2i) -> bool:
# Check if tile has required properties # Check if tile has required properties
if not "tile_type" in tile: if not "tile_type" in tile:
DebugManager.log_warn( DebugManager.log_warn(
"Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3" "Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y],
"Match3"
) )
return false return false
@@ -177,13 +193,18 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
# Validate input parameters # Validate input parameters
if not _is_valid_grid_position(start): if not _is_valid_grid_position(start):
DebugManager.log_error( DebugManager.log_error(
"Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3" (
"Invalid start position for match line: (%d,%d)"
% [start.x, start.y]
),
"Match3"
) )
return [] return []
if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0): if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0):
DebugManager.log_error( DebugManager.log_error(
"Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3" "Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y],
"Match3"
) )
return [] return []
@@ -206,7 +227,10 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
var current = start + dir * offset var current = start + dir * offset
var steps = 0 var steps = 0
# Safety limit prevents infinite loops in case of logic errors # Safety limit prevents infinite loops in case of logic errors
while steps < grid_size.x + grid_size.y and _is_valid_grid_position(current): 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(): if current.y >= grid.size() or current.x >= grid[current.y].size():
break break
@@ -233,7 +257,9 @@ func _clear_matches() -> void:
""" """
# Check grid integrity # Check grid integrity
if not _validate_grid_integrity(): if not _validate_grid_integrity():
DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3") DebugManager.log_error(
"Grid integrity check failed in _clear_matches", "Match3"
)
return return
var match_groups := [] var match_groups := []
@@ -282,7 +308,9 @@ func _clear_matches() -> void:
else: else:
match_score = match_size + max(0, match_size - 2) # n + (n-2) for n >= 4 match_score = match_size + max(0, match_size - 2) # n + (n-2) for n >= 4
total_score += match_score total_score += match_score
print("Debug: Match of ", match_size, " gems = ", match_score, " points") print(
"Debug: Match of ", match_size, " gems = ", match_score, " points"
)
# Remove duplicates from all matches combined # Remove duplicates from all matches combined
var to_clear := [] var to_clear := []
@@ -308,7 +336,9 @@ func _clear_matches() -> void:
# Validate tile has grid_position property # Validate tile has grid_position property
if not "grid_position" in tile: if not "grid_position" in tile:
DebugManager.log_warn("Tile missing grid_position during removal", "Match3") DebugManager.log_warn(
"Tile missing grid_position during removal", "Match3"
)
tile.queue_free() tile.queue_free()
continue continue
@@ -322,7 +352,10 @@ func _clear_matches() -> void:
grid[tile_pos.y][tile_pos.x] = null grid[tile_pos.y][tile_pos.x] = null
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y], (
"Invalid grid position during tile removal: (%d,%d)"
% [tile_pos.x, tile_pos.y]
),
"Match3" "Match3"
) )
@@ -358,7 +391,9 @@ func _drop_tiles():
func _fill_empty_cells(): func _fill_empty_cells():
# Safety check for grid integrity # Safety check for grid integrity
if not _validate_grid_integrity(): if not _validate_grid_integrity():
DebugManager.log_error("Grid integrity check failed in _fill_empty_cells", "Match3") DebugManager.log_error(
"Grid integrity check failed in _fill_empty_cells", "Match3"
)
return return
# Create gem pool for current tile types # Create gem pool for current tile types
@@ -374,14 +409,17 @@ func _fill_empty_cells():
for x in range(grid_size.x): for x in range(grid_size.x):
if x >= grid[y].size(): if x >= grid[y].size():
DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3") DebugManager.log_error(
"Grid column %d does not exist in row %d" % [x, y], "Match3"
)
continue continue
if not grid[y][x]: if not grid[y][x]:
var tile = TILE_SCENE.instantiate() var tile = TILE_SCENE.instantiate()
if not tile: if not tile:
DebugManager.log_error( DebugManager.log_error(
"Failed to instantiate tile at (%d,%d)" % [x, y], "Match3" "Failed to instantiate tile at (%d,%d)" % [x, y],
"Match3"
) )
continue continue
@@ -393,13 +431,17 @@ func _fill_empty_cells():
if tile.has_method("set_active_gem_types"): if tile.has_method("set_active_gem_types"):
tile.set_active_gem_types(gem_indices) tile.set_active_gem_types(gem_indices)
else: else:
DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3") DebugManager.log_warn(
"Tile missing set_active_gem_types method", "Match3"
)
# Set random tile type with bounds checking # Set random tile type with bounds checking
if tile_types > 0: if tile_types > 0:
tile.tile_type = randi() % tile_types tile.tile_type = randi() % tile_types
else: else:
DebugManager.log_error("tile_types is 0, cannot set tile type", "Match3") DebugManager.log_error(
"tile_types is 0, cannot set tile type", "Match3"
)
tile.queue_free() tile.queue_free()
continue continue
@@ -408,7 +450,9 @@ func _fill_empty_cells():
if tile.has_signal("tile_selected"): if tile.has_signal("tile_selected"):
tile.tile_selected.connect(_on_tile_selected) tile.tile_selected.connect(_on_tile_selected)
else: else:
DebugManager.log_warn("Tile missing tile_selected signal", "Match3") DebugManager.log_warn(
"Tile missing tile_selected signal", "Match3"
)
tiles_created += 1 tiles_created += 1
@@ -423,13 +467,16 @@ func _fill_empty_cells():
iteration += 1 iteration += 1
if iteration >= MAX_CASCADE_ITERATIONS: if iteration >= MAX_CASCADE_ITERATIONS:
DebugManager.log_warn( (
DebugManager
. log_warn(
( (
"Maximum cascade iterations reached (%d), stopping to prevent infinite loop" "Maximum cascade iterations reached (%d), stopping to prevent infinite loop"
% MAX_CASCADE_ITERATIONS % MAX_CASCADE_ITERATIONS
), ),
"Match3" "Match3"
) )
)
# Save grid state after cascades complete # Save grid state after cascades complete
save_current_state() save_current_state()
@@ -444,20 +491,27 @@ func regenerate_grid():
or grid_size.y > MAX_GRID_SIZE or grid_size.y > MAX_GRID_SIZE
): ):
DebugManager.log_error( DebugManager.log_error(
"Invalid grid size for regeneration: %dx%d" % [grid_size.x, grid_size.y], "Match3" (
"Invalid grid size for regeneration: %dx%d"
% [grid_size.x, grid_size.y]
),
"Match3"
) )
return return
if tile_types < 3 or tile_types > MAX_TILE_TYPES: if tile_types < 3 or tile_types > MAX_TILE_TYPES:
DebugManager.log_error( DebugManager.log_error(
"Invalid tile types count for regeneration: %d" % tile_types, "Match3" "Invalid tile types count for regeneration: %d" % tile_types,
"Match3"
) )
return 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"
)
# Safe tile cleanup with improved error handling # Safe tile cleanup with improved error handling
var children_to_remove = [] var children_to_remove = []
@@ -477,7 +531,9 @@ func regenerate_grid():
children_to_remove.append(child) children_to_remove.append(child)
removed_count += 1 removed_count += 1
DebugManager.log_debug("Found %d tile children to remove" % removed_count, "Match3") 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()):
@@ -508,20 +564,30 @@ func regenerate_grid():
func set_tile_types(new_count: int): func set_tile_types(new_count: int):
# Input validation # Input validation
if new_count < 3: if new_count < 3:
DebugManager.log_error("Tile types count too low: %d (minimum 3)" % new_count, "Match3") DebugManager.log_error(
"Tile types count too low: %d (minimum 3)" % new_count, "Match3"
)
return return
if new_count > MAX_TILE_TYPES: if new_count > MAX_TILE_TYPES:
DebugManager.log_error( DebugManager.log_error(
"Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3" (
"Tile types count too high: %d (maximum %d)"
% [new_count, MAX_TILE_TYPES]
),
"Match3"
) )
return return
if new_count == tile_types: if new_count == tile_types:
DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3") DebugManager.log_debug(
"Tile types count unchanged, skipping regeneration", "Match3"
)
return return
DebugManager.log_debug("Changing tile types from %d to %d" % [tile_types, new_count], "Match3") 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)
@@ -551,10 +617,14 @@ func set_grid_size(new_size: Vector2i):
return return
if new_size == grid_size: if new_size == grid_size:
DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3") DebugManager.log_debug(
"Grid size unchanged, skipping regeneration", "Match3"
)
return return
DebugManager.log_debug("Changing grid size from %s to %s" % [grid_size, new_size], "Match3") 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
@@ -577,13 +647,19 @@ func reset_all_visual_states() -> void:
func _debug_scene_structure() -> void: func _debug_scene_structure() -> void:
DebugManager.log_debug("=== Scene Structure Debug ===", "Match3") DebugManager.log_debug("=== Scene Structure Debug ===", "Match3")
DebugManager.log_debug("Match3 node children count: %d" % get_child_count(), "Match3") DebugManager.log_debug(
DebugManager.log_debug("Match3 global position: %s" % global_position, "Match3") "Match3 node children count: %d" % get_child_count(), "Match3"
)
DebugManager.log_debug(
"Match3 global position: %s" % global_position, "Match3"
)
DebugManager.log_debug("Match3 scale: %s" % scale, "Match3") DebugManager.log_debug("Match3 scale: %s" % scale, "Match3")
# Check if grid is properly initialized # Check if grid is properly initialized
if not grid or grid.size() == 0: if not grid or grid.size() == 0:
DebugManager.log_error("Grid not initialized when debug structure called", "Match3") DebugManager.log_error(
"Grid not initialized when debug structure called", "Match3"
)
return return
# Check tiles # Check tiles
@@ -593,23 +669,33 @@ func _debug_scene_structure() -> void:
if y < grid.size() and x < grid[y].size() and grid[y][x]: if y < grid.size() and x < grid[y].size() and grid[y][x]:
tile_count += 1 tile_count += 1
DebugManager.log_debug( DebugManager.log_debug(
"Created %d tiles out of %d expected" % [tile_count, grid_size.x * grid_size.y], "Match3" (
"Created %d tiles out of %d expected"
% [tile_count, grid_size.x * grid_size.y]
),
"Match3"
) )
# Check first tile in detail # Check first tile in detail
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
var first_tile = grid[0][0] var first_tile = grid[0][0]
DebugManager.log_debug( DebugManager.log_debug(
"First tile global position: %s" % first_tile.global_position, "Match3" "First tile global position: %s" % first_tile.global_position,
"Match3"
)
DebugManager.log_debug(
"First tile local position: %s" % first_tile.position, "Match3"
) )
DebugManager.log_debug("First tile local position: %s" % first_tile.position, "Match3")
# Check parent chain # Check parent chain
var current_node = self var current_node = self
var depth = 0 var depth = 0
while current_node and depth < 10: while current_node and depth < 10:
DebugManager.log_debug( DebugManager.log_debug(
"Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()], (
"Parent level %d: %s (type: %s)"
% [depth, current_node.name, current_node.get_class()]
),
"Match3" "Match3"
) )
current_node = current_node.get_parent() current_node = current_node.get_parent()
@@ -618,16 +704,27 @@ func _debug_scene_structure() -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
# Debug key to reset all visual states # Debug key to reset all visual states
if event.is_action_pressed("action_east") and DebugManager.is_debug_enabled(): if (
event.is_action_pressed("action_east")
and DebugManager.is_debug_enabled()
):
reset_all_visual_states() reset_all_visual_states()
return return
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: if (
current_state == GameState.SWAPPING
or current_state == GameState.PROCESSING
):
return return
# Handle action_east (B button/ESC) to deselect selected tile # Handle action_east (B button/ESC) to deselect selected tile
if event.is_action_pressed("action_east") and current_state == GameState.SELECTING: if (
DebugManager.log_debug("action_east pressed - deselecting current tile", "Match3") event.is_action_pressed("action_east")
and current_state == GameState.SELECTING
):
DebugManager.log_debug(
"action_east pressed - deselecting current tile", "Match3"
)
_deselect_tile() _deselect_tile()
return return
@@ -652,18 +749,23 @@ func _input(event: InputEvent) -> void:
func _move_cursor(direction: Vector2i) -> void: func _move_cursor(direction: Vector2i) -> void:
# Input validation for direction vector # Input validation for direction vector
if abs(direction.x) > 1 or abs(direction.y) > 1: if abs(direction.x) > 1 or abs(direction.y) > 1:
DebugManager.log_error("Invalid cursor direction vector: " + str(direction), "Match3") DebugManager.log_error(
"Invalid cursor direction vector: " + str(direction), "Match3"
)
return return
if direction.x != 0 and direction.y != 0: if direction.x != 0 and direction.y != 0:
DebugManager.log_error( DebugManager.log_error(
"Diagonal cursor movement not supported: " + str(direction), "Match3" "Diagonal cursor movement not supported: " + str(direction),
"Match3"
) )
return return
# Validate grid integrity before cursor operations # Validate grid integrity before cursor operations
if not _validate_grid_integrity(): if not _validate_grid_integrity():
DebugManager.log_error("Grid integrity check failed in cursor movement", "Match3") DebugManager.log_error(
"Grid integrity check failed in cursor movement", "Match3"
)
return return
var old_pos = cursor_position var old_pos = cursor_position
@@ -676,19 +778,30 @@ func _move_cursor(direction: Vector2i) -> void:
if new_pos != cursor_position: if new_pos != cursor_position:
# Safe access to old tile # Safe access to old tile
var old_tile = _safe_grid_access(old_pos) var old_tile = _safe_grid_access(old_pos)
if old_tile and "is_selected" in old_tile and "is_highlighted" in old_tile: if (
old_tile
and "is_selected" in old_tile
and "is_highlighted" in old_tile
):
if not old_tile.is_selected: if not old_tile.is_selected:
old_tile.is_highlighted = false old_tile.is_highlighted = false
DebugManager.log_debug( DebugManager.log_debug(
"Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], (
"Cursor moved from (%d,%d) to (%d,%d)"
% [old_pos.x, old_pos.y, new_pos.x, new_pos.y]
),
"Match3" "Match3"
) )
cursor_position = new_pos cursor_position = new_pos
# Safe access to new tile # Safe access to new tile
var new_tile = _safe_grid_access(cursor_position) var new_tile = _safe_grid_access(cursor_position)
if new_tile and "is_selected" in new_tile and "is_highlighted" in new_tile: if (
new_tile
and "is_selected" in new_tile
and "is_highlighted" in new_tile
):
if not new_tile.is_selected: if not new_tile.is_selected:
new_tile.is_highlighted = true new_tile.is_highlighted = true
@@ -708,13 +821,19 @@ func _select_tile_at_cursor() -> void:
var tile = _safe_grid_access(cursor_position) var tile = _safe_grid_access(cursor_position)
if tile: if tile:
DebugManager.log_debug( DebugManager.log_debug(
"Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], (
"Keyboard selection at cursor (%d,%d)"
% [cursor_position.x, cursor_position.y]
),
"Match3" "Match3"
) )
_on_tile_selected(tile) _on_tile_selected(tile)
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y], (
"No valid tile at cursor position (%d,%d)"
% [cursor_position.x, cursor_position.y]
),
"Match3" "Match3"
) )
@@ -728,9 +847,15 @@ func _on_tile_selected(tile: Node2D) -> void:
SELECTING -> SWAPPING: Different tile clicked (attempt swap) SELECTING -> SWAPPING: Different tile clicked (attempt swap)
""" """
# Block tile selection during busy states # Block tile selection during busy states
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING: if (
current_state == GameState.SWAPPING
or current_state == GameState.PROCESSING
):
DebugManager.log_debug( DebugManager.log_debug(
"Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]], (
"Tile selection ignored - game busy (state: %s)"
% [GameState.keys()[current_state]]
),
"Match3" "Match3"
) )
return return
@@ -773,7 +898,11 @@ func _select_tile(tile: Node2D) -> void:
tile.is_selected = true tile.is_selected = true
current_state = GameState.SELECTING # State transition: WAITING -> SELECTING current_state = GameState.SELECTING # State transition: WAITING -> SELECTING
DebugManager.log_debug( DebugManager.log_debug(
"Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3" (
"Selected tile at (%d, %d)"
% [tile.grid_position.x, tile.grid_position.y]
),
"Match3"
) )
@@ -784,12 +913,17 @@ func _deselect_tile() -> void:
DebugManager.log_debug( DebugManager.log_debug(
( (
"Deselecting tile at (%d,%d)" "Deselecting tile at (%d,%d)"
% [selected_tile.grid_position.x, selected_tile.grid_position.y] % [
selected_tile.grid_position.x,
selected_tile.grid_position.y
]
), ),
"Match3" "Match3"
) )
else: else:
DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3") DebugManager.log_debug(
"Deselecting tile (no grid position available)", "Match3"
)
# Safe property access for selection state # Safe property access for selection state
if "is_selected" in selected_tile: if "is_selected" in selected_tile:
@@ -833,7 +967,9 @@ func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool:
func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void: func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
if not _are_adjacent(tile1, tile2): if not _are_adjacent(tile1, tile2):
DebugManager.log_debug("Tiles are not adjacent, selecting new tile instead", "Match3") DebugManager.log_debug(
"Tiles are not adjacent, selecting new tile instead", "Match3"
)
_deselect_tile() _deselect_tile()
_select_tile(tile2) _select_tile(tile2)
return return
@@ -886,7 +1022,9 @@ func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void: func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
if not tile1 or not tile2: if not tile1 or not tile2:
DebugManager.log_error("Cannot swap tiles - one or both tiles are null", "Match3") DebugManager.log_error(
"Cannot swap tiles - one or both tiles are null", "Match3"
)
return return
# Update grid positions # Update grid positions
@@ -934,7 +1072,9 @@ func serialize_grid_state() -> Array:
) )
if grid.size() == 0: if grid.size() == 0:
DebugManager.log_error("Grid array is empty during serialization!", "Match3") DebugManager.log_error(
"Grid array is empty during serialization!", "Match3"
)
return [] return []
var serialized_grid = [] var serialized_grid = []
@@ -950,7 +1090,11 @@ func serialize_grid_state() -> Array:
# Only log first few for brevity # Only log first few for brevity
if valid_tiles <= 5: if valid_tiles <= 5:
DebugManager.log_debug( DebugManager.log_debug(
"Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3" (
"Serializing tile (%d,%d): type %d"
% [x, y, grid[y][x].tile_type]
),
"Match3"
) )
else: else:
row.append(-1) # Invalid/empty tile row.append(-1) # Invalid/empty tile
@@ -958,7 +1102,8 @@ func serialize_grid_state() -> Array:
# Only log first few nulls for brevity # Only log first few nulls for brevity
if null_tiles <= 5: if null_tiles <= 5:
DebugManager.log_debug( DebugManager.log_debug(
"Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3" "Serializing tile (%d,%d): NULL/empty (-1)" % [x, y],
"Match3"
) )
serialized_grid.append(row) serialized_grid.append(row)
@@ -1002,7 +1147,9 @@ func save_current_state():
func load_saved_state() -> bool: func load_saved_state() -> bool:
# Check if there's a saved grid state # Check if there's a saved grid state
if not SaveManager.has_saved_grid(): if not SaveManager.has_saved_grid():
DebugManager.log_info("No saved grid state found, using default generation", "Match3") DebugManager.log_info(
"No saved grid state found, using default generation", "Match3"
)
return false return false
var saved_state = SaveManager.get_saved_grid_state() var saved_state = SaveManager.get_saved_grid_state()
@@ -1015,13 +1162,22 @@ func load_saved_state() -> bool:
saved_gems.append(int(gem)) saved_gems.append(int(gem))
var saved_layout = saved_state.grid_layout var saved_layout = saved_state.grid_layout
DebugManager.log_info( (
DebugManager
. log_info(
( (
"[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d" "[%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()] % [
instance_id,
saved_size.x,
saved_size.y,
tile_types,
saved_layout.size()
]
), ),
"Match3" "Match3"
) )
)
# Debug: Print first few rows of loaded layout # Debug: Print first few rows of loaded layout
for y in range(min(3, saved_layout.size())): for y in range(min(3, saved_layout.size())):
@@ -1058,7 +1214,10 @@ func load_saved_state() -> bool:
# Recalculate layout if size changed # Recalculate layout if size changed
if old_size != saved_size: if old_size != saved_size:
DebugManager.log_info( DebugManager.log_info(
"Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size], (
"Grid size changed from %s to %s, recalculating layout"
% [old_size, saved_size]
),
"Match3" "Match3"
) )
_calculate_grid_layout() _calculate_grid_layout()
@@ -1069,7 +1228,9 @@ func load_saved_state() -> bool:
return true return true
func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> void: func _restore_grid_from_layout(
grid_layout: Array, active_gems: Array[int]
) -> void:
DebugManager.log_info( DebugManager.log_info(
( (
"[%s] Starting grid restoration: layout_size=%d, active_gems=%s" "[%s] Starting grid restoration: layout_size=%d, active_gems=%s"
@@ -1088,7 +1249,8 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> v
all_tile_children.append(child) all_tile_children.append(child)
DebugManager.log_debug( DebugManager.log_debug(
"Found %d existing tile children to remove" % all_tile_children.size(), "Match3" "Found %d existing tile children to remove" % all_tile_children.size(),
"Match3"
) )
# Remove all found tile children # Remove all found tile children
@@ -1133,31 +1295,47 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> v
if saved_tile_type >= 0 and saved_tile_type < tile_types: if saved_tile_type >= 0 and saved_tile_type < tile_types:
tile.tile_type = saved_tile_type tile.tile_type = saved_tile_type
DebugManager.log_debug( DebugManager.log_debug(
"✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3" (
"✓ Restored tile (%d,%d) with saved type %d"
% [x, y, saved_tile_type]
),
"Match3"
) )
else: else:
# Fallback for invalid tile types # Fallback for invalid tile types
tile.tile_type = randi() % tile_types tile.tile_type = randi() % tile_types
DebugManager.log_error( (
DebugManager
. log_error(
( (
"✗ Invalid saved tile type %d at (%d,%d), using random %d" "✗ Invalid saved tile type %d at (%d,%d), using random %d"
% [saved_tile_type, x, y, tile.tile_type] % [saved_tile_type, x, y, tile.tile_type]
), ),
"Match3" "Match3"
) )
)
# Connect tile signals # Connect tile signals
tile.tile_selected.connect(_on_tile_selected) tile.tile_selected.connect(_on_tile_selected)
grid[y].append(tile) grid[y].append(tile)
DebugManager.log_info( DebugManager.log_info(
"Completed grid restoration: %d tiles restored" % [grid_size.x * grid_size.y], "Match3" (
"Completed grid restoration: %d tiles restored"
% [grid_size.x * grid_size.y]
),
"Match3"
) )
# Safety and validation helper functions # Safety and validation helper functions
func _is_valid_grid_position(pos: Vector2i) -> bool: 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 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: func _validate_grid_integrity() -> bool:
@@ -1168,7 +1346,8 @@ func _validate_grid_integrity() -> bool:
if grid.size() != grid_size.y: if grid.size() != grid_size.y:
DebugManager.log_error( DebugManager.log_error(
"Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y], "Match3" "Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y],
"Match3"
) )
return false return false
@@ -1179,7 +1358,11 @@ func _validate_grid_integrity() -> bool:
if grid[y].size() != grid_size.x: if grid[y].size() != grid_size.x:
DebugManager.log_error( DebugManager.log_error(
"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), grid_size.x], "Match3" (
"Grid row %d width mismatch: %d vs %d"
% [y, grid[y].size(), grid_size.x]
),
"Match3"
) )
return false return false
@@ -1192,7 +1375,9 @@ func _safe_grid_access(pos: Vector2i) -> Node2D:
return null return null
if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): 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") DebugManager.log_warn(
"Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3"
)
return null return null
var tile = grid[pos.y][pos.x] var tile = grid[pos.y][pos.x]

View File

@@ -11,7 +11,9 @@ extends RefCounted
## var grid_pos = Match3InputHandler.get_grid_position_from_world(node, world_pos, offset, size) ## var grid_pos = Match3InputHandler.get_grid_position_from_world(node, world_pos, offset, size)
static func find_tile_at_position(grid: Array, grid_size: Vector2i, world_pos: Vector2) -> Node2D: static func find_tile_at_position(
grid: Array, grid_size: Vector2i, world_pos: Vector2
) -> Node2D:
## Find the tile that contains the world position. ## Find the tile that contains the world position.
## ##
## Iterates through all tiles and checks if the world position falls within ## Iterates through all tiles and checks if the world position falls within
@@ -31,7 +33,9 @@ static func find_tile_at_position(grid: Array, grid_size: Vector2i, world_pos: V
if tile and tile.has_node("Sprite2D"): if tile and tile.has_node("Sprite2D"):
var sprite = tile.get_node("Sprite2D") var sprite = tile.get_node("Sprite2D")
if sprite and sprite.texture: if sprite and sprite.texture:
var sprite_bounds = get_sprite_world_bounds(tile, sprite) var sprite_bounds = get_sprite_world_bounds(
tile, sprite
)
if is_point_inside_rect(world_pos, sprite_bounds): if is_point_inside_rect(world_pos, sprite_bounds):
return tile return tile
return null return null

View File

@@ -51,7 +51,9 @@ static func serialize_grid_state(grid: Array, grid_size: Vector2i) -> Array:
return serialized_grid return serialized_grid
static func get_active_gem_types_from_grid(grid: Array, tile_types: int) -> Array: static func get_active_gem_types_from_grid(
grid: Array, tile_types: int
) -> Array:
# Get active gem types from the first available tile # Get active gem types from the first available tile
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]: if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
return grid[0][0].active_gem_types.duplicate() return grid[0][0].active_gem_types.duplicate()
@@ -132,9 +134,15 @@ static func restore_grid_from_layout(
tile.tile_type = randi() % tile_types tile.tile_type = randi() % tile_types
# Connect tile signals # Connect tile signals
if tile.has_signal("tile_selected") and match3_node.has_method("_on_tile_selected"): if (
tile.has_signal("tile_selected")
and match3_node.has_method("_on_tile_selected")
):
tile.tile_selected.connect(match3_node._on_tile_selected) tile.tile_selected.connect(match3_node._on_tile_selected)
if tile.has_signal("tile_hovered") and match3_node.has_method("_on_tile_hovered"): if (
tile.has_signal("tile_hovered")
and match3_node.has_method("_on_tile_hovered")
):
tile.tile_hovered.connect(match3_node._on_tile_hovered) tile.tile_hovered.connect(match3_node._on_tile_hovered)
tile.tile_unhovered.connect(match3_node._on_tile_unhovered) tile.tile_unhovered.connect(match3_node._on_tile_unhovered)

View File

@@ -25,7 +25,12 @@ static func is_valid_grid_position(pos: Vector2i, grid_size: Vector2i) -> bool:
## ##
## Returns: ## Returns:
## bool: True if position is valid, False if out of bounds ## bool: True if position is valid, False if out of bounds
return pos.x >= 0 and pos.y >= 0 and pos.x < grid_size.x and pos.y < grid_size.y return (
pos.x >= 0
and pos.y >= 0
and pos.x < grid_size.x
and pos.y < grid_size.y
)
static func validate_grid_integrity(grid: Array, grid_size: Vector2i) -> bool: static func validate_grid_integrity(grid: Array, grid_size: Vector2i) -> bool:
@@ -46,7 +51,8 @@ static func validate_grid_integrity(grid: Array, grid_size: Vector2i) -> bool:
if grid.size() != grid_size.y: if grid.size() != grid_size.y:
DebugManager.log_error( DebugManager.log_error(
"Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y], "Match3" "Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y],
"Match3"
) )
return false return false
@@ -57,20 +63,28 @@ static func validate_grid_integrity(grid: Array, grid_size: Vector2i) -> bool:
if grid[y].size() != grid_size.x: if grid[y].size() != grid_size.x:
DebugManager.log_error( DebugManager.log_error(
"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), grid_size.x], "Match3" (
"Grid row %d width mismatch: %d vs %d"
% [y, grid[y].size(), grid_size.x]
),
"Match3"
) )
return false return false
return true return true
static func safe_grid_access(grid: Array, pos: Vector2i, grid_size: Vector2i) -> Node2D: static func safe_grid_access(
grid: Array, pos: Vector2i, grid_size: Vector2i
) -> Node2D:
# Safe grid access with comprehensive bounds checking # Safe grid access with comprehensive bounds checking
if not is_valid_grid_position(pos, grid_size): if not is_valid_grid_position(pos, grid_size):
return null return null
if pos.y >= grid.size() or pos.x >= grid[pos.y].size(): 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") DebugManager.log_warn(
"Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3"
)
return null return null
var tile = grid[pos.y][pos.x] var tile = grid[pos.y][pos.x]

View File

@@ -123,7 +123,9 @@ func remove_gem_type(gem_index: int) -> bool:
return false return false
if active_gem_types.size() <= 2: # Keep at least 2 gem types if active_gem_types.size() <= 2: # Keep at least 2 gem types
DebugManager.log_warn("Cannot remove gem type - minimum 2 types required", "Tile") DebugManager.log_warn(
"Cannot remove gem type - minimum 2 types required", "Tile"
)
return false return false
active_gem_types.erase(gem_index) active_gem_types.erase(gem_index)
@@ -178,7 +180,12 @@ func _update_visual_feedback() -> void:
DebugManager.log_debug( DebugManager.log_debug(
( (
"SELECTING tile (%d,%d): target scale %.2fx, current scale %s" "SELECTING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale] % [
grid_position.x,
grid_position.y,
scale_multiplier,
sprite.scale
]
), ),
"Match3" "Match3"
) )
@@ -186,13 +193,21 @@ func _update_visual_feedback() -> void:
# Highlighted: subtle glow and larger than original board size # Highlighted: subtle glow and larger than original board size
target_modulate = Color(1.1, 1.1, 1.1, 1.0) target_modulate = Color(1.1, 1.1, 1.1, 1.0)
scale_multiplier = UIConstants.TILE_HIGHLIGHTED_SCALE scale_multiplier = UIConstants.TILE_HIGHLIGHTED_SCALE
DebugManager.log_debug( (
DebugManager
. log_debug(
( (
"HIGHLIGHTING tile (%d,%d): target scale %.2fx, current scale %s" "HIGHLIGHTING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale] % [
grid_position.x,
grid_position.y,
scale_multiplier,
sprite.scale
]
), ),
"Match3" "Match3"
) )
)
else: else:
# Normal state: white and original board size # Normal state: white and original board size
target_modulate = Color.WHITE target_modulate = Color.WHITE
@@ -200,7 +215,12 @@ func _update_visual_feedback() -> void:
DebugManager.log_debug( DebugManager.log_debug(
( (
"NORMALIZING tile (%d,%d): target scale %.2fx, current scale %s" "NORMALIZING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale] % [
grid_position.x,
grid_position.y,
scale_multiplier,
sprite.scale
]
), ),
"Match3" "Match3"
) )
@@ -228,7 +248,11 @@ func _update_visual_feedback() -> void:
tween.tween_callback(_on_scale_animation_completed.bind(target_scale)) tween.tween_callback(_on_scale_animation_completed.bind(target_scale))
else: else:
DebugManager.log_debug( DebugManager.log_debug(
"No scale change needed for tile (%d,%d)" % [grid_position.x, grid_position.y], "Match3" (
"No scale change needed for tile (%d,%d)"
% [grid_position.x, grid_position.y]
),
"Match3"
) )
@@ -264,7 +288,9 @@ func _input(event: InputEvent) -> void:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
# Check if the mouse click is within the tile's bounds # Check if the mouse click is within the tile's bounds
var local_position = to_local(get_global_mouse_position()) var local_position = to_local(get_global_mouse_position())
var sprite_rect = Rect2(-TILE_SIZE / 2.0, -TILE_SIZE / 2.0, TILE_SIZE, TILE_SIZE) var sprite_rect = Rect2(
-TILE_SIZE / 2.0, -TILE_SIZE / 2.0, TILE_SIZE, TILE_SIZE
)
if sprite_rect.has_point(local_position): if sprite_rect.has_point(local_position):
tile_selected.emit(self) tile_selected.emit(self)

View File

@@ -22,7 +22,9 @@ func _setup_splash_screen_connection() -> void:
# Try to find SplashScreen node # Try to find SplashScreen node
splash_screen = get_node_or_null("SplashScreen") splash_screen = get_node_or_null("SplashScreen")
if not splash_screen: if not splash_screen:
DebugManager.log_warn("SplashScreen node not found, trying alternative methods", "Main") DebugManager.log_warn(
"SplashScreen node not found, trying alternative methods", "Main"
)
# Try to find by class or group # Try to find by class or group
var splash_nodes = get_tree().get_nodes_in_group("localizable") var splash_nodes = get_tree().get_nodes_in_group("localizable")
for node in splash_nodes: for node in splash_nodes:
@@ -31,11 +33,15 @@ func _setup_splash_screen_connection() -> void:
break break
if splash_screen: if splash_screen:
DebugManager.log_debug("SplashScreen node found: %s" % splash_screen.name, "Main") DebugManager.log_debug(
"SplashScreen node found: %s" % splash_screen.name, "Main"
)
# Try connecting to the signal if it exists # Try connecting to the signal if it exists
if splash_screen.has_signal("confirm_pressed"): if splash_screen.has_signal("confirm_pressed"):
splash_screen.confirm_pressed.connect(_on_confirm_pressed) splash_screen.confirm_pressed.connect(_on_confirm_pressed)
DebugManager.log_debug("Connected to confirm_pressed signal", "Main") DebugManager.log_debug(
"Connected to confirm_pressed signal", "Main"
)
else: else:
# Fallback: use input handling directly on the main scene # Fallback: use input handling directly on the main scene
DebugManager.log_warn("Using fallback input handling", "Main") DebugManager.log_warn("Using fallback input handling", "Main")

View File

@@ -9,8 +9,10 @@ const YAML_SOURCES: Array[String] = [
# "res://assets/sprites/sprite-sources.yaml", # "res://assets/sprites/sprite-sources.yaml",
] ]
@onready var scroll_container: ScrollContainer = $MarginContainer/VBoxContainer/ScrollContainer @onready
@onready var credits_text: RichTextLabel = $MarginContainer/VBoxContainer/ScrollContainer/CreditsText var scroll_container: ScrollContainer = $MarginContainer/VBoxContainer/ScrollContainer
@onready
var credits_text: RichTextLabel = $MarginContainer/VBoxContainer/ScrollContainer/CreditsText
@onready var back_button: Button = $MarginContainer/VBoxContainer/BackButton @onready var back_button: Button = $MarginContainer/VBoxContainer/BackButton
@@ -40,7 +42,9 @@ func _load_yaml_file(yaml_path: String) -> Dictionary:
var file := FileAccess.open(yaml_path, FileAccess.READ) var file := FileAccess.open(yaml_path, FileAccess.READ)
if not file: if not file:
DebugManager.log_warn("Could not open YAML file: %s" % yaml_path, "Credits") DebugManager.log_warn(
"Could not open YAML file: %s" % yaml_path, "Credits"
)
return {} return {}
var content: String = file.get_as_text() var content: String = file.get_as_text()
@@ -67,10 +71,18 @@ func _parse_yaml_content(yaml_content: String) -> Dictionary:
continue continue
# Top-level section (audio, sprites, textures, etc.) # Top-level section (audio, sprites, textures, etc.)
if not line.begins_with(" ") and not line.begins_with("\t") and trimmed.ends_with(":"): if (
not line.begins_with(" ")
and not line.begins_with("\t")
and trimmed.ends_with(":")
):
if current_asset and not current_asset_data.is_empty(): if current_asset and not current_asset_data.is_empty():
_store_asset_data( _store_asset_data(
result, current_section, current_subsection, current_asset, current_asset_data result,
current_section,
current_subsection,
current_asset,
current_asset_data
) )
current_section = trimmed.trim_suffix(":") current_section = trimmed.trim_suffix(":")
current_subsection = "" current_subsection = ""
@@ -80,22 +92,37 @@ func _parse_yaml_content(yaml_content: String) -> Dictionary:
result[current_section] = {} result[current_section] = {}
# Subsection (music, sfx, characters, etc.) # Subsection (music, sfx, characters, etc.)
elif line.begins_with(" ") and not line.begins_with(" ") and trimmed.ends_with(":"): elif (
line.begins_with(" ")
and not line.begins_with(" ")
and trimmed.ends_with(":")
):
if current_asset and not current_asset_data.is_empty(): if current_asset and not current_asset_data.is_empty():
_store_asset_data( _store_asset_data(
result, current_section, current_subsection, current_asset, current_asset_data result,
current_section,
current_subsection,
current_asset,
current_asset_data
) )
current_subsection = trimmed.trim_suffix(":") current_subsection = trimmed.trim_suffix(":")
current_asset = "" current_asset = ""
current_asset_data = {} current_asset_data = {}
if current_section and not result[current_section].has(current_subsection): if (
current_section
and not result[current_section].has(current_subsection)
):
result[current_section][current_subsection] = {} result[current_section][current_subsection] = {}
# Asset name # Asset name
elif trimmed.begins_with('"') and trimmed.contains('":'): elif trimmed.begins_with('"') and trimmed.contains('":'):
if current_asset and not current_asset_data.is_empty(): if current_asset and not current_asset_data.is_empty():
_store_asset_data( _store_asset_data(
result, current_section, current_subsection, current_asset, current_asset_data result,
current_section,
current_subsection,
current_asset,
current_asset_data
) )
var parts: Array = trimmed.split('"') var parts: Array = trimmed.split('"')
current_asset = parts[1] if parts.size() > 1 else "" current_asset = parts[1] if parts.size() > 1 else ""
@@ -106,21 +133,31 @@ func _parse_yaml_content(yaml_content: String) -> Dictionary:
var parts: Array = trimmed.split(":", false, 1) var parts: Array = trimmed.split(":", false, 1)
if parts.size() == 2: if parts.size() == 2:
var key: String = parts[0].strip_edges() var key: String = parts[0].strip_edges()
var value: String = parts[1].strip_edges().trim_prefix('"').trim_suffix('"') var value: String = (
parts[1].strip_edges().trim_prefix('"').trim_suffix('"')
)
if value and value != '""': if value and value != '""':
current_asset_data[key] = value current_asset_data[key] = value
# Store last asset # Store last asset
if current_asset and not current_asset_data.is_empty(): if current_asset and not current_asset_data.is_empty():
_store_asset_data( _store_asset_data(
result, current_section, current_subsection, current_asset, current_asset_data result,
current_section,
current_subsection,
current_asset,
current_asset_data
) )
return result return result
func _store_asset_data( func _store_asset_data(
result: Dictionary, section: String, subsection: String, asset: String, data: Dictionary result: Dictionary,
section: String,
subsection: String,
asset: String,
data: Dictionary
) -> void: ) -> void:
"""Store parsed asset data into result dictionary""" """Store parsed asset data into result dictionary"""
if not section or not asset: if not section or not asset:
@@ -150,7 +187,9 @@ func _merge_credits_data(target: Dictionary, source: Dictionary) -> void:
func _display_formatted_credits(credits_data: Dictionary) -> void: func _display_formatted_credits(credits_data: Dictionary) -> void:
"""Generate BBCode formatted credits from parsed data""" """Generate BBCode formatted credits from parsed data"""
if not credits_text: if not credits_text:
DebugManager.log_error("Credits text node is null, cannot display credits", "Credits") DebugManager.log_error(
"Credits text node is null, cannot display credits", "Credits"
)
return return
var credits_bbcode: String = "[center][b][font_size=32]CREDITS[/font_size][/b][/center]\n\n" var credits_bbcode: String = "[center][b][font_size=32]CREDITS[/font_size][/b][/center]\n\n"
@@ -227,11 +266,17 @@ func _on_back_button_pressed() -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_back") or event.is_action_pressed("action_east"): if (
event.is_action_pressed("ui_back")
or event.is_action_pressed("action_east")
):
_on_back_button_pressed() _on_back_button_pressed()
elif event.is_action_pressed("move_up") or event.is_action_pressed("ui_up"): elif event.is_action_pressed("move_up") or event.is_action_pressed("ui_up"):
_scroll_credits(-50.0) _scroll_credits(-50.0)
elif event.is_action_pressed("move_down") or event.is_action_pressed("ui_down"): elif (
event.is_action_pressed("move_down")
or event.is_action_pressed("ui_down")
):
_scroll_credits(50.0) _scroll_credits(50.0)
@@ -239,4 +284,6 @@ func _scroll_credits(amount: float) -> void:
"""Scroll the credits by the specified amount""" """Scroll the credits by the specified amount"""
var current_scroll: float = scroll_container.scroll_vertical var current_scroll: float = scroll_container.scroll_vertical
scroll_container.scroll_vertical = int(current_scroll + amount) scroll_container.scroll_vertical = int(current_scroll + amount)
DebugManager.log_debug("Scrolled credits to: %d" % scroll_container.scroll_vertical, "Credits") DebugManager.log_debug(
"Scrolled credits to: %d" % scroll_container.scroll_vertical, "Credits"
)

View File

@@ -17,12 +17,16 @@ func _find_target_scene():
# Fallback: search by common node names # Fallback: search by common node names
if not match3_scene: if not match3_scene:
for possible_name in ["Match3", "match3", "Match3Game"]: for possible_name in ["Match3", "match3", "Match3Game"]:
match3_scene = current_scene.find_child(possible_name, true, false) match3_scene = current_scene.find_child(
possible_name, true, false
)
if match3_scene: if match3_scene:
break break
if match3_scene: if match3_scene:
DebugManager.log_debug("Found match3 scene: " + match3_scene.name, log_category) DebugManager.log_debug(
"Found match3 scene: " + match3_scene.name, log_category
)
_update_ui_from_scene() _update_ui_from_scene()
_stop_search_timer() _stop_search_timer()
else: else:

View File

@@ -8,7 +8,8 @@ const MIN_GRID_SIZE := 3
const MIN_TILE_TYPES := 3 const MIN_TILE_TYPES := 3
const SCENE_SEARCH_COOLDOWN := 0.5 const SCENE_SEARCH_COOLDOWN := 0.5
@export var target_script_path: String = "res://scenes/game/gameplays/Match3Gameplay.gd" @export
var target_script_path: String = "res://scenes/game/gameplays/Match3Gameplay.gd"
@export var log_category: String = "DebugMenu" @export var log_category: String = "DebugMenu"
var match3_scene: Node2D var match3_scene: Node2D
@@ -16,8 +17,10 @@ var search_timer: Timer
var last_scene_search_time: float = 0.0 var last_scene_search_time: float = 0.0
@onready var regenerate_button: Button = $VBoxContainer/RegenerateButton @onready var regenerate_button: Button = $VBoxContainer/RegenerateButton
@onready var gem_types_spinbox: SpinBox = $VBoxContainer/GemTypesContainer/GemTypesSpinBox @onready
@onready var gem_types_label: Label = $VBoxContainer/GemTypesContainer/GemTypesLabel var gem_types_spinbox: SpinBox = $VBoxContainer/GemTypesContainer/GemTypesSpinBox
@onready
var gem_types_label: Label = $VBoxContainer/GemTypesContainer/GemTypesLabel
@onready @onready
var grid_width_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthSpinBox var grid_width_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthSpinBox
@onready @onready
@@ -86,7 +89,9 @@ func _setup_scene_finding() -> void:
# Virtual method - override in derived classes for specific finding logic # Virtual method - override in derived classes for specific finding logic
func _find_target_scene() -> void: func _find_target_scene() -> void:
DebugManager.log_error("_find_target_scene() not implemented in derived class", log_category) DebugManager.log_error(
"_find_target_scene() not implemented in derived class", log_category
)
func _find_node_by_script(node: Node, script_path: String) -> Node: func _find_node_by_script(node: Node, script_path: String) -> Node:
@@ -114,10 +119,14 @@ func _update_ui_from_scene() -> void:
# Connect to grid state loaded signal if not already connected # Connect to grid state loaded signal if not already connected
if ( if (
match3_scene.has_signal("grid_state_loaded") match3_scene.has_signal("grid_state_loaded")
and not match3_scene.grid_state_loaded.is_connected(_on_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) match3_scene.grid_state_loaded.connect(_on_grid_state_loaded)
DebugManager.log_debug("Connected to grid_state_loaded signal", log_category) 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:
@@ -135,7 +144,10 @@ func _update_ui_from_scene() -> void:
func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int) -> void: func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int) -> void:
DebugManager.log_debug( DebugManager.log_debug(
"Grid state loaded signal received: size=%s, types=%d" % [grid_size, tile_types], (
"Grid state loaded signal received: size=%s, types=%d"
% [grid_size, tile_types]
),
log_category log_category
) )
@@ -155,7 +167,10 @@ func _stop_search_timer() -> void:
func _start_search_timer() -> void: func _start_search_timer() -> void:
if search_timer and not search_timer.timeout.is_connected(_find_target_scene): if (
search_timer
and not search_timer.timeout.is_connected(_find_target_scene)
):
search_timer.timeout.connect(_find_target_scene) search_timer.timeout.connect(_find_target_scene)
search_timer.start() search_timer.start()
@@ -176,7 +191,8 @@ func _refresh_current_values() -> void:
# Refresh UI with current values from the scene # Refresh UI with current values from the scene
if match3_scene: if match3_scene:
DebugManager.log_debug( DebugManager.log_debug(
"Refreshing debug menu values from current scene state", log_category "Refreshing debug menu values from current scene state",
log_category
) )
_update_ui_from_scene() _update_ui_from_scene()
@@ -186,14 +202,18 @@ func _on_regenerate_pressed() -> void:
_find_target_scene() _find_target_scene()
if not match3_scene: if not match3_scene:
DebugManager.log_error("Could not find target scene for regeneration", log_category) DebugManager.log_error(
"Could not find target scene for regeneration", log_category
)
return return
if match3_scene.has_method("regenerate_grid"): if match3_scene.has_method("regenerate_grid"):
DebugManager.log_debug("Calling regenerate_grid()", log_category) DebugManager.log_debug("Calling regenerate_grid()", log_category)
await match3_scene.regenerate_grid() await match3_scene.regenerate_grid()
else: else:
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) -> void: func _on_gem_types_changed(value: float) -> void:
@@ -207,7 +227,9 @@ func _on_gem_types_changed(value: float) -> void:
last_scene_search_time = current_time 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 = int(value) var new_value: int = int(value)
@@ -221,15 +243,21 @@ func _on_gem_types_changed(value: float) -> void:
log_category log_category
) )
# Reset to valid value # Reset to valid value
gem_types_spinbox.value = clamp(new_value, MIN_TILE_TYPES, MAX_TILE_TYPES) 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"):
DebugManager.log_debug("Setting tile types to " + str(new_value), log_category) DebugManager.log_debug(
"Setting tile types to " + str(new_value), log_category
)
await match3_scene.set_tile_types(new_value) await match3_scene.set_tile_types(new_value)
gem_types_label.text = "Gem Types: " + str(new_value) gem_types_label.text = "Gem Types: " + str(new_value)
else: else:
DebugManager.log_error("Target scene does not have set_tile_types method", log_category) DebugManager.log_error(
"Target scene does not have set_tile_types method", log_category
)
# Fallback: try to set TILE_TYPES directly # Fallback: try to set TILE_TYPES directly
if "TILE_TYPES" in match3_scene: if "TILE_TYPES" in match3_scene:
match3_scene.TILE_TYPES = new_value match3_scene.TILE_TYPES = new_value
@@ -247,7 +275,9 @@ func _on_grid_width_changed(value: float) -> void:
last_scene_search_time = current_time 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 = int(value) var new_width: int = int(value)
@@ -261,7 +291,9 @@ func _on_grid_width_changed(value: float) -> void:
log_category log_category
) )
# Reset to valid value # Reset to valid value
grid_width_spinbox.value = clamp(new_width, MIN_GRID_SIZE, MAX_GRID_SIZE) 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)
@@ -271,11 +303,19 @@ func _on_grid_width_changed(value: float) -> void:
if match3_scene.has_method("set_grid_size"): if match3_scene.has_method("set_grid_size"):
DebugManager.log_debug( DebugManager.log_debug(
"Setting grid size to " + str(new_width) + "x" + str(current_height), log_category (
"Setting grid size to "
+ str(new_width)
+ "x"
+ str(current_height)
),
log_category
) )
await match3_scene.set_grid_size(Vector2i(new_width, current_height)) await match3_scene.set_grid_size(Vector2i(new_width, current_height))
else: else:
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) -> void: func _on_grid_height_changed(value: float) -> void:
@@ -289,7 +329,9 @@ func _on_grid_height_changed(value: float) -> void:
last_scene_search_time = current_time 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 = int(value) var new_height: int = int(value)
@@ -303,7 +345,9 @@ func _on_grid_height_changed(value: float) -> void:
log_category log_category
) )
# Reset to valid value # Reset to valid value
grid_height_spinbox.value = clamp(new_height, MIN_GRID_SIZE, MAX_GRID_SIZE) 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)
@@ -313,8 +357,16 @@ func _on_grid_height_changed(value: float) -> void:
if match3_scene.has_method("set_grid_size"): if match3_scene.has_method("set_grid_size"):
DebugManager.log_debug( DebugManager.log_debug(
"Setting grid size to " + str(current_width) + "x" + str(new_height), log_category (
"Setting grid size to "
+ str(current_width)
+ "x"
+ str(new_height)
),
log_category
) )
await match3_scene.set_grid_size(Vector2i(current_width, new_height)) await match3_scene.set_grid_size(Vector2i(current_width, new_height))
else: else:
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
)

View File

@@ -81,13 +81,17 @@ func _navigate_menu(direction: int) -> void:
if current_menu_index < 0: if current_menu_index < 0:
current_menu_index = menu_buttons.size() - 1 current_menu_index = menu_buttons.size() - 1
_update_visual_selection() _update_visual_selection()
DebugManager.log_info("Menu navigation: index " + str(current_menu_index), "MainMenu") DebugManager.log_info(
"Menu navigation: index " + str(current_menu_index), "MainMenu"
)
func _activate_current_button() -> void: func _activate_current_button() -> void:
if current_menu_index >= 0 and current_menu_index < menu_buttons.size(): if current_menu_index >= 0 and current_menu_index < menu_buttons.size():
var button: Button = menu_buttons[current_menu_index] var button: Button = menu_buttons[current_menu_index]
DebugManager.log_info("Activating button via keyboard/gamepad: " + button.text, "MainMenu") DebugManager.log_info(
"Activating button via keyboard/gamepad: " + button.text, "MainMenu"
)
button.pressed.emit() button.pressed.emit()
@@ -95,7 +99,9 @@ func _update_visual_selection() -> void:
for i in range(menu_buttons.size()): for i in range(menu_buttons.size()):
var button: Button = menu_buttons[i] var button: Button = menu_buttons[i]
if i == current_menu_index: if i == current_menu_index:
button.scale = original_button_scales[i] * UIConstants.BUTTON_HOVER_SCALE button.scale = (
original_button_scales[i] * UIConstants.BUTTON_HOVER_SCALE
)
button.modulate = Color(1.2, 1.2, 1.0) button.modulate = Color(1.2, 1.2, 1.0)
else: else:
button.scale = original_button_scales[i] button.scale = original_button_scales[i]

View File

@@ -14,10 +14,13 @@ var current_control_index: int = 0
var original_control_scales: Array[Vector2] = [] var original_control_scales: Array[Vector2] = []
var original_control_modulates: Array[Color] = [] var original_control_modulates: Array[Color] = []
@onready var master_slider = $SettingsContainer/MasterVolumeContainer/MasterVolumeSlider @onready
@onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider var master_slider = $SettingsContainer/MasterVolumeContainer/MasterVolumeSlider
@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 @onready var reset_progress_button = $ResetSettingsContainer/ResetProgressButton
@@ -26,11 +29,15 @@ func _ready() -> void:
DebugManager.log_info("SettingsMenu ready", "Settings") DebugManager.log_info("SettingsMenu ready", "Settings")
# Language selector is initialized automatically # Language selector is initialized automatically
var master_callback: Callable = _on_volume_slider_changed.bind("master_volume") var master_callback: Callable = _on_volume_slider_changed.bind(
"master_volume"
)
if not master_slider.value_changed.is_connected(master_callback): if not master_slider.value_changed.is_connected(master_callback):
master_slider.value_changed.connect(master_callback) master_slider.value_changed.connect(master_callback)
var music_callback: Callable = _on_volume_slider_changed.bind("music_volume") var music_callback: Callable = _on_volume_slider_changed.bind(
"music_volume"
)
if not music_slider.value_changed.is_connected(music_callback): if not music_slider.value_changed.is_connected(music_callback):
music_slider.value_changed.connect(music_callback) music_slider.value_changed.connect(music_callback)
@@ -57,20 +64,28 @@ func _update_controls_from_settings() -> void:
func _on_volume_slider_changed(value: float, setting_key: String) -> void: func _on_volume_slider_changed(value: float, setting_key: String) -> void:
# Input validation for volume settings # Input validation for volume settings
if not setting_key in ["master_volume", "music_volume", "sfx_volume"]: if not setting_key in ["master_volume", "music_volume", "sfx_volume"]:
DebugManager.log_error("Invalid volume setting key: " + str(setting_key), "Settings") DebugManager.log_error(
"Invalid volume setting key: " + str(setting_key), "Settings"
)
return return
if typeof(value) != TYPE_FLOAT and typeof(value) != TYPE_INT: if typeof(value) != TYPE_FLOAT and typeof(value) != TYPE_INT:
DebugManager.log_error("Invalid volume value type: " + str(typeof(value)), "Settings") DebugManager.log_error(
"Invalid volume value type: " + str(typeof(value)), "Settings"
)
return return
# Clamp value to valid range # Clamp value to valid range
var clamped_value: float = clamp(float(value), 0.0, 1.0) var clamped_value: float = clamp(float(value), 0.0, 1.0)
if clamped_value != value: if clamped_value != value:
DebugManager.log_warn("Volume value %f clamped to %f" % [value, clamped_value], "Settings") DebugManager.log_warn(
"Volume value %f clamped to %f" % [value, clamped_value], "Settings"
)
if not settings_manager.set_setting(setting_key, clamped_value): if not settings_manager.set_setting(setting_key, clamped_value):
DebugManager.log_error("Failed to set volume setting: " + setting_key, "Settings") DebugManager.log_error(
"Failed to set volume setting: " + setting_key, "Settings"
)
func _exit_settings() -> void: func _exit_settings() -> void:
@@ -80,8 +95,13 @@ func _exit_settings() -> void:
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
if event.is_action_pressed("action_east") or event.is_action_pressed("pause_menu"): if (
DebugManager.log_debug("Cancel/back action pressed in settings", "Settings") event.is_action_pressed("action_east")
or event.is_action_pressed("pause_menu")
):
DebugManager.log_debug(
"Cancel/back action pressed in settings", "Settings"
)
_exit_settings() _exit_settings()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
@@ -116,8 +136,12 @@ func _on_back_button_pressed() -> void:
func update_text() -> void: func update_text() -> void:
$SettingsContainer/SettingsTitle.text = tr("settings_title") $SettingsContainer/SettingsTitle.text = tr("settings_title")
$SettingsContainer/MasterVolumeContainer/MasterVolume.text = tr("master_volume") $SettingsContainer/MasterVolumeContainer/MasterVolume.text = tr(
$SettingsContainer/MusicVolumeContainer/MusicVolume.text = tr("music_volume") "master_volume"
)
$SettingsContainer/MusicVolumeContainer/MusicVolume.text = tr(
"music_volume"
)
$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")
@@ -129,7 +153,9 @@ func _on_reset_setting_button_pressed() -> void:
DebugManager.log_info("Resetting settings", "Settings") DebugManager.log_info("Resetting settings", "Settings")
settings_manager.reset_settings_to_defaults() settings_manager.reset_settings_to_defaults()
_update_controls_from_settings() _update_controls_from_settings()
localization_manager.change_language(settings_manager.get_setting("language")) localization_manager.change_language(
settings_manager.get_setting("language")
)
func _setup_navigation_system() -> void: func _setup_navigation_system() -> void:
@@ -156,15 +182,22 @@ func _setup_navigation_system() -> void:
func _navigate_controls(direction: int) -> void: func _navigate_controls(direction: int) -> void:
AudioManager.play_ui_click() AudioManager.play_ui_click()
current_control_index = (current_control_index + direction) % navigable_controls.size() current_control_index = (
(current_control_index + direction) % navigable_controls.size()
)
if current_control_index < 0: if current_control_index < 0:
current_control_index = navigable_controls.size() - 1 current_control_index = navigable_controls.size() - 1
_update_visual_selection() _update_visual_selection()
DebugManager.log_info("Settings navigation: index " + str(current_control_index), "Settings") DebugManager.log_info(
"Settings navigation: index " + str(current_control_index), "Settings"
)
func _adjust_current_control(direction: int) -> void: func _adjust_current_control(direction: int) -> void:
if current_control_index < 0 or current_control_index >= navigable_controls.size(): if (
current_control_index < 0
or current_control_index >= navigable_controls.size()
):
return return
var control: Control = navigable_controls[current_control_index] var control: Control = navigable_controls[current_control_index]
@@ -178,17 +211,26 @@ func _adjust_current_control(direction: int) -> void:
slider.value = new_value slider.value = new_value
AudioManager.play_ui_click() AudioManager.play_ui_click()
DebugManager.log_info( DebugManager.log_info(
"Slider adjusted: %s = %f" % [_get_control_name(control), new_value], "Settings" (
"Slider adjusted: %s = %f"
% [_get_control_name(control), new_value]
),
"Settings"
) )
# Handle language stepper with left/right # Handle language stepper with left/right
elif control == language_stepper: elif control == language_stepper:
if language_stepper.handle_input_action("move_left" if direction == -1 else "move_right"): if language_stepper.handle_input_action(
"move_left" if direction == -1 else "move_right"
):
AudioManager.play_ui_click() AudioManager.play_ui_click()
func _activate_current_control() -> void: func _activate_current_control() -> void:
if current_control_index < 0 or current_control_index >= navigable_controls.size(): if (
current_control_index < 0
or current_control_index >= navigable_controls.size()
):
return return
var control: Control = navigable_controls[current_control_index] var control: Control = navigable_controls[current_control_index]
@@ -196,12 +238,17 @@ func _activate_current_control() -> void:
# Handle buttons # Handle buttons
if control is Button: if control is Button:
AudioManager.play_ui_click() AudioManager.play_ui_click()
DebugManager.log_info("Activating button via keyboard/gamepad: " + control.text, "Settings") DebugManager.log_info(
"Activating button via keyboard/gamepad: " + control.text,
"Settings"
)
control.pressed.emit() control.pressed.emit()
# Handle language stepper (no action needed on activation, left/right handles it) # Handle language stepper (no action needed on activation, left/right handles it)
elif control == language_stepper: elif control == language_stepper:
DebugManager.log_info("Language stepper selected - use left/right to change", "Settings") DebugManager.log_info(
"Language stepper selected - use left/right to change", "Settings"
)
func _update_visual_selection() -> void: func _update_visual_selection() -> void:
@@ -212,7 +259,10 @@ func _update_visual_selection() -> void:
if control == language_stepper: if control == language_stepper:
language_stepper.set_highlighted(true) language_stepper.set_highlighted(true)
else: else:
control.scale = original_control_scales[i] * UIConstants.UI_CONTROL_HIGHLIGHT_SCALE control.scale = (
original_control_scales[i]
* UIConstants.UI_CONTROL_HIGHLIGHT_SCALE
)
control.modulate = Color(1.1, 1.1, 0.9) control.modulate = Color(1.1, 1.1, 0.9)
else: else:
# Reset highlighting # Reset highlighting
@@ -235,9 +285,17 @@ func _get_control_name(control: Control) -> String:
return "button" return "button"
func _on_language_stepper_value_changed(new_value: String, new_index: float) -> void: func _on_language_stepper_value_changed(
new_value: String, new_index: float
) -> void:
DebugManager.log_info( DebugManager.log_info(
"Language changed via ValueStepper: " + new_value + " (index: " + str(int(new_index)) + ")", (
"Language changed via ValueStepper: "
+ new_value
+ " (index: "
+ str(int(new_index))
+ ")"
),
"Settings" "Settings"
) )

View File

@@ -30,17 +30,25 @@ var is_highlighted: bool = false
func _ready() -> void: func _ready() -> void:
DebugManager.log_info("ValueStepper ready for: " + data_source, "ValueStepper") DebugManager.log_info(
"ValueStepper ready for: " + data_source, "ValueStepper"
)
# Store original visual properties # Store original visual properties
original_scale = scale original_scale = scale
original_modulate = modulate original_modulate = modulate
# Connect button signals # Connect button signals
if left_button and not left_button.pressed.is_connected(_on_left_button_pressed): if (
left_button
and not left_button.pressed.is_connected(_on_left_button_pressed)
):
left_button.pressed.connect(_on_left_button_pressed) left_button.pressed.connect(_on_left_button_pressed)
if right_button and not right_button.pressed.is_connected(_on_right_button_pressed): if (
right_button
and not right_button.pressed.is_connected(_on_right_button_pressed)
):
right_button.pressed.connect(_on_right_button_pressed) right_button.pressed.connect(_on_right_button_pressed)
# Initialize data # Initialize data
@@ -58,7 +66,9 @@ func _load_data() -> void:
"difficulty": "difficulty":
_load_difficulty_data() _load_difficulty_data()
_: _:
DebugManager.log_warn("Unknown data_source: " + data_source, "ValueStepper") DebugManager.log_warn(
"Unknown data_source: " + data_source, "ValueStepper"
)
func _load_language_data() -> void: func _load_language_data() -> void:
@@ -68,22 +78,30 @@ func _load_language_data() -> void:
display_names.clear() display_names.clear()
for lang_code in languages_data.languages.keys(): for lang_code in languages_data.languages.keys():
values.append(lang_code) values.append(lang_code)
display_names.append(languages_data.languages[lang_code]["display_name"]) display_names.append(
languages_data.languages[lang_code]["display_name"]
)
# Set current index based on current language # Set current index based on current language
var current_lang: String = SettingsManager.get_setting("language") var current_lang: String = SettingsManager.get_setting("language")
var index: int = values.find(current_lang) var index: int = values.find(current_lang)
current_index = max(0, index) current_index = max(0, index)
DebugManager.log_info("Loaded %d languages" % values.size(), "ValueStepper") DebugManager.log_info(
"Loaded %d languages" % values.size(), "ValueStepper"
)
func _load_resolution_data() -> void: func _load_resolution_data() -> void:
# Example resolution data - customize as needed # Example resolution data - customize as needed
values = ["1920x1080", "1366x768", "1280x720", "1024x768"] values = ["1920x1080", "1366x768", "1280x720", "1024x768"]
display_names = ["1920×1080 (Full HD)", "1366×768", "1280×720 (HD)", "1024×768"] display_names = [
"1920×1080 (Full HD)", "1366×768", "1280×720 (HD)", "1024×768"
]
current_index = 0 current_index = 0
DebugManager.log_info("Loaded %d resolutions" % values.size(), "ValueStepper") DebugManager.log_info(
"Loaded %d resolutions" % values.size(), "ValueStepper"
)
func _load_difficulty_data() -> void: func _load_difficulty_data() -> void:
@@ -91,12 +109,18 @@ func _load_difficulty_data() -> void:
values = ["easy", "normal", "hard", "nightmare"] values = ["easy", "normal", "hard", "nightmare"]
display_names = ["Easy", "Normal", "Hard", "Nightmare"] display_names = ["Easy", "Normal", "Hard", "Nightmare"]
current_index = 1 # Default to "normal" current_index = 1 # Default to "normal"
DebugManager.log_info("Loaded %d difficulty levels" % values.size(), "ValueStepper") DebugManager.log_info(
"Loaded %d difficulty levels" % values.size(), "ValueStepper"
)
## Updates the display text based on current selection ## Updates the display text based on current selection
func _update_display() -> void: func _update_display() -> void:
if values.size() == 0 or current_index < 0 or current_index >= values.size(): if (
values.size() == 0
or current_index < 0
or current_index >= values.size()
):
value_display.text = "N/A" value_display.text = "N/A"
return return
@@ -109,7 +133,9 @@ func _update_display() -> void:
## Changes the current value by the specified direction (-1 for previous, +1 for next) ## Changes the current value by the specified direction (-1 for previous, +1 for next)
func change_value(direction: int) -> void: func change_value(direction: int) -> void:
if values.size() == 0: if values.size() == 0:
DebugManager.log_warn("No values available for: " + data_source, "ValueStepper") DebugManager.log_warn(
"No values available for: " + data_source, "ValueStepper"
)
return return
var new_index: int = (current_index + direction) % values.size() var new_index: int = (current_index + direction) % values.size()
@@ -123,7 +149,14 @@ func change_value(direction: int) -> void:
_apply_value_change(new_value, current_index) _apply_value_change(new_value, current_index)
value_changed.emit(new_value, current_index) value_changed.emit(new_value, current_index)
DebugManager.log_info( DebugManager.log_info(
"Value changed to: " + new_value + " (index: " + str(current_index) + ")", "ValueStepper" (
"Value changed to: "
+ new_value
+ " (index: "
+ str(current_index)
+ ")"
),
"ValueStepper"
) )
@@ -136,10 +169,14 @@ func _apply_value_change(new_value: String, _index: int) -> void:
LocalizationManager.change_language(new_value) LocalizationManager.change_language(new_value)
"resolution": "resolution":
# Apply resolution change logic here # Apply resolution change logic here
DebugManager.log_info("Resolution would change to: " + new_value, "ValueStepper") DebugManager.log_info(
"Resolution would change to: " + new_value, "ValueStepper"
)
"difficulty": "difficulty":
# Apply difficulty change logic here # Apply difficulty change logic here
DebugManager.log_info("Difficulty would change to: " + new_value, "ValueStepper") DebugManager.log_info(
"Difficulty would change to: " + new_value, "ValueStepper"
)
## Sets up custom values for the stepper ## Sets up custom values for the stepper
@@ -148,16 +185,24 @@ func setup_custom_values(
) -> void: ) -> void:
values = custom_values.duplicate() values = custom_values.duplicate()
display_names = ( display_names = (
custom_display_names.duplicate() if custom_display_names.size() > 0 else values.duplicate() custom_display_names.duplicate()
if custom_display_names.size() > 0
else values.duplicate()
) )
current_index = 0 current_index = 0
_update_display() _update_display()
DebugManager.log_info("Setup custom values: " + str(values.size()) + " items", "ValueStepper") DebugManager.log_info(
"Setup custom values: " + str(values.size()) + " items", "ValueStepper"
)
## Gets the current value ## Gets the current value
func get_current_value() -> String: func get_current_value() -> String:
if values.size() > 0 and current_index >= 0 and current_index < values.size(): if (
values.size() > 0
and current_index >= 0
and current_index < values.size()
):
return values[current_index] return values[current_index]
return "" return ""

View File

@@ -22,7 +22,9 @@ func _ready():
var orig_stream = _load_stream() var orig_stream = _load_stream()
if not orig_stream: if not orig_stream:
DebugManager.log_error("Failed to load music stream: %s" % MUSIC_PATH, "AudioManager") DebugManager.log_error(
"Failed to load music stream: %s" % MUSIC_PATH, "AudioManager"
)
return return
var stream = orig_stream.duplicate(true) as AudioStream var stream = orig_stream.duplicate(true) as AudioStream
@@ -52,7 +54,9 @@ func _configure_stream_loop(stream: AudioStream) -> void:
func _configure_audio_bus() -> void: func _configure_audio_bus() -> void:
music_player.bus = "Music" music_player.bus = "Music"
music_player.volume_db = linear_to_db(SettingsManager.get_setting("music_volume")) music_player.volume_db = linear_to_db(
SettingsManager.get_setting("music_volume")
)
func update_music_volume(volume: float) -> void: func update_music_volume(volume: float) -> void:

View File

@@ -83,7 +83,9 @@ func _log_level_to_string(level: LogLevel) -> String:
return level_strings.get(level, "UNKNOWN") return level_strings.get(level, "UNKNOWN")
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String: func _format_log_message(
level: LogLevel, message: String, category: String = ""
) -> String:
"""Format log message with timestamp, level, category, and content""" """Format log message with timestamp, level, category, and content"""
var timestamp = Time.get_datetime_string_from_system() var timestamp = Time.get_datetime_string_from_system()
var level_str = _log_level_to_string(level) var level_str = _log_level_to_string(level)

View File

@@ -49,14 +49,17 @@ func start_game_with_mode(gameplay_mode: String) -> void:
var packed_scene := load(GAME_SCENE_PATH) var packed_scene := load(GAME_SCENE_PATH)
if not packed_scene or not packed_scene is PackedScene: if not packed_scene or not packed_scene is PackedScene:
DebugManager.log_error("Failed to load Game scene at: %s" % GAME_SCENE_PATH, "GameManager") DebugManager.log_error(
"Failed to load Game scene at: %s" % GAME_SCENE_PATH, "GameManager"
)
is_changing_scene = false is_changing_scene = false
return return
var result = get_tree().change_scene_to_packed(packed_scene) var result = get_tree().change_scene_to_packed(packed_scene)
if result != OK: if result != OK:
DebugManager.log_error( DebugManager.log_error(
"Failed to change to game scene (Error code: %d)" % result, "GameManager" "Failed to change to game scene (Error code: %d)" % result,
"GameManager"
) )
is_changing_scene = false is_changing_scene = false
return return
@@ -67,22 +70,31 @@ func start_game_with_mode(gameplay_mode: String) -> void:
# Validate scene was loaded successfully # Validate scene was loaded successfully
if not get_tree().current_scene: if not get_tree().current_scene:
DebugManager.log_error("Current scene is null after scene change", "GameManager") DebugManager.log_error(
"Current scene is null after scene change", "GameManager"
)
is_changing_scene = false is_changing_scene = false
return return
# Configure game scene with requested gameplay mode # Configure game scene with requested gameplay mode
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 # Load saved score
if get_tree().current_scene.has_method("set_global_score"): if get_tree().current_scene.has_method("set_global_score"):
var saved_score = SaveManager.get_current_score() var saved_score = SaveManager.get_current_score()
DebugManager.log_info("Loading saved score: %d" % saved_score, "GameManager") DebugManager.log_info(
"Loading saved score: %d" % saved_score, "GameManager"
)
get_tree().current_scene.set_global_score(saved_score) 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
@@ -91,11 +103,16 @@ func save_game() -> void:
"""Save current game state and score via SaveManager""" """Save current game state and score via SaveManager"""
# Get current score from the active game scene # Get current score from the active game scene
var current_score = 0 var current_score = 0
if get_tree().current_scene and get_tree().current_scene.has_method("get_global_score"): if (
get_tree().current_scene
and get_tree().current_scene.has_method("get_global_score")
):
current_score = get_tree().current_scene.get_global_score() current_score = get_tree().current_scene.get_global_score()
SaveManager.finish_game(current_score) SaveManager.finish_game(current_score)
DebugManager.log_info("Game saved with score: %d" % current_score, "GameManager") DebugManager.log_info(
"Game saved with score: %d" % current_score, "GameManager"
)
func show_credits() -> void: func show_credits() -> void:
@@ -103,7 +120,8 @@ func show_credits() -> void:
# Prevent concurrent scene changes # Prevent concurrent scene changes
if is_changing_scene: if is_changing_scene:
DebugManager.log_warn( DebugManager.log_warn(
"Scene change already in progress, ignoring show credits request", "GameManager" "Scene change already in progress, ignoring show credits request",
"GameManager"
) )
return return
@@ -113,7 +131,8 @@ func show_credits() -> void:
var packed_scene := load(CREDITS_SCENE_PATH) var packed_scene := load(CREDITS_SCENE_PATH)
if not packed_scene or not packed_scene is PackedScene: if not packed_scene or not packed_scene is PackedScene:
DebugManager.log_error( DebugManager.log_error(
"Failed to load Credits scene at: %s" % CREDITS_SCENE_PATH, "GameManager" "Failed to load Credits scene at: %s" % CREDITS_SCENE_PATH,
"GameManager"
) )
is_changing_scene = false is_changing_scene = false
return return
@@ -121,7 +140,8 @@ func show_credits() -> void:
var result = get_tree().change_scene_to_packed(packed_scene) var result = get_tree().change_scene_to_packed(packed_scene)
if result != OK: if result != OK:
DebugManager.log_error( DebugManager.log_error(
"Failed to change to credits scene (Error code: %d)" % result, "GameManager" "Failed to change to credits scene (Error code: %d)" % result,
"GameManager"
) )
is_changing_scene = false is_changing_scene = false
return return
@@ -137,8 +157,12 @@ func exit_to_main_menu() -> void:
"""Exit to main menu with race condition protection""" """Exit to main menu with race condition protection"""
# Prevent concurrent scene changes # Prevent concurrent scene changes
if is_changing_scene: if is_changing_scene:
DebugManager.log_warn( (
"Scene change already in progress, ignoring exit to main menu request", "GameManager" DebugManager
. log_warn(
"Scene change already in progress, ignoring exit to main menu request",
"GameManager"
)
) )
return return
@@ -147,14 +171,17 @@ func exit_to_main_menu() -> void:
var packed_scene := load(MAIN_SCENE_PATH) var packed_scene := load(MAIN_SCENE_PATH)
if not packed_scene or not packed_scene is PackedScene: if not packed_scene or not packed_scene is PackedScene:
DebugManager.log_error("Failed to load Main scene at: %s" % MAIN_SCENE_PATH, "GameManager") DebugManager.log_error(
"Failed to load Main scene at: %s" % MAIN_SCENE_PATH, "GameManager"
)
is_changing_scene = false is_changing_scene = false
return return
var result = get_tree().change_scene_to_packed(packed_scene) var result = get_tree().change_scene_to_packed(packed_scene)
if result != OK: if result != OK:
DebugManager.log_error( DebugManager.log_error(
"Failed to change to main scene (Error code: %d)" % result, "GameManager" "Failed to change to main scene (Error code: %d)" % result,
"GameManager"
) )
is_changing_scene = false is_changing_scene = false
return return
@@ -170,25 +197,33 @@ func _validate_game_mode_request(gameplay_mode: String) -> bool:
"""Validate gameplay mode request with combined checks""" """Validate gameplay mode request with combined checks"""
# Input validation # Input validation
if not gameplay_mode or gameplay_mode.is_empty(): if not gameplay_mode or gameplay_mode.is_empty():
DebugManager.log_error("Empty or null gameplay mode provided", "GameManager") DebugManager.log_error(
"Empty or null gameplay mode provided", "GameManager"
)
return false return false
if not gameplay_mode is String: if not gameplay_mode is String:
DebugManager.log_error( DebugManager.log_error(
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager" "Invalid gameplay mode type: " + str(typeof(gameplay_mode)),
"GameManager"
) )
return false return false
# Prevent concurrent scene changes (race condition protection) # Prevent concurrent scene changes (race condition protection)
if is_changing_scene: if is_changing_scene:
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager") DebugManager.log_warn(
"Scene change already in progress, ignoring request", "GameManager"
)
return false return false
# Validate gameplay mode # Validate gameplay mode
var valid_modes = ["match3", "clickomania"] var valid_modes = ["match3", "clickomania"]
if not gameplay_mode in valid_modes: if not gameplay_mode in valid_modes:
DebugManager.log_error( DebugManager.log_error(
"Invalid gameplay mode: '%s'. Valid modes: %s" % [gameplay_mode, str(valid_modes)], (
"Invalid gameplay mode: '%s'. Valid modes: %s"
% [gameplay_mode, str(valid_modes)]
),
"GameManager" "GameManager"
) )
return false return false

View File

@@ -42,7 +42,9 @@ func save_game() -> bool:
"""Save current game data with race condition protection and error handling""" """Save current game data with race condition protection and error handling"""
# Prevent concurrent saves # Prevent concurrent saves
if _save_in_progress: if _save_in_progress:
DebugManager.log_warn("Save already in progress, skipping", "SaveManager") DebugManager.log_warn(
"Save already in progress, skipping", "SaveManager"
)
return false return false
_save_in_progress = true _save_in_progress = true
@@ -61,10 +63,13 @@ func _perform_save() -> bool:
# Calculate checksum excluding _checksum field # Calculate checksum excluding _checksum field
save_data["_checksum"] = _calculate_checksum(save_data) save_data["_checksum"] = _calculate_checksum(save_data)
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) var save_file: FileAccess = FileAccess.open(
SAVE_FILE_PATH, FileAccess.WRITE
)
if save_file == null: if save_file == null:
DebugManager.log_error( DebugManager.log_error(
"Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager" "Failed to open save file for writing: %s" % SAVE_FILE_PATH,
"SaveManager"
) )
return false return false
@@ -72,7 +77,9 @@ func _perform_save() -> bool:
# Validate JSON creation # Validate JSON creation
if json_string.is_empty(): if json_string.is_empty():
DebugManager.log_error("Failed to serialize save data to JSON", "SaveManager") DebugManager.log_error(
"Failed to serialize save data to JSON", "SaveManager"
)
save_file.close() save_file.close()
return false return false
@@ -92,7 +99,9 @@ func _perform_save() -> bool:
func load_game() -> void: func load_game() -> void:
"""Load game data from disk with comprehensive validation and error recovery""" """Load game data from disk with comprehensive validation and error recovery"""
if not FileAccess.file_exists(SAVE_FILE_PATH): if not FileAccess.file_exists(SAVE_FILE_PATH):
DebugManager.log_info("No save file found, using defaults", "SaveManager") DebugManager.log_info(
"No save file found, using defaults", "SaveManager"
)
return return
# Reset restore flag # Reset restore flag
@@ -111,7 +120,8 @@ func _load_and_parse_save_file() -> Variant:
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
if save_file == null: if save_file == null:
DebugManager.log_error( DebugManager.log_error(
"Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager" "Failed to open save file for reading: %s" % SAVE_FILE_PATH,
"SaveManager"
) )
return null return null
@@ -119,7 +129,11 @@ func _load_and_parse_save_file() -> Variant:
var file_size: int = save_file.get_length() var file_size: int = save_file.get_length()
if file_size > MAX_FILE_SIZE: if file_size > MAX_FILE_SIZE:
DebugManager.log_error( DebugManager.log_error(
"Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager" (
"Save file too large: %d bytes (max %d)"
% [file_size, MAX_FILE_SIZE]
),
"SaveManager"
) )
save_file.close() save_file.close()
return null return null
@@ -128,21 +142,26 @@ func _load_and_parse_save_file() -> Variant:
save_file.close() save_file.close()
if not json_string is String: if not json_string is String:
DebugManager.log_error("Save file contains invalid data type", "SaveManager") DebugManager.log_error(
"Save file contains invalid data type", "SaveManager"
)
return null return null
var json: JSON = JSON.new() var json: JSON = JSON.new()
var parse_result: Error = json.parse(json_string) var parse_result: Error = json.parse(json_string)
if parse_result != OK: if parse_result != OK:
DebugManager.log_error( DebugManager.log_error(
"Failed to parse save file JSON: %s" % json.error_string, "SaveManager" "Failed to parse save file JSON: %s" % json.error_string,
"SaveManager"
) )
_handle_load_failure("JSON parse failed") _handle_load_failure("JSON parse failed")
return null return null
var loaded_data: Variant = json.data var loaded_data: Variant = json.data
if not loaded_data is Dictionary: if not loaded_data is Dictionary:
DebugManager.log_error("Save file root is not a dictionary", "SaveManager") DebugManager.log_error(
"Save file root is not a dictionary", "SaveManager"
)
_handle_load_failure("Invalid data format") _handle_load_failure("Invalid data format")
return null return null
@@ -154,7 +173,8 @@ func _process_loaded_data(loaded_data: Variant) -> void:
# Validate checksum first # Validate checksum first
if not _validate_checksum(loaded_data): if not _validate_checksum(loaded_data):
DebugManager.log_error( DebugManager.log_error(
"Save file checksum validation failed - possible tampering", "SaveManager" "Save file checksum validation failed - possible tampering",
"SaveManager"
) )
_handle_load_failure("Checksum validation failed") _handle_load_failure("Checksum validation failed")
return return
@@ -162,14 +182,17 @@ func _process_loaded_data(loaded_data: Variant) -> void:
# Handle version migration # Handle version migration
var migrated_data: Variant = _handle_version_migration(loaded_data) var migrated_data: Variant = _handle_version_migration(loaded_data)
if migrated_data == null: if migrated_data == null:
DebugManager.log_error("Save file version migration failed", "SaveManager") DebugManager.log_error(
"Save file version migration failed", "SaveManager"
)
_handle_load_failure("Migration failed") _handle_load_failure("Migration failed")
return return
# Validate and fix loaded data # Validate and fix loaded data
if not _validate_and_fix_save_data(migrated_data): if not _validate_and_fix_save_data(migrated_data):
DebugManager.log_error( DebugManager.log_error(
"Save file failed validation after migration, using defaults", "SaveManager" "Save file failed validation after migration, using defaults",
"SaveManager"
) )
_handle_load_failure("Validation failed") _handle_load_failure("Validation failed")
return return
@@ -184,7 +207,8 @@ func _handle_load_failure(reason: String) -> void:
var backup_restored = _restore_backup_if_exists() var backup_restored = _restore_backup_if_exists()
if not backup_restored: if not backup_restored:
DebugManager.log_warn( DebugManager.log_warn(
"%s and backup restore failed, using defaults" % reason, "SaveManager" "%s and backup restore failed, using defaults" % reason,
"SaveManager"
) )
DebugManager.log_info( DebugManager.log_info(
@@ -199,11 +223,14 @@ func _handle_load_failure(reason: String) -> void:
func update_current_score(score: int) -> void: func update_current_score(score: int) -> void:
# Input validation # Input validation
if score < 0: if score < 0:
DebugManager.log_warn("Negative score rejected: %d" % score, "SaveManager") DebugManager.log_warn(
"Negative score rejected: %d" % score, "SaveManager"
)
return return
if score > MAX_SCORE: if score > MAX_SCORE:
DebugManager.log_warn( DebugManager.log_warn(
"Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager" "Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE],
"SaveManager"
) )
score = MAX_SCORE score = MAX_SCORE
@@ -219,23 +246,33 @@ func start_new_game() -> void:
# Clear saved grid state # Clear saved grid state
game_data.grid_state.grid_layout = [] game_data.grid_state.grid_layout = []
DebugManager.log_info( DebugManager.log_info(
"Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager" "Started new game #%d (cleared grid state)" % game_data.games_played,
"SaveManager"
) )
func finish_game(final_score: int) -> void: func finish_game(final_score: int) -> void:
# Input validation # Input validation
if final_score < 0: if final_score < 0:
DebugManager.log_warn("Negative final score rejected: %d" % final_score, "SaveManager") DebugManager.log_warn(
"Negative final score rejected: %d" % final_score, "SaveManager"
)
return return
if final_score > MAX_SCORE: if final_score > MAX_SCORE:
DebugManager.log_warn( DebugManager.log_warn(
"Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager" (
"Final score too high, capping: %d -> %d"
% [final_score, MAX_SCORE]
),
"SaveManager"
) )
final_score = MAX_SCORE final_score = MAX_SCORE
DebugManager.log_info( DebugManager.log_info(
"Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score], (
"Finishing game with score: %d (previous: %d)"
% [final_score, game_data.current_score]
),
"SaveManager" "SaveManager"
) )
game_data.current_score = final_score game_data.current_score = final_score
@@ -250,7 +287,9 @@ func finish_game(final_score: int) -> void:
if final_score > game_data.high_score: if final_score > game_data.high_score:
game_data.high_score = final_score game_data.high_score = final_score
DebugManager.log_info("New high score achieved: %d" % final_score, "SaveManager") DebugManager.log_info(
"New high score achieved: %d" % final_score, "SaveManager"
)
save_game() save_game()
@@ -271,11 +310,18 @@ func get_total_score() -> int:
func save_grid_state( func save_grid_state(
grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array grid_size: Vector2i,
tile_types_count: int,
active_gem_types: Array,
grid_layout: Array
) -> void: ) -> void:
# Input validation # Input validation
if not _validate_grid_parameters(grid_size, tile_types_count, active_gem_types, grid_layout): if not _validate_grid_parameters(
DebugManager.log_error("Grid state validation failed, not saving", "SaveManager") grid_size, tile_types_count, active_gem_types, grid_layout
):
DebugManager.log_error(
"Grid state validation failed, not saving", "SaveManager"
)
return return
DebugManager.log_info( DebugManager.log_info(
@@ -338,10 +384,13 @@ func reset_all_progress() -> bool:
if FileAccess.file_exists(SAVE_FILE_PATH): if FileAccess.file_exists(SAVE_FILE_PATH):
var error: Error = DirAccess.remove_absolute(SAVE_FILE_PATH) var error: Error = DirAccess.remove_absolute(SAVE_FILE_PATH)
if error == OK: if error == OK:
DebugManager.log_info("Main save file deleted successfully", "SaveManager") DebugManager.log_info(
"Main save file deleted successfully", "SaveManager"
)
else: else:
DebugManager.log_error( DebugManager.log_error(
"Failed to delete main save file: error %d" % error, "SaveManager" "Failed to delete main save file: error %d" % error,
"SaveManager"
) )
# Delete backup file # Delete backup file
@@ -349,21 +398,27 @@ func reset_all_progress() -> bool:
if FileAccess.file_exists(backup_path): if FileAccess.file_exists(backup_path):
var error: Error = DirAccess.remove_absolute(backup_path) var error: Error = DirAccess.remove_absolute(backup_path)
if error == OK: if error == OK:
DebugManager.log_info("Backup save file deleted successfully", "SaveManager") DebugManager.log_info(
"Backup save file deleted successfully", "SaveManager"
)
else: else:
DebugManager.log_error( DebugManager.log_error(
"Failed to delete backup save file: error %d" % error, "SaveManager" "Failed to delete backup save file: error %d" % error,
"SaveManager"
) )
DebugManager.log_info( DebugManager.log_info(
"Progress reset completed - all scores and save data cleared", "SaveManager" "Progress reset completed - all scores and save data cleared",
"SaveManager"
) )
# Clear restore flag # Clear restore flag
_restore_in_progress = false _restore_in_progress = false
# Create fresh save file with default data # Create fresh save file with default data
DebugManager.log_info("Creating fresh save file with default data", "SaveManager") DebugManager.log_info(
"Creating fresh save file with default data", "SaveManager"
)
save_game() save_game()
return true return true
@@ -395,11 +450,17 @@ func _validate_save_data(data: Dictionary) -> bool:
func _validate_required_fields(data: Dictionary) -> bool: func _validate_required_fields(data: Dictionary) -> bool:
"""Validate that all required fields exist""" """Validate that all required fields exist"""
var required_fields: Array[String] = [ var required_fields: Array[String] = [
"high_score", "current_score", "games_played", "total_score", "grid_state" "high_score",
"current_score",
"games_played",
"total_score",
"grid_state"
] ]
for field in required_fields: for field in required_fields:
if not data.has(field): if not data.has(field):
DebugManager.log_error("Missing required field: %s" % field, "SaveManager") DebugManager.log_error(
"Missing required field: %s" % field, "SaveManager"
)
return false return false
return true return true
@@ -409,7 +470,9 @@ func _validate_score_fields(data: Dictionary) -> bool:
var score_fields = ["high_score", "current_score", "total_score"] var score_fields = ["high_score", "current_score", "total_score"]
for field in score_fields: for field in score_fields:
if not _is_valid_score(data.get(field, 0)): if not _is_valid_score(data.get(field, 0)):
DebugManager.log_error("Invalid %s validation failed" % field, "SaveManager") DebugManager.log_error(
"Invalid %s validation failed" % field, "SaveManager"
)
return false return false
return true return true
@@ -419,7 +482,10 @@ func _validate_games_played_field(data: Dictionary) -> bool:
var games_played: Variant = data.get("games_played", 0) var games_played: Variant = data.get("games_played", 0)
if not (games_played is int or games_played is float): if not (games_played is int or games_played is float):
DebugManager.log_error( DebugManager.log_error(
"Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)], (
"Invalid games_played type: %s (type: %s)"
% [str(games_played), typeof(games_played)]
),
"SaveManager" "SaveManager"
) )
return false return false
@@ -427,14 +493,18 @@ func _validate_games_played_field(data: Dictionary) -> bool:
# Check for NaN/Infinity in games_played if it's a float # Check for NaN/Infinity in games_played if it's a float
if games_played is float and (is_nan(games_played) or is_inf(games_played)): if games_played is float and (is_nan(games_played) or is_inf(games_played)):
DebugManager.log_error( DebugManager.log_error(
"Invalid games_played float value: %s" % str(games_played), "SaveManager" "Invalid games_played float value: %s" % str(games_played),
"SaveManager"
) )
return false return false
var games_played_int: int = int(games_played) var games_played_int: int = int(games_played)
if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED: if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED:
DebugManager.log_error( DebugManager.log_error(
"Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED], (
"Invalid games_played value: %d (range: 0-%d)"
% [games_played_int, MAX_GAMES_PLAYED]
),
"SaveManager" "SaveManager"
) )
return false return false
@@ -447,16 +517,23 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
Permissive validation that fixes issues instead of rejecting data entirely. Permissive validation that fixes issues instead of rejecting data entirely.
Used during migration to preserve as much user data as possible. Used during migration to preserve as much user data as possible.
""" """
DebugManager.log_info("Running permissive validation with auto-fix", "SaveManager") DebugManager.log_info(
"Running permissive validation with auto-fix", "SaveManager"
)
# Ensure all required fields exist, create defaults if missing # Ensure all required fields exist, create defaults if missing
var required_fields: Array[String] = [ var required_fields: Array[String] = [
"high_score", "current_score", "games_played", "total_score", "grid_state" "high_score",
"current_score",
"games_played",
"total_score",
"grid_state"
] ]
for field in required_fields: for field in required_fields:
if not data.has(field): if not data.has(field):
DebugManager.log_warn( DebugManager.log_warn(
"Missing required field '%s', adding default value" % field, "SaveManager" "Missing required field '%s', adding default value" % field,
"SaveManager"
) )
match field: match field:
"high_score", "current_score", "total_score": "high_score", "current_score", "total_score":
@@ -475,15 +552,21 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
for field in ["high_score", "current_score", "total_score"]: for field in ["high_score", "current_score", "total_score"]:
var value: Variant = data.get(field, 0) var value: Variant = data.get(field, 0)
if not (value is int or value is float): if not (value is int or value is float):
DebugManager.log_warn("Invalid type for %s, converting to 0" % field, "SaveManager") DebugManager.log_warn(
"Invalid type for %s, converting to 0" % field, "SaveManager"
)
data[field] = 0 data[field] = 0
else: else:
var numeric_value: int = int(value) var numeric_value: int = int(value)
if numeric_value < 0: if numeric_value < 0:
DebugManager.log_warn("Negative %s fixed to 0" % field, "SaveManager") DebugManager.log_warn(
"Negative %s fixed to 0" % field, "SaveManager"
)
data[field] = 0 data[field] = 0
elif numeric_value > MAX_SCORE: elif numeric_value > MAX_SCORE:
DebugManager.log_warn("%s too high, clamped to maximum" % field, "SaveManager") DebugManager.log_warn(
"%s too high, clamped to maximum" % field, "SaveManager"
)
data[field] = MAX_SCORE data[field] = MAX_SCORE
else: else:
data[field] = numeric_value data[field] = numeric_value
@@ -491,7 +574,9 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
# Fix games_played # Fix games_played
var games_played: Variant = data.get("games_played", 0) var games_played: Variant = data.get("games_played", 0)
if not (games_played is int or games_played is float): if not (games_played is int or games_played is float):
DebugManager.log_warn("Invalid games_played type, converting to 0", "SaveManager") DebugManager.log_warn(
"Invalid games_played type, converting to 0", "SaveManager"
)
data["games_played"] = 0 data["games_played"] = 0
else: else:
var games_played_int: int = int(games_played) var games_played_int: int = int(games_played)
@@ -505,7 +590,9 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
# Fix grid_state - ensure it exists and has basic structure # Fix grid_state - ensure it exists and has basic structure
var grid_state: Variant = data.get("grid_state", {}) var grid_state: Variant = data.get("grid_state", {})
if not grid_state is Dictionary: if not grid_state is Dictionary:
DebugManager.log_warn("Invalid grid_state, creating default", "SaveManager") DebugManager.log_warn(
"Invalid grid_state, creating default", "SaveManager"
)
data["grid_state"] = { data["grid_state"] = {
"grid_size": {"x": 8, "y": 8}, "grid_size": {"x": 8, "y": 8},
"tile_types_count": 5, "tile_types_count": 5,
@@ -514,24 +601,48 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
} }
else: else:
# Fix grid_state fields if they're missing or invalid # Fix grid_state fields if they're missing or invalid
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary: if (
DebugManager.log_warn("Invalid grid_size, using default", "SaveManager") not grid_state.has("grid_size")
or not grid_state.grid_size is Dictionary
):
DebugManager.log_warn(
"Invalid grid_size, using default", "SaveManager"
)
grid_state["grid_size"] = {"x": 8, "y": 8} grid_state["grid_size"] = {"x": 8, "y": 8}
if not grid_state.has("tile_types_count") or not grid_state.tile_types_count is int: if (
DebugManager.log_warn("Invalid tile_types_count, using default", "SaveManager") not grid_state.has("tile_types_count")
or not grid_state.tile_types_count is int
):
DebugManager.log_warn(
"Invalid tile_types_count, using default", "SaveManager"
)
grid_state["tile_types_count"] = 5 grid_state["tile_types_count"] = 5
if not grid_state.has("active_gem_types") or not grid_state.active_gem_types is Array: if (
DebugManager.log_warn("Invalid active_gem_types, using default", "SaveManager") not grid_state.has("active_gem_types")
or not grid_state.active_gem_types is Array
):
DebugManager.log_warn(
"Invalid active_gem_types, using default", "SaveManager"
)
grid_state["active_gem_types"] = [0, 1, 2, 3, 4] grid_state["active_gem_types"] = [0, 1, 2, 3, 4]
if not grid_state.has("grid_layout") or not grid_state.grid_layout is Array: if (
DebugManager.log_warn("Invalid grid_layout, clearing saved grid", "SaveManager") not grid_state.has("grid_layout")
or not grid_state.grid_layout is Array
):
DebugManager.log_warn(
"Invalid grid_layout, clearing saved grid", "SaveManager"
)
grid_state["grid_layout"] = [] grid_state["grid_layout"] = []
DebugManager.log_info( (
"Permissive validation completed - data has been fixed and will be loaded", "SaveManager" DebugManager
. log_info(
"Permissive validation completed - data has been fixed and will be loaded",
"SaveManager"
)
) )
return true return true
@@ -569,7 +680,10 @@ func _validate_grid_size(grid_state: Dictionary) -> Dictionary:
"""Validate grid size and return validation result with dimensions""" """Validate grid size and return validation result with dimensions"""
var result = {"valid": false, "width": 0, "height": 0} var result = {"valid": false, "width": 0, "height": 0}
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary: 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") DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
return result return result
@@ -581,8 +695,15 @@ func _validate_grid_size(grid_state: Dictionary) -> Dictionary:
var height: Variant = size.y var height: Variant = size.y
if not width is int or not height is int: if not width is int or not height is int:
return result return result
if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE: if (
DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager") 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 result return result
result.valid = true result.valid = true
@@ -595,16 +716,22 @@ func _validate_tile_types(grid_state: Dictionary) -> int:
"""Validate tile types count and return it, or -1 if invalid""" """Validate tile types count and return it, or -1 if invalid"""
var tile_types: Variant = grid_state.get("tile_types_count", 0) var tile_types: Variant = grid_state.get("tile_types_count", 0)
if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES: 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") DebugManager.log_error(
"Invalid tile_types_count: %s" % str(tile_types), "SaveManager"
)
return -1 return -1
return tile_types return tile_types
func _validate_active_gem_types(grid_state: Dictionary, tile_types: int) -> bool: func _validate_active_gem_types(
grid_state: Dictionary, tile_types: int
) -> bool:
"""Validate active gem types array""" """Validate active gem types array"""
var active_gems: Variant = grid_state.get("active_gem_types", []) var active_gems: Variant = grid_state.get("active_gem_types", [])
if not active_gems is Array: if not active_gems is Array:
DebugManager.log_error("active_gem_types is not an array", "SaveManager") DebugManager.log_error(
"active_gem_types is not an array", "SaveManager"
)
return false return false
# If active_gem_types exists, validate its contents # If active_gem_types exists, validate its contents
@@ -613,12 +740,17 @@ func _validate_active_gem_types(grid_state: Dictionary, tile_types: int) -> bool
var gem_type: Variant = active_gems[i] var gem_type: Variant = active_gems[i]
if not gem_type is int: if not gem_type is int:
DebugManager.log_error( DebugManager.log_error(
"active_gem_types[%d] is not an integer: %s" % [i, str(gem_type)], "SaveManager" (
"active_gem_types[%d] is not an integer: %s"
% [i, str(gem_type)]
),
"SaveManager"
) )
return false return false
if gem_type < 0 or gem_type >= tile_types: if gem_type < 0 or gem_type >= tile_types:
DebugManager.log_error( DebugManager.log_error(
"active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager" "active_gem_types[%d] out of range: %d" % [i, gem_type],
"SaveManager"
) )
return false return false
return true return true
@@ -629,7 +761,10 @@ func _validate_grid_layout(
) -> bool: ) -> bool:
if layout.size() != expected_height: if layout.size() != expected_height:
DebugManager.log_error( DebugManager.log_error(
"Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height], (
"Grid layout height mismatch: %d vs %d"
% [layout.size(), expected_height]
),
"SaveManager" "SaveManager"
) )
return false return false
@@ -637,11 +772,16 @@ func _validate_grid_layout(
for y in range(layout.size()): for y in range(layout.size()):
var row: Variant = layout[y] var row: Variant = layout[y]
if not row is Array: if not row is Array:
DebugManager.log_error("Grid layout row %d is not an array" % y, "SaveManager") DebugManager.log_error(
"Grid layout row %d is not an array" % y, "SaveManager"
)
return false return false
if row.size() != expected_width: if row.size() != expected_width:
DebugManager.log_error( DebugManager.log_error(
"Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width], (
"Grid layout row %d width mismatch: %d vs %d"
% [y, row.size(), expected_width]
),
"SaveManager" "SaveManager"
) )
return false return false
@@ -650,13 +790,20 @@ func _validate_grid_layout(
var tile_type: Variant = row[x] var tile_type: Variant = row[x]
if not tile_type is int: if not tile_type is int:
DebugManager.log_error( DebugManager.log_error(
"Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)], (
"Grid tile [%d][%d] is not an integer: %s"
% [y, x, str(tile_type)]
),
"SaveManager" "SaveManager"
) )
return false return false
if tile_type < -1 or tile_type >= max_tile_type: if tile_type < -1 or tile_type >= max_tile_type:
DebugManager.log_error( DebugManager.log_error(
"Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager" (
"Grid tile [%d][%d] type out of range: %d"
% [y, x, tile_type]
),
"SaveManager"
) )
return false return false
@@ -664,7 +811,10 @@ func _validate_grid_layout(
func _validate_grid_parameters( func _validate_grid_parameters(
grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array grid_size: Vector2i,
tile_types_count: int,
active_gem_types: Array,
grid_layout: Array
) -> bool: ) -> bool:
# Validate grid size # Validate grid size
if ( if (
@@ -685,7 +835,10 @@ func _validate_grid_parameters(
# Validate tile types count # Validate tile types count
if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES: if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES:
DebugManager.log_error( DebugManager.log_error(
"Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES], (
"Invalid tile types count: %d (min 3, max %d)"
% [tile_types_count, MAX_TILE_TYPES]
),
"SaveManager" "SaveManager"
) )
return false return false
@@ -702,14 +855,20 @@ func _validate_grid_parameters(
return false return false
# Validate grid layout # Validate grid layout
return _validate_grid_layout(grid_layout, grid_size.x, grid_size.y, tile_types_count) return _validate_grid_layout(
grid_layout, grid_size.x, grid_size.y, tile_types_count
)
func _is_valid_score(score: Variant) -> bool: func _is_valid_score(score: Variant) -> bool:
# Accept both int and float, but convert to int for validation # Accept both int and float, but convert to int for validation
if not (score is int or score is float): if not (score is int or score is float):
DebugManager.log_error( DebugManager.log_error(
"Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager" (
"Score is not a number: %s (type: %s)"
% [str(score), typeof(score)]
),
"SaveManager"
) )
return false return false
@@ -717,13 +876,16 @@ func _is_valid_score(score: Variant) -> bool:
if score is float: if score is float:
if is_nan(score) or is_inf(score): if is_nan(score) or is_inf(score):
DebugManager.log_error( DebugManager.log_error(
"Score contains invalid float value (NaN/Inf): %s" % str(score), "SaveManager" "Score contains invalid float value (NaN/Inf): %s" % str(score),
"SaveManager"
) )
return false return false
var score_int = int(score) var score_int = int(score)
if score_int < 0 or score_int > MAX_SCORE: if score_int < 0 or score_int > MAX_SCORE:
DebugManager.log_error("Score out of bounds: %d" % score_int, "SaveManager") DebugManager.log_error(
"Score out of bounds: %d" % score_int, "SaveManager"
)
return false return false
return true return true
@@ -737,12 +899,16 @@ func _merge_validated_data(loaded_data: Dictionary) -> void:
# Games played should always be an integer # Games played should always be an integer
if loaded_data.has("games_played"): if loaded_data.has("games_played"):
game_data["games_played"] = _safe_get_numeric_value(loaded_data, "games_played", 0) game_data["games_played"] = _safe_get_numeric_value(
loaded_data, "games_played", 0
)
# Merge grid state carefully # Merge grid state carefully
var loaded_grid: Variant = loaded_data.get("grid_state", {}) var loaded_grid: Variant = loaded_data.get("grid_state", {})
if loaded_grid is Dictionary: if loaded_grid is Dictionary:
for grid_key in ["grid_size", "tile_types_count", "active_gem_types", "grid_layout"]: for grid_key in [
"grid_size", "tile_types_count", "active_gem_types", "grid_layout"
]:
if loaded_grid.has(grid_key): if loaded_grid.has(grid_key):
game_data.grid_state[grid_key] = loaded_grid[grid_key] game_data.grid_state[grid_key] = loaded_grid[grid_key]
@@ -844,16 +1010,23 @@ func _validate_checksum(data: Dictionary) -> bool:
# Try to be more lenient with existing saves to prevent data loss # Try to be more lenient with existing saves to prevent data loss
var data_version: Variant = data.get("_version", 0) var data_version: Variant = data.get("_version", 0)
if data_version <= 1: if data_version <= 1:
DebugManager.log_warn( (
DebugManager
. log_warn(
( (
"Checksum mismatch in v%d save file - may be due to JSON serialization issue " "Checksum mismatch in v%d save file - may be due to JSON serialization issue "
+ ( + (
"(stored: %s, calculated: %s)" "(stored: %s, calculated: %s)"
% [data_version, stored_checksum, calculated_checksum] % [
data_version,
stored_checksum,
calculated_checksum
]
) )
), ),
"SaveManager" "SaveManager"
) )
)
( (
DebugManager DebugManager
. log_info( . log_info(
@@ -876,7 +1049,9 @@ func _validate_checksum(data: Dictionary) -> bool:
return is_valid return is_valid
func _safe_get_numeric_value(data: Dictionary, key: String, default_value: float) -> int: func _safe_get_numeric_value(
data: Dictionary, key: String, default_value: float
) -> int:
"""Safely extract and convert numeric values with comprehensive validation""" """Safely extract and convert numeric values with comprehensive validation"""
var value: Variant = data.get(key, default_value) var value: Variant = data.get(key, default_value)
@@ -910,13 +1085,15 @@ func _safe_get_numeric_value(data: Dictionary, key: String, default_value: float
if key in ["high_score", "current_score", "total_score"]: if key in ["high_score", "current_score", "total_score"]:
if int_value < 0 or int_value > MAX_SCORE: if int_value < 0 or int_value > MAX_SCORE:
DebugManager.log_warn( DebugManager.log_warn(
"Score %s out of bounds: %d, using default" % [key, int_value], "SaveManager" "Score %s out of bounds: %d, using default" % [key, int_value],
"SaveManager"
) )
return int(default_value) return int(default_value)
elif key == "games_played": elif key == "games_played":
if int_value < 0 or int_value > MAX_GAMES_PLAYED: if int_value < 0 or int_value > MAX_GAMES_PLAYED:
DebugManager.log_warn( DebugManager.log_warn(
"Games played out of bounds: %d, using default" % int_value, "SaveManager" "Games played out of bounds: %d, using default" % int_value,
"SaveManager"
) )
return int(default_value) return int(default_value)
@@ -930,7 +1107,8 @@ func _handle_version_migration(data: Dictionary) -> Variant:
if data_version == SAVE_FORMAT_VERSION: if data_version == SAVE_FORMAT_VERSION:
# Current version, no migration needed # Current version, no migration needed
DebugManager.log_info( DebugManager.log_info(
"Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager" "Save file is current version (%d)" % SAVE_FORMAT_VERSION,
"SaveManager"
) )
return data return data
if data_version > SAVE_FORMAT_VERSION: if data_version > SAVE_FORMAT_VERSION:
@@ -945,7 +1123,10 @@ func _handle_version_migration(data: Dictionary) -> Variant:
return null return null
# Older version - migrate # Older version - migrate
DebugManager.log_info( DebugManager.log_info(
"Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION], (
"Migrating save data from version %d to %d"
% [data_version, SAVE_FORMAT_VERSION]
),
"SaveManager" "SaveManager"
) )
return _migrate_save_data(data, data_version) return _migrate_save_data(data, data_version)
@@ -960,7 +1141,9 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
# Add new fields that didn't exist in version 0 # Add new fields that didn't exist in version 0
if not migrated_data.has("total_score"): if not migrated_data.has("total_score"):
migrated_data["total_score"] = 0 migrated_data["total_score"] = 0
DebugManager.log_info("Added total_score field during migration", "SaveManager") DebugManager.log_info(
"Added total_score field during migration", "SaveManager"
)
if not migrated_data.has("grid_state"): if not migrated_data.has("grid_state"):
migrated_data["grid_state"] = { migrated_data["grid_state"] = {
@@ -969,7 +1152,9 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
"active_gem_types": [0, 1, 2, 3, 4], "active_gem_types": [0, 1, 2, 3, 4],
"grid_layout": [] "grid_layout": []
} }
DebugManager.log_info("Added grid_state structure during migration", "SaveManager") DebugManager.log_info(
"Added grid_state structure during migration", "SaveManager"
)
# Ensure all numeric values are within bounds after migration # Ensure all numeric values are within bounds after migration
for score_key in ["high_score", "current_score", "total_score"]: for score_key in ["high_score", "current_score", "total_score"]:
@@ -981,11 +1166,17 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
DebugManager.log_warn( DebugManager.log_warn(
( (
"Clamping %s during migration: %d -> %d" "Clamping %s during migration: %d -> %d"
% [score_key, int_score, clamp(int_score, 0, MAX_SCORE)] % [
score_key,
int_score,
clamp(int_score, 0, MAX_SCORE)
]
), ),
"SaveManager" "SaveManager"
) )
migrated_data[score_key] = clamp(int_score, 0, MAX_SCORE) migrated_data[score_key] = clamp(
int_score, 0, MAX_SCORE
)
# Future migrations would go here # Future migrations would go here
# if from_version < 2: # if from_version < 2:
@@ -997,7 +1188,9 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
# Recalculate checksum after migration # Recalculate checksum after migration
migrated_data["_checksum"] = _calculate_checksum(migrated_data) migrated_data["_checksum"] = _calculate_checksum(migrated_data)
DebugManager.log_info("Save data migration completed successfully", "SaveManager") DebugManager.log_info(
"Save data migration completed successfully", "SaveManager"
)
return migrated_data return migrated_data
@@ -1005,7 +1198,9 @@ func _create_backup() -> void:
# Create backup of current save file # Create backup of current save file
if FileAccess.file_exists(SAVE_FILE_PATH): if FileAccess.file_exists(SAVE_FILE_PATH):
var backup_path: String = SAVE_FILE_PATH + ".backup" var backup_path: String = SAVE_FILE_PATH + ".backup"
var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ) var original: FileAccess = FileAccess.open(
SAVE_FILE_PATH, FileAccess.READ
)
var backup: FileAccess = FileAccess.open(backup_path, FileAccess.WRITE) var backup: FileAccess = FileAccess.open(backup_path, FileAccess.WRITE)
if original and backup: if original and backup:
backup.store_var(original.get_var()) backup.store_var(original.get_var())
@@ -1017,7 +1212,9 @@ func _create_backup() -> void:
func _restore_backup_if_exists() -> bool: func _restore_backup_if_exists() -> bool:
var backup_path: String = SAVE_FILE_PATH + ".backup" var backup_path: String = SAVE_FILE_PATH + ".backup"
if not FileAccess.file_exists(backup_path): if not FileAccess.file_exists(backup_path):
DebugManager.log_warn("No backup file found for recovery", "SaveManager") DebugManager.log_warn(
"No backup file found for recovery", "SaveManager"
)
return false return false
DebugManager.log_info("Attempting to restore from backup", "SaveManager") DebugManager.log_info("Attempting to restore from backup", "SaveManager")
@@ -1025,12 +1222,16 @@ func _restore_backup_if_exists() -> bool:
# Validate backup file size before attempting restore # Validate backup file size before attempting restore
var backup_file: FileAccess = FileAccess.open(backup_path, FileAccess.READ) var backup_file: FileAccess = FileAccess.open(backup_path, FileAccess.READ)
if backup_file == null: if backup_file == null:
DebugManager.log_error("Failed to open backup file for reading", "SaveManager") DebugManager.log_error(
"Failed to open backup file for reading", "SaveManager"
)
return false return false
var backup_size: int = backup_file.get_length() var backup_size: int = backup_file.get_length()
if backup_size > MAX_FILE_SIZE: if backup_size > MAX_FILE_SIZE:
DebugManager.log_error("Backup file too large: %d bytes" % backup_size, "SaveManager") DebugManager.log_error(
"Backup file too large: %d bytes" % backup_size, "SaveManager"
)
backup_file.close() backup_file.close()
return false return false
@@ -1045,13 +1246,17 @@ func _restore_backup_if_exists() -> bool:
# Create new save file from backup # Create new save file from backup
var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE) var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if original == null: if original == null:
DebugManager.log_error("Failed to create new save file from backup", "SaveManager") DebugManager.log_error(
"Failed to create new save file from backup", "SaveManager"
)
return false return false
original.store_var(backup_data) original.store_var(backup_data)
original.close() original.close()
DebugManager.log_info("Backup restored successfully to main save file", "SaveManager") DebugManager.log_info(
"Backup restored successfully to main save file", "SaveManager"
)
# Note: The restored file will be loaded on the next game restart # Note: The restored file will be loaded on the next game restart
# We don't recursively load here to prevent infinite loops # We don't recursively load here to prevent infinite loops
return true return true

View File

@@ -10,7 +10,10 @@ const MAX_SETTING_STRING_LENGTH = 10 # Max length for string settings like lang
var settings: Dictionary = {} var settings: Dictionary = {}
var default_settings: Dictionary = { var default_settings: Dictionary = {
"master_volume": 0.50, "music_volume": 0.40, "sfx_volume": 0.50, "language": "en" "master_volume": 0.50,
"music_volume": 0.40,
"sfx_volume": 0.50,
"language": "en"
} }
var languages_data: Dictionary = {} var languages_data: Dictionary = {}
@@ -32,7 +35,9 @@ func load_settings() -> void:
if load_result == OK: if load_result == OK:
for key in default_settings.keys(): for key in default_settings.keys():
var loaded_value = config.get_value("settings", key, default_settings[key]) var loaded_value = config.get_value(
"settings", key, default_settings[key]
)
# Validate loaded settings before applying # Validate loaded settings before applying
if _validate_setting_value(key, loaded_value): if _validate_setting_value(key, loaded_value):
settings[key] = loaded_value settings[key] = loaded_value
@@ -45,10 +50,15 @@ func load_settings() -> void:
"SettingsManager" "SettingsManager"
) )
settings[key] = default_settings[key] settings[key] = default_settings[key]
DebugManager.log_info("Settings loaded: " + str(settings), "SettingsManager") DebugManager.log_info(
"Settings loaded: " + str(settings), "SettingsManager"
)
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"No settings file found (Error code: %d), using defaults" % load_result, (
"No settings file found (Error code: %d), using defaults"
% load_result
),
"SettingsManager" "SettingsManager"
) )
settings = default_settings.duplicate() settings = default_settings.duplicate()
@@ -58,7 +68,9 @@ func load_settings() -> void:
func _apply_all_settings(): func _apply_all_settings():
DebugManager.log_info("Applying settings: " + str(settings), "SettingsManager") DebugManager.log_info(
"Applying settings: " + str(settings), "SettingsManager"
)
# Apply language setting # Apply language setting
if "language" in settings: if "language" in settings:
@@ -70,24 +82,33 @@ func _apply_all_settings():
var sfx_bus = AudioServer.get_bus_index("SFX") var sfx_bus = AudioServer.get_bus_index("SFX")
if master_bus >= 0 and "master_volume" in settings: if master_bus >= 0 and "master_volume" in settings:
AudioServer.set_bus_volume_db(master_bus, linear_to_db(settings["master_volume"])) AudioServer.set_bus_volume_db(
master_bus, linear_to_db(settings["master_volume"])
)
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"Master audio bus not found or master_volume setting missing", "SettingsManager" "Master audio bus not found or master_volume setting missing",
"SettingsManager"
) )
if music_bus >= 0 and "music_volume" in settings: if music_bus >= 0 and "music_volume" in settings:
AudioServer.set_bus_volume_db(music_bus, linear_to_db(settings["music_volume"])) AudioServer.set_bus_volume_db(
music_bus, linear_to_db(settings["music_volume"])
)
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"Music audio bus not found or music_volume setting missing", "SettingsManager" "Music audio bus not found or music_volume setting missing",
"SettingsManager"
) )
if sfx_bus >= 0 and "sfx_volume" in settings: if sfx_bus >= 0 and "sfx_volume" in settings:
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(settings["sfx_volume"])) AudioServer.set_bus_volume_db(
sfx_bus, linear_to_db(settings["sfx_volume"])
)
else: else:
DebugManager.log_warn( DebugManager.log_warn(
"SFX audio bus not found or sfx_volume setting missing", "SettingsManager" "SFX audio bus not found or sfx_volume setting missing",
"SettingsManager"
) )
@@ -99,7 +120,8 @@ func save_settings():
var save_result = config.save(SETTINGS_FILE) var save_result = config.save(SETTINGS_FILE)
if save_result != OK: if save_result != OK:
DebugManager.log_error( DebugManager.log_error(
"Failed to save settings (Error code: %d)" % save_result, "SettingsManager" "Failed to save settings (Error code: %d)" % save_result,
"SettingsManager"
) )
return false return false
@@ -119,7 +141,8 @@ func set_setting(key: String, value) -> bool:
# Validate value type and range based on key # Validate value type and range based on key
if not _validate_setting_value(key, value): if not _validate_setting_value(key, value):
DebugManager.log_error( DebugManager.log_error(
"Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager" "Invalid value for setting '%s': %s" % [key, str(value)],
"SettingsManager"
) )
return false return false
@@ -157,7 +180,8 @@ func _validate_volume_setting(key: String, value) -> bool:
# Check for NaN and infinity # Check for NaN and infinity
if is_nan(float_value) or is_inf(float_value): if is_nan(float_value) or is_inf(float_value):
DebugManager.log_warn( DebugManager.log_warn(
"Invalid float value for %s: %s" % [key, str(value)], "SettingsManager" "Invalid float value for %s: %s" % [key, str(value)],
"SettingsManager"
) )
return false return false
# Range validation # Range validation
@@ -170,7 +194,8 @@ func _validate_language_setting(value) -> bool:
# Prevent extremely long strings # Prevent extremely long strings
if value.length() > MAX_SETTING_STRING_LENGTH: if value.length() > MAX_SETTING_STRING_LENGTH:
DebugManager.log_warn( DebugManager.log_warn(
"Language code too long: %d characters" % value.length(), "SettingsManager" "Language code too long: %d characters" % value.length(),
"SettingsManager"
) )
return false return false
# Check for valid characters (alphanumeric and common separators only) # Check for valid characters (alphanumeric and common separators only)
@@ -178,11 +203,15 @@ func _validate_language_setting(value) -> bool:
regex.compile("^[a-zA-Z0-9_-]+$") regex.compile("^[a-zA-Z0-9_-]+$")
if not regex.search(value): if not regex.search(value):
DebugManager.log_warn( DebugManager.log_warn(
"Language code contains invalid characters: %s" % value, "SettingsManager" "Language code contains invalid characters: %s" % value,
"SettingsManager"
) )
return false return false
# Check if language is supported # Check if language is supported
if languages_data.has("languages") and languages_data.languages is Dictionary: if (
languages_data.has("languages")
and languages_data.languages is Dictionary
):
return value in languages_data.languages return value in languages_data.languages
# Fallback to basic validation if languages not loaded # Fallback to basic validation if languages not loaded
return value in ["en", "ru"] return value in ["en", "ru"]
@@ -192,7 +221,9 @@ func _validate_default_setting(key: String, value) -> bool:
# Default validation: accept if type matches default setting type # Default validation: accept if type matches default setting type
var default_value = default_settings.get(key) var default_value = default_settings.get(key)
if default_value == null: if default_value == null:
DebugManager.log_warn("Unknown setting key in validation: %s" % key, "SettingsManager") DebugManager.log_warn(
"Unknown setting key in validation: %s" % key, "SettingsManager"
)
return false return false
return typeof(value) == typeof(default_value) return typeof(value) == typeof(default_value)
@@ -210,7 +241,9 @@ func _apply_setting_side_effect(key: String, value) -> void:
AudioManager.update_music_volume(value) AudioManager.update_music_volume(value)
"sfx_volume": "sfx_volume":
if AudioServer.get_bus_index("SFX") >= 0: if AudioServer.get_bus_index("SFX") >= 0:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), linear_to_db(value)) AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("SFX"), linear_to_db(value)
)
func load_languages(): func load_languages():
@@ -230,7 +263,11 @@ func load_languages():
languages_data = parsed_data languages_data = parsed_data
DebugManager.log_info( DebugManager.log_info(
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager" (
"Languages loaded successfully: "
+ str(languages_data.languages.keys())
),
"SettingsManager"
) )
@@ -239,7 +276,8 @@ func _load_languages_file() -> String:
if not file: if not file:
var error_code = FileAccess.get_open_error() var error_code = FileAccess.get_open_error()
DebugManager.log_error( DebugManager.log_error(
"Could not open languages.json (Error code: %d)" % error_code, "SettingsManager" "Could not open languages.json (Error code: %d)" % error_code,
"SettingsManager"
) )
return "" return ""
@@ -247,14 +285,19 @@ func _load_languages_file() -> String:
var file_size = file.get_length() var file_size = file.get_length()
if file_size > MAX_JSON_FILE_SIZE: if file_size > MAX_JSON_FILE_SIZE:
DebugManager.log_error( DebugManager.log_error(
"Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE], (
"Languages.json file too large: %d bytes (max %d)"
% [file_size, MAX_JSON_FILE_SIZE]
),
"SettingsManager" "SettingsManager"
) )
file.close() file.close()
return "" return ""
if file_size == 0: if file_size == 0:
DebugManager.log_error("Languages.json file is empty", "SettingsManager") DebugManager.log_error(
"Languages.json file is empty", "SettingsManager"
)
file.close() file.close()
return "" return ""
@@ -264,7 +307,8 @@ func _load_languages_file() -> String:
if file_error != OK: if file_error != OK:
DebugManager.log_error( DebugManager.log_error(
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager" "Error reading languages.json (Error code: %d)" % file_error,
"SettingsManager"
) )
return "" return ""
@@ -274,27 +318,36 @@ func _load_languages_file() -> String:
func _parse_languages_json(json_string: String) -> Dictionary: func _parse_languages_json(json_string: String) -> Dictionary:
# Validate the JSON string is not empty # Validate the JSON string is not empty
if json_string.is_empty(): if json_string.is_empty():
DebugManager.log_error("Languages.json contains empty content", "SettingsManager") DebugManager.log_error(
"Languages.json contains empty content", "SettingsManager"
)
return {} return {}
var json = JSON.new() var json = JSON.new()
var parse_result = json.parse(json_string) var parse_result = json.parse(json_string)
if parse_result != OK: if parse_result != OK:
DebugManager.log_error( DebugManager.log_error(
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string], (
"JSON parsing failed at line %d: %s"
% [json.error_line, json.error_string]
),
"SettingsManager" "SettingsManager"
) )
return {} return {}
if not json.data or not json.data is Dictionary: if not json.data or not json.data is Dictionary:
DebugManager.log_error("Invalid JSON data structure in languages.json", "SettingsManager") DebugManager.log_error(
"Invalid JSON data structure in languages.json", "SettingsManager"
)
return {} return {}
return json.data return json.data
func _load_default_languages_with_fallback(reason: String): func _load_default_languages_with_fallback(reason: String):
DebugManager.log_warn("Loading default languages due to: " + reason, "SettingsManager") DebugManager.log_warn(
"Loading default languages due to: " + reason, "SettingsManager"
)
_load_default_languages() _load_default_languages()
@@ -302,9 +355,14 @@ func _load_default_languages():
# Fallback language data when JSON file fails to load # Fallback language data when JSON file fails to load
languages_data = { languages_data = {
"languages": "languages":
{"en": {"name": "English", "flag": "🇺🇸"}, "ru": {"name": "Русский", "flag": "🇷🇺"}} {
"en": {"name": "English", "flag": "🇺🇸"},
"ru": {"name": "Русский", "flag": "🇷🇺"}
} }
DebugManager.log_info("Default languages loaded as fallback", "SettingsManager") }
DebugManager.log_info(
"Default languages loaded as fallback", "SettingsManager"
)
func get_languages_data(): func get_languages_data():
@@ -312,15 +370,21 @@ func get_languages_data():
func reset_settings_to_defaults() -> void: func reset_settings_to_defaults() -> void:
DebugManager.log_info("Resetting all settings to defaults", "SettingsManager") DebugManager.log_info(
"Resetting all settings to defaults", "SettingsManager"
)
for key in default_settings.keys(): for key in default_settings.keys():
settings[key] = default_settings[key] settings[key] = default_settings[key]
_apply_setting_side_effect(key, settings[key]) _apply_setting_side_effect(key, settings[key])
var save_success = save_settings() var save_success = save_settings()
if save_success: if save_success:
DebugManager.log_info("Settings reset completed successfully", "SettingsManager") DebugManager.log_info(
"Settings reset completed successfully", "SettingsManager"
)
else: else:
DebugManager.log_error("Failed to save reset settings", "SettingsManager") DebugManager.log_error(
"Failed to save reset settings", "SettingsManager"
)
func _validate_languages_structure(data: Dictionary) -> bool: func _validate_languages_structure(data: Dictionary) -> bool:
@@ -344,16 +408,22 @@ func _validate_languages_structure(data: Dictionary) -> bool:
func _validate_languages_root_structure(data: Dictionary) -> bool: func _validate_languages_root_structure(data: Dictionary) -> bool:
"""Validate the root structure of languages data""" """Validate the root structure of languages data"""
if not data.has("languages"): if not data.has("languages"):
DebugManager.log_error("Languages.json missing 'languages' key", "SettingsManager") DebugManager.log_error(
"Languages.json missing 'languages' key", "SettingsManager"
)
return false return false
var languages = data["languages"] var languages = data["languages"]
if not languages is Dictionary: if not languages is Dictionary:
DebugManager.log_error("'languages' is not a dictionary", "SettingsManager") DebugManager.log_error(
"'languages' is not a dictionary", "SettingsManager"
)
return false return false
if languages.is_empty(): if languages.is_empty():
DebugManager.log_error("Languages dictionary is empty", "SettingsManager") DebugManager.log_error(
"Languages dictionary is empty", "SettingsManager"
)
return false return false
return true return true
@@ -367,28 +437,35 @@ func _validate_individual_languages(languages: Dictionary) -> bool:
return true return true
func _validate_single_language_entry(lang_code: Variant, lang_data: Variant) -> bool: func _validate_single_language_entry(
lang_code: Variant, lang_data: Variant
) -> bool:
"""Validate a single language entry""" """Validate a single language entry"""
if not lang_code is String: if not lang_code is String:
DebugManager.log_error( DebugManager.log_error(
"Language code is not a string: %s" % str(lang_code), "SettingsManager" "Language code is not a string: %s" % str(lang_code),
"SettingsManager"
) )
return false return false
if lang_code.length() > MAX_SETTING_STRING_LENGTH: if lang_code.length() > MAX_SETTING_STRING_LENGTH:
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager") DebugManager.log_error(
"Language code too long: %s" % lang_code, "SettingsManager"
)
return false return false
if not lang_data is Dictionary: if not lang_data is Dictionary:
DebugManager.log_error( DebugManager.log_error(
"Language data for '%s' is not a dictionary" % lang_code, "SettingsManager" "Language data for '%s' is not a dictionary" % lang_code,
"SettingsManager"
) )
return false return false
# Validate required fields in language data # Validate required fields in language data
if not lang_data.has("name") or not lang_data["name"] is String: if not lang_data.has("name") or not lang_data["name"] is String:
DebugManager.log_error( DebugManager.log_error(
"Language '%s' missing valid 'name' field" % lang_code, "SettingsManager" "Language '%s' missing valid 'name' field" % lang_code,
"SettingsManager"
) )
return false return false

View File

@@ -69,12 +69,17 @@ func test_basic_functionality():
# Test that AudioManager has expected methods # Test that AudioManager has expected methods
var expected_methods = ["update_music_volume", "play_ui_click"] var expected_methods = ["update_music_volume", "play_ui_click"]
TestHelperClass.assert_has_methods(audio_manager, expected_methods, "AudioManager methods") TestHelperClass.assert_has_methods(
audio_manager, expected_methods, "AudioManager methods"
)
# Test that AudioManager has expected constants # Test that AudioManager has expected constants
TestHelperClass.assert_true("MUSIC_PATH" in audio_manager, "MUSIC_PATH constant exists")
TestHelperClass.assert_true( TestHelperClass.assert_true(
"UI_CLICK_SOUND_PATH" in audio_manager, "UI_CLICK_SOUND_PATH constant exists" "MUSIC_PATH" in audio_manager, "MUSIC_PATH constant exists"
)
TestHelperClass.assert_true(
"UI_CLICK_SOUND_PATH" in audio_manager,
"UI_CLICK_SOUND_PATH constant exists"
) )
@@ -85,9 +90,12 @@ func test_audio_constants():
var music_path = audio_manager.MUSIC_PATH var music_path = audio_manager.MUSIC_PATH
var click_path = audio_manager.UI_CLICK_SOUND_PATH var click_path = audio_manager.UI_CLICK_SOUND_PATH
TestHelperClass.assert_true(music_path.begins_with("res://"), "Music path uses res:// protocol")
TestHelperClass.assert_true( TestHelperClass.assert_true(
click_path.begins_with("res://"), "Click sound path uses res:// protocol" music_path.begins_with("res://"), "Music path uses res:// protocol"
)
TestHelperClass.assert_true(
click_path.begins_with("res://"),
"Click sound path uses res:// protocol"
) )
# Test file extensions # Test file extensions
@@ -101,11 +109,17 @@ func test_audio_constants():
if click_path.ends_with(ext): if click_path.ends_with(ext):
click_has_valid_ext = true click_has_valid_ext = true
TestHelperClass.assert_true(music_has_valid_ext, "Music file has valid audio extension") TestHelperClass.assert_true(
TestHelperClass.assert_true(click_has_valid_ext, "Click sound has valid audio extension") music_has_valid_ext, "Music file has valid audio extension"
)
TestHelperClass.assert_true(
click_has_valid_ext, "Click sound has valid audio extension"
)
# Test that audio files exist # Test that audio files exist
TestHelperClass.assert_true(ResourceLoader.exists(music_path), "Music file exists at path") TestHelperClass.assert_true(
ResourceLoader.exists(music_path), "Music file exists at path"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
ResourceLoader.exists(click_path), "Click sound file exists at path" ResourceLoader.exists(click_path), "Click sound file exists at path"
) )
@@ -115,9 +129,12 @@ func test_audio_player_initialization():
TestHelperClass.print_step("Audio Player Initialization") TestHelperClass.print_step("Audio Player Initialization")
# Test music player initialization # Test music player initialization
TestHelperClass.assert_not_null(audio_manager.music_player, "Music player is initialized") TestHelperClass.assert_not_null(
audio_manager.music_player, "Music player is initialized"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
audio_manager.music_player is AudioStreamPlayer, "Music player is AudioStreamPlayer type" audio_manager.music_player is AudioStreamPlayer,
"Music player is AudioStreamPlayer type"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
audio_manager.music_player.get_parent() == audio_manager, audio_manager.music_player.get_parent() == audio_manager,
@@ -125,7 +142,9 @@ func test_audio_player_initialization():
) )
# Test UI click player initialization # Test UI click player initialization
TestHelperClass.assert_not_null(audio_manager.ui_click_player, "UI click player is initialized") TestHelperClass.assert_not_null(
audio_manager.ui_click_player, "UI click player is initialized"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
audio_manager.ui_click_player is AudioStreamPlayer, audio_manager.ui_click_player is AudioStreamPlayer,
"UI click player is AudioStreamPlayer type" "UI click player is AudioStreamPlayer type"
@@ -137,10 +156,14 @@ func test_audio_player_initialization():
# Test audio bus assignment # Test audio bus assignment
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"Music", audio_manager.music_player.bus, "Music player assigned to Music bus" "Music",
audio_manager.music_player.bus,
"Music player assigned to Music bus"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"SFX", audio_manager.ui_click_player.bus, "UI click player assigned to SFX bus" "SFX",
audio_manager.ui_click_player.bus,
"UI click player assigned to SFX bus"
) )
@@ -148,26 +171,38 @@ func test_stream_loading_and_validation():
TestHelperClass.print_step("Stream Loading and Validation") TestHelperClass.print_step("Stream Loading and Validation")
# Test music stream loading # Test music stream loading
TestHelperClass.assert_not_null(audio_manager.music_player.stream, "Music stream is loaded") TestHelperClass.assert_not_null(
audio_manager.music_player.stream, "Music stream is loaded"
)
if audio_manager.music_player.stream: if audio_manager.music_player.stream:
TestHelperClass.assert_true( TestHelperClass.assert_true(
audio_manager.music_player.stream is AudioStream, "Music stream is AudioStream type" audio_manager.music_player.stream is AudioStream,
"Music stream is AudioStream type"
) )
# Test click stream loading # Test click stream loading
TestHelperClass.assert_not_null(audio_manager.click_stream, "Click stream is loaded") TestHelperClass.assert_not_null(
audio_manager.click_stream, "Click stream is loaded"
)
if audio_manager.click_stream: if audio_manager.click_stream:
TestHelperClass.assert_true( TestHelperClass.assert_true(
audio_manager.click_stream is AudioStream, "Click stream is AudioStream type" audio_manager.click_stream is AudioStream,
"Click stream is AudioStream type"
) )
# Test stream resource loading directly # Test stream resource loading directly
var loaded_music = load(audio_manager.MUSIC_PATH) var loaded_music = load(audio_manager.MUSIC_PATH)
TestHelperClass.assert_not_null(loaded_music, "Music resource loads successfully") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(loaded_music is AudioStream, "Loaded music is AudioStream type") loaded_music, "Music resource loads successfully"
)
TestHelperClass.assert_true(
loaded_music is AudioStream, "Loaded music is AudioStream type"
)
var loaded_click = load(audio_manager.UI_CLICK_SOUND_PATH) var loaded_click = load(audio_manager.UI_CLICK_SOUND_PATH)
TestHelperClass.assert_not_null(loaded_click, "Click resource loads successfully") TestHelperClass.assert_not_null(
loaded_click, "Click resource loads successfully"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
loaded_click is AudioStream, "Loaded click sound is AudioStream type" loaded_click is AudioStream, "Loaded click sound is AudioStream type"
) )
@@ -186,7 +221,9 @@ func test_audio_bus_configuration():
# Test player bus assignments match actual AudioServer buses # Test player bus assignments match actual AudioServer buses
if music_bus_index >= 0: if music_bus_index >= 0:
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"Music", audio_manager.music_player.bus, "Music player correctly assigned to Music bus" "Music",
audio_manager.music_player.bus,
"Music player correctly assigned to Music bus"
) )
if sfx_bus_index >= 0: if sfx_bus_index >= 0:
@@ -208,20 +245,27 @@ func test_volume_management():
# Test volume update to valid range # Test volume update to valid range
audio_manager.update_music_volume(0.5) audio_manager.update_music_volume(0.5)
TestHelperClass.assert_float_equal( TestHelperClass.assert_float_equal(
linear_to_db(0.5), audio_manager.music_player.volume_db, 0.001, "Music volume set correctly" linear_to_db(0.5),
audio_manager.music_player.volume_db,
0.001,
"Music volume set correctly"
) )
# Test volume update to zero (should stop music) # Test volume update to zero (should stop music)
audio_manager.update_music_volume(0.0) audio_manager.update_music_volume(0.0)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
linear_to_db(0.0), audio_manager.music_player.volume_db, "Zero volume set correctly" linear_to_db(0.0),
audio_manager.music_player.volume_db,
"Zero volume set correctly"
) )
# Note: We don't test playing state as it depends on initialization conditions # Note: We don't test playing state as it depends on initialization conditions
# Test volume update to maximum # Test volume update to maximum
audio_manager.update_music_volume(1.0) audio_manager.update_music_volume(1.0)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
linear_to_db(1.0), audio_manager.music_player.volume_db, "Maximum volume set correctly" linear_to_db(1.0),
audio_manager.music_player.volume_db,
"Maximum volume set correctly"
) )
# Test volume range validation # Test volume range validation
@@ -248,7 +292,8 @@ func test_music_playback_control():
audio_manager.music_player, "Music player exists for playback testing" audio_manager.music_player, "Music player exists for playback testing"
) )
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
audio_manager.music_player.stream, "Music player has stream for playback testing" audio_manager.music_player.stream,
"Music player has stream for playback testing"
) )
# Test playback state management # Test playback state management
@@ -279,8 +324,12 @@ func test_ui_sound_effects():
TestHelperClass.print_step("UI Sound Effects") TestHelperClass.print_step("UI Sound Effects")
# Test UI click functionality # Test UI click functionality
TestHelperClass.assert_not_null(audio_manager.ui_click_player, "UI click player exists") TestHelperClass.assert_not_null(
TestHelperClass.assert_not_null(audio_manager.click_stream, "Click stream is loaded") audio_manager.ui_click_player, "UI click player exists"
)
TestHelperClass.assert_not_null(
audio_manager.click_stream, "Click stream is loaded"
)
# Test that play_ui_click can be called safely # Test that play_ui_click can be called safely
var original_stream = audio_manager.ui_click_player.stream var original_stream = audio_manager.ui_click_player.stream
@@ -296,7 +345,9 @@ func test_ui_sound_effects():
# Test multiple rapid clicks (should not cause errors) # Test multiple rapid clicks (should not cause errors)
for i in range(3): for i in range(3):
audio_manager.play_ui_click() audio_manager.play_ui_click()
TestHelperClass.assert_true(true, "Rapid click %d handled safely" % (i + 1)) TestHelperClass.assert_true(
true, "Rapid click %d handled safely" % (i + 1)
)
# Test click with null stream # Test click with null stream
var backup_stream = audio_manager.click_stream var backup_stream = audio_manager.click_stream
@@ -315,7 +366,9 @@ func test_stream_loop_configuration():
if music_stream is AudioStreamWAV: if music_stream is AudioStreamWAV:
# For WAV files, check loop mode # For WAV files, check loop mode
var has_loop_mode = "loop_mode" in music_stream var has_loop_mode = "loop_mode" in music_stream
TestHelperClass.assert_true(has_loop_mode, "WAV stream has loop_mode property") TestHelperClass.assert_true(
has_loop_mode, "WAV stream has loop_mode property"
)
if has_loop_mode: if has_loop_mode:
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
AudioStreamWAV.LOOP_FORWARD, AudioStreamWAV.LOOP_FORWARD,
@@ -325,12 +378,18 @@ func test_stream_loop_configuration():
elif music_stream is AudioStreamOggVorbis: elif music_stream is AudioStreamOggVorbis:
# For OGG files, check loop property # For OGG files, check loop property
var has_loop = "loop" in music_stream var has_loop = "loop" in music_stream
TestHelperClass.assert_true(has_loop, "OGG stream has loop property") TestHelperClass.assert_true(
has_loop, "OGG stream has loop property"
)
if has_loop: if has_loop:
TestHelperClass.assert_true(music_stream.loop, "OGG stream loop enabled") TestHelperClass.assert_true(
music_stream.loop, "OGG stream loop enabled"
)
# Test loop configuration for different stream types # Test loop configuration for different stream types
TestHelperClass.assert_true(true, "Stream loop configuration tested based on type") TestHelperClass.assert_true(
true, "Stream loop configuration tested based on type"
)
func test_error_handling(): func test_error_handling():
@@ -341,15 +400,18 @@ func test_error_handling():
# Test that AudioManager initializes even with potential issues # Test that AudioManager initializes even with potential issues
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
audio_manager, "AudioManager initializes despite potential resource issues" audio_manager,
"AudioManager initializes despite potential resource issues"
) )
# Test that players are still created even if streams fail to load # Test that players are still created even if streams fail to load
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
audio_manager.music_player, "Music player created regardless of stream loading" audio_manager.music_player,
"Music player created regardless of stream loading"
) )
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
audio_manager.ui_click_player, "UI click player created regardless of stream loading" audio_manager.ui_click_player,
"UI click player created regardless of stream loading"
) )
# Test null stream handling in play_ui_click # Test null stream handling in play_ui_click
@@ -358,7 +420,9 @@ func test_error_handling():
# This should not crash # This should not crash
audio_manager.play_ui_click() audio_manager.play_ui_click()
TestHelperClass.assert_true(true, "play_ui_click handles null stream gracefully") TestHelperClass.assert_true(
true, "play_ui_click handles null stream gracefully"
)
# Restore original stream # Restore original stream
audio_manager.click_stream = original_click_stream audio_manager.click_stream = original_click_stream

View File

@@ -57,7 +57,9 @@ func test_basic_functionality():
# Test that GameManager has expected properties # Test that GameManager has expected properties
TestHelperClass.assert_has_properties( TestHelperClass.assert_has_properties(
game_manager, ["pending_gameplay_mode", "is_changing_scene"], "GameManager properties" game_manager,
["pending_gameplay_mode", "is_changing_scene"],
"GameManager properties"
) )
# Test that GameManager has expected methods # Test that GameManager has expected methods
@@ -70,13 +72,19 @@ func test_basic_functionality():
"save_game", "save_game",
"exit_to_main_menu" "exit_to_main_menu"
] ]
TestHelperClass.assert_has_methods(game_manager, expected_methods, "GameManager methods") TestHelperClass.assert_has_methods(
game_manager, expected_methods, "GameManager methods"
)
# Test initial state # Test initial state
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"match3", game_manager.pending_gameplay_mode, "Default pending gameplay mode" "match3",
game_manager.pending_gameplay_mode,
"Default pending gameplay mode"
)
TestHelperClass.assert_false(
game_manager.is_changing_scene, "Initial scene change flag"
) )
TestHelperClass.assert_false(game_manager.is_changing_scene, "Initial scene change flag")
func test_scene_constants(): func test_scene_constants():
@@ -97,15 +105,23 @@ func test_scene_constants():
TestHelperClass.assert_true( TestHelperClass.assert_true(
game_path.begins_with("res://"), "Game scene path uses res:// protocol" game_path.begins_with("res://"), "Game scene path uses res:// protocol"
) )
TestHelperClass.assert_true(game_path.ends_with(".tscn"), "Game scene path has .tscn extension") TestHelperClass.assert_true(
game_path.ends_with(".tscn"), "Game scene path has .tscn extension"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
main_path.begins_with("res://"), "Main scene path uses res:// protocol" main_path.begins_with("res://"), "Main scene path uses res:// protocol"
) )
TestHelperClass.assert_true(main_path.ends_with(".tscn"), "Main scene path has .tscn extension") TestHelperClass.assert_true(
main_path.ends_with(".tscn"), "Main scene path has .tscn extension"
)
# Test that scene files exist # Test that scene files exist
TestHelperClass.assert_true(ResourceLoader.exists(game_path), "Game scene file exists at path") TestHelperClass.assert_true(
TestHelperClass.assert_true(ResourceLoader.exists(main_path), "Main scene file exists at path") ResourceLoader.exists(game_path), "Game scene file exists at path"
)
TestHelperClass.assert_true(
ResourceLoader.exists(main_path), "Main scene file exists at path"
)
func test_input_validation(): func test_input_validation():
@@ -118,38 +134,50 @@ func test_input_validation():
# Test empty string validation # Test empty string validation
game_manager.start_game_with_mode("") game_manager.start_game_with_mode("")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Empty string mode rejected" original_mode,
game_manager.pending_gameplay_mode,
"Empty string mode rejected"
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after empty mode" game_manager.is_changing_scene,
"Scene change flag unchanged after empty mode"
) )
# Test null validation - GameManager expects String, so this tests the type safety # Test null validation - GameManager expects String, so this tests the type safety
# Note: In Godot 4.4, passing null to String parameter causes script error as expected # Note: In Godot 4.4, passing null to String parameter causes script error as expected
# The function properly validates empty strings instead # The function properly validates empty strings instead
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty string test" original_mode,
game_manager.pending_gameplay_mode,
"Mode preserved after empty string test"
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after validation tests" game_manager.is_changing_scene,
"Scene change flag unchanged after validation tests"
) )
# Test invalid mode validation # Test invalid mode validation
game_manager.start_game_with_mode("invalid_mode") game_manager.start_game_with_mode("invalid_mode")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Invalid mode rejected" original_mode,
game_manager.pending_gameplay_mode,
"Invalid mode rejected"
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after invalid mode" game_manager.is_changing_scene,
"Scene change flag unchanged after invalid mode"
) )
# Test case sensitivity # Test case sensitivity
game_manager.start_game_with_mode("MATCH3") game_manager.start_game_with_mode("MATCH3")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Case-sensitive mode validation" original_mode,
game_manager.pending_gameplay_mode,
"Case-sensitive mode validation"
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after wrong case" game_manager.is_changing_scene,
"Scene change flag unchanged after wrong case"
) )
@@ -165,14 +193,19 @@ func test_race_condition_protection():
# Verify second request was rejected # Verify second request was rejected
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Concurrent scene change blocked" original_mode,
game_manager.pending_gameplay_mode,
"Concurrent scene change blocked"
)
TestHelperClass.assert_true(
game_manager.is_changing_scene, "Scene change flag preserved"
) )
TestHelperClass.assert_true(game_manager.is_changing_scene, "Scene change flag preserved")
# Test exit to main menu during scene change # Test exit to main menu during scene change
game_manager.exit_to_main_menu() game_manager.exit_to_main_menu()
TestHelperClass.assert_true( TestHelperClass.assert_true(
game_manager.is_changing_scene, "Exit request blocked during scene change" game_manager.is_changing_scene,
"Exit request blocked during scene change"
) )
# Reset state # Reset state
@@ -191,13 +224,17 @@ func test_gameplay_mode_validation():
# Create a temporary mock to test validation # Create a temporary mock to test validation
var test_mode_valid = mode in ["match3", "clickomania"] var test_mode_valid = mode in ["match3", "clickomania"]
TestHelperClass.assert_true(test_mode_valid, "Valid mode accepted: " + mode) TestHelperClass.assert_true(
test_mode_valid, "Valid mode accepted: " + mode
)
# Test whitelist enforcement # Test whitelist enforcement
var invalid_modes = ["puzzle", "arcade", "adventure", "rpg", "action"] var invalid_modes = ["puzzle", "arcade", "adventure", "rpg", "action"]
for mode in invalid_modes: for mode in invalid_modes:
var test_mode_invalid = not (mode in ["match3", "clickomania"]) var test_mode_invalid = not (mode in ["match3", "clickomania"])
TestHelperClass.assert_true(test_mode_invalid, "Invalid mode rejected: " + mode) TestHelperClass.assert_true(
test_mode_invalid, "Invalid mode rejected: " + mode
)
func test_scene_transition_safety(): func test_scene_transition_safety():
@@ -209,12 +246,20 @@ func test_scene_transition_safety():
# Test scene resource loading # Test scene resource loading
var game_scene = load(game_scene_path) var game_scene = load(game_scene_path)
TestHelperClass.assert_not_null(game_scene, "Game scene resource loads successfully") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(game_scene is PackedScene, "Game scene is PackedScene type") game_scene, "Game scene resource loads successfully"
)
TestHelperClass.assert_true(
game_scene is PackedScene, "Game scene is PackedScene type"
)
var main_scene = load(main_scene_path) var main_scene = load(main_scene_path)
TestHelperClass.assert_not_null(main_scene, "Main scene resource loads successfully") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(main_scene is PackedScene, "Main scene is PackedScene type") main_scene, "Main scene resource loads successfully"
)
TestHelperClass.assert_true(
main_scene is PackedScene, "Main scene is PackedScene type"
)
# Test that current scene exists # Test that current scene exists
TestHelperClass.assert_not_null(current_scene, "Current scene exists") TestHelperClass.assert_not_null(current_scene, "Current scene exists")
@@ -234,10 +279,14 @@ func test_error_handling():
# Verify state preservation after invalid inputs # Verify state preservation after invalid inputs
game_manager.start_game_with_mode("") game_manager.start_game_with_mode("")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_changing, game_manager.is_changing_scene, "State preserved after empty mode error" original_changing,
game_manager.is_changing_scene,
"State preserved after empty mode error"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty mode error" original_mode,
game_manager.pending_gameplay_mode,
"Mode preserved after empty mode error"
) )
game_manager.start_game_with_mode("invalid") game_manager.start_game_with_mode("invalid")
@@ -247,7 +296,9 @@ func test_error_handling():
"State preserved after invalid mode error" "State preserved after invalid mode error"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after invalid mode error" original_mode,
game_manager.pending_gameplay_mode,
"Mode preserved after invalid mode error"
) )
@@ -263,9 +314,15 @@ func test_scene_method_validation():
var has_set_global_score = mock_scene.has_method("set_global_score") var has_set_global_score = mock_scene.has_method("set_global_score")
var has_get_global_score = mock_scene.has_method("get_global_score") var has_get_global_score = mock_scene.has_method("get_global_score")
TestHelperClass.assert_false(has_set_gameplay_mode, "Mock scene lacks set_gameplay_mode method") TestHelperClass.assert_false(
TestHelperClass.assert_false(has_set_global_score, "Mock scene lacks set_global_score method") has_set_gameplay_mode, "Mock scene lacks set_gameplay_mode method"
TestHelperClass.assert_false(has_get_global_score, "Mock scene lacks get_global_score method") )
TestHelperClass.assert_false(
has_set_global_score, "Mock scene lacks set_global_score method"
)
TestHelperClass.assert_false(
has_get_global_score, "Mock scene lacks get_global_score method"
)
# Clean up mock scene # Clean up mock scene
mock_scene.queue_free() mock_scene.queue_free()
@@ -284,7 +341,9 @@ func test_pending_mode_management():
# This simulates what would happen in start_game_with_mode # This simulates what would happen in start_game_with_mode
game_manager.pending_gameplay_mode = test_mode game_manager.pending_gameplay_mode = test_mode
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
test_mode, game_manager.pending_gameplay_mode, "Pending mode set correctly" test_mode,
game_manager.pending_gameplay_mode,
"Pending mode set correctly"
) )
# Test mode preservation during errors # Test mode preservation during errors

View File

@@ -51,7 +51,9 @@ func setup_test_environment():
# Load Match3 scene # Load Match3 scene
match3_scene = load("res://scenes/game/gameplays/Match3Gameplay.tscn") match3_scene = load("res://scenes/game/gameplays/Match3Gameplay.tscn")
TestHelperClass.assert_not_null(match3_scene, "Match3 scene loads successfully") TestHelperClass.assert_not_null(
match3_scene, "Match3 scene loads successfully"
)
# Create test viewport for isolated testing # Create test viewport for isolated testing
test_viewport = SubViewport.new() test_viewport = SubViewport.new()
@@ -62,7 +64,9 @@ func setup_test_environment():
if match3_scene: if match3_scene:
match3_instance = match3_scene.instantiate() match3_instance = match3_scene.instantiate()
test_viewport.add_child(match3_instance) test_viewport.add_child(match3_instance)
TestHelperClass.assert_not_null(match3_instance, "Match3 instance created successfully") TestHelperClass.assert_not_null(
match3_instance, "Match3 instance created successfully"
)
# Wait for initialization # Wait for initialization
await process_frame await process_frame
@@ -73,28 +77,44 @@ func test_basic_functionality():
TestHelperClass.print_step("Basic Functionality") TestHelperClass.print_step("Basic Functionality")
if not match3_instance: if not match3_instance:
TestHelperClass.assert_true(false, "Match3 instance not available for testing") TestHelperClass.assert_true(
false, "Match3 instance not available for testing"
)
return return
# Test that Match3 has expected properties # Test that Match3 has expected properties
var expected_properties = [ var expected_properties = [
"GRID_SIZE", "TILE_TYPES", "grid", "current_state", "selected_tile", "cursor_position" "GRID_SIZE",
"TILE_TYPES",
"grid",
"current_state",
"selected_tile",
"cursor_position"
] ]
for prop in expected_properties: for prop in expected_properties:
TestHelperClass.assert_true(prop in match3_instance, "Match3 has property: " + prop) TestHelperClass.assert_true(
prop in match3_instance, "Match3 has property: " + prop
)
# Test that Match3 has expected methods # Test that Match3 has expected methods
var expected_methods = [ var expected_methods = [
"_has_match_at", "_check_for_matches", "_get_match_line", "_clear_matches" "_has_match_at",
"_check_for_matches",
"_get_match_line",
"_clear_matches"
] ]
TestHelperClass.assert_has_methods(match3_instance, expected_methods, "Match3 gameplay methods") TestHelperClass.assert_has_methods(
match3_instance, expected_methods, "Match3 gameplay methods"
)
# Test signals # Test signals
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_signal("score_changed"), "Match3 has score_changed signal" match3_instance.has_signal("score_changed"),
"Match3 has score_changed signal"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_signal("grid_state_loaded"), "Match3 has grid_state_loaded signal" match3_instance.has_signal("grid_state_loaded"),
"Match3 has grid_state_loaded signal"
) )
@@ -105,26 +125,41 @@ func test_constants_and_safety_limits():
return return
# Test safety constants exist # Test safety constants exist
TestHelperClass.assert_true("MAX_GRID_SIZE" in match3_instance, "MAX_GRID_SIZE constant exists") TestHelperClass.assert_true(
"MAX_GRID_SIZE" in match3_instance, "MAX_GRID_SIZE constant exists"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
"MAX_TILE_TYPES" in match3_instance, "MAX_TILE_TYPES constant exists" "MAX_TILE_TYPES" in match3_instance, "MAX_TILE_TYPES constant exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
"MAX_CASCADE_ITERATIONS" in match3_instance, "MAX_CASCADE_ITERATIONS constant exists" "MAX_CASCADE_ITERATIONS" in match3_instance,
"MAX_CASCADE_ITERATIONS constant exists"
)
TestHelperClass.assert_true(
"MIN_GRID_SIZE" in match3_instance, "MIN_GRID_SIZE constant exists"
) )
TestHelperClass.assert_true("MIN_GRID_SIZE" in match3_instance, "MIN_GRID_SIZE constant exists")
TestHelperClass.assert_true( TestHelperClass.assert_true(
"MIN_TILE_TYPES" in match3_instance, "MIN_TILE_TYPES constant exists" "MIN_TILE_TYPES" in match3_instance, "MIN_TILE_TYPES constant exists"
) )
# Test safety limit values are reasonable # Test safety limit values are reasonable
TestHelperClass.assert_equal(15, match3_instance.MAX_GRID_SIZE, "MAX_GRID_SIZE is reasonable")
TestHelperClass.assert_equal(10, match3_instance.MAX_TILE_TYPES, "MAX_TILE_TYPES is reasonable")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
20, match3_instance.MAX_CASCADE_ITERATIONS, "MAX_CASCADE_ITERATIONS prevents infinite loops" 15, match3_instance.MAX_GRID_SIZE, "MAX_GRID_SIZE is reasonable"
)
TestHelperClass.assert_equal(
10, match3_instance.MAX_TILE_TYPES, "MAX_TILE_TYPES is reasonable"
)
TestHelperClass.assert_equal(
20,
match3_instance.MAX_CASCADE_ITERATIONS,
"MAX_CASCADE_ITERATIONS prevents infinite loops"
)
TestHelperClass.assert_equal(
3, match3_instance.MIN_GRID_SIZE, "MIN_GRID_SIZE is reasonable"
)
TestHelperClass.assert_equal(
3, match3_instance.MIN_TILE_TYPES, "MIN_TILE_TYPES is reasonable"
) )
TestHelperClass.assert_equal(3, match3_instance.MIN_GRID_SIZE, "MIN_GRID_SIZE is reasonable")
TestHelperClass.assert_equal(3, match3_instance.MIN_TILE_TYPES, "MIN_TILE_TYPES is reasonable")
# Test current values are within safety limits # Test current values are within safety limits
TestHelperClass.assert_in_range( TestHelperClass.assert_in_range(
@@ -148,13 +183,16 @@ func test_constants_and_safety_limits():
# Test timing constants # Test timing constants
TestHelperClass.assert_true( TestHelperClass.assert_true(
"CASCADE_WAIT_TIME" in match3_instance, "CASCADE_WAIT_TIME constant exists" "CASCADE_WAIT_TIME" in match3_instance,
"CASCADE_WAIT_TIME constant exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
"SWAP_ANIMATION_TIME" in match3_instance, "SWAP_ANIMATION_TIME constant exists" "SWAP_ANIMATION_TIME" in match3_instance,
"SWAP_ANIMATION_TIME constant exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
"TILE_DROP_WAIT_TIME" in match3_instance, "TILE_DROP_WAIT_TIME constant exists" "TILE_DROP_WAIT_TIME" in match3_instance,
"TILE_DROP_WAIT_TIME constant exists"
) )
@@ -165,8 +203,12 @@ func test_grid_initialization():
return return
# Test grid structure # Test grid structure
TestHelperClass.assert_not_null(match3_instance.grid, "Grid array is initialized") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(match3_instance.grid is Array, "Grid is Array type") match3_instance.grid, "Grid array is initialized"
)
TestHelperClass.assert_true(
match3_instance.grid is Array, "Grid is Array type"
)
# Test grid dimensions # Test grid dimensions
var expected_height = match3_instance.GRID_SIZE.y var expected_height = match3_instance.GRID_SIZE.y
@@ -180,7 +222,9 @@ func test_grid_initialization():
for y in range(match3_instance.grid.size()): for y in range(match3_instance.grid.size()):
if y < expected_height: if y < expected_height:
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
expected_width, match3_instance.grid[y].size(), "Grid row %d has correct width" % y expected_width,
match3_instance.grid[y].size(),
"Grid row %d has correct width" % y
) )
# Test tiles are properly instantiated # Test tiles are properly instantiated
@@ -195,10 +239,12 @@ func test_grid_initialization():
if tile and is_instance_valid(tile): if tile and is_instance_valid(tile):
valid_tile_count += 1 valid_tile_count += 1
TestHelperClass.assert_true( TestHelperClass.assert_true(
"tile_type" in tile, "Tile at (%d,%d) has tile_type property" % [x, y] "tile_type" in tile,
"Tile at (%d,%d) has tile_type property" % [x, y]
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
"grid_position" in tile, "Tile at (%d,%d) has grid_position property" % [x, y] "grid_position" in tile,
"Tile at (%d,%d) has grid_position property" % [x, y]
) )
# Test tile type is within valid range # Test tile type is within valid range
@@ -222,15 +268,24 @@ func test_grid_layout_calculation():
return return
# Test tile size calculation # Test tile size calculation
TestHelperClass.assert_true(match3_instance.tile_size > 0, "Tile size is positive")
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.tile_size <= 200, "Tile size is reasonable (not too large)" match3_instance.tile_size > 0, "Tile size is positive"
)
TestHelperClass.assert_true(
match3_instance.tile_size <= 200,
"Tile size is reasonable (not too large)"
) )
# Test grid offset # Test grid offset
TestHelperClass.assert_not_null(match3_instance.grid_offset, "Grid offset is set") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(match3_instance.grid_offset.x >= 0, "Grid offset X is non-negative") match3_instance.grid_offset, "Grid offset is set"
TestHelperClass.assert_true(match3_instance.grid_offset.y >= 0, "Grid offset Y is non-negative") )
TestHelperClass.assert_true(
match3_instance.grid_offset.x >= 0, "Grid offset X is non-negative"
)
TestHelperClass.assert_true(
match3_instance.grid_offset.y >= 0, "Grid offset Y is non-negative"
)
# Test layout constants # Test layout constants
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
@@ -242,7 +297,9 @@ func test_grid_layout_calculation():
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
50.0, match3_instance.GRID_LEFT_MARGIN, "Grid left margin constant" 50.0, match3_instance.GRID_LEFT_MARGIN, "Grid left margin constant"
) )
TestHelperClass.assert_equal(50.0, match3_instance.GRID_TOP_MARGIN, "Grid top margin constant") TestHelperClass.assert_equal(
50.0, match3_instance.GRID_TOP_MARGIN, "Grid top margin constant"
)
func test_state_management(): func test_state_management():
@@ -253,23 +310,30 @@ func test_state_management():
# Test GameState enum exists and has expected values # Test GameState enum exists and has expected values
var game_state_class = match3_instance.get_script().get_global_class() var game_state_class = match3_instance.get_script().get_global_class()
TestHelperClass.assert_true("GameState" in match3_instance, "GameState enum accessible") TestHelperClass.assert_true(
"GameState" in match3_instance, "GameState enum accessible"
)
# Test current state is valid # Test current state is valid
TestHelperClass.assert_not_null(match3_instance.current_state, "Current state is set") TestHelperClass.assert_not_null(
match3_instance.current_state, "Current state is set"
)
# Test initialization flags # Test initialization flags
TestHelperClass.assert_true( TestHelperClass.assert_true(
"grid_initialized" in match3_instance, "Grid initialized flag exists" "grid_initialized" in match3_instance, "Grid initialized flag exists"
) )
TestHelperClass.assert_true(match3_instance.grid_initialized, "Grid is marked as initialized") TestHelperClass.assert_true(
match3_instance.grid_initialized, "Grid is marked as initialized"
)
# Test instance ID for debugging # Test instance ID for debugging
TestHelperClass.assert_true( TestHelperClass.assert_true(
"instance_id" in match3_instance, "Instance ID exists for debugging" "instance_id" in match3_instance, "Instance ID exists for debugging"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.instance_id.begins_with("Match3_"), "Instance ID has correct format" match3_instance.instance_id.begins_with("Match3_"),
"Instance ID has correct format"
) )
@@ -281,13 +345,16 @@ func test_match_detection():
# Test match detection methods exist and can be called safely # Test match detection methods exist and can be called safely
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_method("_has_match_at"), "_has_match_at method exists" match3_instance.has_method("_has_match_at"),
"_has_match_at method exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_method("_check_for_matches"), "_check_for_matches method exists" match3_instance.has_method("_check_for_matches"),
"_check_for_matches method exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_method("_get_match_line"), "_get_match_line method exists" match3_instance.has_method("_get_match_line"),
"_get_match_line method exists"
) )
# Test boundary checking with invalid positions # Test boundary checking with invalid positions
@@ -310,7 +377,10 @@ func test_match_detection():
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
is_invalid, is_invalid,
"Invalid position (%d,%d) is correctly identified as invalid" % [pos.x, pos.y] (
"Invalid position (%d,%d) is correctly identified as invalid"
% [pos.x, pos.y]
)
) )
# Test valid positions through public interface # Test valid positions through public interface
@@ -324,7 +394,8 @@ func test_match_detection():
and pos.y < match3_instance.GRID_SIZE.y and pos.y < match3_instance.GRID_SIZE.y
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
is_valid, "Valid position (%d,%d) is within grid bounds" % [x, y] is_valid,
"Valid position (%d,%d) is within grid bounds" % [x, y]
) )
@@ -339,12 +410,14 @@ func test_scoring_system():
# Test that the match3 instance can handle scoring (indirectly through clearing matches) # Test that the match3 instance can handle scoring (indirectly through clearing matches)
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_method("_clear_matches"), "Scoring system method exists" match3_instance.has_method("_clear_matches"),
"Scoring system method exists"
) )
# Test that score_changed signal exists # Test that score_changed signal exists
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_signal("score_changed"), "Score changed signal exists" match3_instance.has_signal("score_changed"),
"Score changed signal exists"
) )
# Test scoring formula logic (based on the documented formula) # Test scoring formula logic (based on the documented formula)
@@ -360,7 +433,9 @@ func test_scoring_system():
calculated_score = match_size + max(0, match_size - 2) calculated_score = match_size + max(0, match_size - 2)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
expected_score, calculated_score, "Scoring formula correct for %d gems" % match_size expected_score,
calculated_score,
"Scoring formula correct for %d gems" % match_size
) )
@@ -375,22 +450,26 @@ func test_input_validation():
match3_instance.cursor_position, "Cursor position is initialized" match3_instance.cursor_position, "Cursor position is initialized"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.cursor_position is Vector2i, "Cursor position is Vector2i type" match3_instance.cursor_position is Vector2i,
"Cursor position is Vector2i type"
) )
# Test keyboard navigation flag # Test keyboard navigation flag
TestHelperClass.assert_true( TestHelperClass.assert_true(
"keyboard_navigation_enabled" in match3_instance, "Keyboard navigation flag exists" "keyboard_navigation_enabled" in match3_instance,
"Keyboard navigation flag exists"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.keyboard_navigation_enabled is bool, "Keyboard navigation flag is boolean" match3_instance.keyboard_navigation_enabled is bool,
"Keyboard navigation flag is boolean"
) )
# Test selected tile safety # Test selected tile safety
# selected_tile can be null initially, which is valid # selected_tile can be null initially, which is valid
if match3_instance.selected_tile: if match3_instance.selected_tile:
TestHelperClass.assert_true( TestHelperClass.assert_true(
is_instance_valid(match3_instance.selected_tile), "Selected tile is valid if not null" is_instance_valid(match3_instance.selected_tile),
"Selected tile is valid if not null"
) )
@@ -412,20 +491,25 @@ func test_memory_safety():
var tile = match3_instance.grid[y][x] var tile = match3_instance.grid[y][x]
if tile: if tile:
TestHelperClass.assert_true( TestHelperClass.assert_true(
is_instance_valid(tile), "Grid tile at (%d,%d) is valid instance" % [x, y] is_instance_valid(tile),
"Grid tile at (%d,%d) is valid instance" % [x, y]
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile.get_parent() == match3_instance, "Tile properly parented to Match3" tile.get_parent() == match3_instance,
"Tile properly parented to Match3"
) )
# Test position validation # Test position validation
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.has_method("_is_valid_grid_position"), "Position validation method exists" match3_instance.has_method("_is_valid_grid_position"),
"Position validation method exists"
) )
# Test safe tile access patterns exist # Test safe tile access patterns exist
# The Match3 code uses comprehensive bounds checking and null validation # The Match3 code uses comprehensive bounds checking and null validation
TestHelperClass.assert_true(true, "Memory safety patterns implemented in Match3 code") TestHelperClass.assert_true(
true, "Memory safety patterns implemented in Match3 code"
)
func test_performance_requirements(): func test_performance_requirements():
@@ -449,13 +533,16 @@ func test_performance_requirements():
# Test timing constants are reasonable for 60fps gameplay # Test timing constants are reasonable for 60fps gameplay
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.CASCADE_WAIT_TIME >= 0.05, "Cascade wait time allows for smooth animation" match3_instance.CASCADE_WAIT_TIME >= 0.05,
"Cascade wait time allows for smooth animation"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.SWAP_ANIMATION_TIME <= 0.5, "Swap animation time is responsive" match3_instance.SWAP_ANIMATION_TIME <= 0.5,
"Swap animation time is responsive"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
match3_instance.TILE_DROP_WAIT_TIME <= 0.3, "Tile drop wait time is responsive" match3_instance.TILE_DROP_WAIT_TIME <= 0.3,
"Tile drop wait time is responsive"
) )
# Test grid initialization performance # Test grid initialization performance

View File

@@ -58,7 +58,9 @@ func test_migration_compatibility():
# The checksums should be different (old system broken) # The checksums should be different (old system broken)
TestHelperClass.assert_not_equal( TestHelperClass.assert_not_equal(
old_checksum, new_checksum, "Old and new checksum formats should be different" old_checksum,
new_checksum,
"Old and new checksum formats should be different"
) )
print("Old checksum: %s" % old_checksum) print("Old checksum: %s" % old_checksum)
print("New checksum: %s" % new_checksum) print("New checksum: %s" % new_checksum)
@@ -85,9 +87,13 @@ func test_migration_compatibility():
print("Consistent checksum: %s" % first_checksum) print("Consistent checksum: %s" % first_checksum)
TestHelperClass.print_step("Migration Strategy Verification") TestHelperClass.print_step("Migration Strategy Verification")
TestHelperClass.assert_true(true, "Version-based checksum handling implemented") TestHelperClass.assert_true(
true, "Version-based checksum handling implemented"
)
print("✓ Files without _checksum: Allow (backward compatibility)") print("✓ Files without _checksum: Allow (backward compatibility)")
print("✓ Files with version < current: Recalculate checksum after migration") print(
"✓ Files with version < current: Recalculate checksum after migration"
)
print("✓ Files with current version: Use new checksum validation") print("✓ Files with current version: Use new checksum validation")

View File

@@ -46,7 +46,9 @@ func test_scene_discovery():
for directory in scene_directories: for directory in scene_directories:
discover_scenes_in_directory(directory) discover_scenes_in_directory(directory)
TestHelperClass.assert_true(discovered_scenes.size() > 0, "Found scenes in project") TestHelperClass.assert_true(
discovered_scenes.size() > 0, "Found scenes in project"
)
print("Discovered %d scene files" % discovered_scenes.size()) print("Discovered %d scene files" % discovered_scenes.size())
# List discovered scenes for reference # List discovered scenes for reference
@@ -89,23 +91,31 @@ func validate_scene_loading(scene_path: String):
# Check if resource exists # Check if resource exists
if not ResourceLoader.exists(scene_path): if not ResourceLoader.exists(scene_path):
validation_results[scene_path] = "Resource does not exist" validation_results[scene_path] = "Resource does not exist"
TestHelperClass.assert_false(true, "%s - Resource does not exist" % scene_name) TestHelperClass.assert_false(
true, "%s - Resource does not exist" % scene_name
)
return return
# Attempt to load the scene # Attempt to load the scene
var packed_scene = load(scene_path) var packed_scene = load(scene_path)
if not packed_scene: if not packed_scene:
validation_results[scene_path] = "Failed to load scene" validation_results[scene_path] = "Failed to load scene"
TestHelperClass.assert_false(true, "%s - Failed to load scene" % scene_name) TestHelperClass.assert_false(
true, "%s - Failed to load scene" % scene_name
)
return return
if not packed_scene is PackedScene: if not packed_scene is PackedScene:
validation_results[scene_path] = "Resource is not a PackedScene" validation_results[scene_path] = "Resource is not a PackedScene"
TestHelperClass.assert_false(true, "%s - Resource is not a PackedScene" % scene_name) TestHelperClass.assert_false(
true, "%s - Resource is not a PackedScene" % scene_name
)
return return
validation_results[scene_path] = "Loading successful" validation_results[scene_path] = "Loading successful"
TestHelperClass.assert_true(true, "%s - Scene loads successfully" % scene_name) TestHelperClass.assert_true(
true, "%s - Scene loads successfully" % scene_name
)
func test_scene_instantiation(): func test_scene_instantiation():
@@ -127,12 +137,15 @@ func validate_scene_instantiation(scene_path: String):
var scene_instance = packed_scene.instantiate() var scene_instance = packed_scene.instantiate()
if not scene_instance: if not scene_instance:
validation_results[scene_path] = "Failed to instantiate scene" validation_results[scene_path] = "Failed to instantiate scene"
TestHelperClass.assert_false(true, "%s - Failed to instantiate scene" % scene_name) TestHelperClass.assert_false(
true, "%s - Failed to instantiate scene" % scene_name
)
return return
# Validate the instance # Validate the instance
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
scene_instance, "%s - Scene instantiation creates valid node" % scene_name scene_instance,
"%s - Scene instantiation creates valid node" % scene_name
) )
# Clean up the instance # Clean up the instance
@@ -160,10 +173,15 @@ func test_critical_scenes():
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"Full validation successful", "Full validation successful",
status, status,
"Critical scene %s must pass all validation" % scene_path.get_file() (
"Critical scene %s must pass all validation"
% scene_path.get_file()
)
) )
else: else:
TestHelperClass.assert_false(true, "Critical scene missing: %s" % scene_path) TestHelperClass.assert_false(
true, "Critical scene missing: %s" % scene_path
)
func print_validation_summary(): func print_validation_summary():
@@ -175,7 +193,10 @@ func print_validation_summary():
for scene_path in discovered_scenes: for scene_path in discovered_scenes:
var status = validation_results.get(scene_path, "Not tested") var status = validation_results.get(scene_path, "Not tested")
if status == "Full validation successful" or status == "Loading successful": if (
status == "Full validation successful"
or status == "Loading successful"
):
successful_scenes += 1 successful_scenes += 1
else: else:
failed_scenes += 1 failed_scenes += 1

View File

@@ -64,23 +64,35 @@ func test_basic_functionality():
# Test that SettingsManager has expected methods # Test that SettingsManager has expected methods
var expected_methods = [ var expected_methods = [
"get_setting", "set_setting", "save_settings", "load_settings", "reset_settings_to_defaults" "get_setting",
"set_setting",
"save_settings",
"load_settings",
"reset_settings_to_defaults"
] ]
TestHelperClass.assert_has_methods( TestHelperClass.assert_has_methods(
settings_manager, expected_methods, "SettingsManager methods" settings_manager, expected_methods, "SettingsManager methods"
) )
# Test default settings structure # Test default settings structure
var expected_defaults = ["master_volume", "music_volume", "sfx_volume", "language"] var expected_defaults = [
"master_volume", "music_volume", "sfx_volume", "language"
]
for key in expected_defaults: for key in expected_defaults:
TestHelperClass.assert_has_key( TestHelperClass.assert_has_key(
settings_manager.default_settings, key, "Default setting key: " + key settings_manager.default_settings,
key,
"Default setting key: " + key
) )
# Test getting settings # Test getting settings
var master_volume = settings_manager.get_setting("master_volume") var master_volume = settings_manager.get_setting("master_volume")
TestHelperClass.assert_not_null(master_volume, "Can get master_volume setting") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(master_volume is float, "master_volume is float type") master_volume, "Can get master_volume setting"
)
TestHelperClass.assert_true(
master_volume is float, "master_volume is float type"
)
func test_input_validation_security(): func test_input_validation_security():
@@ -88,38 +100,56 @@ func test_input_validation_security():
# Test NaN validation # Test NaN validation
var nan_result = settings_manager.set_setting("master_volume", NAN) var nan_result = settings_manager.set_setting("master_volume", NAN)
TestHelperClass.assert_false(nan_result, "NaN values rejected for volume settings") TestHelperClass.assert_false(
nan_result, "NaN values rejected for volume settings"
)
# Test Infinity validation # Test Infinity validation
var inf_result = settings_manager.set_setting("master_volume", INF) var inf_result = settings_manager.set_setting("master_volume", INF)
TestHelperClass.assert_false(inf_result, "Infinity values rejected for volume settings") TestHelperClass.assert_false(
inf_result, "Infinity values rejected for volume settings"
)
# Test negative infinity validation # Test negative infinity validation
var neg_inf_result = settings_manager.set_setting("master_volume", -INF) var neg_inf_result = settings_manager.set_setting("master_volume", -INF)
TestHelperClass.assert_false(neg_inf_result, "Negative infinity values rejected") TestHelperClass.assert_false(
neg_inf_result, "Negative infinity values rejected"
)
# Test range validation for volumes # Test range validation for volumes
var negative_volume = settings_manager.set_setting("master_volume", -0.5) var negative_volume = settings_manager.set_setting("master_volume", -0.5)
TestHelperClass.assert_false(negative_volume, "Negative volume values rejected") TestHelperClass.assert_false(
negative_volume, "Negative volume values rejected"
)
var excessive_volume = settings_manager.set_setting("master_volume", 1.5) var excessive_volume = settings_manager.set_setting("master_volume", 1.5)
TestHelperClass.assert_false(excessive_volume, "Volume values > 1.0 rejected") TestHelperClass.assert_false(
excessive_volume, "Volume values > 1.0 rejected"
)
# Test valid volume range # Test valid volume range
var valid_volume = settings_manager.set_setting("master_volume", 0.5) var valid_volume = settings_manager.set_setting("master_volume", 0.5)
TestHelperClass.assert_true(valid_volume, "Valid volume values accepted") TestHelperClass.assert_true(valid_volume, "Valid volume values accepted")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
0.5, settings_manager.get_setting("master_volume"), "Volume value set correctly" 0.5,
settings_manager.get_setting("master_volume"),
"Volume value set correctly"
) )
# Test string length validation for language # Test string length validation for language
var long_language = "a".repeat(20) # Exceeds MAX_SETTING_STRING_LENGTH var long_language = "a".repeat(20) # Exceeds MAX_SETTING_STRING_LENGTH
var long_lang_result = settings_manager.set_setting("language", long_language) var long_lang_result = settings_manager.set_setting(
TestHelperClass.assert_false(long_lang_result, "Excessively long language codes rejected") "language", long_language
)
TestHelperClass.assert_false(
long_lang_result, "Excessively long language codes rejected"
)
# Test invalid characters in language code # Test invalid characters in language code
var invalid_chars = settings_manager.set_setting("language", "en<script>") var invalid_chars = settings_manager.set_setting("language", "en<script>")
TestHelperClass.assert_false(invalid_chars, "Language codes with invalid characters rejected") TestHelperClass.assert_false(
invalid_chars, "Language codes with invalid characters rejected"
)
# Test valid language code # Test valid language code
var valid_lang = settings_manager.set_setting("language", "en") var valid_lang = settings_manager.set_setting("language", "en")
@@ -141,10 +171,14 @@ func test_file_io_security():
# Test loading with backup scenario # Test loading with backup scenario
settings_manager.load_settings() settings_manager.load_settings()
TestHelperClass.assert_not_null(settings_manager.settings, "Settings loaded successfully") TestHelperClass.assert_not_null(
settings_manager.settings, "Settings loaded successfully"
)
# Test that settings file exists after save # Test that settings file exists after save
TestHelperClass.assert_file_exists("user://settings.cfg", "Settings file created after save") TestHelperClass.assert_file_exists(
"user://settings.cfg", "Settings file created after save"
)
func test_json_parsing_security(): func test_json_parsing_security():
@@ -157,7 +191,9 @@ func test_json_parsing_security():
temp_files.append(invalid_json_path) temp_files.append(invalid_json_path)
# Create oversized JSON file # Create oversized JSON file
var large_json_content = '{"languages": {"' + "x".repeat(70000) + '": "test"}}' var large_json_content = (
'{"languages": {"' + "x".repeat(70000) + '": "test"}}'
)
var oversized_json_path = TestHelper.create_temp_file( var oversized_json_path = TestHelper.create_temp_file(
"oversized_languages.json", large_json_content "oversized_languages.json", large_json_content
) )
@@ -178,11 +214,15 @@ func test_language_validation():
var supported_langs = ["en", "ru"] var supported_langs = ["en", "ru"]
for lang in supported_langs: for lang in supported_langs:
var result = settings_manager.set_setting("language", lang) var result = settings_manager.set_setting("language", lang)
TestHelperClass.assert_true(result, "Supported language accepted: " + lang) TestHelperClass.assert_true(
result, "Supported language accepted: " + lang
)
# Test unsupported language # Test unsupported language
var unsupported_result = settings_manager.set_setting("language", "xyz") var unsupported_result = settings_manager.set_setting("language", "xyz")
TestHelperClass.assert_false(unsupported_result, "Unsupported language rejected") TestHelperClass.assert_false(
unsupported_result, "Unsupported language rejected"
)
# Test empty language # Test empty language
var empty_result = settings_manager.set_setting("language", "") var empty_result = settings_manager.set_setting("language", "")
@@ -201,26 +241,32 @@ func test_volume_validation():
for setting in volume_settings: for setting in volume_settings:
# Test boundary values # Test boundary values
TestHelperClass.assert_true( TestHelperClass.assert_true(
settings_manager.set_setting(setting, 0.0), "Volume 0.0 accepted for " + setting settings_manager.set_setting(setting, 0.0),
"Volume 0.0 accepted for " + setting
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
settings_manager.set_setting(setting, 1.0), "Volume 1.0 accepted for " + setting settings_manager.set_setting(setting, 1.0),
"Volume 1.0 accepted for " + setting
) )
# Test out of range values # Test out of range values
TestHelperClass.assert_false( TestHelperClass.assert_false(
settings_manager.set_setting(setting, -0.1), "Negative volume rejected for " + setting settings_manager.set_setting(setting, -0.1),
"Negative volume rejected for " + setting
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
settings_manager.set_setting(setting, 1.1), "Volume > 1.0 rejected for " + setting settings_manager.set_setting(setting, 1.1),
"Volume > 1.0 rejected for " + setting
) )
# Test invalid types # Test invalid types
TestHelperClass.assert_false( TestHelperClass.assert_false(
settings_manager.set_setting(setting, "0.5"), "String volume rejected for " + setting settings_manager.set_setting(setting, "0.5"),
"String volume rejected for " + setting
) )
TestHelperClass.assert_false( TestHelperClass.assert_false(
settings_manager.set_setting(setting, null), "Null volume rejected for " + setting settings_manager.set_setting(setting, null),
"Null volume rejected for " + setting
) )
@@ -228,8 +274,12 @@ func test_error_handling_and_recovery():
TestHelperClass.print_step("Error Handling and Recovery") TestHelperClass.print_step("Error Handling and Recovery")
# Test unknown setting key # Test unknown setting key
var unknown_result = settings_manager.set_setting("unknown_setting", "value") var unknown_result = settings_manager.set_setting(
TestHelperClass.assert_false(unknown_result, "Unknown setting keys rejected") "unknown_setting", "value"
)
TestHelperClass.assert_false(
unknown_result, "Unknown setting keys rejected"
)
# Test recovery from corrupted settings # Test recovery from corrupted settings
# Save current state # Save current state
@@ -248,13 +298,18 @@ func test_error_handling_and_recovery():
# Test fallback language loading # Test fallback language loading
TestHelperClass.assert_true( TestHelperClass.assert_true(
settings_manager.languages_data.has("languages"), "Fallback languages loaded" settings_manager.languages_data.has("languages"),
"Fallback languages loaded"
) )
TestHelperClass.assert_has_key( TestHelperClass.assert_has_key(
settings_manager.languages_data["languages"], "en", "English fallback language available" settings_manager.languages_data["languages"],
"en",
"English fallback language available"
) )
TestHelperClass.assert_has_key( TestHelperClass.assert_has_key(
settings_manager.languages_data["languages"], "ru", "Russian fallback language available" settings_manager.languages_data["languages"],
"ru",
"Russian fallback language available"
) )
@@ -281,7 +336,9 @@ func test_reset_functionality():
) )
# Test that reset saves automatically # Test that reset saves automatically
TestHelperClass.assert_file_exists("user://settings.cfg", "Settings file exists after reset") TestHelperClass.assert_file_exists(
"user://settings.cfg", "Settings file exists after reset"
)
func test_performance_benchmarks(): func test_performance_benchmarks():
@@ -290,18 +347,24 @@ func test_performance_benchmarks():
# Test settings load performance # Test settings load performance
TestHelperClass.start_performance_test("load_settings") TestHelperClass.start_performance_test("load_settings")
settings_manager.load_settings() settings_manager.load_settings()
TestHelperClass.end_performance_test("load_settings", 100.0, "Settings load within 100ms") TestHelperClass.end_performance_test(
"load_settings", 100.0, "Settings load within 100ms"
)
# Test settings save performance # Test settings save performance
TestHelperClass.start_performance_test("save_settings") TestHelperClass.start_performance_test("save_settings")
settings_manager.save_settings() settings_manager.save_settings()
TestHelperClass.end_performance_test("save_settings", 50.0, "Settings save within 50ms") TestHelperClass.end_performance_test(
"save_settings", 50.0, "Settings save within 50ms"
)
# Test validation performance # Test validation performance
TestHelperClass.start_performance_test("validation") TestHelperClass.start_performance_test("validation")
for i in range(100): for i in range(100):
settings_manager.set_setting("master_volume", 0.5) settings_manager.set_setting("master_volume", 0.5)
TestHelperClass.end_performance_test("validation", 50.0, "100 validations within 50ms") TestHelperClass.end_performance_test(
"validation", 50.0, "100 validations within 50ms"
)
func cleanup_tests(): func cleanup_tests():

View File

@@ -62,7 +62,9 @@ func setup_test_environment():
if tile_scene: if tile_scene:
tile_instance = tile_scene.instantiate() tile_instance = tile_scene.instantiate()
test_viewport.add_child(tile_instance) test_viewport.add_child(tile_instance)
TestHelperClass.assert_not_null(tile_instance, "Tile instance created successfully") TestHelperClass.assert_not_null(
tile_instance, "Tile instance created successfully"
)
# Wait for initialization # Wait for initialization
await process_frame await process_frame
@@ -73,7 +75,9 @@ func test_basic_functionality():
TestHelperClass.print_step("Basic Functionality") TestHelperClass.print_step("Basic Functionality")
if not tile_instance: if not tile_instance:
TestHelperClass.assert_true(false, "Tile instance not available for testing") TestHelperClass.assert_true(
false, "Tile instance not available for testing"
)
return return
# Test that Tile has expected properties # Test that Tile has expected properties
@@ -86,7 +90,9 @@ func test_basic_functionality():
"active_gem_types" "active_gem_types"
] ]
for prop in expected_properties: for prop in expected_properties:
TestHelperClass.assert_true(prop in tile_instance, "Tile has property: " + prop) TestHelperClass.assert_true(
prop in tile_instance, "Tile has property: " + prop
)
# Test that Tile has expected methods # Test that Tile has expected methods
var expected_methods = [ var expected_methods = [
@@ -96,19 +102,28 @@ func test_basic_functionality():
"remove_gem_type", "remove_gem_type",
"force_reset_visual_state" "force_reset_visual_state"
] ]
TestHelperClass.assert_has_methods(tile_instance, expected_methods, "Tile component methods") TestHelperClass.assert_has_methods(
tile_instance, expected_methods, "Tile component methods"
)
# Test signals # Test signals
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.has_signal("tile_selected"), "Tile has tile_selected signal" tile_instance.has_signal("tile_selected"),
"Tile has tile_selected signal"
) )
# Test sprite reference # Test sprite reference
TestHelperClass.assert_not_null(tile_instance.sprite, "Sprite node is available") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(tile_instance.sprite is Sprite2D, "Sprite is Sprite2D type") tile_instance.sprite, "Sprite node is available"
)
TestHelperClass.assert_true(
tile_instance.sprite is Sprite2D, "Sprite is Sprite2D type"
)
# Test group membership # Test group membership
TestHelperClass.assert_true(tile_instance.is_in_group("tiles"), "Tile is in 'tiles' group") TestHelperClass.assert_true(
tile_instance.is_in_group("tiles"), "Tile is in 'tiles' group"
)
func test_tile_constants(): func test_tile_constants():
@@ -118,22 +133,33 @@ func test_tile_constants():
return return
# Test TILE_SIZE constant # Test TILE_SIZE constant
TestHelperClass.assert_equal(48, tile_instance.TILE_SIZE, "TILE_SIZE constant is correct") TestHelperClass.assert_equal(
48, tile_instance.TILE_SIZE, "TILE_SIZE constant is correct"
)
# Test all_gem_textures array # Test all_gem_textures array
TestHelperClass.assert_not_null(tile_instance.all_gem_textures, "All gem textures array exists") TestHelperClass.assert_not_null(
tile_instance.all_gem_textures, "All gem textures array exists"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.all_gem_textures is Array, "All gem textures is Array type" tile_instance.all_gem_textures is Array,
"All gem textures is Array type"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
8, tile_instance.all_gem_textures.size(), "All gem textures has expected count" 8,
tile_instance.all_gem_textures.size(),
"All gem textures has expected count"
) )
# Test that all gem textures are valid # Test that all gem textures are valid
for i in range(tile_instance.all_gem_textures.size()): for i in range(tile_instance.all_gem_textures.size()):
var texture = tile_instance.all_gem_textures[i] var texture = tile_instance.all_gem_textures[i]
TestHelperClass.assert_not_null(texture, "Gem texture %d is not null" % i) TestHelperClass.assert_not_null(
TestHelperClass.assert_true(texture is Texture2D, "Gem texture %d is Texture2D type" % i) texture, "Gem texture %d is not null" % i
)
TestHelperClass.assert_true(
texture is Texture2D, "Gem texture %d is Texture2D type" % i
)
func test_texture_management(): func test_texture_management():
@@ -147,10 +173,12 @@ func test_texture_management():
tile_instance.active_gem_types, "Active gem types is initialized" tile_instance.active_gem_types, "Active gem types is initialized"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.active_gem_types is Array, "Active gem types is Array type" tile_instance.active_gem_types is Array,
"Active gem types is Array type"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.active_gem_types.size() > 0, "Active gem types has content" tile_instance.active_gem_types.size() > 0,
"Active gem types has content"
) )
# Test texture assignment for valid tile types # Test texture assignment for valid tile types
@@ -164,7 +192,8 @@ func test_texture_management():
if tile_instance.sprite: if tile_instance.sprite:
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
tile_instance.sprite.texture, "Sprite texture assigned for type %d" % i tile_instance.sprite.texture,
"Sprite texture assigned for type %d" % i
) )
# Restore original type # Restore original type
@@ -184,42 +213,60 @@ func test_gem_type_management():
var test_gems = [0, 1, 2] var test_gems = [0, 1, 2]
tile_instance.set_active_gem_types(test_gems) tile_instance.set_active_gem_types(test_gems)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
3, tile_instance.get_active_gem_count(), "Active gem count set correctly" 3,
tile_instance.get_active_gem_count(),
"Active gem count set correctly"
) )
# Test add_gem_type # Test add_gem_type
var add_result = tile_instance.add_gem_type(3) var add_result = tile_instance.add_gem_type(3)
TestHelperClass.assert_true(add_result, "Valid gem type added successfully") TestHelperClass.assert_true(add_result, "Valid gem type added successfully")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
4, tile_instance.get_active_gem_count(), "Gem count increased after addition" 4,
tile_instance.get_active_gem_count(),
"Gem count increased after addition"
) )
# Test adding duplicate gem type # Test adding duplicate gem type
var duplicate_result = tile_instance.add_gem_type(3) var duplicate_result = tile_instance.add_gem_type(3)
TestHelperClass.assert_false(duplicate_result, "Duplicate gem type addition rejected") TestHelperClass.assert_false(
duplicate_result, "Duplicate gem type addition rejected"
)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
4, tile_instance.get_active_gem_count(), "Gem count unchanged after duplicate" 4,
tile_instance.get_active_gem_count(),
"Gem count unchanged after duplicate"
) )
# Test add_gem_type with invalid index # Test add_gem_type with invalid index
var invalid_add = tile_instance.add_gem_type(99) var invalid_add = tile_instance.add_gem_type(99)
TestHelperClass.assert_false(invalid_add, "Invalid gem index addition rejected") TestHelperClass.assert_false(
invalid_add, "Invalid gem index addition rejected"
)
# Test remove_gem_type # Test remove_gem_type
var remove_result = tile_instance.remove_gem_type(3) var remove_result = tile_instance.remove_gem_type(3)
TestHelperClass.assert_true(remove_result, "Valid gem type removed successfully") TestHelperClass.assert_true(
remove_result, "Valid gem type removed successfully"
)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
3, tile_instance.get_active_gem_count(), "Gem count decreased after removal" 3,
tile_instance.get_active_gem_count(),
"Gem count decreased after removal"
) )
# Test removing non-existent gem type # Test removing non-existent gem type
var nonexistent_remove = tile_instance.remove_gem_type(99) var nonexistent_remove = tile_instance.remove_gem_type(99)
TestHelperClass.assert_false(nonexistent_remove, "Non-existent gem type removal rejected") TestHelperClass.assert_false(
nonexistent_remove, "Non-existent gem type removal rejected"
)
# Test minimum gem types protection # Test minimum gem types protection
tile_instance.set_active_gem_types([0, 1]) # Set to minimum tile_instance.set_active_gem_types([0, 1]) # Set to minimum
var protected_remove = tile_instance.remove_gem_type(0) var protected_remove = tile_instance.remove_gem_type(0)
TestHelperClass.assert_false(protected_remove, "Minimum gem types protection active") TestHelperClass.assert_false(
protected_remove, "Minimum gem types protection active"
)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
2, tile_instance.get_active_gem_count(), "Minimum gem count preserved" 2, tile_instance.get_active_gem_count(), "Minimum gem count preserved"
) )
@@ -240,7 +287,9 @@ func test_visual_feedback_system():
# Test selection visual feedback # Test selection visual feedback
tile_instance.is_selected = true tile_instance.is_selected = true
TestHelperClass.assert_true(tile_instance.is_selected, "Selection state set correctly") TestHelperClass.assert_true(
tile_instance.is_selected, "Selection state set correctly"
)
# Wait for potential animation # Wait for potential animation
await process_frame await process_frame
@@ -256,21 +305,29 @@ func test_visual_feedback_system():
# Test highlight visual feedback # Test highlight visual feedback
tile_instance.is_selected = false tile_instance.is_selected = false
tile_instance.is_highlighted = true tile_instance.is_highlighted = true
TestHelperClass.assert_true(tile_instance.is_highlighted, "Highlight state set correctly") TestHelperClass.assert_true(
tile_instance.is_highlighted, "Highlight state set correctly"
)
# Wait for potential animation # Wait for potential animation
await process_frame await process_frame
# Test normal state # Test normal state
tile_instance.is_highlighted = false tile_instance.is_highlighted = false
TestHelperClass.assert_false(tile_instance.is_highlighted, "Normal state restored") TestHelperClass.assert_false(
tile_instance.is_highlighted, "Normal state restored"
)
# Test force reset # Test force reset
tile_instance.is_selected = true tile_instance.is_selected = true
tile_instance.is_highlighted = true tile_instance.is_highlighted = true
tile_instance.force_reset_visual_state() tile_instance.force_reset_visual_state()
TestHelperClass.assert_false(tile_instance.is_selected, "Force reset clears selection") TestHelperClass.assert_false(
TestHelperClass.assert_false(tile_instance.is_highlighted, "Force reset clears highlight") tile_instance.is_selected, "Force reset clears selection"
)
TestHelperClass.assert_false(
tile_instance.is_highlighted, "Force reset clears highlight"
)
# Restore original state # Restore original state
tile_instance.is_selected = original_selected tile_instance.is_selected = original_selected
@@ -284,12 +341,16 @@ func test_state_management():
return return
# Test initial state # Test initial state
TestHelperClass.assert_true(tile_instance.tile_type >= 0, "Initial tile type is non-negative")
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.grid_position is Vector2i, "Grid position is Vector2i type" tile_instance.tile_type >= 0, "Initial tile type is non-negative"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.original_scale is Vector2, "Original scale is Vector2 type" tile_instance.grid_position is Vector2i,
"Grid position is Vector2i type"
)
TestHelperClass.assert_true(
tile_instance.original_scale is Vector2,
"Original scale is Vector2 type"
) )
# Test tile type bounds checking # Test tile type bounds checking
@@ -324,20 +385,23 @@ func test_input_validation():
tile_instance.set_active_gem_types([]) tile_instance.set_active_gem_types([])
# Should fall back to defaults or maintain previous state # Should fall back to defaults or maintain previous state
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.get_active_gem_count() > 0, "Empty gem array handled gracefully" tile_instance.get_active_gem_count() > 0,
"Empty gem array handled gracefully"
) )
# Test null gem types array # Test null gem types array
tile_instance.set_active_gem_types(null) tile_instance.set_active_gem_types(null)
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.get_active_gem_count() > 0, "Null gem array handled gracefully" tile_instance.get_active_gem_count() > 0,
"Null gem array handled gracefully"
) )
# Test invalid gem indices in array # Test invalid gem indices in array
tile_instance.set_active_gem_types([0, 1, 99, 2]) # 99 is invalid tile_instance.set_active_gem_types([0, 1, 99, 2]) # 99 is invalid
# Should use fallback or filter invalid indices # Should use fallback or filter invalid indices
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.get_active_gem_count() > 0, "Invalid gem indices handled gracefully" tile_instance.get_active_gem_count() > 0,
"Invalid gem indices handled gracefully"
) )
# Test negative gem indices # Test negative gem indices
@@ -345,7 +409,9 @@ func test_input_validation():
TestHelperClass.assert_false(negative_add, "Negative gem index rejected") TestHelperClass.assert_false(negative_add, "Negative gem index rejected")
# Test out-of-bounds gem indices # Test out-of-bounds gem indices
var oob_add = tile_instance.add_gem_type(tile_instance.all_gem_textures.size()) var oob_add = tile_instance.add_gem_type(
tile_instance.all_gem_textures.size()
)
TestHelperClass.assert_false(oob_add, "Out-of-bounds gem index rejected") TestHelperClass.assert_false(oob_add, "Out-of-bounds gem index rejected")
# Restore original state # Restore original state
@@ -359,9 +425,15 @@ func test_scaling_and_sizing():
return return
# Test original scale calculation # Test original scale calculation
TestHelperClass.assert_not_null(tile_instance.original_scale, "Original scale is calculated") TestHelperClass.assert_not_null(
TestHelperClass.assert_true(tile_instance.original_scale.x > 0, "Original scale X is positive") tile_instance.original_scale, "Original scale is calculated"
TestHelperClass.assert_true(tile_instance.original_scale.y > 0, "Original scale Y is positive") )
TestHelperClass.assert_true(
tile_instance.original_scale.x > 0, "Original scale X is positive"
)
TestHelperClass.assert_true(
tile_instance.original_scale.y > 0, "Original scale Y is positive"
)
# Test that tile size is respected # Test that tile size is respected
if tile_instance.sprite and tile_instance.sprite.texture: if tile_instance.sprite and tile_instance.sprite.texture:
@@ -369,11 +441,14 @@ func test_scaling_and_sizing():
var scaled_size = texture_size * tile_instance.original_scale var scaled_size = texture_size * tile_instance.original_scale
var max_dimension = max(scaled_size.x, scaled_size.y) var max_dimension = max(scaled_size.x, scaled_size.y)
TestHelperClass.assert_true( TestHelperClass.assert_true(
max_dimension <= tile_instance.TILE_SIZE + 1, "Scaled tile fits within TILE_SIZE" max_dimension <= tile_instance.TILE_SIZE + 1,
"Scaled tile fits within TILE_SIZE"
) )
# Test scale animation for visual feedback # Test scale animation for visual feedback
var original_scale = tile_instance.sprite.scale if tile_instance.sprite else Vector2.ONE var original_scale = (
tile_instance.sprite.scale if tile_instance.sprite else Vector2.ONE
)
# Test selection scaling # Test selection scaling
tile_instance.is_selected = true tile_instance.is_selected = true
@@ -383,7 +458,8 @@ func test_scaling_and_sizing():
if tile_instance.sprite: if tile_instance.sprite:
var selected_scale = tile_instance.sprite.scale var selected_scale = tile_instance.sprite.scale
TestHelperClass.assert_true( TestHelperClass.assert_true(
selected_scale.x >= original_scale.x, "Selected tile scale is larger or equal" selected_scale.x >= original_scale.x,
"Selected tile scale is larger or equal"
) )
# Reset to normal # Reset to normal
@@ -420,7 +496,8 @@ func test_memory_safety():
# Test gem types array integrity # Test gem types array integrity
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.active_gem_types is Array, "Active gem types maintains Array type" tile_instance.active_gem_types is Array,
"Active gem types maintains Array type"
) )
# Test that gem indices are within bounds # Test that gem indices are within bounds
@@ -444,12 +521,16 @@ func test_error_handling():
# Test that tile type setting handles null sprite gracefully # Test that tile type setting handles null sprite gracefully
tile_instance.tile_type = 0 # Use public property instead tile_instance.tile_type = 0 # Use public property instead
TestHelperClass.assert_true(true, "Tile type setting handles null sprite gracefully") TestHelperClass.assert_true(
true, "Tile type setting handles null sprite gracefully"
)
# Test that scaling handles null sprite gracefully # Test that scaling handles null sprite gracefully
# Force redraw to trigger scaling logic # Force redraw to trigger scaling logic
tile_instance.queue_redraw() tile_instance.queue_redraw()
TestHelperClass.assert_true(true, "Sprite scaling handles null sprite gracefully") TestHelperClass.assert_true(
true, "Sprite scaling handles null sprite gracefully"
)
# Restore sprite # Restore sprite
tile_instance.sprite = backup_sprite tile_instance.sprite = backup_sprite
@@ -473,7 +554,10 @@ func test_error_handling():
tile_instance.set_active_gem_types(large_gem_types) tile_instance.set_active_gem_types(large_gem_types)
# Should fall back to safe defaults # Should fall back to safe defaults
TestHelperClass.assert_true( TestHelperClass.assert_true(
tile_instance.get_active_gem_count() <= tile_instance.all_gem_textures.size(), (
tile_instance.get_active_gem_count()
<= tile_instance.all_gem_textures.size()
),
"Large gem array handled safely" "Large gem array handled safely"
) )

View File

@@ -55,7 +55,9 @@ func setup_test_environment():
# Load ValueStepper scene # Load ValueStepper scene
stepper_scene = load("res://scenes/ui/components/ValueStepper.tscn") stepper_scene = load("res://scenes/ui/components/ValueStepper.tscn")
TestHelperClass.assert_not_null(stepper_scene, "ValueStepper scene loads successfully") TestHelperClass.assert_not_null(
stepper_scene, "ValueStepper scene loads successfully"
)
# Create test viewport for isolated testing # Create test viewport for isolated testing
test_viewport = SubViewport.new() test_viewport = SubViewport.new()
@@ -79,15 +81,23 @@ func test_basic_functionality():
TestHelperClass.print_step("Basic Functionality") TestHelperClass.print_step("Basic Functionality")
if not stepper_instance: if not stepper_instance:
TestHelperClass.assert_true(false, "ValueStepper instance not available for testing") TestHelperClass.assert_true(
false, "ValueStepper instance not available for testing"
)
return return
# Test that ValueStepper has expected properties # Test that ValueStepper has expected properties
var expected_properties = [ var expected_properties = [
"data_source", "custom_format_function", "values", "display_names", "current_index" "data_source",
"custom_format_function",
"values",
"display_names",
"current_index"
] ]
for prop in expected_properties: for prop in expected_properties:
TestHelperClass.assert_true(prop in stepper_instance, "ValueStepper has property: " + prop) TestHelperClass.assert_true(
prop in stepper_instance, "ValueStepper has property: " + prop
)
# Test that ValueStepper has expected methods # Test that ValueStepper has expected methods
var expected_methods = [ var expected_methods = [
@@ -105,12 +115,17 @@ func test_basic_functionality():
# Test signals # Test signals
TestHelperClass.assert_true( TestHelperClass.assert_true(
stepper_instance.has_signal("value_changed"), "ValueStepper has value_changed signal" stepper_instance.has_signal("value_changed"),
"ValueStepper has value_changed signal"
) )
# Test UI components # Test UI components
TestHelperClass.assert_not_null(stepper_instance.left_button, "Left button is available") TestHelperClass.assert_not_null(
TestHelperClass.assert_not_null(stepper_instance.right_button, "Right button is available") stepper_instance.left_button, "Left button is available"
)
TestHelperClass.assert_not_null(
stepper_instance.right_button, "Right button is available"
)
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
stepper_instance.value_display, "Value display label is available" stepper_instance.value_display, "Value display label is available"
) )
@@ -135,24 +150,33 @@ func test_data_source_loading():
# Test default language data source # Test default language data source
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"language", stepper_instance.data_source, "Default data source is language" "language",
stepper_instance.data_source,
"Default data source is language"
) )
# Test that values are loaded # Test that values are loaded
TestHelperClass.assert_not_null(stepper_instance.values, "Values array is initialized") TestHelperClass.assert_not_null(
stepper_instance.values, "Values array is initialized"
)
TestHelperClass.assert_not_null( TestHelperClass.assert_not_null(
stepper_instance.display_names, "Display names array is initialized" stepper_instance.display_names, "Display names array is initialized"
) )
TestHelperClass.assert_true(stepper_instance.values is Array, "Values is Array type") TestHelperClass.assert_true(
stepper_instance.values is Array, "Values is Array type"
)
TestHelperClass.assert_true( TestHelperClass.assert_true(
stepper_instance.display_names is Array, "Display names is Array type" stepper_instance.display_names is Array, "Display names is Array type"
) )
# Test that language data is loaded correctly # Test that language data is loaded correctly
if stepper_instance.data_source == "language": if stepper_instance.data_source == "language":
TestHelperClass.assert_true(stepper_instance.values.size() > 0, "Language values loaded")
TestHelperClass.assert_true( TestHelperClass.assert_true(
stepper_instance.display_names.size() > 0, "Language display names loaded" stepper_instance.values.size() > 0, "Language values loaded"
)
TestHelperClass.assert_true(
stepper_instance.display_names.size() > 0,
"Language display names loaded"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
stepper_instance.values.size(), stepper_instance.values.size(),
@@ -161,7 +185,9 @@ func test_data_source_loading():
) )
# Test that current language is properly selected # Test that current language is properly selected
var current_lang = root.get_node("SettingsManager").get_setting("language") var current_lang = root.get_node("SettingsManager").get_setting(
"language"
)
var expected_index = stepper_instance.values.find(current_lang) var expected_index = stepper_instance.values.find(current_lang)
if expected_index >= 0: if expected_index >= 0:
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
@@ -176,9 +202,13 @@ func test_data_source_loading():
test_viewport.add_child(resolution_stepper) test_viewport.add_child(resolution_stepper)
await process_frame await process_frame
TestHelperClass.assert_true(resolution_stepper.values.size() > 0, "Resolution values loaded") TestHelperClass.assert_true(
resolution_stepper.values.size() > 0, "Resolution values loaded"
)
TestHelperClass.assert_contains( TestHelperClass.assert_contains(
resolution_stepper.values, "1920x1080", "Resolution data contains expected value" resolution_stepper.values,
"1920x1080",
"Resolution data contains expected value"
) )
resolution_stepper.queue_free() resolution_stepper.queue_free()
@@ -189,9 +219,13 @@ func test_data_source_loading():
test_viewport.add_child(difficulty_stepper) test_viewport.add_child(difficulty_stepper)
await process_frame await process_frame
TestHelperClass.assert_true(difficulty_stepper.values.size() > 0, "Difficulty values loaded") TestHelperClass.assert_true(
difficulty_stepper.values.size() > 0, "Difficulty values loaded"
)
TestHelperClass.assert_contains( TestHelperClass.assert_contains(
difficulty_stepper.values, "normal", "Difficulty data contains expected value" difficulty_stepper.values,
"normal",
"Difficulty data contains expected value"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
1, difficulty_stepper.current_index, "Difficulty defaults to normal" 1, difficulty_stepper.current_index, "Difficulty defaults to normal"
@@ -214,13 +248,17 @@ func test_value_navigation():
var initial_value = stepper_instance.get_current_value() var initial_value = stepper_instance.get_current_value()
stepper_instance.change_value(1) stepper_instance.change_value(1)
var next_value = stepper_instance.get_current_value() var next_value = stepper_instance.get_current_value()
TestHelperClass.assert_not_equal(initial_value, next_value, "Forward navigation changes value") TestHelperClass.assert_not_equal(
initial_value, next_value, "Forward navigation changes value"
)
# Test backward navigation # Test backward navigation
stepper_instance.change_value(-1) stepper_instance.change_value(-1)
var back_value = stepper_instance.get_current_value() var back_value = stepper_instance.get_current_value()
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
initial_value, back_value, "Backward navigation returns to original value" initial_value,
back_value,
"Backward navigation returns to original value"
) )
# Test wrap-around forward # Test wrap-around forward
@@ -228,14 +266,18 @@ func test_value_navigation():
stepper_instance.current_index = max_index stepper_instance.current_index = max_index
stepper_instance.change_value(1) stepper_instance.change_value(1)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
0, stepper_instance.current_index, "Forward navigation wraps to beginning" 0,
stepper_instance.current_index,
"Forward navigation wraps to beginning"
) )
# Test wrap-around backward # Test wrap-around backward
stepper_instance.current_index = 0 stepper_instance.current_index = 0
stepper_instance.change_value(-1) stepper_instance.change_value(-1)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
max_index, stepper_instance.current_index, "Backward navigation wraps to end" max_index,
stepper_instance.current_index,
"Backward navigation wraps to end"
) )
# Restore original state # Restore original state
@@ -258,13 +300,19 @@ func test_custom_values():
var custom_values = ["apple", "banana", "cherry"] var custom_values = ["apple", "banana", "cherry"]
stepper_instance.setup_custom_values(custom_values) stepper_instance.setup_custom_values(custom_values)
TestHelperClass.assert_equal(3, stepper_instance.values.size(), "Custom values set correctly") TestHelperClass.assert_equal(
TestHelperClass.assert_equal("apple", stepper_instance.values[0], "First custom value correct") 3, stepper_instance.values.size(), "Custom values set correctly"
)
TestHelperClass.assert_equal(
"apple", stepper_instance.values[0], "First custom value correct"
)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
0, stepper_instance.current_index, "Index reset to 0 for custom values" 0, stepper_instance.current_index, "Index reset to 0 for custom values"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"apple", stepper_instance.get_current_value(), "Current value matches first custom value" "apple",
stepper_instance.get_current_value(),
"Current value matches first custom value"
) )
# Test custom values with display names # Test custom values with display names
@@ -272,31 +320,43 @@ func test_custom_values():
stepper_instance.setup_custom_values(custom_values, custom_display_names) stepper_instance.setup_custom_values(custom_values, custom_display_names)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
3, stepper_instance.display_names.size(), "Custom display names set correctly" 3,
stepper_instance.display_names.size(),
"Custom display names set correctly"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"Red Apple", stepper_instance.display_names[0], "First display name correct" "Red Apple",
stepper_instance.display_names[0],
"First display name correct"
) )
# Test navigation with custom values # Test navigation with custom values
stepper_instance.change_value(1) stepper_instance.change_value(1)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"banana", stepper_instance.get_current_value(), "Navigation works with custom values" "banana",
stepper_instance.get_current_value(),
"Navigation works with custom values"
) )
# Test set_current_value # Test set_current_value
stepper_instance.set_current_value("cherry") stepper_instance.set_current_value("cherry")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"cherry", stepper_instance.get_current_value(), "set_current_value works correctly" "cherry",
stepper_instance.get_current_value(),
"set_current_value works correctly"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
2, stepper_instance.current_index, "Index updated correctly by set_current_value" 2,
stepper_instance.current_index,
"Index updated correctly by set_current_value"
) )
# Test invalid value # Test invalid value
stepper_instance.set_current_value("grape") stepper_instance.set_current_value("grape")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"cherry", stepper_instance.get_current_value(), "Invalid value doesn't change current value" "cherry",
stepper_instance.get_current_value(),
"Invalid value doesn't change current value"
) )
# Restore original state # Restore original state
@@ -319,7 +379,9 @@ func test_input_handling():
var left_handled = stepper_instance.handle_input_action("move_left") var left_handled = stepper_instance.handle_input_action("move_left")
TestHelperClass.assert_true(left_handled, "Left input action handled") TestHelperClass.assert_true(left_handled, "Left input action handled")
TestHelperClass.assert_not_equal( TestHelperClass.assert_not_equal(
original_value, stepper_instance.get_current_value(), "Left action changes value" original_value,
stepper_instance.get_current_value(),
"Left action changes value"
) )
# Test right input action # Test right input action
@@ -333,14 +395,18 @@ func test_input_handling():
# Test invalid input action # Test invalid input action
var invalid_handled = stepper_instance.handle_input_action("invalid_action") var invalid_handled = stepper_instance.handle_input_action("invalid_action")
TestHelperClass.assert_false(invalid_handled, "Invalid input action not handled") TestHelperClass.assert_false(
invalid_handled, "Invalid input action not handled"
)
# Test button press simulation # Test button press simulation
if stepper_instance.left_button: if stepper_instance.left_button:
var before_left = stepper_instance.get_current_value() var before_left = stepper_instance.get_current_value()
stepper_instance.handle_input_action("move_left") stepper_instance.handle_input_action("move_left")
TestHelperClass.assert_not_equal( TestHelperClass.assert_not_equal(
before_left, stepper_instance.get_current_value(), "Left button press changes value" before_left,
stepper_instance.get_current_value(),
"Left button press changes value"
) )
if stepper_instance.right_button: if stepper_instance.right_button:
@@ -365,9 +431,12 @@ func test_visual_feedback():
# Test highlighting # Test highlighting
stepper_instance.set_highlighted(true) stepper_instance.set_highlighted(true)
TestHelperClass.assert_true(stepper_instance.is_highlighted, "Highlighted state set correctly")
TestHelperClass.assert_true( TestHelperClass.assert_true(
stepper_instance.scale.x > original_scale.x, "Scale increased when highlighted" stepper_instance.is_highlighted, "Highlighted state set correctly"
)
TestHelperClass.assert_true(
stepper_instance.scale.x > original_scale.x,
"Scale increased when highlighted"
) )
# Test unhighlighting # Test unhighlighting
@@ -376,17 +445,25 @@ func test_visual_feedback():
stepper_instance.is_highlighted, "Highlighted state cleared correctly" stepper_instance.is_highlighted, "Highlighted state cleared correctly"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_scale, stepper_instance.scale, "Scale restored when unhighlighted" original_scale,
stepper_instance.scale,
"Scale restored when unhighlighted"
) )
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
original_modulate, stepper_instance.modulate, "Modulate restored when unhighlighted" original_modulate,
stepper_instance.modulate,
"Modulate restored when unhighlighted"
) )
# Test display update # Test display update
if stepper_instance.value_display: if stepper_instance.value_display:
var current_text = stepper_instance.value_display.text var current_text = stepper_instance.value_display.text
TestHelperClass.assert_true(current_text.length() > 0, "Value display has text content") TestHelperClass.assert_true(
TestHelperClass.assert_not_equal("N/A", current_text, "Value display shows valid content") current_text.length() > 0, "Value display has text content"
)
TestHelperClass.assert_not_equal(
"N/A", current_text, "Value display shows valid content"
)
func test_settings_integration(): func test_settings_integration():
@@ -413,13 +490,17 @@ func test_settings_integration():
# Value change is applied automatically through set_current_value # Value change is applied automatically through set_current_value
# Verify setting was updated # Verify setting was updated
var updated_lang = root.get_node("SettingsManager").get_setting("language") var updated_lang = root.get_node("SettingsManager").get_setting(
"language"
)
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
target_lang, updated_lang, "Language setting updated correctly" target_lang, updated_lang, "Language setting updated correctly"
) )
# Restore original language # Restore original language
root.get_node("SettingsManager").set_setting("language", original_lang) root.get_node("SettingsManager").set_setting(
"language", original_lang
)
func test_boundary_conditions(): func test_boundary_conditions():
@@ -435,12 +516,16 @@ func test_boundary_conditions():
await process_frame await process_frame
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"", empty_stepper.get_current_value(), "Empty values array returns empty string" "",
empty_stepper.get_current_value(),
"Empty values array returns empty string"
) )
# Test change_value with empty array # Test change_value with empty array
empty_stepper.change_value(1) # Should not crash empty_stepper.change_value(1) # Should not crash
TestHelperClass.assert_true(true, "change_value handles empty array gracefully") TestHelperClass.assert_true(
true, "change_value handles empty array gracefully"
)
empty_stepper.queue_free() empty_stepper.queue_free()
@@ -450,14 +535,18 @@ func test_boundary_conditions():
stepper_instance.current_index = -1 stepper_instance.current_index = -1
# Display updates automatically when value changes # Display updates automatically when value changes
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"N/A", stepper_instance.value_display.text, "Negative index shows N/A" "N/A",
stepper_instance.value_display.text,
"Negative index shows N/A"
) )
# Test out-of-bounds index handling # Test out-of-bounds index handling
stepper_instance.current_index = stepper_instance.values.size() stepper_instance.current_index = stepper_instance.values.size()
# Display updates automatically when value changes # Display updates automatically when value changes
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"N/A", stepper_instance.value_display.text, "Out-of-bounds index shows N/A" "N/A",
stepper_instance.value_display.text,
"Out-of-bounds index shows N/A"
) )
# Restore valid index # Restore valid index
@@ -487,7 +576,8 @@ func test_error_handling():
control_name.ends_with("_stepper"), "Control name has correct suffix" control_name.ends_with("_stepper"), "Control name has correct suffix"
) )
TestHelperClass.assert_true( TestHelperClass.assert_true(
control_name.begins_with(stepper_instance.data_source), "Control name includes data source" control_name.begins_with(stepper_instance.data_source),
"Control name includes data source"
) )
# Test custom values with mismatched arrays # Test custom values with mismatched arrays
@@ -496,16 +586,22 @@ func test_error_handling():
stepper_instance.setup_custom_values(values_3, names_2) stepper_instance.setup_custom_values(values_3, names_2)
# Should handle gracefully - display_names should be duplicated from values # Should handle gracefully - display_names should be duplicated from values
TestHelperClass.assert_equal(3, stepper_instance.values.size(), "Values array size preserved")
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
2, stepper_instance.display_names.size(), "Display names size preserved as provided" 3, stepper_instance.values.size(), "Values array size preserved"
)
TestHelperClass.assert_equal(
2,
stepper_instance.display_names.size(),
"Display names size preserved as provided"
) )
# Test navigation with mismatched arrays # Test navigation with mismatched arrays
stepper_instance.current_index = 2 # Index where display_names doesn't exist stepper_instance.current_index = 2 # Index where display_names doesn't exist
# Display updates automatically when value changes # Display updates automatically when value changes
TestHelperClass.assert_equal( TestHelperClass.assert_equal(
"c", stepper_instance.value_display.text, "Falls back to value when display name missing" "c",
stepper_instance.value_display.text,
"Falls back to value when display name missing"
) )

View File

@@ -984,12 +984,10 @@ def _create_signal_flows_diagram(code_map: dict[str, Any]) -> str:
lines = ["graph LR"] lines = ["graph LR"]
connections = code_map.get("dependencies", {}).get("signal_connections", []) connections = code_map.get("dependencies", {}).get("signal_connections", [])
for conn in connections[:20]: # Limit to first 20 to avoid clutter for conn in connections[:2000]:
scene = Path(conn.get("scene", "")).stem
signal = conn.get("signal", "unknown") signal = conn.get("signal", "unknown")
from_node = conn.get("from_node", "") from_node = conn.get("from_node", "")
to_node = conn.get("to_node", "") to_node = conn.get("to_node", "")
method = conn.get("method", "")
if from_node and to_node: if from_node and to_node:
lines.append(f" {from_node} -->|{signal}| {to_node}") lines.append(f" {from_node} -->|{signal}| {to_node}")
@@ -1038,10 +1036,13 @@ def _create_dependency_graph(code_map: dict[str, Any]) -> str:
) )
def _render_diagrams_with_matplotlib(mmd_files: list[Path], verbose: bool = False) -> list[Path]: def _render_diagrams_with_matplotlib(
mmd_files: list[Path], verbose: bool = False
) -> list[Path]:
"""Render diagrams from Mermaid source using matplotlib""" """Render diagrams from Mermaid source using matplotlib"""
try: try:
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import matplotlib.patches as patches import matplotlib.patches as patches
@@ -1064,28 +1065,28 @@ def _render_diagrams_with_matplotlib(mmd_files: list[Path], verbose: bool = Fals
fig, ax = plt.subplots(figsize=(12, 8)) fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim(0, 10) ax.set_xlim(0, 10)
ax.set_ylim(0, 10) ax.set_ylim(0, 10)
ax.axis('off') ax.axis("off")
# Add title # Add title
title_map = { title_map = {
"architecture": "Autoload System Architecture", "architecture": "Autoload System Architecture",
"signal_flows": "Signal Flow Connections", "signal_flows": "Signal Flow Connections",
"scene_hierarchy": "Scene Hierarchy", "scene_hierarchy": "Scene Hierarchy",
"dependency_graph": "Module Dependencies" "dependency_graph": "Module Dependencies",
} }
title = title_map.get(diagram_name, diagram_name.replace("_", " ").title()) title = title_map.get(diagram_name, diagram_name.replace("_", " ").title())
ax.text(5, 9.5, title, ha='center', va='top', fontsize=16, weight='bold') ax.text(5, 9.5, title, ha="center", va="top", fontsize=16, weight="bold")
# Parse simple nodes from Mermaid (basic extraction) # Parse simple nodes from Mermaid (basic extraction)
nodes = [] nodes = []
for line in content.split('\n'): for line in content.split("\n"):
line = line.strip() line = line.strip()
if '[' in line and ']' in line: if "[" in line and "]" in line:
# Extract node name # Extract node name
parts = line.split('[') parts = line.split("[")
if len(parts) > 1: if len(parts) > 1:
node_name = parts[1].split(']')[0] node_name = parts[1].split("]")[0]
if node_name and not node_name.startswith('_'): if node_name and not node_name.startswith("_"):
nodes.append(node_name) nodes.append(node_name)
# Remove duplicates while preserving order # Remove duplicates while preserving order
@@ -1113,28 +1114,48 @@ def _render_diagrams_with_matplotlib(mmd_files: list[Path], verbose: bool = Fals
# Draw box # Draw box
rect = patches.FancyBboxPatch( rect = patches.FancyBboxPatch(
(x - 1, y - 0.3), 2, 0.6, (x - 1, y - 0.3),
2,
0.6,
boxstyle="round,pad=0.1", boxstyle="round,pad=0.1",
edgecolor='#3498db', edgecolor="#3498db",
facecolor='#ecf0f1', facecolor="#ecf0f1",
linewidth=2 linewidth=2,
) )
ax.add_patch(rect) ax.add_patch(rect)
# Add text # Add text
ax.text(x, y, node, ha='center', va='center', fontsize=9, weight='bold') ax.text(
x, y, node, ha="center", va="center", fontsize=9, weight="bold"
)
else: else:
# No nodes found # No nodes found
ax.text(5, 5, "No diagram data available", ha='center', va='center', fontsize=12, style='italic') ax.text(
5,
5,
"No diagram data available",
ha="center",
va="center",
fontsize=12,
style="italic",
)
# Add note # Add note
ax.text(5, 0.3, "Auto-generated diagram", ax.text(
ha='center', va='bottom', fontsize=8, style='italic', color='gray') 5,
0.3,
"Auto-generated diagram",
ha="center",
va="bottom",
fontsize=8,
style="italic",
color="gray",
)
plt.tight_layout() plt.tight_layout()
png_file = mmd_file.with_suffix(".png") png_file = mmd_file.with_suffix(".png")
plt.savefig(png_file, dpi=150, bbox_inches='tight', facecolor='white') plt.savefig(png_file, dpi=150, bbox_inches="tight", facecolor="white")
plt.close() plt.close()
rendered_files.append(png_file) rendered_files.append(png_file)
@@ -1205,7 +1226,7 @@ def _generate_autoloads_api_doc(
# Embed architecture diagram if exists # Embed architecture diagram if exists
arch_diagram = diagrams_dir / "architecture.png" arch_diagram = diagrams_dir / "architecture.png"
if arch_diagram.exists(): if arch_diagram.exists():
output.append(f"![Architecture Diagram](diagrams/architecture.png)") output.append("![Architecture Diagram](diagrams/architecture.png)")
output.append("") output.append("")
for autoload in code_map.get("autoloads", []): for autoload in code_map.get("autoloads", []):
@@ -1266,7 +1287,7 @@ def _generate_signals_catalog(
# Embed signal flows diagram if exists # Embed signal flows diagram if exists
signals_diagram = diagrams_dir / "signal_flows.png" signals_diagram = diagrams_dir / "signal_flows.png"
if signals_diagram.exists(): if signals_diagram.exists():
output.append(f"![Signal Flows](diagrams/signal_flows.png)") output.append("![Signal Flows](diagrams/signal_flows.png)")
output.append("") output.append("")
output.append("## Signal Definitions") output.append("## Signal Definitions")
@@ -1334,7 +1355,7 @@ def _generate_scene_reference(
# Embed scene hierarchy diagram if exists # Embed scene hierarchy diagram if exists
scene_diagram = diagrams_dir / "scene_hierarchy.png" scene_diagram = diagrams_dir / "scene_hierarchy.png"
if scene_diagram.exists(): if scene_diagram.exists():
output.append(f"![Scene Hierarchy](diagrams/scene_hierarchy.png)") output.append("![Scene Hierarchy](diagrams/scene_hierarchy.png)")
output.append("") output.append("")
for scene_path, scene_data in code_map.get("scenes", {}).items(): for scene_path, scene_data in code_map.get("scenes", {}).items():
@@ -1385,7 +1406,7 @@ def _generate_metrics_dashboard(
import matplotlib import matplotlib
matplotlib.use("Agg") # Non-GUI backend matplotlib.use("Agg") # Non-GUI backend
import matplotlib.pyplot as plt import matplotlib.pyplot as plt # noqa: F401 - Used in chart generation functions
except ImportError: except ImportError:
if verbose: if verbose:
print(" ⚠️ matplotlib not available, skipping metrics") print(" ⚠️ matplotlib not available, skipping metrics")
@@ -1489,8 +1510,8 @@ def _create_dashboard_markdown(code_map: dict[str, Any]) -> str:
"", "",
"## Project Statistics", "## Project Statistics",
"", "",
f"| Metric | Count |", "| Metric | Count |",
f"|--------|-------|", "|--------|-------|",
f"| Scripts | {total_scripts} |", f"| Scripts | {total_scripts} |",
f"| Scenes | {total_scenes} |", f"| Scenes | {total_scenes} |",
f"| Functions | {total_functions} |", f"| Functions | {total_functions} |",

View File

@@ -333,13 +333,31 @@ def print_result(success: bool, output: str = "", silent: bool = False) -> None:
def get_gd_files(project_root: Path) -> List[Path]: def get_gd_files(project_root: Path) -> List[Path]:
"""Get all .gd files in the project.""" """Get all .gd files in the project, respecting gitignore."""
return list(project_root.rglob("*.gd")) gitignore_patterns = read_gitignore(project_root)
gd_files = []
for gd_file in project_root.rglob("*.gd"):
if gd_file.is_file() and not is_ignored_by_gitignore(
gd_file, project_root, gitignore_patterns
):
gd_files.append(gd_file)
return gd_files
def get_py_files(project_root: Path) -> List[Path]: def get_py_files(project_root: Path) -> List[Path]:
"""Get all .py files in the project.""" """Get all .py files in the project, respecting gitignore."""
return list(project_root.rglob("*.py")) gitignore_patterns = read_gitignore(project_root)
py_files = []
for py_file in project_root.rglob("*.py"):
if py_file.is_file() and not is_ignored_by_gitignore(
py_file, project_root, gitignore_patterns
):
py_files.append(py_file)
return py_files
def read_gitignore(project_root: Path) -> List[str]: def read_gitignore(project_root: Path) -> List[str]:
@@ -661,15 +679,21 @@ def check_naming_convention(file_path: Path) -> Tuple[bool, str]:
def get_naming_files(project_root: Path) -> List[Path]: def get_naming_files(project_root: Path) -> List[Path]:
"""Get all .tscn and .gd files that should follow naming conventions.""" """Get all .tscn and .gd files that should follow naming conventions, respecting gitignore."""
gitignore_patterns = read_gitignore(project_root)
files = [] files = []
for pattern in ["**/*.tscn", "**/*.gd"]:
files.extend(project_root.glob(pattern))
# Filter out files that should be ignored for pattern in ["**/*.tscn", "**/*.gd"]:
for file_path in project_root.glob(pattern):
if file_path.is_file():
files.append(file_path)
# Filter out files that should be ignored (gitignore + TestHelper)
filtered_files = [] filtered_files = []
for file_path in files: for file_path in files:
if not should_skip_file(file_path): if not should_skip_file(file_path) and not is_ignored_by_gitignore(
file_path, project_root, gitignore_patterns
):
filtered_files.append(file_path) filtered_files.append(file_path)
return filtered_files return filtered_files
@@ -1783,7 +1807,15 @@ def run_tests(
success = failed_tests == 0 success = failed_tests == 0
if yaml_output: if yaml_output:
output_yaml_results("test", {**stats, "results": test_results}, success) yaml_results = {**stats, "results": test_results}
if failed_tests > 0:
yaml_results["failed_test_details"] = [
{"test": name, "error": error}
for name, passed, error in test_results
if not passed and error
]
output_yaml_results("test", yaml_results, success)
else: else:
print_summary("Test Execution Summary", stats, silent) print_summary("Test Execution Summary", stats, silent)
if not silent: if not silent:
@@ -1890,7 +1922,15 @@ async def run_tests_async(
} }
if yaml_output: if yaml_output:
output_yaml_results("test", {**stats, "results": test_results}, failed == 0) yaml_results = {**stats, "results": test_results}
if failed > 0:
yaml_results["failed_test_details"] = [
{"test": name, "error": error}
for name, passed, error in test_results
if not passed and error
]
output_yaml_results("test", yaml_results, failed == 0)
elif not silent: elif not silent:
print_summary("Test Summary", stats) print_summary("Test Summary", stats)
print() print()