Add gdlint and gdformat scripts

This commit is contained in:
2025-09-27 20:40:13 +04:00
parent 86439abea8
commit 06f0f87970
40 changed files with 2314 additions and 732 deletions

13
.gdformatrc Normal file
View File

@@ -0,0 +1,13 @@
# GDFormat configuration file
# This file configures the gdformat tool for consistent GDScript formatting
# Maximum line length (default is 100)
# Godot's style guide recommends keeping lines under 100 characters
line_length = 100
# Whether to use tabs or spaces for indentation
# Godot uses tabs by default
use_tabs = true
# Number of spaces per tab (when displaying)
tab_width = 4

View File

@@ -1,7 +1,9 @@
- The documentation of the project is located in docs/ directory.
- The documentation of the project is located in docs/ directory;
- Get following files in context before doing anything else:
- docs\CLAUDE.md
- docs\CODE_OF_CONDUCT.md
- project.godot
- Use TDD methodology for development
- Keep documentation up to date
- Use TDD methodology for development;
- Use static data types;
- Keep documentation up to date;
- Always run gdlint, gdformat and run tests;

View File

@@ -1,15 +1,19 @@
# Example of how to use the ValueStepper component in any scene
extends Control
@onready var language_stepper: ValueStepper = $VBoxContainer/Examples/LanguageContainer/LanguageStepper
@onready var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContainer/DifficultyStepper
@onready var resolution_stepper: ValueStepper = $VBoxContainer/Examples/ResolutionContainer/ResolutionStepper
@onready
var language_stepper: ValueStepper = $VBoxContainer/Examples/LanguageContainer/LanguageStepper
@onready
var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContainer/DifficultyStepper
@onready
var resolution_stepper: ValueStepper = $VBoxContainer/Examples/ResolutionContainer/ResolutionStepper
@onready var custom_stepper: ValueStepper = $VBoxContainer/Examples/CustomContainer/CustomStepper
# Example of setting up custom navigation
var navigable_steppers: Array[ValueStepper] = []
var current_stepper_index: int = 0
func _ready():
DebugManager.log_info("ValueStepper example ready", "Example")
@@ -30,6 +34,7 @@ func _ready():
# Highlight first stepper
_update_stepper_highlighting()
func _input(event: InputEvent):
# Example navigation handling
if event.is_action_pressed("move_up"):
@@ -45,6 +50,7 @@ func _input(event: InputEvent):
_handle_stepper_input("move_right")
get_viewport().set_input_as_handled()
func _navigate_steppers(direction: int):
current_stepper_index = (current_stepper_index + direction) % navigable_steppers.size()
if current_stepper_index < 0:
@@ -52,21 +58,27 @@ func _navigate_steppers(direction: int):
_update_stepper_highlighting()
DebugManager.log_info("Stepper navigation: index " + str(current_stepper_index), "Example")
func _handle_stepper_input(action: String):
if current_stepper_index >= 0 and current_stepper_index < navigable_steppers.size():
var stepper = navigable_steppers[current_stepper_index]
if stepper.handle_input_action(action):
AudioManager.play_ui_click()
func _update_stepper_highlighting():
for i in range(navigable_steppers.size()):
navigable_steppers[i].set_highlighted(i == current_stepper_index)
func _on_stepper_value_changed(new_value: String, new_index: int):
DebugManager.log_info("Stepper value changed to: " + new_value + " (index: " + str(new_index) + ")", "Example")
DebugManager.log_info(
"Stepper value changed to: " + new_value + " (index: " + str(new_index) + ")", "Example"
)
# Handle value change in your scene
# For example: apply settings, save preferences, update UI, etc.
# Example of programmatically setting values
func _on_reset_to_defaults_pressed():
AudioManager.play_ui_click()

46
gdlintrc Normal file
View File

@@ -0,0 +1,46 @@
class-definitions-order:
- tools
- classnames
- extends
- signals
- enums
- consts
- exports
- pubvars
- prvvars
- onreadypubvars
- onreadyprvvars
- others
class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
class-name: ([A-Z][a-z0-9]*)+
class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
comparison-with-itself: null
constant-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'
disable: []
duplicated-load: null
enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'
enum-name: ([A-Z][a-z0-9]*)+
excluded_directories: !!set
.git: null
expression-not-assigned: null
function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
function-arguments-number: 10
function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
function-preload-variable-name: ([A-Z][a-z0-9]*)+
function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
load-constant-name: (([A-Z][a-z0-9]*)+|[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)
loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
max-file-lines: 1000
max-line-length: 100
max-public-methods: 20
max-returns: 6
mixed-tabs-and-spaces: null
no-elif-return: null
no-else-return: null
private-method-call: null
signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
sub-class-name: _?([A-Z][a-z0-9]*)+
tab-characters: 1
trailing-whitespace: null
unnecessary-pass: null
unused-argument: null

89
run_all.bat Normal file
View File

@@ -0,0 +1,89 @@
@echo off
setlocal enabledelayedexpansion
echo ================================
echo Development Workflow Runner
echo ================================
echo.
echo This script will run the complete development workflow:
echo 1. Code linting (gdlint)
echo 2. Code formatting (gdformat)
echo 3. Test execution (godot tests)
echo.
set start_time=%time%
REM Step 1: Run Linters
echo --------------------------------
echo Step 1: Running Linters
echo --------------------------------
call run_lint.bat
set lint_result=!errorlevel!
if !lint_result! neq 0 (
echo.
echo ❌ LINTING FAILED - Workflow aborted
echo Please fix linting errors before continuing
pause
exit /b 1
)
echo ✅ Linting completed successfully
echo.
REM Step 2: Run Formatters
echo --------------------------------
echo Step 2: Running Formatters
echo --------------------------------
call run_format.bat
set format_result=!errorlevel!
if !format_result! neq 0 (
echo.
echo ❌ FORMATTING FAILED - Workflow aborted
echo Please fix formatting errors before continuing
pause
exit /b 1
)
echo ✅ Formatting completed successfully
echo.
REM Step 3: Run Tests
echo --------------------------------
echo Step 3: Running Tests
echo --------------------------------
call run_tests.bat
set test_result=!errorlevel!
if !test_result! neq 0 (
echo.
echo ❌ TESTS FAILED - Workflow completed with errors
set workflow_failed=1
) else (
echo ✅ Tests completed successfully
set workflow_failed=0
)
echo.
REM Calculate elapsed time
set end_time=%time%
echo ================================
echo Workflow Summary
echo ================================
echo Linting: ✅ PASSED
echo Formatting: ✅ PASSED
if !workflow_failed! equ 0 (
echo Testing: ✅ PASSED
echo.
echo ✅ ALL WORKFLOW STEPS COMPLETED SUCCESSFULLY!
echo Your code is ready for commit.
) else (
echo Testing: ❌ FAILED
echo.
echo ❌ WORKFLOW COMPLETED WITH TEST FAILURES
echo Please review and fix failing tests before committing.
)
echo.
echo Start time: %start_time%
echo End time: %end_time%
pause
exit /b !workflow_failed!

103
run_format.bat Normal file
View File

@@ -0,0 +1,103 @@
@echo off
setlocal enabledelayedexpansion
echo ================================
echo GDScript Formatter
echo ================================
echo.
REM Check if Python is available
python --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: Python is not installed or not in PATH
echo.
echo Installation instructions:
echo 1. Install Python: winget install Python.Python.3.13
echo 2. Restart your command prompt
echo 3. Run this script again
echo.
pause
exit /b 1
)
REM Check if pip is available
pip --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: pip is not installed or not in PATH
echo Please ensure Python was installed correctly with pip
pause
exit /b 1
)
REM Check if gdformat is available
gdformat --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: gdformat is not installed or not in PATH
echo.
echo Installation instructions:
echo 1. pip install --upgrade "setuptools<81"
echo 2. pip install gdtoolkit==4
echo 3. Restart your command prompt
echo 4. Run this script again
echo.
pause
exit /b 1
)
echo Formatting GDScript files...
echo.
REM Count total .gd files
set total_files=0
for /r %%f in (*.gd) do (
set /a total_files+=1
)
echo Found !total_files! GDScript files to format.
echo.
REM Format all .gd files recursively
set formatted_files=0
set failed_files=0
for /r %%f in (*.gd) do (
echo Formatting: %%~nxf
REM Skip TestHelper.gd due to static var syntax incompatibility with gdformat
if "%%~nxf"=="TestHelper.gd" (
echo ⚠️ Skipped (static var syntax not supported by gdformat)
set /a formatted_files+=1
echo.
goto :continue_format_loop
)
gdformat "%%f"
if !errorlevel! equ 0 (
echo ✅ Success
set /a formatted_files+=1
) else (
echo ❌ FAILED: %%f
set /a failed_files+=1
)
echo.
:continue_format_loop
)
echo.
echo ================================
echo Formatting Summary
echo ================================
echo Total files: !total_files!
echo Successfully formatted: !formatted_files!
echo Failed: !failed_files!
if !failed_files! gtr 0 (
echo.
echo ⚠️ WARNING: Some files failed to format
exit /b 1
) else (
echo.
echo ✅ All GDScript files formatted successfully!
exit /b 0
)

122
run_lint.bat Normal file
View File

@@ -0,0 +1,122 @@
@echo off
setlocal enabledelayedexpansion
echo ================================
echo GDScript Linter
echo ================================
echo.
REM Check if Python is available
python --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: Python is not installed or not in PATH
echo.
echo Installation instructions:
echo 1. Install Python: winget install Python.Python.3.13
echo 2. Restart your command prompt
echo 3. Run this script again
echo.
pause
exit /b 1
)
REM Check if pip is available
pip --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: pip is not installed or not in PATH
echo Please ensure Python was installed correctly with pip
pause
exit /b 1
)
REM Check if gdlint is available
gdlint --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: gdlint is not installed or not in PATH
echo.
echo Installation instructions:
echo 1. pip install --upgrade "setuptools<81"
echo 2. pip install gdtoolkit==4
echo 3. Restart your command prompt
echo 4. Run this script again
echo.
pause
exit /b 1
)
echo Linting GDScript files...
echo.
REM Count total .gd files
set total_files=0
for /r %%f in (*.gd) do (
set /a total_files+=1
)
echo Found !total_files! GDScript files to lint.
echo.
REM Lint all .gd files recursively
set linted_files=0
set failed_files=0
set warning_files=0
for /r %%f in (*.gd) do (
echo Linting: %%~nxf
REM Skip TestHelper.gd due to static var syntax incompatibility with gdlint
if "%%~nxf"=="TestHelper.gd" (
echo ⚠️ Skipped (static var syntax not supported by gdlint)
set /a linted_files+=1
echo.
goto :continue_loop
)
gdlint "%%f" >temp_lint_output.txt 2>&1
set lint_exit_code=!errorlevel!
REM Check if there's output (warnings/errors)
for %%A in (temp_lint_output.txt) do set size=%%~zA
if !lint_exit_code! equ 0 (
if !size! gtr 0 (
echo WARNINGS found:
type temp_lint_output.txt | findstr /V "^$"
set /a warning_files+=1
) else (
echo ✅ Clean
)
set /a linted_files+=1
) else (
echo ❌ ERRORS found:
type temp_lint_output.txt | findstr /V "^$"
set /a failed_files+=1
)
del temp_lint_output.txt >nul 2>&1
echo.
:continue_loop
)
echo ================================
echo Linting Summary
echo ================================
echo Total files: !total_files!
echo Clean files: !linted_files!
echo Files with warnings: !warning_files!
echo Files with errors: !failed_files!
if !failed_files! gtr 0 (
echo.
echo ❌ Linting FAILED - Please fix the errors above
exit /b 1
) else if !warning_files! gtr 0 (
echo.
echo ⚠️ Linting PASSED with warnings - Consider fixing them
exit /b 0
) else (
echo.
echo ✅ All GDScript files passed linting!
exit /b 0
)

View File

@@ -1,9 +1,27 @@
@echo off
setlocal enabledelayedexpansion
echo Automated Test Suite Runner
echo ============================
echo ================================
echo GDScript Test Runner
echo ================================
echo.
REM Check if Godot is available
godot --version >nul 2>&1
if !errorlevel! neq 0 (
echo ERROR: Godot is not installed or not in PATH
echo.
echo Installation instructions:
echo 1. Download Godot from https://godotengine.org/download
echo 2. Add Godot executable to your PATH environment variable
echo 3. Or place godot.exe in this project directory
echo 4. Restart your command prompt
echo 5. Run this script again
echo.
pause
exit /b 1
)
echo Scanning for test files in tests\ directory...
set total_tests=0
@@ -34,9 +52,9 @@ echo Tests Passed: !passed_tests!
echo Tests Failed: !failed_tests!
if !failed_tests! equ 0 (
echo ALL TESTS PASSED!
echo ALL TESTS PASSED!
) else (
echo !failed_tests! TEST(S) FAILED
echo !failed_tests! TEST(S) FAILED
)
pause
@@ -95,4 +113,4 @@ REM Clean up temporary file
if exist temp_test_output.txt del temp_test_output.txt
echo.
goto :eof
goto :eof

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env bash
echo "Automated Test Suite Runner"
echo "==========================="
echo ""
echo "Scanning for test files in tests/ directory..."
# Function to run a single test file
run_test() {
local test_file="$1"
local test_name="$2"
echo ""
echo "=== $test_name ==="
echo "Running: $test_file"
if godot --headless --script "$test_file"; then
echo "✅ PASSED: $test_name"
else
echo "❌ FAILED: $test_name"
((failed_tests++))
fi
((total_tests++))
echo ""
}
# Initialize counters
total_tests=0
failed_tests=0
start_time=$(date +%s)
# Find and run all test files
echo "Discovered test files:"
# Core test files in tests/ directory
for test_file in tests/test_*.gd; do
if [ -f "$test_file" ]; then
# Extract descriptive name from filename
filename=$(basename "$test_file" .gd)
test_name=$(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')
echo " 📄 $test_file -> $test_name"
fi
done
# Additional test directories
if [ -d "tests/unit" ]; then
for test_file in tests/unit/test_*.gd; do
if [ -f "$test_file" ]; then
filename=$(basename "$test_file" .gd)
test_name="Unit: $(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')"
echo " 📄 $test_file -> $test_name"
fi
done
fi
if [ -d "tests/integration" ]; then
for test_file in tests/integration/test_*.gd; do
if [ -f "$test_file" ]; then
filename=$(basename "$test_file" .gd)
test_name="Integration: $(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')"
echo " 📄 $test_file -> $test_name"
fi
done
fi
echo ""
echo "Starting test execution..."
echo ""
# Run core tests
for test_file in tests/test_*.gd; do
if [ -f "$test_file" ]; then
filename=$(basename "$test_file" .gd)
test_name=$(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')
run_test "$test_file" "$test_name"
fi
done
# Run unit tests
if [ -d "tests/unit" ]; then
for test_file in tests/unit/test_*.gd; do
if [ -f "$test_file" ]; then
filename=$(basename "$test_file" .gd)
test_name="Unit: $(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')"
run_test "$test_file" "$test_name"
fi
done
fi
# Run integration tests
if [ -d "tests/integration" ]; then
for test_file in tests/integration/test_*.gd; do
if [ -f "$test_file" ]; then
filename=$(basename "$test_file" .gd)
test_name="Integration: $(echo "$filename" | sed 's/test_//' | sed 's/_/ /g' | sed 's/\b\w/\U&/g')"
run_test "$test_file" "$test_name"
fi
done
fi
# Calculate execution time
end_time=$(date +%s)
execution_time=$((end_time - start_time))
# Print summary
echo "================================"
echo "📊 Test Execution Summary"
echo "================================"
echo "Total Tests Run: $total_tests"
echo "Tests Passed: $((total_tests - failed_tests))"
echo "Tests Failed: $failed_tests"
echo "Execution Time: ${execution_time}s"
if [ $failed_tests -eq 0 ]; then
echo "✅ ALL TESTS PASSED!"
exit 0
else
echo "$failed_tests TEST(S) FAILED"
exit 1
fi

View File

@@ -10,14 +10,19 @@ const GAMEPLAY_SCENES = {
@onready var score_display: Label = $UI/ScoreDisplay
var current_gameplay_mode: String
var global_score: int = 0 : set = set_global_score
var global_score: int = 0:
set = set_global_score
func _ready() -> void:
if not back_button.pressed.is_connected(_on_back_button_pressed):
back_button.pressed.connect(_on_back_button_pressed)
# GameManager will set the gameplay mode, don't set default here
DebugManager.log_debug("Game _ready() completed, waiting for GameManager to set gameplay mode", "Game")
DebugManager.log_debug(
"Game _ready() completed, waiting for GameManager to set gameplay mode", "Game"
)
func set_gameplay_mode(mode: String) -> void:
DebugManager.log_info("set_gameplay_mode called with mode: %s" % mode, "Game")
@@ -25,6 +30,7 @@ func set_gameplay_mode(mode: String) -> void:
await load_gameplay(mode)
DebugManager.log_info("set_gameplay_mode completed for mode: %s" % mode, "Game")
func load_gameplay(mode: String) -> void:
DebugManager.log_debug("Loading gameplay mode: %s" % mode, "Game")
@@ -38,7 +44,10 @@ func load_gameplay(mode: String) -> void:
# Wait for children to be properly removed from scene tree
await get_tree().process_frame
DebugManager.log_debug("Children removal complete, container count: %d" % gameplay_container.get_child_count(), "Game")
DebugManager.log_debug(
"Children removal complete, container count: %d" % gameplay_container.get_child_count(),
"Game"
)
# Load new gameplay
if GAMEPLAY_SCENES.has(mode):
@@ -47,7 +56,13 @@ func load_gameplay(mode: String) -> void:
var gameplay_instance = gameplay_scene.instantiate()
DebugManager.log_debug("Instantiated gameplay: %s" % gameplay_instance.name, "Game")
gameplay_container.add_child(gameplay_instance)
DebugManager.log_debug("Added gameplay to container, child count now: %d" % gameplay_container.get_child_count(), "Game")
DebugManager.log_debug(
(
"Added gameplay to container, child count now: %d"
% gameplay_container.get_child_count()
),
"Game"
)
# Connect gameplay signals to shared systems
if gameplay_instance.has_signal("score_changed"):
@@ -56,23 +71,28 @@ func load_gameplay(mode: String) -> void:
else:
DebugManager.log_error("Gameplay mode '%s' not found in GAMEPLAY_SCENES" % mode, "Game")
func set_global_score(value: int) -> void:
global_score = value
if score_display:
score_display.text = "Score: " + str(global_score)
func _on_score_changed(points: int) -> void:
self.global_score += points
SaveManager.update_current_score(self.global_score)
func get_global_score() -> int:
return global_score
func _get_current_gameplay_instance() -> Node:
if gameplay_container.get_child_count() > 0:
return gameplay_container.get_child(0)
return null
func _on_back_button_pressed() -> void:
DebugManager.log_debug("Back button pressed in game scene", "Game")
AudioManager.play_ui_click()
@@ -91,6 +111,7 @@ func _on_back_button_pressed() -> void:
SaveManager.finish_game(global_score)
GameManager.exit_to_main_menu()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_back"):
# Handle gamepad/keyboard back action - same as back button

View File

@@ -1,5 +1,6 @@
extends DebugMenuBase
func _ready():
# Set specific configuration for Match3DebugMenu
log_category = "Match3"
@@ -15,6 +16,7 @@ func _ready():
if current_debug_state:
_on_debug_toggled(true)
func _find_target_scene():
# Debug menu is now: Match3 -> UILayer -> Match3DebugMenu
# So we need to go up two levels: get_parent() = UILayer, get_parent().get_parent() = Match3
@@ -25,7 +27,15 @@ func _find_target_scene():
var script_path = potential_match3.get_script().resource_path
if script_path == target_script_path:
match3_scene = potential_match3
DebugManager.log_debug("Found match3 scene: " + match3_scene.name + " at path: " + str(match3_scene.get_path()), log_category)
DebugManager.log_debug(
(
"Found match3 scene: "
+ match3_scene.name
+ " at path: "
+ str(match3_scene.get_path())
),
log_category
)
_update_ui_from_scene()
_stop_search_timer()
return

View File

@@ -2,6 +2,7 @@ extends Node2D
signal score_changed(points: int)
func _ready():
DebugManager.log_info("Clickomania gameplay loaded", "Clickomania")
# Example: Add some score after a few seconds to test the system

View File

@@ -3,12 +3,7 @@ extends Node2D
signal score_changed(points: int)
signal grid_state_loaded(grid_size: Vector2i, tile_types: int)
enum GameState {
WAITING, # Waiting for player input
SELECTING, # First tile selected
SWAPPING, # Animating tile swap
PROCESSING # Processing matches and cascades
}
enum GameState { WAITING, SELECTING, SWAPPING, PROCESSING } # Waiting for player input # First tile selected # Animating tile swap # Processing matches and cascades
var GRID_SIZE := Vector2i(8, 8)
var TILE_TYPES := 5
@@ -32,22 +27,26 @@ const CASCADE_WAIT_TIME := 0.1
const SWAP_ANIMATION_TIME := 0.3
const TILE_DROP_WAIT_TIME := 0.2
var grid := []
var grid: Array[Array] = []
var tile_size: float = 48.0
var grid_offset: Vector2
var grid_offset: Vector2 = Vector2.ZERO
var current_state: GameState = GameState.WAITING
var selected_tile: Node2D = null
var cursor_position: Vector2i = Vector2i(0, 0)
var keyboard_navigation_enabled: bool = false
var grid_initialized: bool = false
var instance_id: String
var instance_id: String = ""
func _ready():
func _ready() -> void:
# Generate instance ID
instance_id = "Match3_%d" % get_instance_id()
if grid_initialized:
DebugManager.log_warn("[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id, "Match3")
DebugManager.log_warn(
"[%s] Match3 _ready() called multiple times, skipping initialization" % instance_id,
"Match3"
)
return
DebugManager.log_debug("[%s] Match3 _ready() started" % instance_id, "Match3")
@@ -72,6 +71,7 @@ func _ready():
# Debug: Check scene tree structure
call_deferred("_debug_scene_structure")
func _calculate_grid_layout():
var viewport_size = get_viewport().get_visible_rect().size
var available_width = viewport_size.x * SCREEN_WIDTH_USAGE
@@ -85,13 +85,13 @@ func _calculate_grid_layout():
# Align grid to left side with margins
var total_grid_height = tile_size * GRID_SIZE.y
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
)
func _initialize_grid():
# Create gem pool for current tile types
var gem_indices = []
var gem_indices: Array[int] = []
for i in range(TILE_TYPES):
gem_indices.append(i)
@@ -113,9 +113,16 @@ func _initialize_grid():
# Connect tile signals
tile.tile_selected.connect(_on_tile_selected)
DebugManager.log_debug("Created tile at grid(%d,%d) world_pos(%s) with type %d" % [x, y, tile_position, new_type], "Match3")
DebugManager.log_debug(
(
"Created tile at grid(%d,%d) world_pos(%s) with type %d"
% [x, y, tile_position, new_type]
),
"Match3"
)
grid[y].append(tile)
func _has_match_at(pos: Vector2i) -> bool:
# Bounds and null checks
if not _is_valid_grid_position(pos):
@@ -131,7 +138,9 @@ func _has_match_at(pos: Vector2i) -> bool:
# Check if tile has required properties
if not "tile_type" in tile:
DebugManager.log_warn("Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3")
DebugManager.log_warn(
"Tile at (%d,%d) missing tile_type property" % [pos.x, pos.y], "Match3"
)
return false
var matches_horizontal = _get_match_line(pos, Vector2i(1, 0))
@@ -141,6 +150,7 @@ func _has_match_at(pos: Vector2i) -> bool:
var matches_vertical = _get_match_line(pos, Vector2i(0, 1))
return matches_vertical.size() >= 3
# Check for any matches on the board
func _check_for_matches() -> bool:
for y in range(GRID_SIZE.y):
@@ -149,14 +159,19 @@ func _check_for_matches() -> bool:
return true
return false
func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
# Validate input parameters
if not _is_valid_grid_position(start):
DebugManager.log_error("Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3")
DebugManager.log_error(
"Invalid start position for match line: (%d,%d)" % [start.x, start.y], "Match3"
)
return []
if abs(dir.x) + abs(dir.y) != 1 or (dir.x != 0 and dir.y != 0):
DebugManager.log_error("Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3")
DebugManager.log_error(
"Invalid direction vector for match line: (%d,%d)" % [dir.x, dir.y], "Match3"
)
return []
# Check grid bounds and tile validity
@@ -194,6 +209,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
return line
func _clear_matches():
# Check grid integrity
if not _validate_grid_integrity():
@@ -278,10 +294,17 @@ func _clear_matches():
var tile_pos = tile.grid_position
# Validate grid position before clearing reference
if _is_valid_grid_position(tile_pos) and tile_pos.y < grid.size() and tile_pos.x < grid[tile_pos.y].size():
if (
_is_valid_grid_position(tile_pos)
and tile_pos.y < grid.size()
and tile_pos.x < grid[tile_pos.y].size()
):
grid[tile_pos.y][tile_pos.x] = null
else:
DebugManager.log_warn("Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y], "Match3")
DebugManager.log_warn(
"Invalid grid position during tile removal: (%d,%d)" % [tile_pos.x, tile_pos.y],
"Match3"
)
tile.queue_free()
@@ -291,6 +314,7 @@ func _clear_matches():
await get_tree().create_timer(TILE_DROP_WAIT_TIME).timeout
_fill_empty_cells()
func _drop_tiles():
var moved = true
while moved:
@@ -309,6 +333,7 @@ func _drop_tiles():
tile.position = grid_offset + Vector2(x, y + 1) * tile_size
moved = true
func _fill_empty_cells():
# Safety check for grid integrity
if not _validate_grid_integrity():
@@ -316,7 +341,7 @@ func _fill_empty_cells():
return
# Create gem pool for current tile types
var gem_indices = []
var gem_indices: Array[int] = []
for i in range(TILE_TYPES):
gem_indices.append(i)
@@ -334,7 +359,9 @@ func _fill_empty_cells():
if not grid[y][x]:
var tile = TILE_SCENE.instantiate()
if not tile:
DebugManager.log_error("Failed to instantiate tile at (%d,%d)" % [x, y], "Match3")
DebugManager.log_error(
"Failed to instantiate tile at (%d,%d)" % [x, y], "Match3"
)
continue
tile.grid_position = Vector2i(x, y)
@@ -375,19 +402,35 @@ func _fill_empty_cells():
iteration += 1
if iteration >= MAX_CASCADE_ITERATIONS:
DebugManager.log_warn("Maximum cascade iterations reached (%d), stopping to prevent infinite loop" % MAX_CASCADE_ITERATIONS, "Match3")
DebugManager.log_warn(
(
"Maximum cascade iterations reached (%d), stopping to prevent infinite loop"
% MAX_CASCADE_ITERATIONS
),
"Match3"
)
# Save grid state after cascades complete
save_current_state()
func regenerate_grid():
# Validate grid size before regeneration
if GRID_SIZE.x < MIN_GRID_SIZE or GRID_SIZE.y < MIN_GRID_SIZE or GRID_SIZE.x > MAX_GRID_SIZE or GRID_SIZE.y > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3")
if (
GRID_SIZE.x < MIN_GRID_SIZE
or GRID_SIZE.y < MIN_GRID_SIZE
or GRID_SIZE.x > MAX_GRID_SIZE
or GRID_SIZE.y > MAX_GRID_SIZE
):
DebugManager.log_error(
"Invalid grid size for regeneration: %dx%d" % [GRID_SIZE.x, GRID_SIZE.y], "Match3"
)
return
if TILE_TYPES < 3 or TILE_TYPES > MAX_TILE_TYPES:
DebugManager.log_error("Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3")
DebugManager.log_error(
"Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3"
)
return
# Use time-based seed to ensure different patterns each time
@@ -440,6 +483,7 @@ func regenerate_grid():
# Regenerate the grid with safety checks
_initialize_grid()
func set_tile_types(new_count: int):
# Input validation
if new_count < 3:
@@ -447,7 +491,9 @@ func set_tile_types(new_count: int):
return
if new_count > MAX_TILE_TYPES:
DebugManager.log_error("Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3")
DebugManager.log_error(
"Tile types count too high: %d (maximum %d)" % [new_count, MAX_TILE_TYPES], "Match3"
)
return
if new_count == TILE_TYPES:
@@ -460,14 +506,27 @@ func set_tile_types(new_count: int):
# Regenerate grid with new tile types (gem pool is updated in regenerate_grid)
await regenerate_grid()
func set_grid_size(new_size: Vector2i):
# Comprehensive input validation
if new_size.x < MIN_GRID_SIZE or new_size.y < MIN_GRID_SIZE:
DebugManager.log_error("Grid size too small: %dx%d (minimum %dx%d)" % [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE], "Match3")
DebugManager.log_error(
(
"Grid size too small: %dx%d (minimum %dx%d)"
% [new_size.x, new_size.y, MIN_GRID_SIZE, MIN_GRID_SIZE]
),
"Match3"
)
return
if new_size.x > MAX_GRID_SIZE or new_size.y > MAX_GRID_SIZE:
DebugManager.log_error("Grid size too large: %dx%d (maximum %dx%d)" % [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "Match3")
DebugManager.log_error(
(
"Grid size too large: %dx%d (maximum %dx%d)"
% [new_size.x, new_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE]
),
"Match3"
)
return
if new_size == GRID_SIZE:
@@ -480,6 +539,7 @@ func set_grid_size(new_size: Vector2i):
# Regenerate grid with new size
await regenerate_grid()
func reset_all_visual_states() -> void:
# Debug function to reset all tile visual states
DebugManager.log_debug("Resetting all tile visual states", "Match3")
@@ -493,6 +553,7 @@ func reset_all_visual_states() -> void:
current_state = GameState.WAITING
keyboard_navigation_enabled = false
func _debug_scene_structure() -> void:
DebugManager.log_debug("=== Scene Structure Debug ===", "Match3")
DebugManager.log_debug("Match3 node children count: %d" % get_child_count(), "Match3")
@@ -510,22 +571,30 @@ func _debug_scene_structure() -> void:
for x in range(GRID_SIZE.x):
if y < grid.size() and x < grid[y].size() and grid[y][x]:
tile_count += 1
DebugManager.log_debug("Created %d tiles out of %d expected" % [tile_count, GRID_SIZE.x * GRID_SIZE.y], "Match3")
DebugManager.log_debug(
"Created %d tiles out of %d expected" % [tile_count, GRID_SIZE.x * GRID_SIZE.y], "Match3"
)
# Check first tile in detail
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
var first_tile = grid[0][0]
DebugManager.log_debug("First tile global position: %s" % first_tile.global_position, "Match3")
DebugManager.log_debug(
"First tile global position: %s" % first_tile.global_position, "Match3"
)
DebugManager.log_debug("First tile local position: %s" % first_tile.position, "Match3")
# Check parent chain
var current_node = self
var depth = 0
while current_node and depth < 10:
DebugManager.log_debug("Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()], "Match3")
DebugManager.log_debug(
"Parent level %d: %s (type: %s)" % [depth, current_node.name, current_node.get_class()],
"Match3"
)
current_node = current_node.get_parent()
depth += 1
func _input(event: InputEvent) -> void:
# Debug key to reset all visual states
if event.is_action_pressed("action_east") and DebugManager.is_debug_enabled():
@@ -560,7 +629,9 @@ func _move_cursor(direction: Vector2i) -> void:
return
if direction.x != 0 and direction.y != 0:
DebugManager.log_error("Diagonal cursor movement not supported: " + str(direction), "Match3")
DebugManager.log_error(
"Diagonal cursor movement not supported: " + str(direction), "Match3"
)
return
# Validate grid integrity before cursor operations
@@ -582,7 +653,10 @@ func _move_cursor(direction: Vector2i) -> void:
if not old_tile.is_selected:
old_tile.is_highlighted = false
DebugManager.log_debug("Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y], "Match3")
DebugManager.log_debug(
"Cursor moved from (%d,%d) to (%d,%d)" % [old_pos.x, old_pos.y, new_pos.x, new_pos.y],
"Match3"
)
cursor_position = new_pos
# Safe access to new tile
@@ -591,25 +665,48 @@ func _move_cursor(direction: Vector2i) -> void:
if not new_tile.is_selected:
new_tile.is_highlighted = true
func _select_tile_at_cursor() -> void:
# Validate cursor position and grid integrity
if not _is_valid_grid_position(cursor_position):
DebugManager.log_warn("Invalid cursor position for selection: (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
DebugManager.log_warn(
(
"Invalid cursor position for selection: (%d,%d)"
% [cursor_position.x, cursor_position.y]
),
"Match3"
)
return
var tile = _safe_grid_access(cursor_position)
if tile:
DebugManager.log_debug("Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
DebugManager.log_debug(
"Keyboard selection at cursor (%d,%d)" % [cursor_position.x, cursor_position.y],
"Match3"
)
_on_tile_selected(tile)
else:
DebugManager.log_warn("No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
DebugManager.log_warn(
"No valid tile at cursor position (%d,%d)" % [cursor_position.x, cursor_position.y],
"Match3"
)
func _on_tile_selected(tile: Node2D) -> void:
if current_state == GameState.SWAPPING or current_state == GameState.PROCESSING:
DebugManager.log_debug("Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]], "Match3")
DebugManager.log_debug(
"Tile selection ignored - game busy (state: %s)" % [GameState.keys()[current_state]],
"Match3"
)
return
DebugManager.log_debug("Tile selected at (%d,%d), gem type: %d" % [tile.grid_position.x, tile.grid_position.y, tile.tile_type], "Match3")
DebugManager.log_debug(
(
"Tile selected at (%d,%d), gem type: %d"
% [tile.grid_position.x, tile.grid_position.y, tile.tile_type]
),
"Match3"
)
if current_state == GameState.WAITING:
# First tile selection
@@ -621,7 +718,18 @@ func _on_tile_selected(tile: Node2D) -> void:
_deselect_tile()
else:
# Attempt to swap with selected tile
DebugManager.log_debug("Attempting swap between (%d,%d) and (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y, tile.grid_position.x, tile.grid_position.y], "Match3")
DebugManager.log_debug(
(
"Attempting swap between (%d,%d) and (%d,%d)"
% [
selected_tile.grid_position.x,
selected_tile.grid_position.y,
tile.grid_position.x,
tile.grid_position.y
]
),
"Match3"
)
_attempt_swap(selected_tile, tile)
@@ -629,13 +737,22 @@ func _select_tile(tile: Node2D) -> void:
selected_tile = tile
tile.is_selected = true
current_state = GameState.SELECTING
DebugManager.log_debug("Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3")
DebugManager.log_debug(
"Selected tile at (%d, %d)" % [tile.grid_position.x, tile.grid_position.y], "Match3"
)
func _deselect_tile() -> void:
if selected_tile and is_instance_valid(selected_tile):
# Safe access to tile properties
if "grid_position" in selected_tile:
DebugManager.log_debug("Deselecting tile at (%d,%d)" % [selected_tile.grid_position.x, selected_tile.grid_position.y], "Match3")
DebugManager.log_debug(
(
"Deselecting tile at (%d,%d)"
% [selected_tile.grid_position.x, selected_tile.grid_position.y]
),
"Match3"
)
else:
DebugManager.log_debug("Deselecting tile (no grid position available)", "Match3")
@@ -653,7 +770,13 @@ func _deselect_tile() -> void:
var cursor_tile = _safe_grid_access(cursor_position)
if cursor_tile and "is_highlighted" in cursor_tile:
cursor_tile.is_highlighted = true
DebugManager.log_debug("Restored cursor highlighting at (%d,%d)" % [cursor_position.x, cursor_position.y], "Match3")
DebugManager.log_debug(
(
"Restored cursor highlighting at (%d,%d)"
% [cursor_position.x, cursor_position.y]
),
"Match3"
)
else:
# For mouse navigation, just clear highlighting
if "is_highlighted" in selected_tile:
@@ -662,6 +785,7 @@ func _deselect_tile() -> void:
selected_tile = null
current_state = GameState.WAITING
func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool:
if not tile1 or not tile2:
return false
@@ -671,19 +795,44 @@ func _are_adjacent(tile1: Node2D, tile2: Node2D) -> bool:
var diff = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y)
return diff == 1
func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
if not _are_adjacent(tile1, tile2):
DebugManager.log_debug("Tiles are not adjacent, cannot swap", "Match3")
return
DebugManager.log_debug("Starting swap animation: (%d,%d)[type:%d] <-> (%d,%d)[type:%d]" % [tile1.grid_position.x, tile1.grid_position.y, tile1.tile_type, tile2.grid_position.x, tile2.grid_position.y, tile2.tile_type], "Match3")
DebugManager.log_debug(
(
"Starting swap animation: (%d,%d)[type:%d] <-> (%d,%d)[type:%d]"
% [
tile1.grid_position.x,
tile1.grid_position.y,
tile1.tile_type,
tile2.grid_position.x,
tile2.grid_position.y,
tile2.tile_type
]
),
"Match3"
)
current_state = GameState.SWAPPING
await _swap_tiles(tile1, tile2)
# Check if swap creates matches
if _has_match_at(tile1.grid_position) or _has_match_at(tile2.grid_position):
DebugManager.log_info("Valid swap created matches at (%d,%d) or (%d,%d)" % [tile1.grid_position.x, tile1.grid_position.y, tile2.grid_position.x, tile2.grid_position.y], "Match3")
DebugManager.log_info(
(
"Valid swap created matches at (%d,%d) or (%d,%d)"
% [
tile1.grid_position.x,
tile1.grid_position.y,
tile2.grid_position.x,
tile2.grid_position.y
]
),
"Match3"
)
_deselect_tile()
current_state = GameState.PROCESSING
_clear_matches()
@@ -697,6 +846,7 @@ func _attempt_swap(tile1: Node2D, tile2: Node2D) -> void:
_deselect_tile()
current_state = GameState.WAITING
func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
if not tile1 or not tile2:
DebugManager.log_error("Cannot swap tiles - one or both tiles are null", "Match3")
@@ -706,7 +856,13 @@ func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
var pos1 = tile1.grid_position
var pos2 = tile2.grid_position
DebugManager.log_debug("Swapping tile positions: (%d,%d) -> (%d,%d), (%d,%d) -> (%d,%d)" % [pos1.x, pos1.y, pos2.x, pos2.y, pos2.x, pos2.y, pos1.x, pos1.y], "Match3")
DebugManager.log_debug(
(
"Swapping tile positions: (%d,%d) -> (%d,%d), (%d,%d) -> (%d,%d)"
% [pos1.x, pos1.y, pos2.x, pos2.y, pos2.x, pos2.y, pos1.x, pos1.y]
),
"Match3"
)
tile1.grid_position = pos2
tile2.grid_position = pos1
@@ -729,9 +885,16 @@ func _swap_tiles(tile1: Node2D, tile2: Node2D) -> void:
await tween.finished
DebugManager.log_trace("Tile swap animation completed", "Match3")
func serialize_grid_state() -> Array:
# Convert the current grid to a serializable 2D array
DebugManager.log_info("Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)" % [grid.size(), GRID_SIZE.x, GRID_SIZE.y], "Match3")
DebugManager.log_info(
(
"Starting serialization: grid.size()=%d, GRID_SIZE=(%d,%d)"
% [grid.size(), GRID_SIZE.x, GRID_SIZE.y]
),
"Match3"
)
if grid.size() == 0:
DebugManager.log_error("Grid array is empty during serialization!", "Match3")
@@ -749,18 +912,29 @@ func serialize_grid_state() -> Array:
valid_tiles += 1
# Only log first few for brevity
if valid_tiles <= 5:
DebugManager.log_debug("Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3")
DebugManager.log_debug(
"Serializing tile (%d,%d): type %d" % [x, y, grid[y][x].tile_type], "Match3"
)
else:
row.append(-1) # Invalid/empty tile
null_tiles += 1
# Only log first few nulls for brevity
if null_tiles <= 5:
DebugManager.log_debug("Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3")
DebugManager.log_debug(
"Serializing tile (%d,%d): NULL/empty (-1)" % [x, y], "Match3"
)
serialized_grid.append(row)
DebugManager.log_info("Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles" % [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles], "Match3")
DebugManager.log_info(
(
"Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles"
% [GRID_SIZE.x, GRID_SIZE.y, valid_tiles, null_tiles]
),
"Match3"
)
return serialized_grid
func get_active_gem_types() -> Array:
# Get active gem types from the first available tile
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
@@ -772,15 +946,23 @@ func get_active_gem_types() -> Array:
default_types.append(i)
return default_types
func save_current_state():
# Save complete game state
var grid_layout = serialize_grid_state()
var active_gems = get_active_gem_types()
DebugManager.log_info("Saving match3 state: size(%d,%d), %d tile types, %d active gems" % [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()], "Match3")
DebugManager.log_info(
(
"Saving match3 state: size(%d,%d), %d tile types, %d active gems"
% [GRID_SIZE.x, GRID_SIZE.y, TILE_TYPES, active_gems.size()]
),
"Match3"
)
SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout)
func load_saved_state() -> bool:
# Check if there's a saved grid state
if not SaveManager.has_saved_grid():
@@ -792,10 +974,18 @@ func load_saved_state() -> bool:
# Restore grid settings
var saved_size = Vector2i(saved_state.grid_size.x, saved_state.grid_size.y)
TILE_TYPES = saved_state.tile_types_count
var saved_gems = saved_state.active_gem_types
var saved_gems: Array[int] = []
for gem in saved_state.active_gem_types:
saved_gems.append(int(gem))
var saved_layout = saved_state.grid_layout
DebugManager.log_info("[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d" % [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()], "Match3")
DebugManager.log_info(
(
"[%s] Loading saved grid state: size(%d,%d), %d tile types, layout_size=%d"
% [instance_id, saved_size.x, saved_size.y, TILE_TYPES, saved_layout.size()]
),
"Match3"
)
# Debug: Print first few rows of loaded layout
for y in range(min(3, saved_layout.size())):
@@ -806,11 +996,23 @@ func load_saved_state() -> bool:
# Validate saved data
if saved_layout.size() != saved_size.y:
DebugManager.log_error("Saved grid layout height mismatch: expected %d, got %d" % [saved_size.y, saved_layout.size()], "Match3")
DebugManager.log_error(
(
"Saved grid layout height mismatch: expected %d, got %d"
% [saved_size.y, saved_layout.size()]
),
"Match3"
)
return false
if saved_layout.size() > 0 and saved_layout[0].size() != saved_size.x:
DebugManager.log_error("Saved grid layout width mismatch: expected %d, got %d" % [saved_size.x, saved_layout[0].size()], "Match3")
DebugManager.log_error(
(
"Saved grid layout width mismatch: expected %d, got %d"
% [saved_size.x, saved_layout[0].size()]
),
"Match3"
)
return false
# Apply the saved settings
@@ -819,7 +1021,10 @@ func load_saved_state() -> bool:
# Recalculate layout if size changed
if old_size != saved_size:
DebugManager.log_info("Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size], "Match3")
DebugManager.log_info(
"Grid size changed from %s to %s, recalculating layout" % [old_size, saved_size],
"Match3"
)
_calculate_grid_layout()
await _restore_grid_from_layout(saved_layout, saved_gems)
@@ -827,8 +1032,15 @@ func load_saved_state() -> bool:
DebugManager.log_info("Successfully loaded saved grid state", "Match3")
return true
func _restore_grid_from_layout(grid_layout: Array, active_gems: Array):
DebugManager.log_info("[%s] Starting grid restoration: layout_size=%d, active_gems=%s" % [instance_id, grid_layout.size(), active_gems], "Match3")
func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> void:
DebugManager.log_info(
(
"[%s] Starting grid restoration: layout_size=%d, active_gems=%s"
% [instance_id, grid_layout.size(), active_gems]
),
"Match3"
)
# Clear ALL existing tile children, not just ones in grid array
# This ensures no duplicate layers are created
@@ -839,7 +1051,9 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array):
if script_path == "res://scenes/game/gameplays/tile.gd":
all_tile_children.append(child)
DebugManager.log_debug("Found %d existing tile children to remove" % all_tile_children.size(), "Match3")
DebugManager.log_debug(
"Found %d existing tile children to remove" % all_tile_children.size(), "Match3"
)
# Remove all found tile children
for child in all_tile_children:
@@ -872,26 +1086,44 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array):
# Set the saved tile type
var saved_tile_type = grid_layout[y][x]
DebugManager.log_debug("Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d" % [x, y, saved_tile_type, TILE_TYPES], "Match3")
DebugManager.log_debug(
(
"Setting tile (%d,%d): saved_type=%d, TILE_TYPES=%d"
% [x, y, saved_tile_type, TILE_TYPES]
),
"Match3"
)
if saved_tile_type >= 0 and saved_tile_type < TILE_TYPES:
tile.tile_type = saved_tile_type
DebugManager.log_debug("✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3")
DebugManager.log_debug(
"✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3"
)
else:
# Fallback for invalid tile types
tile.tile_type = randi() % TILE_TYPES
DebugManager.log_error("✗ Invalid saved tile type %d at (%d,%d), using random %d" % [saved_tile_type, x, y, tile.tile_type], "Match3")
DebugManager.log_error(
(
"✗ Invalid saved tile type %d at (%d,%d), using random %d"
% [saved_tile_type, x, y, tile.tile_type]
),
"Match3"
)
# Connect tile signals
tile.tile_selected.connect(_on_tile_selected)
grid[y].append(tile)
DebugManager.log_info("Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3")
DebugManager.log_info(
"Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3"
)
# Safety and validation helper functions
func _is_valid_grid_position(pos: Vector2i) -> bool:
return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y
func _validate_grid_integrity() -> bool:
# Check if grid array structure is valid
if not grid is Array:
@@ -899,7 +1131,9 @@ func _validate_grid_integrity() -> bool:
return false
if grid.size() != GRID_SIZE.y:
DebugManager.log_error("Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3")
DebugManager.log_error(
"Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3"
)
return false
for y in range(grid.size()):
@@ -908,11 +1142,14 @@ func _validate_grid_integrity() -> bool:
return false
if grid[y].size() != GRID_SIZE.x:
DebugManager.log_error("Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3")
DebugManager.log_error(
"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), GRID_SIZE.x], "Match3"
)
return false
return true
func _safe_grid_access(pos: Vector2i) -> Node2D:
# Safe grid access with comprehensive bounds checking
if not _is_valid_grid_position(pos):
@@ -928,6 +1165,7 @@ func _safe_grid_access(pos: Vector2i) -> Node2D:
return tile
func _safe_tile_access(tile: Node2D, property: String):
# Safe property access on tiles
if not tile or not is_instance_valid(tile):

View File

@@ -2,10 +2,13 @@ extends Node2D
signal tile_selected(tile: Node2D)
@export var tile_type: int = 0 : set = _set_tile_type
@export var tile_type: int = 0:
set = _set_tile_type
var grid_position: Vector2i
var is_selected: bool = false : set = _set_selected
var is_highlighted: bool = false : set = _set_highlighted
var is_selected: bool = false:
set = _set_selected
var is_highlighted: bool = false:
set = _set_highlighted
var original_scale: Vector2 = Vector2.ONE # Store the original scale for the board
@onready var sprite: Sprite2D = $Sprite2D
@@ -14,19 +17,20 @@ var original_scale: Vector2 = Vector2.ONE # Store the original scale for the bo
const TILE_SIZE = 48 # Slightly smaller than 54 to leave some padding
# All available gem textures
var all_gem_textures = [
preload("res://assets/sprites/gems/bg_19.png"), # 0 - Blue gem
preload("res://assets/sprites/gems/dg_19.png"), # 1 - Dark gem
preload("res://assets/sprites/gems/gg_19.png"), # 2 - Green gem
preload("res://assets/sprites/gems/mg_19.png"), # 3 - Magenta gem
preload("res://assets/sprites/gems/rg_19.png"), # 4 - Red gem
preload("res://assets/sprites/gems/yg_19.png"), # 5 - Yellow gem
preload("res://assets/sprites/gems/pg_19.png"), # 6 - Purple gem
preload("res://assets/sprites/gems/sg_19.png"), # 7 - Silver gem
var all_gem_textures: Array[Texture2D] = [
preload("res://assets/sprites/gems/bg_19.png"), # 0 - Blue gem
preload("res://assets/sprites/gems/dg_19.png"), # 1 - Dark gem
preload("res://assets/sprites/gems/gg_19.png"), # 2 - Green gem
preload("res://assets/sprites/gems/mg_19.png"), # 3 - Magenta gem
preload("res://assets/sprites/gems/rg_19.png"), # 4 - Red gem
preload("res://assets/sprites/gems/yg_19.png"), # 5 - Yellow gem
preload("res://assets/sprites/gems/pg_19.png"), # 6 - Purple gem
preload("res://assets/sprites/gems/sg_19.png"), # 7 - Silver gem
]
# Currently active gem types (indices into all_gem_textures)
var active_gem_types = [] # Will be set from TileManager
var active_gem_types: Array[int] = [] # Will be set from TileManager
func _set_tile_type(value: int) -> void:
tile_type = value
@@ -38,7 +42,16 @@ func _set_tile_type(value: int) -> void:
sprite.texture = all_gem_textures[texture_index]
_scale_sprite_to_fit()
else:
DebugManager.log_error("Invalid tile type: " + str(value) + ". Available types: 0-" + str(active_gem_types.size() - 1), "Match3")
DebugManager.log_error(
(
"Invalid tile type: "
+ str(value)
+ ". Available types: 0-"
+ str(active_gem_types.size() - 1)
),
"Match3"
)
func _scale_sprite_to_fit() -> void:
# Fixed: Add additional null checks
@@ -48,9 +61,16 @@ func _scale_sprite_to_fit() -> void:
var scale_factor = TILE_SIZE / max_dimension
original_scale = Vector2(scale_factor, scale_factor)
sprite.scale = original_scale
DebugManager.log_debug("Set original scale to %s for tile (%d,%d)" % [original_scale, grid_position.x, grid_position.y], "Match3")
DebugManager.log_debug(
(
"Set original scale to %s for tile (%d,%d)"
% [original_scale, grid_position.x, grid_position.y]
),
"Match3"
)
func set_active_gem_types(gem_indices: Array) -> void:
func set_active_gem_types(gem_indices: Array[int]) -> void:
if not gem_indices or gem_indices.is_empty():
DebugManager.log_error("Empty gem indices array provided", "Tile")
return
@@ -60,7 +80,13 @@ func set_active_gem_types(gem_indices: Array) -> void:
# Validate all gem indices are within bounds
for gem_index in active_gem_types:
if gem_index < 0 or gem_index >= all_gem_textures.size():
DebugManager.log_error("Invalid gem index: %d (valid range: 0-%d)" % [gem_index, all_gem_textures.size() - 1], "Tile")
DebugManager.log_error(
(
"Invalid gem index: %d (valid range: 0-%d)"
% [gem_index, all_gem_textures.size() - 1]
),
"Tile"
)
# Use default fallback
active_gem_types = [0, 1, 2, 3, 4]
break
@@ -72,9 +98,11 @@ func set_active_gem_types(gem_indices: Array) -> void:
_set_tile_type(tile_type)
func get_active_gem_count() -> int:
return active_gem_types.size()
func add_gem_type(gem_index: int) -> bool:
if gem_index < 0 or gem_index >= all_gem_textures.size():
DebugManager.log_error("Invalid gem index: %d" % gem_index, "Tile")
@@ -86,6 +114,7 @@ func add_gem_type(gem_index: int) -> bool:
return false
func remove_gem_type(gem_index: int) -> bool:
var type_index = active_gem_types.find(gem_index)
if type_index == -1:
@@ -104,18 +133,33 @@ func remove_gem_type(gem_index: int) -> bool:
return true
func _set_selected(value: bool) -> void:
var old_value = is_selected
is_selected = value
DebugManager.log_debug("Tile (%d,%d) selection changed: %s -> %s" % [grid_position.x, grid_position.y, old_value, value], "Match3")
DebugManager.log_debug(
(
"Tile (%d,%d) selection changed: %s -> %s"
% [grid_position.x, grid_position.y, old_value, value]
),
"Match3"
)
_update_visual_feedback()
func _set_highlighted(value: bool) -> void:
var old_value = is_highlighted
is_highlighted = value
DebugManager.log_debug("Tile (%d,%d) highlight changed: %s -> %s" % [grid_position.x, grid_position.y, old_value, value], "Match3")
DebugManager.log_debug(
(
"Tile (%d,%d) highlight changed: %s -> %s"
% [grid_position.x, grid_position.y, old_value, value]
),
"Match3"
)
_update_visual_feedback()
func _update_visual_feedback() -> void:
if not sprite:
return
@@ -129,17 +173,35 @@ func _update_visual_feedback() -> void:
# Selected: bright and 20% larger than original board size
target_modulate = Color(1.2, 1.2, 1.2, 1.0)
scale_multiplier = 1.2
DebugManager.log_debug("SELECTING tile (%d,%d): target scale %.2fx, current scale %s" % [grid_position.x, grid_position.y, scale_multiplier, sprite.scale], "Match3")
DebugManager.log_debug(
(
"SELECTING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale]
),
"Match3"
)
elif is_highlighted:
# Highlighted: subtle glow and 10% larger than original board size
target_modulate = Color(1.1, 1.1, 1.1, 1.0)
scale_multiplier = 1.1
DebugManager.log_debug("HIGHLIGHTING tile (%d,%d): target scale %.2fx, current scale %s" % [grid_position.x, grid_position.y, scale_multiplier, sprite.scale], "Match3")
DebugManager.log_debug(
(
"HIGHLIGHTING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale]
),
"Match3"
)
else:
# Normal state: white and original board size
target_modulate = Color.WHITE
scale_multiplier = 1.0
DebugManager.log_debug("NORMALIZING tile (%d,%d): target scale %.2fx, current scale %s" % [grid_position.x, grid_position.y, scale_multiplier, sprite.scale], "Match3")
DebugManager.log_debug(
(
"NORMALIZING tile (%d,%d): target scale %.2fx, current scale %s"
% [grid_position.x, grid_position.y, scale_multiplier, sprite.scale]
),
"Match3"
)
# Calculate target scale relative to original board scale
target_scale = original_scale * scale_multiplier
@@ -149,7 +211,13 @@ func _update_visual_feedback() -> void:
# Only animate scale if it's actually changing
if sprite.scale != target_scale:
DebugManager.log_debug("Animating scale from %s to %s for tile (%d,%d)" % [sprite.scale, target_scale, grid_position.x, grid_position.y], "Match3")
DebugManager.log_debug(
(
"Animating scale from %s to %s for tile (%d,%d)"
% [sprite.scale, target_scale, grid_position.x, grid_position.y]
),
"Match3"
)
var tween = create_tween()
tween.tween_property(sprite, "scale", target_scale, 0.15)
@@ -157,10 +225,20 @@ func _update_visual_feedback() -> void:
# Add completion callback for debugging
tween.tween_callback(_on_scale_animation_completed.bind(target_scale))
else:
DebugManager.log_debug("No scale change needed for tile (%d,%d)" % [grid_position.x, grid_position.y], "Match3")
DebugManager.log_debug(
"No scale change needed for tile (%d,%d)" % [grid_position.x, grid_position.y], "Match3"
)
func _on_scale_animation_completed(expected_scale: Vector2) -> void:
DebugManager.log_debug("Scale animation completed for tile (%d,%d): expected %s, actual %s" % [grid_position.x, grid_position.y, expected_scale, sprite.scale], "Match3")
DebugManager.log_debug(
(
"Scale animation completed for tile (%d,%d): expected %s, actual %s"
% [grid_position.x, grid_position.y, expected_scale, sprite.scale]
),
"Match3"
)
func force_reset_visual_state() -> void:
# Force reset all visual states - debug function
@@ -169,7 +247,27 @@ func force_reset_visual_state() -> void:
if sprite:
sprite.modulate = Color.WHITE
sprite.scale = original_scale # Reset to original board scale, not 1.0
DebugManager.log_debug("Forced visual reset on tile (%d,%d) to original scale %s" % [grid_position.x, grid_position.y, original_scale], "Match3")
DebugManager.log_debug(
(
"Forced visual reset on tile (%d,%d) to original scale %s"
% [grid_position.x, grid_position.y, original_scale]
),
"Match3"
)
# Handle input for tile selection
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
# Check if the mouse click is within the tile's bounds
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)
if sprite_rect.has_point(local_position):
tile_selected.emit(self)
get_viewport().set_input_as_handled()
# Called when the node enters the scene tree for the first time.
func _ready() -> void:

View File

@@ -1,17 +1,19 @@
extends Control
@onready var splash_screen = $SplashScreen
var current_menu = null
@onready var splash_screen: Node = $SplashScreen
var current_menu: Control = null
const MAIN_MENU_SCENE = preload("res://scenes/ui/MainMenu.tscn")
const SETTINGS_MENU_SCENE = preload("res://scenes/ui/SettingsMenu.tscn")
func _ready():
func _ready() -> void:
DebugManager.log_debug("Main scene ready", "Main")
# Use alternative connection method with input handling
_setup_splash_screen_connection()
func _setup_splash_screen_connection():
func _setup_splash_screen_connection() -> void:
# Wait for all nodes to be ready
await get_tree().process_frame
await get_tree().process_frame
@@ -41,45 +43,53 @@ func _setup_splash_screen_connection():
DebugManager.log_error("Could not find SplashScreen node", "Main")
_use_fallback_input_handling()
func _use_fallback_input_handling():
func _use_fallback_input_handling() -> void:
# Fallback: handle input directly in the main scene
set_process_unhandled_input(true)
func _unhandled_input(event):
func _unhandled_input(event: InputEvent) -> void:
if splash_screen and splash_screen.is_inside_tree():
# Forward input to splash screen or handle directly
if event.is_action_pressed("action_south"):
_on_any_key_pressed()
get_viewport().set_input_as_handled()
func _on_any_key_pressed():
func _on_any_key_pressed() -> void:
DebugManager.log_debug("Transitioning to main menu", "Main")
splash_screen.queue_free()
show_main_menu()
func show_main_menu():
func show_main_menu() -> void:
clear_current_menu()
var main_menu = MAIN_MENU_SCENE.instantiate()
main_menu.open_settings.connect(_on_open_settings)
add_child(main_menu)
current_menu = main_menu
func show_settings_menu():
func show_settings_menu() -> void:
clear_current_menu()
var settings_menu = SETTINGS_MENU_SCENE.instantiate()
settings_menu.back_to_main_menu.connect(_on_back_to_main_menu)
add_child(settings_menu)
current_menu = settings_menu
func clear_current_menu():
func clear_current_menu() -> void:
if current_menu:
current_menu.queue_free()
current_menu = null
func _on_open_settings():
func _on_open_settings() -> void:
DebugManager.log_debug("Opening settings menu", "Main")
show_settings_menu()
func _on_back_to_main_menu():
func _on_back_to_main_menu() -> void:
DebugManager.log_debug("Back to main menu", "Main")
show_main_menu()

View File

@@ -2,15 +2,26 @@ extends Control
signal any_key_pressed
func _ready():
func _ready() -> void:
DebugManager.log_debug("SplashScreen ready", "SplashScreen")
update_text()
func _input(event):
if event.is_action_pressed("action_south") or event is InputEventScreenTouch or (event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed):
func _input(event: InputEvent) -> void:
if (
event.is_action_pressed("action_south")
or event is InputEventScreenTouch
or (
event is InputEventMouseButton
and event.button_index == MOUSE_BUTTON_LEFT
and event.pressed
)
):
DebugManager.log_debug("Action pressed: " + str(event), "SplashScreen")
any_key_pressed.emit()
get_viewport().set_input_as_handled()
func update_text():
func update_text() -> void:
$SplashContainer/ContinueLabel.text = tr("press_ok_continue")

View File

@@ -2,6 +2,7 @@ extends Control
@onready var button: Button = $Button
func _ready():
button.pressed.connect(_on_button_pressed)
DebugManager.debug_ui_toggled.connect(_on_debug_ui_toggled)
@@ -10,8 +11,10 @@ func _ready():
var current_state = DebugManager.is_debug_ui_visible()
button.text = "Debug UI: " + ("ON" if current_state else "OFF")
func _on_button_pressed():
DebugManager.toggle_debug_ui()
func _on_debug_ui_toggled(visible: bool):
button.text = "Debug UI: " + ("ON" if visible else "OFF")
button.text = "Debug UI: " + ("ON" if visible else "OFF")

View File

@@ -1,5 +1,6 @@
extends DebugMenuBase
func _find_target_scene():
# Fixed: Search more thoroughly for match3 scene
if match3_scene:

View File

@@ -4,10 +4,14 @@ extends Control
@onready var regenerate_button: Button = $VBoxContainer/RegenerateButton
@onready var gem_types_spinbox: SpinBox = $VBoxContainer/GemTypesContainer/GemTypesSpinBox
@onready var gem_types_label: Label = $VBoxContainer/GemTypesContainer/GemTypesLabel
@onready var grid_width_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthSpinBox
@onready var grid_height_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridHeightContainer/GridHeightSpinBox
@onready var grid_width_label: Label = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthLabel
@onready var grid_height_label: Label = $VBoxContainer/GridSizeContainer/GridHeightContainer/GridHeightLabel
@onready
var grid_width_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthSpinBox
@onready
var grid_height_spinbox: SpinBox = $VBoxContainer/GridSizeContainer/GridHeightContainer/GridHeightSpinBox
@onready
var grid_width_label: Label = $VBoxContainer/GridSizeContainer/GridWidthContainer/GridWidthLabel
@onready
var grid_height_label: Label = $VBoxContainer/GridSizeContainer/GridHeightContainer/GridHeightLabel
@export var target_script_path: String = "res://scenes/game/gameplays/match3_gameplay.gd"
@export var log_category: String = "DebugMenu"
@@ -23,16 +27,18 @@ var search_timer: Timer
var last_scene_search_time: float = 0.0
const SCENE_SEARCH_COOLDOWN := 0.5 # Prevent excessive scene searching
func _exit_tree():
func _exit_tree() -> void:
if search_timer:
search_timer.queue_free()
func _ready():
func _ready() -> void:
DebugManager.log_debug("DebugMenuBase _ready() called", log_category)
DebugManager.debug_toggled.connect(_on_debug_toggled)
# Initialize with current debug state
var current_debug_state = DebugManager.is_debug_enabled()
var current_debug_state: bool = DebugManager.is_debug_enabled()
visible = current_debug_state
# Connect signals
@@ -50,7 +56,8 @@ func _ready():
# Start searching for target scene
_find_target_scene()
func _initialize_spinboxes():
func _initialize_spinboxes() -> void:
# Initialize gem types spinbox with safety limits
gem_types_spinbox.min_value = MIN_TILE_TYPES
gem_types_spinbox.max_value = MAX_TILE_TYPES
@@ -68,40 +75,47 @@ func _initialize_spinboxes():
grid_height_spinbox.step = 1
grid_height_spinbox.value = 8 # Default value
func _setup_scene_finding():
func _setup_scene_finding() -> void:
# Create timer for periodic scene search with longer intervals to reduce CPU usage
search_timer = Timer.new()
search_timer.wait_time = 0.5 # Reduced frequency from 0.1 to 0.5 seconds
search_timer.timeout.connect(_find_target_scene)
add_child(search_timer)
# Virtual method - override in derived classes for specific finding logic
func _find_target_scene():
func _find_target_scene() -> void:
DebugManager.log_error("_find_target_scene() not implemented in derived class", log_category)
func _find_node_by_script(node: Node, script_path: String) -> Node:
# Helper function to find node by its script path
if not node:
return null
if node.get_script():
var node_script = node.get_script()
var node_script: Script = node.get_script()
if node_script.resource_path == script_path:
return node
for child in node.get_children():
var result = _find_node_by_script(child, script_path)
var result: Node = _find_node_by_script(child, script_path)
if result:
return result
return null
func _update_ui_from_scene():
func _update_ui_from_scene() -> void:
if not match3_scene:
return
# Connect to grid state loaded signal if not already connected
if match3_scene.has_signal("grid_state_loaded") and not match3_scene.grid_state_loaded.is_connected(_on_grid_state_loaded):
if (
match3_scene.has_signal("grid_state_loaded")
and not match3_scene.grid_state_loaded.is_connected(_on_grid_state_loaded)
):
match3_scene.grid_state_loaded.connect(_on_grid_state_loaded)
DebugManager.log_debug("Connected to grid_state_loaded signal", log_category)
@@ -112,14 +126,18 @@ func _update_ui_from_scene():
# Update grid size display
if "GRID_SIZE" in match3_scene:
var grid_size = match3_scene.GRID_SIZE
var grid_size: Vector2i = match3_scene.GRID_SIZE
grid_width_spinbox.value = grid_size.x
grid_height_spinbox.value = grid_size.y
grid_width_label.text = "Width: " + str(grid_size.x)
grid_height_label.text = "Height: " + str(grid_size.y)
func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int):
DebugManager.log_debug("Grid state loaded signal received: size=%s, types=%d" % [grid_size, tile_types], log_category)
func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int) -> void:
DebugManager.log_debug(
"Grid state loaded signal received: size=%s, types=%d" % [grid_size, tile_types],
log_category
)
# Update the UI with the actual loaded values
gem_types_spinbox.value = tile_types
@@ -130,16 +148,19 @@ func _on_grid_state_loaded(grid_size: Vector2i, tile_types: int):
grid_width_label.text = "Width: " + str(grid_size.x)
grid_height_label.text = "Height: " + str(grid_size.y)
func _stop_search_timer():
func _stop_search_timer() -> void:
if search_timer and search_timer.timeout.is_connected(_find_target_scene):
search_timer.stop()
func _start_search_timer():
func _start_search_timer() -> void:
if search_timer and not search_timer.timeout.is_connected(_find_target_scene):
search_timer.timeout.connect(_find_target_scene)
search_timer.start()
func _on_debug_toggled(enabled: bool):
func _on_debug_toggled(enabled: bool) -> void:
DebugManager.log_debug("Debug toggled to " + str(enabled), log_category)
visible = enabled
if enabled:
@@ -150,13 +171,17 @@ func _on_debug_toggled(enabled: bool):
# Force refresh the values in case they changed while debug was hidden
_refresh_current_values()
func _refresh_current_values():
func _refresh_current_values() -> void:
# Refresh UI with current values from the scene
if match3_scene:
DebugManager.log_debug("Refreshing debug menu values from current scene state", log_category)
DebugManager.log_debug(
"Refreshing debug menu values from current scene state", log_category
)
_update_ui_from_scene()
func _on_regenerate_pressed():
func _on_regenerate_pressed() -> void:
if not match3_scene:
_find_target_scene()
@@ -170,9 +195,10 @@ func _on_regenerate_pressed():
else:
DebugManager.log_error("Target scene does not have regenerate_grid method", log_category)
func _on_gem_types_changed(value: float):
func _on_gem_types_changed(value: float) -> void:
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
var current_time: float = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
@@ -184,10 +210,16 @@ func _on_gem_types_changed(value: float):
DebugManager.log_error("Could not find target scene for gem types change", log_category)
return
var new_value = int(value)
var new_value: int = int(value)
# Enhanced input validation with safety constants
if new_value < MIN_TILE_TYPES or new_value > MAX_TILE_TYPES:
DebugManager.log_error("Invalid gem types value: %d (range: %d-%d)" % [new_value, MIN_TILE_TYPES, MAX_TILE_TYPES], log_category)
DebugManager.log_error(
(
"Invalid gem types value: %d (range: %d-%d)"
% [new_value, MIN_TILE_TYPES, MAX_TILE_TYPES]
),
log_category
)
# Reset to valid value
gem_types_spinbox.value = clamp(new_value, MIN_TILE_TYPES, MAX_TILE_TYPES)
return
@@ -203,9 +235,10 @@ func _on_gem_types_changed(value: float):
match3_scene.TILE_TYPES = new_value
gem_types_label.text = "Gem Types: " + str(new_value)
func _on_grid_width_changed(value: float):
func _on_grid_width_changed(value: float) -> void:
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
var current_time: float = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
@@ -217,10 +250,16 @@ func _on_grid_width_changed(value: float):
DebugManager.log_error("Could not find target scene for grid width change", log_category)
return
var new_width = int(value)
var new_width: int = int(value)
# Enhanced input validation with safety constants
if new_width < MIN_GRID_SIZE or new_width > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid width value: %d (range: %d-%d)" % [new_width, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category)
DebugManager.log_error(
(
"Invalid grid width value: %d (range: %d-%d)"
% [new_width, MIN_GRID_SIZE, MAX_GRID_SIZE]
),
log_category
)
# Reset to valid value
grid_width_spinbox.value = clamp(new_width, MIN_GRID_SIZE, MAX_GRID_SIZE)
return
@@ -228,17 +267,20 @@ func _on_grid_width_changed(value: float):
grid_width_label.text = "Width: " + str(new_width)
# Get current height
var current_height = int(grid_height_spinbox.value)
var current_height: int = int(grid_height_spinbox.value)
if match3_scene.has_method("set_grid_size"):
DebugManager.log_debug("Setting grid size to " + str(new_width) + "x" + str(current_height), log_category)
DebugManager.log_debug(
"Setting grid size to " + str(new_width) + "x" + str(current_height), log_category
)
await match3_scene.set_grid_size(Vector2i(new_width, current_height))
else:
DebugManager.log_error("Target scene does not have set_grid_size method", log_category)
func _on_grid_height_changed(value: float):
func _on_grid_height_changed(value: float) -> void:
# Rate limiting for scene searches
var current_time = Time.get_ticks_msec() / 1000.0
var current_time: float = Time.get_ticks_msec() / 1000.0
if current_time - last_scene_search_time < SCENE_SEARCH_COOLDOWN:
return
@@ -250,10 +292,16 @@ func _on_grid_height_changed(value: float):
DebugManager.log_error("Could not find target scene for grid height change", log_category)
return
var new_height = int(value)
var new_height: int = int(value)
# Enhanced input validation with safety constants
if new_height < MIN_GRID_SIZE or new_height > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid height value: %d (range: %d-%d)" % [new_height, MIN_GRID_SIZE, MAX_GRID_SIZE], log_category)
DebugManager.log_error(
(
"Invalid grid height value: %d (range: %d-%d)"
% [new_height, MIN_GRID_SIZE, MAX_GRID_SIZE]
),
log_category
)
# Reset to valid value
grid_height_spinbox.value = clamp(new_height, MIN_GRID_SIZE, MAX_GRID_SIZE)
return
@@ -261,10 +309,12 @@ func _on_grid_height_changed(value: float):
grid_height_label.text = "Height: " + str(new_height)
# Get current width
var current_width = int(grid_width_spinbox.value)
var current_width: int = int(grid_width_spinbox.value)
if match3_scene.has_method("set_grid_size"):
DebugManager.log_debug("Setting grid size to " + str(current_width) + "x" + str(new_height), log_category)
DebugManager.log_debug(
"Setting grid size to " + str(current_width) + "x" + str(new_height), log_category
)
await match3_scene.set_grid_size(Vector2i(current_width, new_height))
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

@@ -1,5 +1,6 @@
extends Button
func _ready():
pressed.connect(_on_pressed)
DebugManager.debug_toggled.connect(_on_debug_toggled)
@@ -8,8 +9,10 @@ func _ready():
var current_state = DebugManager.is_debug_enabled()
text = "Debug: " + ("ON" if current_state else "OFF")
func _on_pressed():
DebugManager.toggle_debug()
func _on_debug_toggled(enabled: bool):
text = "Debug: " + ("ON" if enabled else "OFF")

View File

@@ -6,14 +6,16 @@ signal open_settings
var current_menu_index: int = 0
var original_button_scales: Array[Vector2] = []
func _ready():
func _ready() -> void:
DebugManager.log_info("MainMenu ready", "MainMenu")
_setup_menu_navigation()
_update_new_game_button()
func _on_new_game_button_pressed():
func _on_new_game_button_pressed() -> void:
AudioManager.play_ui_click()
var button_text = $MenuContainer/NewGameButton.text
var button_text: String = $MenuContainer/NewGameButton.text
if button_text == "Continue":
DebugManager.log_info("Continue pressed", "MainMenu")
GameManager.continue_game()
@@ -21,17 +23,20 @@ func _on_new_game_button_pressed():
DebugManager.log_info("New Game pressed", "MainMenu")
GameManager.start_new_game()
func _on_settings_button_pressed():
func _on_settings_button_pressed() -> void:
AudioManager.play_ui_click()
DebugManager.log_info("Settings pressed", "MainMenu")
open_settings.emit()
func _on_exit_button_pressed():
func _on_exit_button_pressed() -> void:
AudioManager.play_ui_click()
DebugManager.log_info("Exit pressed", "MainMenu")
get_tree().quit()
func _setup_menu_navigation():
func _setup_menu_navigation() -> void:
menu_buttons.clear()
original_button_scales.clear()
@@ -44,6 +49,7 @@ func _setup_menu_navigation():
_update_visual_selection()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("move_up"):
_navigate_menu(-1)
@@ -60,7 +66,8 @@ func _input(event: InputEvent) -> void:
DebugManager.log_info("Quit game shortcut pressed", "MainMenu")
get_tree().quit()
func _navigate_menu(direction: int):
func _navigate_menu(direction: int) -> void:
AudioManager.play_ui_click()
current_menu_index = (current_menu_index + direction) % menu_buttons.size()
if current_menu_index < 0:
@@ -68,15 +75,17 @@ func _navigate_menu(direction: int):
_update_visual_selection()
DebugManager.log_info("Menu navigation: index " + str(current_menu_index), "MainMenu")
func _activate_current_button():
func _activate_current_button() -> void:
if current_menu_index >= 0 and current_menu_index < menu_buttons.size():
var 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")
button.pressed.emit()
func _update_visual_selection():
func _update_visual_selection() -> void:
for i in range(menu_buttons.size()):
var button = menu_buttons[i]
var button: Button = menu_buttons[i]
if i == current_menu_index:
button.scale = original_button_scales[i] * 1.1
button.modulate = Color(1.2, 1.2, 1.0)
@@ -84,16 +93,23 @@ func _update_visual_selection():
button.scale = original_button_scales[i]
button.modulate = Color.WHITE
func _update_new_game_button():
# Check if there's an existing save with progress
var current_score = SaveManager.get_current_score()
var games_played = SaveManager.get_games_played()
var has_saved_grid = SaveManager.has_saved_grid()
var new_game_button = $MenuContainer/NewGameButton
func _update_new_game_button() -> void:
# Check if there's an existing save with progress
var current_score: int = SaveManager.get_current_score()
var games_played: int = SaveManager.get_games_played()
var has_saved_grid: bool = SaveManager.has_saved_grid()
var new_game_button: Button = $MenuContainer/NewGameButton
if current_score > 0 or games_played > 0 or has_saved_grid:
new_game_button.text = "Continue"
DebugManager.log_info("Updated button to Continue (score: %d, games: %d, grid: %s)" % [current_score, games_played, has_saved_grid], "MainMenu")
DebugManager.log_info(
(
"Updated button to Continue (score: %d, games: %d, grid: %s)"
% [current_score, games_played, has_saved_grid]
),
"MainMenu"
)
else:
new_game_button.text = "New Game"
DebugManager.log_info("Updated button to New Game", "MainMenu")

View File

@@ -14,27 +14,27 @@ signal back_to_main_menu
# Progress reset confirmation dialog
var confirmation_dialog: AcceptDialog
# Navigation system variables
var navigable_controls: Array[Control] = []
var current_control_index: int = 0
var original_control_scales: Array[Vector2] = []
var original_control_modulates: Array[Color] = []
func _ready():
func _ready() -> void:
add_to_group("localizable")
DebugManager.log_info("SettingsMenu ready", "Settings")
# Language selector is initialized automatically
var master_callback = _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):
master_slider.value_changed.connect(master_callback)
var music_callback = _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):
music_slider.value_changed.connect(music_callback)
var sfx_callback = _on_volume_slider_changed.bind("sfx_volume")
var sfx_callback: Callable = _on_volume_slider_changed.bind("sfx_volume")
if not sfx_slider.value_changed.is_connected(sfx_callback):
sfx_slider.value_changed.connect(sfx_callback)
@@ -45,37 +45,41 @@ func _ready():
_setup_navigation_system()
_setup_confirmation_dialog()
func _update_controls_from_settings():
func _update_controls_from_settings() -> void:
master_slider.value = settings_manager.get_setting("master_volume")
music_slider.value = settings_manager.get_setting("music_volume")
sfx_slider.value = settings_manager.get_setting("sfx_volume")
# Language display is handled by the ValueStepper component
func _on_volume_slider_changed(value, setting_key):
func _on_volume_slider_changed(value: float, setting_key: String) -> void:
# Input validation for volume settings
if not setting_key in ["master_volume", "music_volume", "sfx_volume"]:
DebugManager.log_error("Invalid volume setting key: " + str(setting_key), "Settings")
return
if not (value is float or value is int):
if typeof(value) != TYPE_FLOAT and typeof(value) != TYPE_INT:
DebugManager.log_error("Invalid volume value type: " + str(typeof(value)), "Settings")
return
# Clamp value to valid range
var clamped_value = clamp(float(value), 0.0, 1.0)
var clamped_value: float = clamp(float(value), 0.0, 1.0)
if clamped_value != value:
DebugManager.log_warn("Volume value %f clamped to %f" % [value, clamped_value], "Settings")
if not settings_manager.set_setting(setting_key, clamped_value):
DebugManager.log_error("Failed to set volume setting: " + setting_key, "Settings")
func _exit_settings():
func _exit_settings() -> void:
DebugManager.log_info("Exiting settings", "Settings")
settings_manager.save_settings()
back_to_main_menu.emit()
func _input(event):
func _input(event: InputEvent) -> void:
if 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()
@@ -103,14 +107,14 @@ func _input(event):
_activate_current_control()
get_viewport().set_input_as_handled()
func _on_back_button_pressed():
func _on_back_button_pressed() -> void:
AudioManager.play_ui_click()
DebugManager.log_info("Back button pressed", "Settings")
_exit_settings()
func update_text():
func update_text() -> void:
$SettingsContainer/SettingsTitle.text = tr("settings_title")
$SettingsContainer/MasterVolumeContainer/MasterVolume.text = tr("master_volume")
$SettingsContainer/MusicVolumeContainer/MusicVolume.text = tr("music_volume")
@@ -127,7 +131,8 @@ func _on_reset_setting_button_pressed() -> void:
_update_controls_from_settings()
localization_manager.change_language(settings_manager.get_setting("language"))
func _setup_navigation_system():
func _setup_navigation_system() -> void:
navigable_controls.clear()
original_control_scales.clear()
original_control_modulates.clear()
@@ -148,7 +153,8 @@ func _setup_navigation_system():
_update_visual_selection()
func _navigate_controls(direction: int):
func _navigate_controls(direction: int) -> void:
AudioManager.play_ui_click()
current_control_index = (current_control_index + direction) % navigable_controls.size()
if current_control_index < 0:
@@ -156,32 +162,36 @@ func _navigate_controls(direction: int):
_update_visual_selection()
DebugManager.log_info("Settings navigation: index " + str(current_control_index), "Settings")
func _adjust_current_control(direction: int):
func _adjust_current_control(direction: int) -> void:
if current_control_index < 0 or current_control_index >= navigable_controls.size():
return
var control = navigable_controls[current_control_index]
var control: Control = navigable_controls[current_control_index]
# Handle sliders
if control is HSlider:
var slider = control as HSlider
var step = slider.step if slider.step > 0 else 0.1
var new_value = slider.value + (direction * step)
var slider: HSlider = control as HSlider
var step: float = slider.step if slider.step > 0 else 0.1
var new_value: float = slider.value + (direction * step)
new_value = clamp(new_value, slider.min_value, slider.max_value)
slider.value = new_value
AudioManager.play_ui_click()
DebugManager.log_info("Slider adjusted: %s = %f" % [_get_control_name(control), new_value], "Settings")
DebugManager.log_info(
"Slider adjusted: %s = %f" % [_get_control_name(control), new_value], "Settings"
)
# Handle language stepper with left/right
elif control == language_stepper:
if language_stepper.handle_input_action("move_left" if direction == -1 else "move_right"):
AudioManager.play_ui_click()
func _activate_current_control():
func _activate_current_control() -> void:
if current_control_index < 0 or current_control_index >= navigable_controls.size():
return
var control = navigable_controls[current_control_index]
var control: Control = navigable_controls[current_control_index]
# Handle buttons
if control is Button:
@@ -193,7 +203,8 @@ func _activate_current_control():
elif control == language_stepper:
DebugManager.log_info("Language stepper selected - use left/right to change", "Settings")
func _update_visual_selection():
func _update_visual_selection() -> void:
for i in range(navigable_controls.size()):
var control = navigable_controls[i]
if i == current_control_index:
@@ -211,6 +222,7 @@ func _update_visual_selection():
control.scale = original_control_scales[i]
control.modulate = original_control_modulates[i]
func _get_control_name(control: Control) -> String:
if control == master_slider:
return "master_volume"
@@ -223,10 +235,15 @@ func _get_control_name(control: Control) -> String:
else:
return "button"
func _on_language_stepper_value_changed(new_value: String, new_index: int):
DebugManager.log_info("Language changed via ValueStepper: " + new_value + " (index: " + str(new_index) + ")", "Settings")
func _setup_confirmation_dialog():
func _on_language_stepper_value_changed(new_value: String, new_index: float) -> void:
DebugManager.log_info(
"Language changed via ValueStepper: " + new_value + " (index: " + str(int(new_index)) + ")",
"Settings"
)
func _setup_confirmation_dialog() -> void:
"""Create confirmation dialog for progress reset"""
confirmation_dialog = AcceptDialog.new()
confirmation_dialog.title = tr("confirm_reset_title")
@@ -244,7 +261,8 @@ func _setup_confirmation_dialog():
add_child(confirmation_dialog)
func _on_reset_progress_button_pressed():
func _on_reset_progress_button_pressed() -> void:
"""Handle reset progress button press with confirmation"""
AudioManager.play_ui_click()
DebugManager.log_info("Reset progress button pressed", "Settings")
@@ -257,7 +275,8 @@ func _on_reset_progress_button_pressed():
# Show confirmation dialog
confirmation_dialog.popup_centered()
func _on_reset_progress_confirmed():
func _on_reset_progress_confirmed() -> void:
"""Actually reset the progress after confirmation"""
AudioManager.play_ui_click()
DebugManager.log_info("Progress reset confirmed by user", "Settings")
@@ -267,7 +286,7 @@ func _on_reset_progress_confirmed():
DebugManager.log_info("All progress successfully reset", "Settings")
# Show success message
var success_dialog = AcceptDialog.new()
var success_dialog: AcceptDialog = AcceptDialog.new()
success_dialog.title = tr("reset_success_title")
success_dialog.dialog_text = tr("reset_success_message")
success_dialog.ok_button_text = tr("ok")
@@ -283,7 +302,7 @@ func _on_reset_progress_confirmed():
DebugManager.log_error("Failed to reset progress", "Settings")
# Show error message
var error_dialog = AcceptDialog.new()
var error_dialog: AcceptDialog = AcceptDialog.new()
error_dialog.title = tr("reset_error_title")
error_dialog.dialog_text = tr("reset_error_message")
error_dialog.ok_button_text = tr("ok")
@@ -291,7 +310,8 @@ func _on_reset_progress_confirmed():
error_dialog.popup_centered()
error_dialog.confirmed.connect(func(): error_dialog.queue_free())
func _on_reset_progress_canceled():
func _on_reset_progress_canceled() -> void:
"""Handle reset progress cancellation"""
AudioManager.play_ui_click()
DebugManager.log_info("Progress reset canceled by user", "Settings")

View File

@@ -29,7 +29,8 @@ var original_scale: Vector2
var original_modulate: Color
var is_highlighted: bool = false
func _ready():
func _ready() -> void:
DebugManager.log_info("ValueStepper ready for: " + data_source, "ValueStepper")
# Store original visual properties
@@ -47,8 +48,9 @@ func _ready():
_load_data()
_update_display()
## Loads data based on the data_source type
func _load_data():
func _load_data() -> void:
match data_source:
"language":
_load_language_data()
@@ -59,8 +61,9 @@ func _load_data():
_:
DebugManager.log_warn("Unknown data_source: " + data_source, "ValueStepper")
func _load_language_data():
var languages_data = SettingsManager.get_languages_data()
func _load_language_data() -> void:
var languages_data: Dictionary = SettingsManager.get_languages_data()
if languages_data.has("languages"):
values.clear()
display_names.clear()
@@ -69,28 +72,31 @@ func _load_language_data():
display_names.append(languages_data.languages[lang_code]["display_name"])
# Set current index based on current language
var current_lang = SettingsManager.get_setting("language")
var index = values.find(current_lang)
var current_lang: String = SettingsManager.get_setting("language")
var index: int = values.find(current_lang)
current_index = max(0, index)
DebugManager.log_info("Loaded %d languages" % values.size(), "ValueStepper")
func _load_resolution_data():
func _load_resolution_data() -> void:
# Example resolution data - customize as needed
values = ["1920x1080", "1366x768", "1280x720", "1024x768"]
display_names = ["1920×1080 (Full HD)", "1366×768", "1280×720 (HD)", "1024×768"]
current_index = 0
DebugManager.log_info("Loaded %d resolutions" % values.size(), "ValueStepper")
func _load_difficulty_data():
func _load_difficulty_data() -> void:
# Example difficulty data - customize as needed
values = ["easy", "normal", "hard", "nightmare"]
display_names = ["Easy", "Normal", "Hard", "Nightmare"]
current_index = 1 # Default to "normal"
DebugManager.log_info("Loaded %d difficulty levels" % values.size(), "ValueStepper")
## Updates the display text based on current selection
func _update_display():
func _update_display() -> void:
if values.size() == 0 or current_index < 0 or current_index >= values.size():
value_display.text = "N/A"
return
@@ -100,26 +106,30 @@ func _update_display():
else:
value_display.text = values[current_index]
## Changes the current value by the specified direction (-1 for previous, +1 for next)
func change_value(direction: int):
func change_value(direction: int) -> void:
if values.size() == 0:
DebugManager.log_warn("No values available for: " + data_source, "ValueStepper")
return
var new_index = (current_index + direction) % values.size()
var new_index: int = (current_index + direction) % values.size()
if new_index < 0:
new_index = values.size() - 1
current_index = new_index
var new_value = values[current_index]
var new_value: String = values[current_index]
_update_display()
_apply_value_change(new_value, current_index)
value_changed.emit(new_value, current_index)
DebugManager.log_info("Value changed to: " + new_value + " (index: " + str(current_index) + ")", "ValueStepper")
DebugManager.log_info(
"Value changed to: " + new_value + " (index: " + str(current_index) + ")", "ValueStepper"
)
## Override this method for custom value application logic
func _apply_value_change(new_value: String, _index: int):
func _apply_value_change(new_value: String, _index: int) -> void:
match data_source:
"language":
SettingsManager.set_setting("language", new_value)
@@ -132,29 +142,37 @@ func _apply_value_change(new_value: String, _index: int):
# Apply difficulty change logic here
DebugManager.log_info("Difficulty would change to: " + new_value, "ValueStepper")
## Sets up custom values for the stepper
func setup_custom_values(custom_values: Array[String], custom_display_names: Array[String] = []):
func setup_custom_values(
custom_values: Array[String], custom_display_names: Array[String] = []
) -> void:
values = custom_values.duplicate()
display_names = custom_display_names.duplicate() if custom_display_names.size() > 0 else values.duplicate()
display_names = (
custom_display_names.duplicate() if custom_display_names.size() > 0 else values.duplicate()
)
current_index = 0
_update_display()
DebugManager.log_info("Setup custom values: " + str(values.size()) + " items", "ValueStepper")
## Gets the current value
func get_current_value() -> String:
if values.size() > 0 and current_index >= 0 and current_index < values.size():
return values[current_index]
return ""
## Sets the current value by string
func set_current_value(value: String):
var index = values.find(value)
func set_current_value(value: String) -> void:
var index: int = values.find(value)
if index >= 0:
current_index = index
_update_display()
## Visual highlighting for navigation systems
func set_highlighted(highlighted: bool):
func set_highlighted(highlighted: bool) -> void:
is_highlighted = highlighted
if highlighted:
scale = original_scale * 1.05
@@ -163,6 +181,7 @@ func set_highlighted(highlighted: bool):
scale = original_scale
modulate = original_modulate
## Handle input actions for navigation integration
func handle_input_action(action: String) -> bool:
match action:
@@ -175,16 +194,19 @@ func handle_input_action(action: String) -> bool:
_:
return false
func _on_left_button_pressed():
func _on_left_button_pressed() -> void:
AudioManager.play_ui_click()
DebugManager.log_info("Left button clicked", "ValueStepper")
change_value(-1)
func _on_right_button_pressed():
func _on_right_button_pressed() -> void:
AudioManager.play_ui_click()
DebugManager.log_info("Right button clicked", "ValueStepper")
change_value(1)
## For navigation system integration
func get_control_name() -> String:
return data_source + "_stepper"

View File

@@ -7,6 +7,7 @@ var music_player: AudioStreamPlayer
var ui_click_player: AudioStreamPlayer
var click_stream: AudioStream
func _ready():
music_player = AudioStreamPlayer.new()
add_child(music_player)
@@ -32,22 +33,26 @@ func _ready():
_start_music()
func _load_stream() -> AudioStream:
var res = load(MUSIC_PATH)
if not res or not res is AudioStream:
return null
return res
func _configure_stream_loop(stream: AudioStream) -> void:
if stream is AudioStreamWAV:
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
elif stream is AudioStreamOggVorbis:
stream.loop = true
func _configure_audio_bus() -> void:
music_player.bus = "Music"
music_player.volume_db = linear_to_db(SettingsManager.get_setting("music_volume"))
func update_music_volume(volume: float) -> void:
var volume_db = linear_to_db(volume)
music_player.volume_db = volume_db
@@ -58,16 +63,19 @@ func update_music_volume(volume: float) -> void:
else:
_stop_music()
func _start_music() -> void:
if music_player.playing:
return
music_player.play()
func _stop_music() -> void:
if not music_player.playing:
return
music_player.stop()
func play_ui_click() -> void:
if not click_stream:
return

View File

@@ -2,54 +2,58 @@ extends Node
signal debug_toggled(enabled: bool)
enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
FATAL = 5
}
enum LogLevel { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, FATAL = 5 }
var debug_enabled: bool = false
var debug_overlay_visible: bool = false
var current_log_level: LogLevel = LogLevel.INFO
func _ready():
log_info("DebugManager loaded")
func toggle_debug():
debug_enabled = !debug_enabled
debug_toggled.emit(debug_enabled)
log_info("Debug mode: " + ("ON" if debug_enabled else "OFF"))
func set_debug_enabled(enabled: bool):
if debug_enabled != enabled:
debug_enabled = enabled
debug_toggled.emit(debug_enabled)
func is_debug_enabled() -> bool:
return debug_enabled
func toggle_overlay():
debug_overlay_visible = !debug_overlay_visible
func set_overlay_visible(visible: bool):
debug_overlay_visible = visible
func is_overlay_visible() -> bool:
return debug_overlay_visible
func set_log_level(level: LogLevel):
current_log_level = level
log_info("Log level set to: " + _log_level_to_string(level))
func get_log_level() -> LogLevel:
return current_log_level
func _should_log(level: LogLevel) -> bool:
return level >= current_log_level
func _log_level_to_string(level: LogLevel) -> String:
match level:
LogLevel.TRACE:
@@ -67,41 +71,48 @@ func _log_level_to_string(level: LogLevel) -> String:
_:
return "UNKNOWN"
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String:
var timestamp = Time.get_datetime_string_from_system()
var level_str = _log_level_to_string(level)
var category_str = (" [" + category + "]") if category != "" else ""
return "[%s] %s%s: %s" % [timestamp, level_str, category_str, message]
func log_trace(message: String, category: String = ""):
if _should_log(LogLevel.TRACE):
var formatted = _format_log_message(LogLevel.TRACE, message, category)
if debug_enabled:
print(formatted)
func log_debug(message: String, category: String = ""):
if _should_log(LogLevel.DEBUG):
var formatted = _format_log_message(LogLevel.DEBUG, message, category)
if debug_enabled:
print(formatted)
func log_info(message: String, category: String = ""):
if _should_log(LogLevel.INFO):
var formatted = _format_log_message(LogLevel.INFO, message, category)
print(formatted)
func log_warn(message: String, category: String = ""):
if _should_log(LogLevel.WARN):
var formatted = _format_log_message(LogLevel.WARN, message, category)
print(formatted)
push_warning(formatted)
func log_error(message: String, category: String = ""):
if _should_log(LogLevel.ERROR):
var formatted = _format_log_message(LogLevel.ERROR, message, category)
print(formatted)
push_error(formatted)
func log_fatal(message: String, category: String = ""):
if _should_log(LogLevel.FATAL):
var formatted = _format_log_message(LogLevel.FATAL, message, category)

View File

@@ -6,22 +6,27 @@ const MAIN_SCENE_PATH := "res://scenes/main/main.tscn"
var pending_gameplay_mode: String = "match3"
var is_changing_scene: bool = false
func start_new_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("match3")
func continue_game() -> void:
# Don't reset score
start_game_with_mode("match3")
func start_match3_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("match3")
func start_clickomania_game() -> void:
SaveManager.start_new_game()
start_game_with_mode("clickomania")
func start_game_with_mode(gameplay_mode: String) -> void:
# Input validation
if not gameplay_mode or gameplay_mode.is_empty():
@@ -29,7 +34,9 @@ func start_game_with_mode(gameplay_mode: String) -> void:
return
if not gameplay_mode is String:
DebugManager.log_error("Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager")
DebugManager.log_error(
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager"
)
return
# Prevent concurrent scene changes
@@ -40,7 +47,10 @@ func start_game_with_mode(gameplay_mode: String) -> void:
# Validate gameplay mode
var valid_modes = ["match3", "clickomania"]
if not gameplay_mode in valid_modes:
DebugManager.log_error("Invalid gameplay mode: '%s'. Valid modes: %s" % [gameplay_mode, str(valid_modes)], "GameManager")
DebugManager.log_error(
"Invalid gameplay mode: '%s'. Valid modes: %s" % [gameplay_mode, str(valid_modes)],
"GameManager"
)
return
is_changing_scene = true
@@ -54,7 +64,9 @@ func start_game_with_mode(gameplay_mode: String) -> void:
var result = get_tree().change_scene_to_packed(packed_scene)
if result != OK:
DebugManager.log_error("Failed to change to game scene (Error code: %d)" % result, "GameManager")
DebugManager.log_error(
"Failed to change to game scene (Error code: %d)" % result, "GameManager"
)
is_changing_scene = false
return
@@ -83,6 +95,7 @@ func start_game_with_mode(gameplay_mode: String) -> void:
is_changing_scene = false
func save_game() -> void:
# Get current score from the active game scene
var current_score = 0
@@ -92,10 +105,13 @@ func save_game() -> void:
SaveManager.finish_game(current_score)
DebugManager.log_info("Game saved with score: %d" % current_score, "GameManager")
func exit_to_main_menu() -> void:
# Prevent concurrent scene changes
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
is_changing_scene = true
@@ -109,7 +125,9 @@ func exit_to_main_menu() -> void:
var result = get_tree().change_scene_to_packed(packed_scene)
if result != OK:
DebugManager.log_error("Failed to change to main scene (Error code: %d)" % result, "GameManager")
DebugManager.log_error(
"Failed to change to main scene (Error code: %d)" % result, "GameManager"
)
is_changing_scene = false
return

View File

@@ -1,10 +1,12 @@
extends Node
func _ready():
# Set default locale if not already set
if TranslationServer.get_locale() == "":
TranslationServer.set_locale("en")
func change_language(locale: String):
TranslationServer.set_locale(locale)
# Signal to update UI elements

View File

@@ -1,23 +1,24 @@
extends Node
const SAVE_FILE_PATH = "user://savegame.save"
const SAVE_FORMAT_VERSION = 1
const MAX_GRID_SIZE = 15
const MAX_TILE_TYPES = 10
const MAX_SCORE = 999999999
const MAX_GAMES_PLAYED = 100000
const MAX_FILE_SIZE = 1048576 # 1MB limit
const SAVE_FILE_PATH: String = "user://savegame.save"
const SAVE_FORMAT_VERSION: int = 1
const MAX_GRID_SIZE: int = 15
const MAX_TILE_TYPES: int = 10
const MAX_SCORE: int = 999999999
const MAX_GAMES_PLAYED: int = 100000
const MAX_FILE_SIZE: int = 1048576 # 1MB limit
# Save operation protection
var _save_in_progress: bool = false
var _restore_in_progress: bool = false
var game_data = {
var game_data: Dictionary = {
"high_score": 0,
"current_score": 0,
"games_played": 0,
"total_score": 0,
"grid_state": {
"grid_state":
{
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
@@ -25,36 +26,41 @@ var game_data = {
}
}
func _ready():
func _ready() -> void:
load_game()
func save_game():
func save_game() -> bool:
# Prevent concurrent saves
if _save_in_progress:
DebugManager.log_warn("Save already in progress, skipping", "SaveManager")
return false
_save_in_progress = true
var result = _perform_save()
var result: bool = _perform_save()
_save_in_progress = false
return result
func _perform_save():
func _perform_save() -> bool:
# Create backup before saving
_create_backup()
# Add version and checksum
var save_data = game_data.duplicate(true)
var save_data: Dictionary = game_data.duplicate(true)
save_data["_version"] = SAVE_FORMAT_VERSION
# Calculate checksum excluding _checksum field
save_data["_checksum"] = _calculate_checksum(save_data)
var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if save_file == null:
DebugManager.log_error("Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager")
DebugManager.log_error(
"Failed to open save file for writing: %s" % SAVE_FILE_PATH, "SaveManager"
)
return false
var json_string = JSON.stringify(save_data)
var json_string: String = JSON.stringify(save_data)
# Validate JSON creation
if json_string.is_empty():
@@ -65,10 +71,17 @@ func _perform_save():
save_file.store_var(json_string)
save_file.close()
DebugManager.log_info("Game saved successfully. High score: %d, Current score: %d" % [game_data.high_score, game_data.current_score], "SaveManager")
DebugManager.log_info(
(
"Game saved successfully. High score: %d, Current score: %d"
% [game_data.high_score, game_data.current_score]
),
"SaveManager"
)
return true
func load_game():
func load_game() -> void:
if not FileAccess.file_exists(SAVE_FILE_PATH):
DebugManager.log_info("No save file found, using defaults", "SaveManager")
return
@@ -76,70 +89,90 @@ func load_game():
# Reset restore flag
_restore_in_progress = false
var save_file = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
if save_file == null:
DebugManager.log_error("Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager")
DebugManager.log_error(
"Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager"
)
return
# Check file size
var file_size = save_file.get_length()
var file_size: int = save_file.get_length()
if file_size > MAX_FILE_SIZE:
DebugManager.log_error("Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager")
DebugManager.log_error(
"Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager"
)
save_file.close()
return
var json_string = save_file.get_var()
var json_string: Variant = save_file.get_var()
save_file.close()
if not json_string is String:
DebugManager.log_error("Save file contains invalid data type", "SaveManager")
return
var json = JSON.new()
var parse_result = json.parse(json_string)
var json: JSON = JSON.new()
var parse_result: Error = json.parse(json_string)
if parse_result != OK:
DebugManager.log_error("Failed to parse save file JSON: %s" % json.error_string, "SaveManager")
DebugManager.log_error(
"Failed to parse save file JSON: %s" % json.error_string, "SaveManager"
)
if not _restore_in_progress:
var backup_restored = _restore_backup_if_exists()
if not backup_restored:
DebugManager.log_warn("JSON parse failed and backup restore failed, using defaults", "SaveManager")
DebugManager.log_warn(
"JSON parse failed and backup restore failed, using defaults", "SaveManager"
)
return
var loaded_data = json.data
var loaded_data: Variant = json.data
if not loaded_data is Dictionary:
DebugManager.log_error("Save file root is not a dictionary", "SaveManager")
if not _restore_in_progress:
var backup_restored = _restore_backup_if_exists()
if not backup_restored:
DebugManager.log_warn("Invalid data format and backup restore failed, using defaults", "SaveManager")
DebugManager.log_warn(
"Invalid data format and backup restore failed, using defaults", "SaveManager"
)
return
# Validate checksum first
if not _validate_checksum(loaded_data):
DebugManager.log_error("Save file checksum validation failed - possible tampering", "SaveManager")
DebugManager.log_error(
"Save file checksum validation failed - possible tampering", "SaveManager"
)
if not _restore_in_progress:
var backup_restored = _restore_backup_if_exists()
if not backup_restored:
DebugManager.log_warn("Backup restore failed, using default game data", "SaveManager")
DebugManager.log_warn(
"Backup restore failed, using default game data", "SaveManager"
)
return
# Handle version migration
var migrated_data = _handle_version_migration(loaded_data)
var migrated_data: Variant = _handle_version_migration(loaded_data)
if migrated_data == null:
DebugManager.log_error("Save file version migration failed", "SaveManager")
if not _restore_in_progress:
var backup_restored = _restore_backup_if_exists()
if not backup_restored:
DebugManager.log_warn("Migration failed and backup restore failed, using defaults", "SaveManager")
DebugManager.log_warn(
"Migration failed and backup restore failed, using defaults", "SaveManager"
)
return
# Validate and fix loaded data
if not _validate_and_fix_save_data(migrated_data):
DebugManager.log_error("Save file failed validation after migration, using defaults", "SaveManager")
DebugManager.log_error(
"Save file failed validation after migration, using defaults", "SaveManager"
)
if not _restore_in_progress:
var backup_restored = _restore_backup_if_exists()
if not backup_restored:
DebugManager.log_warn("Validation failed and backup restore failed, using defaults", "SaveManager")
DebugManager.log_warn(
"Validation failed and backup restore failed, using defaults", "SaveManager"
)
return
# Use migrated data
@@ -148,15 +181,24 @@ func load_game():
# Safely merge validated data
_merge_validated_data(loaded_data)
DebugManager.log_info("Game loaded successfully. High score: %d, Games played: %d" % [game_data.high_score, game_data.games_played], "SaveManager")
DebugManager.log_info(
(
"Game loaded successfully. High score: %d, Games played: %d"
% [game_data.high_score, game_data.games_played]
),
"SaveManager"
)
func update_current_score(score: int):
func update_current_score(score: int) -> void:
# Input validation
if score < 0:
DebugManager.log_warn("Negative score rejected: %d" % score, "SaveManager")
return
if score > MAX_SCORE:
DebugManager.log_warn("Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager")
DebugManager.log_warn(
"Score too high, capping at maximum: %d -> %d" % [score, MAX_SCORE], "SaveManager"
)
score = MAX_SCORE
game_data.current_score = score
@@ -164,27 +206,36 @@ func update_current_score(score: int):
game_data.high_score = score
DebugManager.log_info("New high score: %d" % score, "SaveManager")
func start_new_game():
func start_new_game() -> void:
game_data.current_score = 0
game_data.games_played += 1
# Clear saved grid state
game_data.grid_state.grid_layout = []
DebugManager.log_info("Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager")
DebugManager.log_info(
"Started new game #%d (cleared grid state)" % game_data.games_played, "SaveManager"
)
func finish_game(final_score: int):
func finish_game(final_score: int) -> void:
# Input validation
if final_score < 0:
DebugManager.log_warn("Negative final score rejected: %d" % final_score, "SaveManager")
return
if final_score > MAX_SCORE:
DebugManager.log_warn("Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager")
DebugManager.log_warn(
"Final score too high, capping: %d -> %d" % [final_score, MAX_SCORE], "SaveManager"
)
final_score = MAX_SCORE
DebugManager.log_info("Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score], "SaveManager")
DebugManager.log_info(
"Finishing game with score: %d (previous: %d)" % [final_score, game_data.current_score],
"SaveManager"
)
game_data.current_score = final_score
# Prevent overflow
var new_total = game_data.total_score + final_score
var new_total: int = game_data.total_score + final_score
if new_total < game_data.total_score: # Overflow check
DebugManager.log_warn("Total score overflow prevented", "SaveManager")
game_data.total_score = MAX_SCORE
@@ -196,25 +247,38 @@ func finish_game(final_score: int):
DebugManager.log_info("New high score achieved: %d" % final_score, "SaveManager")
save_game()
func get_high_score() -> int:
return game_data.high_score
func get_current_score() -> int:
return game_data.current_score
func get_games_played() -> int:
return game_data.games_played
func get_total_score() -> int:
return game_data.total_score
func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array):
func save_grid_state(
grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array
) -> void:
# Input validation
if not _validate_grid_parameters(grid_size, tile_types_count, active_gem_types, grid_layout):
DebugManager.log_error("Grid state validation failed, not saving", "SaveManager")
return
DebugManager.log_info("Saving grid state: size(%d,%d), types=%d, layout_rows=%d" % [grid_size.x, grid_size.y, tile_types_count, grid_layout.size()], "SaveManager")
DebugManager.log_info(
(
"Saving grid state: size(%d,%d), types=%d, layout_rows=%d"
% [grid_size.x, grid_size.y, tile_types_count, grid_layout.size()]
),
"SaveManager"
)
game_data.grid_state.grid_size = {"x": grid_size.x, "y": grid_size.y}
game_data.grid_state.tile_types_count = tile_types_count
@@ -223,25 +287,29 @@ func save_grid_state(grid_size: Vector2i, tile_types_count: int, active_gem_type
# Debug: Print first rows
for y in range(min(3, grid_layout.size())):
var row_str = ""
var row_str: String = ""
for x in range(min(8, grid_layout[y].size())):
row_str += str(grid_layout[y][x]) + " "
DebugManager.log_debug("Saved row %d: %s" % [y, row_str], "SaveManager")
save_game()
func get_saved_grid_state() -> Dictionary:
return game_data.grid_state
func has_saved_grid() -> bool:
return game_data.grid_state.grid_layout.size() > 0
func clear_grid_state():
func clear_grid_state() -> void:
DebugManager.log_info("Clearing saved grid state", "SaveManager")
game_data.grid_state.grid_layout = []
save_game()
func reset_all_progress():
func reset_all_progress() -> bool:
"""Reset all progress and delete save files"""
DebugManager.log_info("Starting complete progress reset", "SaveManager")
@@ -251,7 +319,8 @@ func reset_all_progress():
"current_score": 0,
"games_played": 0,
"total_score": 0,
"grid_state": {
"grid_state":
{
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
@@ -261,22 +330,28 @@ func reset_all_progress():
# Delete main save file
if FileAccess.file_exists(SAVE_FILE_PATH):
var error = DirAccess.remove_absolute(SAVE_FILE_PATH)
var error: Error = DirAccess.remove_absolute(SAVE_FILE_PATH)
if error == OK:
DebugManager.log_info("Main save file deleted successfully", "SaveManager")
else:
DebugManager.log_error("Failed to delete main save file: error %d" % error, "SaveManager")
DebugManager.log_error(
"Failed to delete main save file: error %d" % error, "SaveManager"
)
# Delete backup file
var backup_path = SAVE_FILE_PATH + ".backup"
var backup_path: String = SAVE_FILE_PATH + ".backup"
if FileAccess.file_exists(backup_path):
var error = DirAccess.remove_absolute(backup_path)
var error: Error = DirAccess.remove_absolute(backup_path)
if error == OK:
DebugManager.log_info("Backup save file deleted successfully", "SaveManager")
else:
DebugManager.log_error("Failed to delete backup save file: error %d" % error, "SaveManager")
DebugManager.log_error(
"Failed to delete backup save file: error %d" % error, "SaveManager"
)
DebugManager.log_info("Progress reset completed - all scores and save data cleared", "SaveManager")
DebugManager.log_info(
"Progress reset completed - all scores and save data cleared", "SaveManager"
)
# Clear restore flag
_restore_in_progress = false
@@ -287,10 +362,13 @@ func reset_all_progress():
return true
# Security and validation helper functions
func _validate_save_data(data: Dictionary) -> bool:
# Check required fields exist and have correct types
var required_fields = ["high_score", "current_score", "games_played", "total_score", "grid_state"]
var required_fields: Array[String] = [
"high_score", "current_score", "games_played", "total_score", "grid_state"
]
for field in required_fields:
if not data.has(field):
DebugManager.log_error("Missing required field: %s" % field, "SaveManager")
@@ -308,29 +386,38 @@ func _validate_save_data(data: Dictionary) -> bool:
return false
# Use safe getter for games_played validation
var games_played = 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):
DebugManager.log_error("Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)], "SaveManager")
DebugManager.log_error(
"Invalid games_played type: %s (type: %s)" % [str(games_played), typeof(games_played)],
"SaveManager"
)
return false
# 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)):
DebugManager.log_error("Invalid games_played float value: %s" % str(games_played), "SaveManager")
DebugManager.log_error(
"Invalid games_played float value: %s" % str(games_played), "SaveManager"
)
return false
var games_played_int = int(games_played)
var games_played_int: int = int(games_played)
if games_played_int < 0 or games_played_int > MAX_GAMES_PLAYED:
DebugManager.log_error("Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED], "SaveManager")
DebugManager.log_error(
"Invalid games_played value: %d (range: 0-%d)" % [games_played_int, MAX_GAMES_PLAYED],
"SaveManager"
)
return false
# Validate grid state
var grid_state = data.get("grid_state", {})
var grid_state: Variant = data.get("grid_state", {})
if not grid_state is Dictionary:
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
return false
return _validate_grid_state(grid_state)
func _validate_and_fix_save_data(data: Dictionary) -> bool:
"""
Permissive validation that fixes issues instead of rejecting data entirely.
@@ -339,10 +426,14 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
DebugManager.log_info("Running permissive validation with auto-fix", "SaveManager")
# Ensure all required fields exist, create defaults if missing
var required_fields = ["high_score", "current_score", "games_played", "total_score", "grid_state"]
var required_fields: Array[String] = [
"high_score", "current_score", "games_played", "total_score", "grid_state"
]
for field in required_fields:
if not data.has(field):
DebugManager.log_warn("Missing required field '%s', adding default value" % field, "SaveManager")
DebugManager.log_warn(
"Missing required field '%s', adding default value" % field, "SaveManager"
)
match field:
"high_score", "current_score", "total_score":
data[field] = 0
@@ -358,12 +449,12 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
# Fix numeric fields - clamp to valid ranges instead of rejecting
for field in ["high_score", "current_score", "total_score"]:
var value = data.get(field, 0)
var value: Variant = data.get(field, 0)
if not (value is int or value is float):
DebugManager.log_warn("Invalid type for %s, converting to 0" % field, "SaveManager")
data[field] = 0
else:
var numeric_value = int(value)
var numeric_value: int = int(value)
if numeric_value < 0:
DebugManager.log_warn("Negative %s fixed to 0" % field, "SaveManager")
data[field] = 0
@@ -374,12 +465,12 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
data[field] = numeric_value
# Fix games_played
var games_played = 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):
DebugManager.log_warn("Invalid games_played type, converting to 0", "SaveManager")
data["games_played"] = 0
else:
var games_played_int = int(games_played)
var games_played_int: int = int(games_played)
if games_played_int < 0:
data["games_played"] = 0
elif games_played_int > MAX_GAMES_PLAYED:
@@ -388,7 +479,7 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
data["games_played"] = games_played_int
# Fix grid_state - ensure it exists and has basic structure
var grid_state = data.get("grid_state", {})
var grid_state: Variant = data.get("grid_state", {})
if not grid_state is Dictionary:
DebugManager.log_warn("Invalid grid_state, creating default", "SaveManager")
data["grid_state"] = {
@@ -415,21 +506,24 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
DebugManager.log_warn("Invalid grid_layout, clearing saved grid", "SaveManager")
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
func _validate_grid_state(grid_state: Dictionary) -> bool:
# Check grid size
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary:
DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
return false
var size = grid_state.grid_size
var size: Variant = grid_state.grid_size
if not size.has("x") or not size.has("y"):
return false
var width = size.x
var height = size.y
var width: Variant = size.x
var height: Variant = size.y
if not width is int or not height is int:
return false
if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE:
@@ -437,13 +531,13 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
return false
# Check tile types
var tile_types = 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:
DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager")
return false
# Validate active_gem_types if present
var active_gems = grid_state.get("active_gem_types", [])
var active_gems: Variant = grid_state.get("active_gem_types", [])
if not active_gems is Array:
DebugManager.log_error("active_gem_types is not an array", "SaveManager")
return false
@@ -451,16 +545,20 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
# If active_gem_types exists, validate its contents
if active_gems.size() > 0:
for i in range(active_gems.size()):
var gem_type = active_gems[i]
var gem_type: Variant = active_gems[i]
if not gem_type is int:
DebugManager.log_error("active_gem_types[%d] is not an integer: %s" % [i, str(gem_type)], "SaveManager")
DebugManager.log_error(
"active_gem_types[%d] is not an integer: %s" % [i, str(gem_type)], "SaveManager"
)
return false
if gem_type < 0 or gem_type >= tile_types:
DebugManager.log_error("active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager")
DebugManager.log_error(
"active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager"
)
return false
# Validate grid layout if present
var layout = grid_state.get("grid_layout", [])
var layout: Variant = grid_state.get("grid_layout", [])
if not layout is Array:
DebugManager.log_error("grid_layout is not an array", "SaveManager")
return false
@@ -470,60 +568,102 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
return true
func _validate_grid_layout(layout: Array, expected_width: int, expected_height: int, max_tile_type: int) -> bool:
func _validate_grid_layout(
layout: Array, expected_width: int, expected_height: int, max_tile_type: int
) -> bool:
if layout.size() != expected_height:
DebugManager.log_error("Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height], "SaveManager")
DebugManager.log_error(
"Grid layout height mismatch: %d vs %d" % [layout.size(), expected_height],
"SaveManager"
)
return false
for y in range(layout.size()):
var row = layout[y]
var row: Variant = layout[y]
if not row is Array:
DebugManager.log_error("Grid layout row %d is not an array" % y, "SaveManager")
return false
if row.size() != expected_width:
DebugManager.log_error("Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width], "SaveManager")
DebugManager.log_error(
"Grid layout row %d width mismatch: %d vs %d" % [y, row.size(), expected_width],
"SaveManager"
)
return false
for x in range(row.size()):
var tile_type = row[x]
var tile_type: Variant = row[x]
if not tile_type is int:
DebugManager.log_error("Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)], "SaveManager")
DebugManager.log_error(
"Grid tile [%d][%d] is not an integer: %s" % [y, x, str(tile_type)],
"SaveManager"
)
return false
if tile_type < -1 or tile_type >= max_tile_type:
DebugManager.log_error("Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager")
DebugManager.log_error(
"Grid tile [%d][%d] type out of range: %d" % [y, x, tile_type], "SaveManager"
)
return false
return true
func _validate_grid_parameters(grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array) -> bool:
func _validate_grid_parameters(
grid_size: Vector2i, tile_types_count: int, active_gem_types: Array, grid_layout: Array
) -> bool:
# Validate grid size
if grid_size.x < 3 or grid_size.y < 3 or grid_size.x > MAX_GRID_SIZE or grid_size.y > MAX_GRID_SIZE:
DebugManager.log_error("Invalid grid size: %dx%d (min 3x3, max %dx%d)" % [grid_size.x, grid_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE], "SaveManager")
if (
grid_size.x < 3
or grid_size.y < 3
or grid_size.x > MAX_GRID_SIZE
or grid_size.y > MAX_GRID_SIZE
):
DebugManager.log_error(
(
"Invalid grid size: %dx%d (min 3x3, max %dx%d)"
% [grid_size.x, grid_size.y, MAX_GRID_SIZE, MAX_GRID_SIZE]
),
"SaveManager"
)
return false
# Validate tile types count
if tile_types_count < 3 or tile_types_count > MAX_TILE_TYPES:
DebugManager.log_error("Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES], "SaveManager")
DebugManager.log_error(
"Invalid tile types count: %d (min 3, max %d)" % [tile_types_count, MAX_TILE_TYPES],
"SaveManager"
)
return false
# Validate active gem types
if active_gem_types.size() != tile_types_count:
DebugManager.log_error("Active gem types size mismatch: %d vs %d" % [active_gem_types.size(), tile_types_count], "SaveManager")
DebugManager.log_error(
(
"Active gem types size mismatch: %d vs %d"
% [active_gem_types.size(), tile_types_count]
),
"SaveManager"
)
return false
# Validate grid layout
return _validate_grid_layout(grid_layout, grid_size.x, grid_size.y, tile_types_count)
func _is_valid_score(score) -> bool:
func _is_valid_score(score: Variant) -> bool:
# Accept both int and float, but convert to int for validation
if not (score is int or score is float):
DebugManager.log_error("Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager")
DebugManager.log_error(
"Score is not a number: %s (type: %s)" % [str(score), typeof(score)], "SaveManager"
)
return false
# Check for NaN and infinity values
if score is float:
if is_nan(score) or is_inf(score):
DebugManager.log_error("Score contains invalid float value (NaN/Inf): %s" % str(score), "SaveManager")
DebugManager.log_error(
"Score contains invalid float value (NaN/Inf): %s" % str(score), "SaveManager"
)
return false
var score_int = int(score)
@@ -532,7 +672,8 @@ func _is_valid_score(score) -> bool:
return false
return true
func _merge_validated_data(loaded_data: Dictionary):
func _merge_validated_data(loaded_data: Dictionary) -> void:
# Safely merge only validated fields, converting floats to ints for scores
for key in ["high_score", "current_score", "total_score"]:
if loaded_data.has(key):
@@ -544,31 +685,33 @@ func _merge_validated_data(loaded_data: Dictionary):
game_data["games_played"] = _safe_get_numeric_value(loaded_data, "games_played", 0)
# Merge grid state carefully
var loaded_grid = loaded_data.get("grid_state", {})
var loaded_grid: Variant = loaded_data.get("grid_state", {})
if loaded_grid is Dictionary:
for grid_key in ["grid_size", "tile_types_count", "active_gem_types", "grid_layout"]:
if loaded_grid.has(grid_key):
game_data.grid_state[grid_key] = loaded_grid[grid_key]
func _calculate_checksum(data: Dictionary) -> String:
# Calculate deterministic checksum EXCLUDING the checksum field itself
var data_copy = data.duplicate(true)
var data_copy: Dictionary = data.duplicate(true)
data_copy.erase("_checksum") # Remove checksum before calculation
# Create deterministic checksum using sorted keys to ensure consistency
var checksum_string = _create_deterministic_string(data_copy)
var checksum_string: String = _create_deterministic_string(data_copy)
return str(checksum_string.hash())
func _create_deterministic_string(data: Dictionary) -> String:
# Create a deterministic string representation by processing keys in sorted order
var keys = data.keys()
var keys: Array = data.keys()
keys.sort() # Ensure consistent ordering
var parts = []
var parts: Array[String] = []
for key in keys:
var value = data[key]
var key_str = str(key)
var value_str = ""
var value: Variant = data[key]
var key_str: String = str(key)
var value_str: String = ""
if value is Dictionary:
value_str = _create_deterministic_string(value)
@@ -582,9 +725,10 @@ func _create_deterministic_string(data: Dictionary) -> String:
return "{" + ",".join(parts) + "}"
func _create_deterministic_array_string(arr: Array) -> String:
# Create deterministic string representation of arrays
var parts = []
var parts: Array[String] = []
for item in arr:
if item is Dictionary:
parts.append(_create_deterministic_string(item))
@@ -596,7 +740,8 @@ func _create_deterministic_array_string(arr: Array) -> String:
return "[" + ",".join(parts) + "]"
func _normalize_value_for_checksum(value) -> String:
func _normalize_value_for_checksum(value: Variant) -> String:
"""
CRITICAL FIX: Normalize values for consistent checksum calculation
This prevents JSON serialization type conversion from breaking checksums
@@ -620,82 +765,134 @@ func _normalize_value_for_checksum(value) -> String:
else:
return str(value)
func _validate_checksum(data: Dictionary) -> bool:
# Validate checksum to detect tampering
if not data.has("_checksum"):
DebugManager.log_warn("No checksum found in save data", "SaveManager")
return true # Allow saves without checksum for backward compatibility
var stored_checksum = data["_checksum"]
var calculated_checksum = _calculate_checksum(data)
var is_valid = stored_checksum == calculated_checksum
var stored_checksum: Variant = data["_checksum"]
var calculated_checksum: String = _calculate_checksum(data)
var is_valid: bool = stored_checksum == calculated_checksum
if not is_valid:
# MIGRATION COMPATIBILITY: If this is a version 1 save file, it might have the old checksum bug
# Try to be more lenient with existing saves to prevent data loss
var data_version = data.get("_version", 0)
var data_version: Variant = data.get("_version", 0)
if data_version <= 1:
DebugManager.log_warn("Checksum mismatch in v%d save file - may be due to JSON serialization issue (stored: %s, calculated: %s)" % [data_version, stored_checksum, calculated_checksum], "SaveManager")
DebugManager.log_info("Allowing load for backward compatibility - checksum will be recalculated on next save", "SaveManager")
(
DebugManager
. log_warn(
(
"Checksum mismatch in v%d save file - may be due to JSON serialization issue (stored: %s, calculated: %s)"
% [data_version, stored_checksum, calculated_checksum]
),
"SaveManager"
)
)
(
DebugManager
. log_info(
"Allowing load for backward compatibility - checksum will be recalculated on next save",
"SaveManager"
)
)
# Mark for checksum regeneration by removing the invalid one
data.erase("_checksum")
return true
else:
DebugManager.log_error("Checksum mismatch - stored: %s, calculated: %s" % [stored_checksum, calculated_checksum], "SaveManager")
DebugManager.log_error(
(
"Checksum mismatch - stored: %s, calculated: %s"
% [stored_checksum, calculated_checksum]
),
"SaveManager"
)
return false
return is_valid
func _safe_get_numeric_value(data: Dictionary, key: String, default_value: float) -> int:
"""Safely extract and convert numeric values with comprehensive validation"""
var value = data.get(key, default_value)
var value: Variant = data.get(key, default_value)
# Type validation
if not (value is float or value is int):
DebugManager.log_warn("Non-numeric value for %s: %s, using default %s" % [key, str(value), str(default_value)], "SaveManager")
DebugManager.log_warn(
(
"Non-numeric value for %s: %s, using default %s"
% [key, str(value), str(default_value)]
),
"SaveManager"
)
return int(default_value)
# NaN/Infinity validation for floats
if value is float:
if is_nan(value) or is_inf(value):
DebugManager.log_warn("Invalid float value for %s: %s, using default %s" % [key, str(value), str(default_value)], "SaveManager")
DebugManager.log_warn(
(
"Invalid float value for %s: %s, using default %s"
% [key, str(value), str(default_value)]
),
"SaveManager"
)
return int(default_value)
# Convert to integer and validate bounds
var int_value = int(value)
var int_value: int = int(value)
# Apply bounds checking based on field type
if key in ["high_score", "current_score", "total_score"]:
if int_value < 0 or int_value > MAX_SCORE:
DebugManager.log_warn("Score %s out of bounds: %d, using default" % [key, int_value], "SaveManager")
DebugManager.log_warn(
"Score %s out of bounds: %d, using default" % [key, int_value], "SaveManager"
)
return int(default_value)
elif key == "games_played":
if int_value < 0 or int_value > MAX_GAMES_PLAYED:
DebugManager.log_warn("Games played out of bounds: %d, using default" % int_value, "SaveManager")
DebugManager.log_warn(
"Games played out of bounds: %d, using default" % int_value, "SaveManager"
)
return int(default_value)
return int_value
func _handle_version_migration(data: Dictionary):
func _handle_version_migration(data: Dictionary) -> Variant:
"""Handle save data version migration and compatibility"""
var data_version = data.get("_version", 0) # Default to version 0 for old saves
var data_version: Variant = data.get("_version", 0) # Default to version 0 for old saves
if data_version == SAVE_FORMAT_VERSION:
# Current version, no migration needed
DebugManager.log_info("Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager")
DebugManager.log_info(
"Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager"
)
return data
elif data_version > SAVE_FORMAT_VERSION:
# Future version - cannot handle
DebugManager.log_error("Save file version (%d) is newer than supported (%d)" % [data_version, SAVE_FORMAT_VERSION], "SaveManager")
DebugManager.log_error(
(
"Save file version (%d) is newer than supported (%d)"
% [data_version, SAVE_FORMAT_VERSION]
),
"SaveManager"
)
return null
else:
# Older version - migrate
DebugManager.log_info("Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION], "SaveManager")
DebugManager.log_info(
"Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION],
"SaveManager"
)
return _migrate_save_data(data, data_version)
func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
"""Migrate save data from older versions to current format"""
var migrated_data = data.duplicate(true)
var migrated_data: Dictionary = data.duplicate(true)
# Migration from version 0 (no version field) to version 1
if from_version < 1:
@@ -716,11 +913,17 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
# Ensure all numeric values are within bounds after migration
for score_key in ["high_score", "current_score", "total_score"]:
if migrated_data.has(score_key):
var score_value = migrated_data[score_key]
var score_value: Variant = migrated_data[score_key]
if score_value is float or score_value is int:
var int_score = int(score_value)
var int_score: int = int(score_value)
if int_score < 0 or int_score > MAX_SCORE:
DebugManager.log_warn("Clamping %s during migration: %d -> %d" % [score_key, int_score, clamp(int_score, 0, MAX_SCORE)], "SaveManager")
DebugManager.log_warn(
(
"Clamping %s during migration: %d -> %d"
% [score_key, int_score, clamp(int_score, 0, MAX_SCORE)]
),
"SaveManager"
)
migrated_data[score_key] = clamp(int_score, 0, MAX_SCORE)
# Future migrations would go here
@@ -736,20 +939,22 @@ func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
DebugManager.log_info("Save data migration completed successfully", "SaveManager")
return migrated_data
func _create_backup():
func _create_backup() -> void:
# Create backup of current save file
if FileAccess.file_exists(SAVE_FILE_PATH):
var backup_path = SAVE_FILE_PATH + ".backup"
var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
var backup = FileAccess.open(backup_path, FileAccess.WRITE)
var backup_path: String = SAVE_FILE_PATH + ".backup"
var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
var backup: FileAccess = FileAccess.open(backup_path, FileAccess.WRITE)
if original and backup:
backup.store_var(original.get_var())
backup.close()
if original:
original.close()
func _restore_backup_if_exists():
var backup_path = SAVE_FILE_PATH + ".backup"
func _restore_backup_if_exists() -> bool:
var backup_path: String = SAVE_FILE_PATH + ".backup"
if not FileAccess.file_exists(backup_path):
DebugManager.log_warn("No backup file found for recovery", "SaveManager")
return false
@@ -757,19 +962,19 @@ func _restore_backup_if_exists():
DebugManager.log_info("Attempting to restore from backup", "SaveManager")
# Validate backup file size before attempting restore
var backup_file = FileAccess.open(backup_path, FileAccess.READ)
var backup_file: FileAccess = FileAccess.open(backup_path, FileAccess.READ)
if backup_file == null:
DebugManager.log_error("Failed to open backup file for reading", "SaveManager")
return false
var backup_size = backup_file.get_length()
var backup_size: int = backup_file.get_length()
if backup_size > MAX_FILE_SIZE:
DebugManager.log_error("Backup file too large: %d bytes" % backup_size, "SaveManager")
backup_file.close()
return false
# Attempt to restore backup
var backup_data = backup_file.get_var()
var backup_data: Variant = backup_file.get_var()
backup_file.close()
if backup_data == null:
@@ -777,7 +982,7 @@ func _restore_backup_if_exists():
return false
# Create new save file from backup
var original = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
var original: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.WRITE)
if original == null:
DebugManager.log_error("Failed to create new save file from backup", "SaveManager")
return false

View File

@@ -7,24 +7,22 @@ const MAX_SETTING_STRING_LENGTH = 10 # Max length for string settings like lang
# dev `user://`=`%APPDATA%\Godot\app_userdata\Skelly`
# prod `user://`=`%APPDATA%\Skelly\`
var settings = {
var settings: Dictionary = {}
var default_settings: Dictionary = {
"master_volume": 0.50, "music_volume": 0.40, "sfx_volume": 0.50, "language": "en"
}
var default_settings = {
"master_volume": 0.50,
"music_volume": 0.40,
"sfx_volume": 0.50,
"language": "en"
}
var languages_data: Dictionary = {}
var languages_data = {}
func _ready():
func _ready() -> void:
DebugManager.log_info("SettingsManager ready", "SettingsManager")
load_languages()
load_settings()
func load_settings():
func load_settings() -> void:
var config = ConfigFile.new()
var load_result = config.load(SETTINGS_FILE)
@@ -39,16 +37,26 @@ func load_settings():
if _validate_setting_value(key, loaded_value):
settings[key] = loaded_value
else:
DebugManager.log_warn("Invalid setting value for '%s', using default: %s" % [key, str(default_settings[key])], "SettingsManager")
DebugManager.log_warn(
(
"Invalid setting value for '%s', using default: %s"
% [key, str(default_settings[key])]
),
"SettingsManager"
)
settings[key] = default_settings[key]
DebugManager.log_info("Settings loaded: " + str(settings), "SettingsManager")
else:
DebugManager.log_warn("No settings file found (Error code: %d), using defaults" % load_result, "SettingsManager")
DebugManager.log_warn(
"No settings file found (Error code: %d), using defaults" % load_result,
"SettingsManager"
)
settings = default_settings.duplicate()
# Apply settings with error handling
_apply_all_settings()
func _apply_all_settings():
DebugManager.log_info("Applying settings: " + str(settings), "SettingsManager")
@@ -64,17 +72,24 @@ func _apply_all_settings():
if master_bus >= 0 and "master_volume" in settings:
AudioServer.set_bus_volume_db(master_bus, linear_to_db(settings["master_volume"]))
else:
DebugManager.log_warn("Master audio bus not found or master_volume setting missing", "SettingsManager")
DebugManager.log_warn(
"Master audio bus not found or master_volume setting missing", "SettingsManager"
)
if music_bus >= 0 and "music_volume" in settings:
AudioServer.set_bus_volume_db(music_bus, linear_to_db(settings["music_volume"]))
else:
DebugManager.log_warn("Music audio bus not found or music_volume setting missing", "SettingsManager")
DebugManager.log_warn(
"Music audio bus not found or music_volume setting missing", "SettingsManager"
)
if sfx_bus >= 0 and "sfx_volume" in settings:
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(settings["sfx_volume"]))
else:
DebugManager.log_warn("SFX audio bus not found or sfx_volume setting missing", "SettingsManager")
DebugManager.log_warn(
"SFX audio bus not found or sfx_volume setting missing", "SettingsManager"
)
func save_settings():
var config = ConfigFile.new()
@@ -83,15 +98,19 @@ func save_settings():
var save_result = config.save(SETTINGS_FILE)
if save_result != OK:
DebugManager.log_error("Failed to save settings (Error code: %d)" % save_result, "SettingsManager")
DebugManager.log_error(
"Failed to save settings (Error code: %d)" % save_result, "SettingsManager"
)
return false
DebugManager.log_info("Settings saved: " + str(settings), "SettingsManager")
return true
func get_setting(key: String):
return settings.get(key)
func set_setting(key: String, value) -> bool:
if not key in default_settings:
DebugManager.log_error("Unknown setting key: " + key, "SettingsManager")
@@ -99,13 +118,16 @@ func set_setting(key: String, value) -> bool:
# Validate value type and range based on key
if not _validate_setting_value(key, value):
DebugManager.log_error("Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager")
DebugManager.log_error(
"Invalid value for setting '%s': %s" % [key, str(value)], "SettingsManager"
)
return false
settings[key] = value
_apply_setting_side_effect(key, value)
return true
func _validate_setting_value(key: String, value) -> bool:
match key:
"master_volume", "music_volume", "sfx_volume":
@@ -116,7 +138,9 @@ func _validate_setting_value(key: String, value) -> bool:
var float_value = float(value)
# Check for NaN and infinity
if is_nan(float_value) or is_inf(float_value):
DebugManager.log_warn("Invalid float value for %s: %s" % [key, str(value)], "SettingsManager")
DebugManager.log_warn(
"Invalid float value for %s: %s" % [key, str(value)], "SettingsManager"
)
return false
# Range validation
return float_value >= 0.0 and float_value <= 1.0
@@ -125,13 +149,17 @@ func _validate_setting_value(key: String, value) -> bool:
return false
# Prevent extremely long strings
if value.length() > MAX_SETTING_STRING_LENGTH:
DebugManager.log_warn("Language code too long: %d characters" % value.length(), "SettingsManager")
DebugManager.log_warn(
"Language code too long: %d characters" % value.length(), "SettingsManager"
)
return false
# Check for valid characters (alphanumeric and common separators only)
var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9_-]+$")
if not regex.search(value):
DebugManager.log_warn("Language code contains invalid characters: %s" % value, "SettingsManager")
DebugManager.log_warn(
"Language code contains invalid characters: %s" % value, "SettingsManager"
)
return false
# Check if language is supported
if languages_data.has("languages") and languages_data.languages is Dictionary:
@@ -147,31 +175,40 @@ func _validate_setting_value(key: String, value) -> bool:
return false
return typeof(value) == typeof(default_value)
func _apply_setting_side_effect(key: String, value) -> void:
match key:
"language":
TranslationServer.set_locale(value)
"master_volume":
if AudioServer.get_bus_index("Master") >= 0:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Master"), linear_to_db(value))
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Master"), linear_to_db(value)
)
"music_volume":
AudioManager.update_music_volume(value)
"sfx_volume":
if AudioServer.get_bus_index("SFX") >= 0:
AudioServer.set_bus_volume_db(AudioServer.get_bus_index("SFX"), linear_to_db(value))
func load_languages():
var file = FileAccess.open(LANGUAGES_JSON_PATH, FileAccess.READ)
if not file:
var error_code = FileAccess.get_open_error()
DebugManager.log_error("Could not open languages.json (Error code: %d)" % error_code, "SettingsManager")
DebugManager.log_error(
"Could not open languages.json (Error code: %d)" % error_code, "SettingsManager"
)
_load_default_languages()
return
# Check file size to prevent memory exhaustion
var file_size = file.get_length()
if file_size > MAX_JSON_FILE_SIZE:
DebugManager.log_error("Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE], "SettingsManager")
DebugManager.log_error(
"Languages.json file too large: %d bytes (max %d)" % [file_size, MAX_JSON_FILE_SIZE],
"SettingsManager"
)
file.close()
_load_default_languages()
return
@@ -187,7 +224,9 @@ func load_languages():
file.close()
if file_error != OK:
DebugManager.log_error("Error reading languages.json (Error code: %d)" % file_error, "SettingsManager")
DebugManager.log_error(
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager"
)
_load_default_languages()
return
@@ -200,7 +239,10 @@ func load_languages():
var json = JSON.new()
var parse_result = json.parse(json_string)
if parse_result != OK:
DebugManager.log_error("JSON parsing failed at line %d: %s" % [json.error_line, json.error_string], "SettingsManager")
DebugManager.log_error(
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string],
"SettingsManager"
)
_load_default_languages()
return
@@ -216,21 +258,24 @@ func load_languages():
return
languages_data = json.data
DebugManager.log_info("Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager")
DebugManager.log_info(
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
)
func _load_default_languages():
# Fallback language data when JSON file fails to load
languages_data = {
"languages": {
"en": {"name": "English", "flag": "🇺🇸"},
"ru": {"name": "Русский", "flag": "🇷🇺"}
}
"languages":
{"en": {"name": "English", "flag": "🇺🇸"}, "ru": {"name": "Русский", "flag": "🇷🇺"}}
}
DebugManager.log_info("Default languages loaded as fallback", "SettingsManager")
func get_languages_data():
return languages_data
func reset_settings_to_defaults() -> void:
DebugManager.log_info("Resetting all settings to defaults", "SettingsManager")
for key in default_settings.keys():
@@ -242,6 +287,7 @@ func reset_settings_to_defaults() -> void:
else:
DebugManager.log_error("Failed to save reset settings", "SettingsManager")
func _validate_languages_structure(data: Dictionary) -> bool:
"""Validate the structure and content of languages.json data"""
if not data.has("languages"):
@@ -260,7 +306,9 @@ func _validate_languages_structure(data: Dictionary) -> bool:
# Validate each language entry
for lang_code in languages.keys():
if not lang_code is String:
DebugManager.log_error("Language code is not a string: %s" % str(lang_code), "SettingsManager")
DebugManager.log_error(
"Language code is not a string: %s" % str(lang_code), "SettingsManager"
)
return false
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
@@ -269,12 +317,16 @@ func _validate_languages_structure(data: Dictionary) -> bool:
var lang_data = languages[lang_code]
if not lang_data is Dictionary:
DebugManager.log_error("Language data for '%s' is not a dictionary" % lang_code, "SettingsManager")
DebugManager.log_error(
"Language data for '%s' is not a dictionary" % lang_code, "SettingsManager"
)
return false
# Validate required fields in language data
if not lang_data.has("name") or not lang_data["name"] is String:
DebugManager.log_error("Language '%s' missing valid 'name' field" % lang_code, "SettingsManager")
DebugManager.log_error(
"Language '%s' missing valid 'name' field" % lang_code, "SettingsManager"
)
return false
return true

View File

@@ -36,5 +36,6 @@ const FONT_SIZE_NORMAL := 18
const FONT_SIZE_LARGE := 24
const FONT_SIZE_TITLE := 32
func _ready():
DebugManager.log_info("UIConstants loaded successfully", "UIConstants")
DebugManager.log_info("UIConstants loaded successfully", "UIConstants")

View File

@@ -1,5 +1,5 @@
extends RefCounted
class_name TestHelper
extends RefCounted
## Common test utilities and assertions for Skelly project testing
##
@@ -7,13 +7,13 @@ class_name TestHelper
## to ensure consistent test behavior across all test files.
## Test result tracking
static var tests_run := 0
static var tests_passed := 0
static var tests_failed := 0
static var tests_run = 0
static var tests_passed = 0
static var tests_failed = 0
## Performance tracking
static var test_start_time := 0.0
static var performance_data := {}
static var test_start_time = 0.0
static var performance_data = {}
## Print test section header with consistent formatting
static func print_test_header(test_name: String):

View File

@@ -12,6 +12,7 @@ var audio_manager: Node
var original_music_volume: float
var original_sfx_volume: float
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -22,6 +23,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("AudioManager")
@@ -54,11 +56,16 @@ func run_tests():
TestHelper.print_test_footer("AudioManager")
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
# Test that AudioManager has expected properties
TestHelper.assert_has_properties(audio_manager, ["music_player", "ui_click_player", "click_stream"], "AudioManager properties")
TestHelper.assert_has_properties(
audio_manager,
["music_player", "ui_click_player", "click_stream"],
"AudioManager properties"
)
# Test that AudioManager has expected methods
var expected_methods = ["update_music_volume", "play_ui_click"]
@@ -66,7 +73,10 @@ func test_basic_functionality():
# Test that AudioManager has expected constants
TestHelper.assert_true("MUSIC_PATH" in audio_manager, "MUSIC_PATH constant exists")
TestHelper.assert_true("UI_CLICK_SOUND_PATH" in audio_manager, "UI_CLICK_SOUND_PATH constant exists")
TestHelper.assert_true(
"UI_CLICK_SOUND_PATH" in audio_manager, "UI_CLICK_SOUND_PATH constant exists"
)
func test_audio_constants():
TestHelper.print_step("Audio File Constants")
@@ -76,7 +86,9 @@ func test_audio_constants():
var click_path = audio_manager.UI_CLICK_SOUND_PATH
TestHelper.assert_true(music_path.begins_with("res://"), "Music path uses res:// protocol")
TestHelper.assert_true(click_path.begins_with("res://"), "Click sound path uses res:// protocol")
TestHelper.assert_true(
click_path.begins_with("res://"), "Click sound path uses res:// protocol"
)
# Test file extensions
var valid_audio_extensions = [".wav", ".ogg", ".mp3"]
@@ -96,22 +108,39 @@ func test_audio_constants():
TestHelper.assert_true(ResourceLoader.exists(music_path), "Music file exists at path")
TestHelper.assert_true(ResourceLoader.exists(click_path), "Click sound file exists at path")
func test_audio_player_initialization():
TestHelper.print_step("Audio Player Initialization")
# Test music player initialization
TestHelper.assert_not_null(audio_manager.music_player, "Music player is initialized")
TestHelper.assert_true(audio_manager.music_player is AudioStreamPlayer, "Music player is AudioStreamPlayer type")
TestHelper.assert_true(audio_manager.music_player.get_parent() == audio_manager, "Music player is child of AudioManager")
TestHelper.assert_true(
audio_manager.music_player is AudioStreamPlayer, "Music player is AudioStreamPlayer type"
)
TestHelper.assert_true(
audio_manager.music_player.get_parent() == audio_manager,
"Music player is child of AudioManager"
)
# Test UI click player initialization
TestHelper.assert_not_null(audio_manager.ui_click_player, "UI click player is initialized")
TestHelper.assert_true(audio_manager.ui_click_player is AudioStreamPlayer, "UI click player is AudioStreamPlayer type")
TestHelper.assert_true(audio_manager.ui_click_player.get_parent() == audio_manager, "UI click player is child of AudioManager")
TestHelper.assert_true(
audio_manager.ui_click_player is AudioStreamPlayer,
"UI click player is AudioStreamPlayer type"
)
TestHelper.assert_true(
audio_manager.ui_click_player.get_parent() == audio_manager,
"UI click player is child of AudioManager"
)
# Test audio bus assignment
TestHelper.assert_equal("Music", audio_manager.music_player.bus, "Music player assigned to Music bus")
TestHelper.assert_equal("SFX", audio_manager.ui_click_player.bus, "UI click player assigned to SFX bus")
TestHelper.assert_equal(
"Music", audio_manager.music_player.bus, "Music player assigned to Music bus"
)
TestHelper.assert_equal(
"SFX", audio_manager.ui_click_player.bus, "UI click player assigned to SFX bus"
)
func test_stream_loading_and_validation():
TestHelper.print_step("Stream Loading and Validation")
@@ -119,12 +148,16 @@ func test_stream_loading_and_validation():
# Test music stream loading
TestHelper.assert_not_null(audio_manager.music_player.stream, "Music stream is loaded")
if audio_manager.music_player.stream:
TestHelper.assert_true(audio_manager.music_player.stream is AudioStream, "Music stream is AudioStream type")
TestHelper.assert_true(
audio_manager.music_player.stream is AudioStream, "Music stream is AudioStream type"
)
# Test click stream loading
TestHelper.assert_not_null(audio_manager.click_stream, "Click stream is loaded")
if audio_manager.click_stream:
TestHelper.assert_true(audio_manager.click_stream is AudioStream, "Click stream is AudioStream type")
TestHelper.assert_true(
audio_manager.click_stream is AudioStream, "Click stream is AudioStream type"
)
# Test stream resource loading directly
var loaded_music = load(audio_manager.MUSIC_PATH)
@@ -135,6 +168,7 @@ func test_stream_loading_and_validation():
TestHelper.assert_not_null(loaded_click, "Click resource loads successfully")
TestHelper.assert_true(loaded_click is AudioStream, "Loaded click sound is AudioStream type")
func test_audio_bus_configuration():
TestHelper.print_step("Audio Bus Configuration")
@@ -147,10 +181,17 @@ func test_audio_bus_configuration():
# Test player bus assignments match actual AudioServer buses
if music_bus_index >= 0:
TestHelper.assert_equal("Music", audio_manager.music_player.bus, "Music player correctly assigned to Music bus")
TestHelper.assert_equal(
"Music", audio_manager.music_player.bus, "Music player correctly assigned to Music bus"
)
if sfx_bus_index >= 0:
TestHelper.assert_equal("SFX", audio_manager.ui_click_player.bus, "UI click player correctly assigned to SFX bus")
TestHelper.assert_equal(
"SFX",
audio_manager.ui_click_player.bus,
"UI click player correctly assigned to SFX bus"
)
func test_volume_management():
TestHelper.print_step("Volume Management")
@@ -162,33 +203,49 @@ func test_volume_management():
# Test volume update to valid range
audio_manager.update_music_volume(0.5)
TestHelper.assert_float_equal(linear_to_db(0.5), audio_manager.music_player.volume_db, 0.001, "Music volume set correctly")
TestHelper.assert_float_equal(
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)
audio_manager.update_music_volume(0.0)
TestHelper.assert_equal(linear_to_db(0.0), audio_manager.music_player.volume_db, "Zero volume set correctly")
TestHelper.assert_equal(
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
# Test volume update to maximum
audio_manager.update_music_volume(1.0)
TestHelper.assert_equal(linear_to_db(1.0), audio_manager.music_player.volume_db, "Maximum volume set correctly")
TestHelper.assert_equal(
linear_to_db(1.0), audio_manager.music_player.volume_db, "Maximum volume set correctly"
)
# Test volume range validation
var test_volumes = [0.0, 0.25, 0.5, 0.75, 1.0]
for volume in test_volumes:
audio_manager.update_music_volume(volume)
var expected_db = linear_to_db(volume)
TestHelper.assert_float_equal(expected_db, audio_manager.music_player.volume_db, 0.001, "Volume %f converts correctly to dB" % volume)
TestHelper.assert_float_equal(
expected_db,
audio_manager.music_player.volume_db,
0.001,
"Volume %f converts correctly to dB" % volume
)
# Restore original volume
audio_manager.update_music_volume(original_volume)
func test_music_playback_control():
TestHelper.print_step("Music Playback Control")
# Test that music player exists and has a stream
TestHelper.assert_not_null(audio_manager.music_player, "Music player exists for playback testing")
TestHelper.assert_not_null(audio_manager.music_player.stream, "Music player has stream for playback testing")
TestHelper.assert_not_null(
audio_manager.music_player, "Music player exists for playback testing"
)
TestHelper.assert_not_null(
audio_manager.music_player.stream, "Music player has stream for playback testing"
)
# Test playback state management
# Note: We test the control methods exist and can be called safely
@@ -213,6 +270,7 @@ func test_music_playback_control():
audio_manager.update_music_volume(0.0)
TestHelper.assert_true(true, "Volume-based playback stop works")
func test_ui_sound_effects():
TestHelper.print_step("UI Sound Effects")
@@ -225,7 +283,11 @@ func test_ui_sound_effects():
audio_manager.play_ui_click()
# Verify click stream was assigned to player
TestHelper.assert_equal(audio_manager.click_stream, audio_manager.ui_click_player.stream, "Click stream assigned to player")
TestHelper.assert_equal(
audio_manager.click_stream,
audio_manager.ui_click_player.stream,
"Click stream assigned to player"
)
# Test multiple rapid clicks (should not cause errors)
for i in range(3):
@@ -239,6 +301,7 @@ func test_ui_sound_effects():
TestHelper.assert_true(true, "Null click stream handled safely")
audio_manager.click_stream = backup_stream
func test_stream_loop_configuration():
TestHelper.print_step("Stream Loop Configuration")
@@ -250,7 +313,11 @@ func test_stream_loop_configuration():
var has_loop_mode = "loop_mode" in music_stream
TestHelper.assert_true(has_loop_mode, "WAV stream has loop_mode property")
if has_loop_mode:
TestHelper.assert_equal(AudioStreamWAV.LOOP_FORWARD, music_stream.loop_mode, "WAV stream set to forward loop")
TestHelper.assert_equal(
AudioStreamWAV.LOOP_FORWARD,
music_stream.loop_mode,
"WAV stream set to forward loop"
)
elif music_stream is AudioStreamOggVorbis:
# For OGG files, check loop property
var has_loop = "loop" in music_stream
@@ -261,6 +328,7 @@ func test_stream_loop_configuration():
# Test loop configuration for different stream types
TestHelper.assert_true(true, "Stream loop configuration tested based on type")
func test_error_handling():
TestHelper.print_step("Error Handling")
@@ -268,11 +336,17 @@ func test_error_handling():
# We can't actually break the resources in tests, but we can verify error handling patterns
# Test that AudioManager initializes even with potential issues
TestHelper.assert_not_null(audio_manager, "AudioManager initializes despite potential resource issues")
TestHelper.assert_not_null(
audio_manager, "AudioManager initializes despite potential resource issues"
)
# Test that players are still created even if streams fail to load
TestHelper.assert_not_null(audio_manager.music_player, "Music player created regardless of stream loading")
TestHelper.assert_not_null(audio_manager.ui_click_player, "UI click player created regardless of stream loading")
TestHelper.assert_not_null(
audio_manager.music_player, "Music player created regardless of stream loading"
)
TestHelper.assert_not_null(
audio_manager.ui_click_player, "UI click player created regardless of stream loading"
)
# Test null stream handling in play_ui_click
var original_click_stream = audio_manager.click_stream
@@ -292,6 +366,7 @@ func test_error_handling():
audio_manager.update_music_volume(1.0)
TestHelper.assert_true(true, "Maximum volume handled safely")
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -303,4 +378,4 @@ func cleanup_tests():
# Update AudioManager to original settings
audio_manager.update_music_volume(original_music_volume)
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")

View File

@@ -10,6 +10,7 @@ var game_manager: Node
var original_scene: Node
var test_scenes_created: Array[String] = []
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -20,6 +21,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("GameManager")
@@ -49,20 +51,34 @@ func run_tests():
TestHelper.print_test_footer("GameManager")
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
# Test that GameManager has expected properties
TestHelper.assert_has_properties(game_manager, ["pending_gameplay_mode", "is_changing_scene"], "GameManager properties")
TestHelper.assert_has_properties(
game_manager, ["pending_gameplay_mode", "is_changing_scene"], "GameManager properties"
)
# Test that GameManager has expected methods
var expected_methods = ["start_new_game", "continue_game", "start_match3_game", "start_clickomania_game", "start_game_with_mode", "save_game", "exit_to_main_menu"]
var expected_methods = [
"start_new_game",
"continue_game",
"start_match3_game",
"start_clickomania_game",
"start_game_with_mode",
"save_game",
"exit_to_main_menu"
]
TestHelper.assert_has_methods(game_manager, expected_methods, "GameManager methods")
# Test initial state
TestHelper.assert_equal("match3", game_manager.pending_gameplay_mode, "Default pending gameplay mode")
TestHelper.assert_equal(
"match3", game_manager.pending_gameplay_mode, "Default pending gameplay mode"
)
TestHelper.assert_false(game_manager.is_changing_scene, "Initial scene change flag")
func test_scene_constants():
TestHelper.print_step("Scene Path Constants")
@@ -83,6 +99,7 @@ func test_scene_constants():
TestHelper.assert_true(ResourceLoader.exists(game_path), "Game scene file exists at path")
TestHelper.assert_true(ResourceLoader.exists(main_path), "Main scene file exists at path")
func test_input_validation():
TestHelper.print_step("Input Validation")
@@ -92,24 +109,41 @@ func test_input_validation():
# Test empty string validation
game_manager.start_game_with_mode("")
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Empty string mode rejected")
TestHelper.assert_false(game_manager.is_changing_scene, "Scene change flag unchanged after empty mode")
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Empty string mode rejected"
)
TestHelper.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after empty mode"
)
# 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
# The function properly validates empty strings instead
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty string test")
TestHelper.assert_false(game_manager.is_changing_scene, "Scene change flag unchanged after validation tests")
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty string test"
)
TestHelper.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after validation tests"
)
# Test invalid mode validation
game_manager.start_game_with_mode("invalid_mode")
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Invalid mode rejected")
TestHelper.assert_false(game_manager.is_changing_scene, "Scene change flag unchanged after invalid mode")
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Invalid mode rejected"
)
TestHelper.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after invalid mode"
)
# Test case sensitivity
game_manager.start_game_with_mode("MATCH3")
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Case-sensitive mode validation")
TestHelper.assert_false(game_manager.is_changing_scene, "Scene change flag unchanged after wrong case")
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Case-sensitive mode validation"
)
TestHelper.assert_false(
game_manager.is_changing_scene, "Scene change flag unchanged after wrong case"
)
func test_race_condition_protection():
TestHelper.print_step("Race Condition Protection")
@@ -122,16 +156,21 @@ func test_race_condition_protection():
game_manager.start_game_with_mode("match3")
# Verify second request was rejected
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Concurrent scene change blocked")
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Concurrent scene change blocked"
)
TestHelper.assert_true(game_manager.is_changing_scene, "Scene change flag preserved")
# Test exit to main menu during scene change
game_manager.exit_to_main_menu()
TestHelper.assert_true(game_manager.is_changing_scene, "Exit request blocked during scene change")
TestHelper.assert_true(
game_manager.is_changing_scene, "Exit request blocked during scene change"
)
# Reset state
game_manager.is_changing_scene = false
func test_gameplay_mode_validation():
TestHelper.print_step("Gameplay Mode Validation")
@@ -152,6 +191,7 @@ func test_gameplay_mode_validation():
var test_mode_invalid = not (mode in ["match3", "clickomania"])
TestHelper.assert_true(test_mode_invalid, "Invalid mode rejected: " + mode)
func test_scene_transition_safety():
TestHelper.print_step("Scene Transition Safety")
@@ -171,6 +211,7 @@ func test_scene_transition_safety():
# Test that current scene exists
TestHelper.assert_not_null(current_scene, "Current scene exists")
func test_error_handling():
TestHelper.print_step("Error Handling")
@@ -184,12 +225,23 @@ func test_error_handling():
# Verify state preservation after invalid inputs
game_manager.start_game_with_mode("")
TestHelper.assert_equal(original_changing, game_manager.is_changing_scene, "State preserved after empty mode error")
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty mode error")
TestHelper.assert_equal(
original_changing, game_manager.is_changing_scene, "State preserved after empty mode error"
)
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after empty mode error"
)
game_manager.start_game_with_mode("invalid")
TestHelper.assert_equal(original_changing, game_manager.is_changing_scene, "State preserved after invalid mode error")
TestHelper.assert_equal(original_mode, game_manager.pending_gameplay_mode, "Mode preserved after invalid mode error")
TestHelper.assert_equal(
original_changing,
game_manager.is_changing_scene,
"State preserved after invalid mode error"
)
TestHelper.assert_equal(
original_mode, game_manager.pending_gameplay_mode, "Mode preserved after invalid mode error"
)
func test_scene_method_validation():
TestHelper.print_step("Scene Method Validation")
@@ -210,6 +262,7 @@ func test_scene_method_validation():
# Clean up mock scene
mock_scene.queue_free()
func test_pending_mode_management():
TestHelper.print_step("Pending Mode Management")
@@ -222,7 +275,9 @@ func test_pending_mode_management():
if test_mode in ["match3", "clickomania"]:
# This simulates what would happen in start_game_with_mode
game_manager.pending_gameplay_mode = test_mode
TestHelper.assert_equal(test_mode, game_manager.pending_gameplay_mode, "Pending mode set correctly")
TestHelper.assert_equal(
test_mode, game_manager.pending_gameplay_mode, "Pending mode set correctly"
)
# Test mode preservation during errors
game_manager.pending_gameplay_mode = "match3"
@@ -230,11 +285,16 @@ func test_pending_mode_management():
# Attempt invalid operation (this should not change pending mode)
# The actual start_game_with_mode with invalid input won't change pending_gameplay_mode
TestHelper.assert_equal(preserved_mode, game_manager.pending_gameplay_mode, "Mode preserved during invalid operations")
TestHelper.assert_equal(
preserved_mode,
game_manager.pending_gameplay_mode,
"Mode preserved during invalid operations"
)
# Restore original mode
game_manager.pending_gameplay_mode = original_mode
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -248,4 +308,4 @@ func cleanup_tests():
# Note: Can't actually delete from res:// in tests, just track for manual cleanup
pass
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")

View File

@@ -4,12 +4,14 @@ extends SceneTree
# This script validates all log levels, filtering, and formatting functionality
# Usage: Add to scene or autoload temporarily to run tests
func _initialize():
# Wait a frame for debug_manager to initialize
await process_frame
test_logging_system()
quit()
func test_logging_system():
print("=== Starting Logging System Tests ===")
@@ -30,6 +32,7 @@ func test_logging_system():
print("=== Logging System Tests Complete ===")
func test_basic_logging(debug_manager):
print("\n--- Test 1: Basic Log Level Functionality ---")
@@ -43,6 +46,7 @@ func test_basic_logging(debug_manager):
debug_manager.log_error("ERROR: This error should appear")
debug_manager.log_fatal("FATAL: This fatal error should appear")
func test_log_level_filtering(debug_manager):
print("\n--- Test 2: Log Level Filtering ---")
@@ -64,6 +68,7 @@ func test_log_level_filtering(debug_manager):
# Reset to INFO for remaining tests
debug_manager.set_log_level(debug_manager.LogLevel.INFO)
func test_category_logging(debug_manager):
print("\n--- Test 3: Category Functionality ---")
@@ -73,6 +78,7 @@ func test_category_logging(debug_manager):
debug_manager.log_warn("Warning with VALIDATION category", "VALIDATION")
debug_manager.log_error("Error with SYSTEM category", "SYSTEM")
func test_debug_mode_integration(debug_manager):
print("\n--- Test 4: Debug Mode Integration ---")
@@ -99,6 +105,7 @@ func test_debug_mode_integration(debug_manager):
debug_manager.set_debug_enabled(original_debug_state)
debug_manager.set_log_level(debug_manager.LogLevel.INFO)
# Helper function to validate log level enum values
func test_log_level_enum(debug_manager):
print("\n--- Log Level Enum Values ---")

View File

@@ -10,6 +10,7 @@ var match3_scene: PackedScene
var match3_instance: Node2D
var test_viewport: SubViewport
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -20,6 +21,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Match3 Gameplay")
@@ -43,6 +45,7 @@ func run_tests():
TestHelper.print_test_footer("Match3 Gameplay")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
@@ -65,6 +68,7 @@ func setup_test_environment():
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
@@ -73,17 +77,26 @@ func test_basic_functionality():
return
# Test that Match3 has expected properties
var expected_properties = ["GRID_SIZE", "TILE_TYPES", "grid", "current_state", "selected_tile", "cursor_position"]
var expected_properties = [
"GRID_SIZE", "TILE_TYPES", "grid", "current_state", "selected_tile", "cursor_position"
]
for prop in expected_properties:
TestHelper.assert_true(prop in match3_instance, "Match3 has property: " + prop)
# Test that Match3 has expected methods
var expected_methods = ["_has_match_at", "_check_for_matches", "_get_match_line", "_clear_matches"]
var expected_methods = [
"_has_match_at", "_check_for_matches", "_get_match_line", "_clear_matches"
]
TestHelper.assert_has_methods(match3_instance, expected_methods, "Match3 gameplay methods")
# Test signals
TestHelper.assert_true(match3_instance.has_signal("score_changed"), "Match3 has score_changed signal")
TestHelper.assert_true(match3_instance.has_signal("grid_state_loaded"), "Match3 has grid_state_loaded signal")
TestHelper.assert_true(
match3_instance.has_signal("score_changed"), "Match3 has score_changed signal"
)
TestHelper.assert_true(
match3_instance.has_signal("grid_state_loaded"), "Match3 has grid_state_loaded signal"
)
func test_constants_and_safety_limits():
TestHelper.print_step("Constants and Safety Limits")
@@ -94,26 +107,52 @@ func test_constants_and_safety_limits():
# Test safety constants exist
TestHelper.assert_true("MAX_GRID_SIZE" in match3_instance, "MAX_GRID_SIZE constant exists")
TestHelper.assert_true("MAX_TILE_TYPES" in match3_instance, "MAX_TILE_TYPES constant exists")
TestHelper.assert_true("MAX_CASCADE_ITERATIONS" in match3_instance, "MAX_CASCADE_ITERATIONS constant exists")
TestHelper.assert_true(
"MAX_CASCADE_ITERATIONS" in match3_instance, "MAX_CASCADE_ITERATIONS constant exists"
)
TestHelper.assert_true("MIN_GRID_SIZE" in match3_instance, "MIN_GRID_SIZE constant exists")
TestHelper.assert_true("MIN_TILE_TYPES" in match3_instance, "MIN_TILE_TYPES constant exists")
# Test safety limit values are reasonable
TestHelper.assert_equal(15, match3_instance.MAX_GRID_SIZE, "MAX_GRID_SIZE is reasonable")
TestHelper.assert_equal(10, match3_instance.MAX_TILE_TYPES, "MAX_TILE_TYPES is reasonable")
TestHelper.assert_equal(20, match3_instance.MAX_CASCADE_ITERATIONS, "MAX_CASCADE_ITERATIONS prevents infinite loops")
TestHelper.assert_equal(
20, match3_instance.MAX_CASCADE_ITERATIONS, "MAX_CASCADE_ITERATIONS prevents infinite loops"
)
TestHelper.assert_equal(3, match3_instance.MIN_GRID_SIZE, "MIN_GRID_SIZE is reasonable")
TestHelper.assert_equal(3, match3_instance.MIN_TILE_TYPES, "MIN_TILE_TYPES is reasonable")
# Test current values are within safety limits
TestHelper.assert_in_range(match3_instance.GRID_SIZE.x, match3_instance.MIN_GRID_SIZE, match3_instance.MAX_GRID_SIZE, "Grid width within safety limits")
TestHelper.assert_in_range(match3_instance.GRID_SIZE.y, match3_instance.MIN_GRID_SIZE, match3_instance.MAX_GRID_SIZE, "Grid height within safety limits")
TestHelper.assert_in_range(match3_instance.TILE_TYPES, match3_instance.MIN_TILE_TYPES, match3_instance.MAX_TILE_TYPES, "Tile types within safety limits")
TestHelper.assert_in_range(
match3_instance.GRID_SIZE.x,
match3_instance.MIN_GRID_SIZE,
match3_instance.MAX_GRID_SIZE,
"Grid width within safety limits"
)
TestHelper.assert_in_range(
match3_instance.GRID_SIZE.y,
match3_instance.MIN_GRID_SIZE,
match3_instance.MAX_GRID_SIZE,
"Grid height within safety limits"
)
TestHelper.assert_in_range(
match3_instance.TILE_TYPES,
match3_instance.MIN_TILE_TYPES,
match3_instance.MAX_TILE_TYPES,
"Tile types within safety limits"
)
# Test timing constants
TestHelper.assert_true("CASCADE_WAIT_TIME" in match3_instance, "CASCADE_WAIT_TIME constant exists")
TestHelper.assert_true("SWAP_ANIMATION_TIME" in match3_instance, "SWAP_ANIMATION_TIME constant exists")
TestHelper.assert_true("TILE_DROP_WAIT_TIME" in match3_instance, "TILE_DROP_WAIT_TIME constant exists")
TestHelper.assert_true(
"CASCADE_WAIT_TIME" in match3_instance, "CASCADE_WAIT_TIME constant exists"
)
TestHelper.assert_true(
"SWAP_ANIMATION_TIME" in match3_instance, "SWAP_ANIMATION_TIME constant exists"
)
TestHelper.assert_true(
"TILE_DROP_WAIT_TIME" in match3_instance, "TILE_DROP_WAIT_TIME constant exists"
)
func test_grid_initialization():
TestHelper.print_step("Grid Initialization")
@@ -134,7 +173,9 @@ func test_grid_initialization():
# Test each row has correct width
for y in range(match3_instance.grid.size()):
if y < expected_height:
TestHelper.assert_equal(expected_width, match3_instance.grid[y].size(), "Grid row %d has correct width" % y)
TestHelper.assert_equal(
expected_width, match3_instance.grid[y].size(), "Grid row %d has correct width" % y
)
# Test tiles are properly instantiated
var tile_count = 0
@@ -147,15 +188,25 @@ func test_grid_initialization():
if tile and is_instance_valid(tile):
valid_tile_count += 1
TestHelper.assert_true("tile_type" in tile, "Tile at (%d,%d) has tile_type property" % [x, y])
TestHelper.assert_true("grid_position" in tile, "Tile at (%d,%d) has grid_position property" % [x, y])
TestHelper.assert_true(
"tile_type" in tile, "Tile at (%d,%d) has tile_type property" % [x, y]
)
TestHelper.assert_true(
"grid_position" in tile, "Tile at (%d,%d) has grid_position property" % [x, y]
)
# Test tile type is within valid range
if "tile_type" in tile:
TestHelper.assert_in_range(tile.tile_type, 0, match3_instance.TILE_TYPES - 1, "Tile type in valid range")
TestHelper.assert_in_range(
tile.tile_type,
0,
match3_instance.TILE_TYPES - 1,
"Tile type in valid range"
)
TestHelper.assert_equal(tile_count, valid_tile_count, "All grid positions have valid tiles")
func test_grid_layout_calculation():
TestHelper.print_step("Grid Layout Calculation")
@@ -164,7 +215,9 @@ func test_grid_layout_calculation():
# Test tile size calculation
TestHelper.assert_true(match3_instance.tile_size > 0, "Tile size is positive")
TestHelper.assert_true(match3_instance.tile_size <= 200, "Tile size is reasonable (not too large)")
TestHelper.assert_true(
match3_instance.tile_size <= 200, "Tile size is reasonable (not too large)"
)
# Test grid offset
TestHelper.assert_not_null(match3_instance.grid_offset, "Grid offset is set")
@@ -173,10 +226,13 @@ func test_grid_layout_calculation():
# Test layout constants
TestHelper.assert_equal(0.8, match3_instance.SCREEN_WIDTH_USAGE, "Screen width usage constant")
TestHelper.assert_equal(0.7, match3_instance.SCREEN_HEIGHT_USAGE, "Screen height usage constant")
TestHelper.assert_equal(
0.7, match3_instance.SCREEN_HEIGHT_USAGE, "Screen height usage constant"
)
TestHelper.assert_equal(50.0, match3_instance.GRID_LEFT_MARGIN, "Grid left margin constant")
TestHelper.assert_equal(50.0, match3_instance.GRID_TOP_MARGIN, "Grid top margin constant")
func test_state_management():
TestHelper.print_step("State Management")
@@ -196,7 +252,10 @@ func test_state_management():
# Test instance ID for debugging
TestHelper.assert_true("instance_id" in match3_instance, "Instance ID exists for debugging")
TestHelper.assert_true(match3_instance.instance_id.begins_with("Match3_"), "Instance ID has correct format")
TestHelper.assert_true(
match3_instance.instance_id.begins_with("Match3_"), "Instance ID has correct format"
)
func test_match_detection():
TestHelper.print_step("Match Detection Logic")
@@ -205,9 +264,15 @@ func test_match_detection():
return
# Test match detection methods exist and can be called safely
TestHelper.assert_true(match3_instance.has_method("_has_match_at"), "_has_match_at method exists")
TestHelper.assert_true(match3_instance.has_method("_check_for_matches"), "_check_for_matches method exists")
TestHelper.assert_true(match3_instance.has_method("_get_match_line"), "_get_match_line method exists")
TestHelper.assert_true(
match3_instance.has_method("_has_match_at"), "_has_match_at method exists"
)
TestHelper.assert_true(
match3_instance.has_method("_check_for_matches"), "_check_for_matches method exists"
)
TestHelper.assert_true(
match3_instance.has_method("_get_match_line"), "_get_match_line method exists"
)
# Test boundary checking with invalid positions
var invalid_positions = [
@@ -227,7 +292,10 @@ func test_match_detection():
for x in range(min(3, match3_instance.GRID_SIZE.x)):
var pos = Vector2i(x, y)
var result = match3_instance._has_match_at(pos)
TestHelper.assert_true(result is bool, "Valid position (%d,%d) returns boolean" % [x, y])
TestHelper.assert_true(
result is bool, "Valid position (%d,%d) returns boolean" % [x, y]
)
func test_scoring_system():
TestHelper.print_step("Scoring System")
@@ -239,18 +307,17 @@ func test_scoring_system():
# The scoring system uses: 3 gems = 3 points, 4+ gems = n + (n-2) points
# Test that the match3 instance can handle scoring (indirectly through clearing matches)
TestHelper.assert_true(match3_instance.has_method("_clear_matches"), "Scoring system method exists")
TestHelper.assert_true(
match3_instance.has_method("_clear_matches"), "Scoring system method exists"
)
# Test that score_changed signal exists
TestHelper.assert_true(match3_instance.has_signal("score_changed"), "Score changed signal exists")
TestHelper.assert_true(
match3_instance.has_signal("score_changed"), "Score changed signal exists"
)
# Test scoring formula logic (based on the documented formula)
var test_scores = {
3: 3, # 3 gems = exactly 3 points
4: 6, # 4 gems = 4 + (4-2) = 6 points
5: 8, # 5 gems = 5 + (5-2) = 8 points
6: 10 # 6 gems = 6 + (6-2) = 10 points
}
var test_scores = {3: 3, 4: 6, 5: 8, 6: 10} # 3 gems = exactly 3 points # 4 gems = 4 + (4-2) = 6 points # 5 gems = 5 + (5-2) = 8 points # 6 gems = 6 + (6-2) = 10 points
for match_size in test_scores.keys():
var expected_score = test_scores[match_size]
@@ -260,7 +327,10 @@ func test_scoring_system():
else:
calculated_score = match_size + max(0, match_size - 2)
TestHelper.assert_equal(expected_score, calculated_score, "Scoring formula correct for %d gems" % match_size)
TestHelper.assert_equal(
expected_score, calculated_score, "Scoring formula correct for %d gems" % match_size
)
func test_input_validation():
TestHelper.print_step("Input Validation")
@@ -270,16 +340,25 @@ func test_input_validation():
# Test cursor position bounds
TestHelper.assert_not_null(match3_instance.cursor_position, "Cursor position is initialized")
TestHelper.assert_true(match3_instance.cursor_position is Vector2i, "Cursor position is Vector2i type")
TestHelper.assert_true(
match3_instance.cursor_position is Vector2i, "Cursor position is Vector2i type"
)
# Test keyboard navigation flag
TestHelper.assert_true("keyboard_navigation_enabled" in match3_instance, "Keyboard navigation flag exists")
TestHelper.assert_true(match3_instance.keyboard_navigation_enabled is bool, "Keyboard navigation flag is boolean")
TestHelper.assert_true(
"keyboard_navigation_enabled" in match3_instance, "Keyboard navigation flag exists"
)
TestHelper.assert_true(
match3_instance.keyboard_navigation_enabled is bool, "Keyboard navigation flag is boolean"
)
# Test selected tile safety
# selected_tile can be null initially, which is valid
if match3_instance.selected_tile:
TestHelper.assert_true(is_instance_valid(match3_instance.selected_tile), "Selected tile is valid if not null")
TestHelper.assert_true(
is_instance_valid(match3_instance.selected_tile), "Selected tile is valid if not null"
)
func test_memory_safety():
TestHelper.print_step("Memory Safety")
@@ -288,23 +367,33 @@ func test_memory_safety():
return
# Test grid integrity validation
TestHelper.assert_true(match3_instance.has_method("_validate_grid_integrity"), "Grid integrity validation method exists")
TestHelper.assert_true(
match3_instance.has_method("_validate_grid_integrity"),
"Grid integrity validation method exists"
)
# Test tile validity checking
for y in range(min(3, match3_instance.grid.size())):
for x in range(min(3, match3_instance.grid[y].size())):
var tile = match3_instance.grid[y][x]
if tile:
TestHelper.assert_true(is_instance_valid(tile), "Grid tile at (%d,%d) is valid instance" % [x, y])
TestHelper.assert_true(tile.get_parent() == match3_instance, "Tile properly parented to Match3")
TestHelper.assert_true(
is_instance_valid(tile), "Grid tile at (%d,%d) is valid instance" % [x, y]
)
TestHelper.assert_true(
tile.get_parent() == match3_instance, "Tile properly parented to Match3"
)
# Test position validation
TestHelper.assert_true(match3_instance.has_method("_is_valid_grid_position"), "Position validation method exists")
TestHelper.assert_true(
match3_instance.has_method("_is_valid_grid_position"), "Position validation method exists"
)
# Test safe tile access patterns exist
# The Match3 code uses comprehensive bounds checking and null validation
TestHelper.assert_true(true, "Memory safety patterns implemented in Match3 code")
func test_performance_requirements():
TestHelper.print_step("Performance Requirements")
@@ -316,12 +405,22 @@ func test_performance_requirements():
TestHelper.assert_true(total_tiles <= 225, "Total tiles within performance limit (15x15=225)")
# Test cascade iteration limit prevents infinite loops
TestHelper.assert_equal(20, match3_instance.MAX_CASCADE_ITERATIONS, "Cascade iteration limit prevents infinite loops")
TestHelper.assert_equal(
20,
match3_instance.MAX_CASCADE_ITERATIONS,
"Cascade iteration limit prevents infinite loops"
)
# Test timing constants are reasonable for 60fps gameplay
TestHelper.assert_true(match3_instance.CASCADE_WAIT_TIME >= 0.05, "Cascade wait time allows for smooth animation")
TestHelper.assert_true(match3_instance.SWAP_ANIMATION_TIME <= 0.5, "Swap animation time is responsive")
TestHelper.assert_true(match3_instance.TILE_DROP_WAIT_TIME <= 0.3, "Tile drop wait time is responsive")
TestHelper.assert_true(
match3_instance.CASCADE_WAIT_TIME >= 0.05, "Cascade wait time allows for smooth animation"
)
TestHelper.assert_true(
match3_instance.SWAP_ANIMATION_TIME <= 0.5, "Swap animation time is responsive"
)
TestHelper.assert_true(
match3_instance.TILE_DROP_WAIT_TIME <= 0.3, "Tile drop wait time is responsive"
)
# Test grid initialization performance
TestHelper.start_performance_test("grid_access")
@@ -332,6 +431,7 @@ func test_performance_requirements():
var tile_type = tile.tile_type
TestHelper.end_performance_test("grid_access", 10.0, "Grid access performance within limits")
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -346,4 +446,4 @@ func cleanup_tests():
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")

View File

@@ -5,6 +5,7 @@ extends SceneTree
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -15,11 +16,13 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Migration Compatibility")
test_migration_compatibility()
TestHelper.print_test_footer("Migration Compatibility")
func test_migration_compatibility():
TestHelper.print_step("Old Save File Compatibility")
var old_save_data = {
@@ -28,7 +31,8 @@ func test_migration_compatibility():
"current_score": 0,
"games_played": 5,
"total_score": 450,
"grid_state": {
"grid_state":
{
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
@@ -53,7 +57,9 @@ func test_migration_compatibility():
print("New checksum format: %s" % new_checksum)
# The checksums should be different (old system broken)
TestHelper.assert_not_equal(old_checksum, new_checksum, "Old and new checksum formats should be different")
TestHelper.assert_not_equal(
old_checksum, new_checksum, "Old and new checksum formats should be different"
)
print("Old checksum: %s" % old_checksum)
print("New checksum: %s" % new_checksum)
@@ -71,7 +77,11 @@ func test_migration_compatibility():
var second_checksum = _calculate_new_checksum(reloaded_data)
TestHelper.assert_equal(first_checksum, second_checksum, "New system should be self-consistent across save/load cycles")
TestHelper.assert_equal(
first_checksum,
second_checksum,
"New system should be self-consistent across save/load cycles"
)
print("Consistent checksum: %s" % first_checksum)
TestHelper.print_step("Migration Strategy Verification")
@@ -80,6 +90,7 @@ func test_migration_compatibility():
print("✓ Files with version < current: Recalculate checksum after migration")
print("✓ Files with current version: Use new checksum validation")
# Simulate old checksum calculation (before the fix)
func _calculate_old_checksum(data: Dictionary) -> String:
# Old broken checksum (without normalization)
@@ -88,6 +99,7 @@ func _calculate_old_checksum(data: Dictionary) -> String:
var old_string = JSON.stringify(data_copy) # Direct JSON without normalization
return str(old_string.hash())
# Implement new checksum calculation (the fixed version with normalization)
func _calculate_new_checksum(data: Dictionary) -> String:
# Calculate deterministic checksum EXCLUDING the checksum field itself
@@ -97,6 +109,7 @@ func _calculate_new_checksum(data: Dictionary) -> String:
var checksum_string = _create_deterministic_string(data_copy)
return str(checksum_string.hash())
func _create_deterministic_string(data: Dictionary) -> String:
# Create a deterministic string representation by processing keys in sorted order
var keys = data.keys()
@@ -116,6 +129,7 @@ func _create_deterministic_string(data: Dictionary) -> String:
parts.append(key_str + ":" + value_str)
return "{" + ",".join(parts) + "}"
func _create_deterministic_array_string(arr: Array) -> String:
var parts = []
for item in arr:
@@ -128,6 +142,7 @@ func _create_deterministic_array_string(arr: Array) -> String:
parts.append(_normalize_value_for_checksum(item))
return "[" + ",".join(parts) + "]"
func _normalize_value_for_checksum(value) -> String:
"""
CRITICAL FIX: Normalize values for consistent checksum calculation

View File

@@ -10,6 +10,7 @@ const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var discovered_scenes: Array[String] = []
var validation_results: Dictionary = {}
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -20,6 +21,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Scene Validation")
@@ -34,14 +36,12 @@ func run_tests():
TestHelper.print_test_footer("Scene Validation")
func test_scene_discovery():
TestHelper.print_step("Scene Discovery")
# Discover scenes in key directories
var scene_directories = [
"res://scenes/",
"res://examples/"
]
var scene_directories = ["res://scenes/", "res://examples/"]
for directory in scene_directories:
discover_scenes_in_directory(directory)
@@ -53,6 +53,7 @@ func test_scene_discovery():
for scene_path in discovered_scenes:
print(" - %s" % scene_path)
func discover_scenes_in_directory(directory_path: String):
var dir = DirAccess.open(directory_path)
if not dir:
@@ -74,12 +75,14 @@ func discover_scenes_in_directory(directory_path: String):
file_name = dir.get_next()
func test_scene_loading():
TestHelper.print_step("Scene Loading Validation")
for scene_path in discovered_scenes:
validate_scene_loading(scene_path)
func validate_scene_loading(scene_path: String):
var scene_name = scene_path.get_file()
@@ -104,6 +107,7 @@ func validate_scene_loading(scene_path: String):
validation_results[scene_path] = "Loading successful"
TestHelper.assert_true(true, "%s - Scene loads successfully" % scene_name)
func test_scene_instantiation():
TestHelper.print_step("Scene Instantiation Testing")
@@ -112,6 +116,7 @@ func test_scene_instantiation():
if validation_results.get(scene_path, "") == "Loading successful":
validate_scene_instantiation(scene_path)
func validate_scene_instantiation(scene_path: String):
var scene_name = scene_path.get_file()
@@ -126,7 +131,9 @@ func validate_scene_instantiation(scene_path: String):
return
# Validate the instance
TestHelper.assert_not_null(scene_instance, "%s - Scene instantiation creates valid node" % scene_name)
TestHelper.assert_not_null(
scene_instance, "%s - Scene instantiation creates valid node" % scene_name
)
# Clean up the instance
scene_instance.queue_free()
@@ -135,6 +142,7 @@ func validate_scene_instantiation(scene_path: String):
if validation_results[scene_path] == "Loading successful":
validation_results[scene_path] = "Full validation successful"
func test_critical_scenes():
TestHelper.print_step("Critical Scene Validation")
@@ -149,11 +157,15 @@ func test_critical_scenes():
for scene_path in critical_scenes:
if scene_path in discovered_scenes:
var status = validation_results.get(scene_path, "Unknown")
TestHelper.assert_equal("Full validation successful", status,
"Critical scene %s must pass all validation" % scene_path.get_file())
TestHelper.assert_equal(
"Full validation successful",
status,
"Critical scene %s must pass all validation" % scene_path.get_file()
)
else:
TestHelper.assert_false(true, "Critical scene missing: %s" % scene_path)
func print_validation_summary():
print("\n=== Scene Validation Summary ===")
@@ -176,4 +188,4 @@ func print_validation_summary():
if failed_scenes == 0:
print("✅ All scenes passed validation!")
else:
print("%d scene(s) failed validation" % failed_scenes)
print("%d scene(s) failed validation" % failed_scenes)

View File

@@ -10,6 +10,7 @@ var settings_manager: Node
var original_settings: Dictionary
var temp_files: Array[String] = []
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -20,6 +21,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("SettingsManager")
@@ -49,26 +51,36 @@ func run_tests():
TestHelper.print_test_footer("SettingsManager")
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
# Test that SettingsManager has expected properties
TestHelper.assert_has_properties(settings_manager, ["settings", "default_settings", "languages_data"], "SettingsManager properties")
TestHelper.assert_has_properties(
settings_manager,
["settings", "default_settings", "languages_data"],
"SettingsManager properties"
)
# Test that SettingsManager has expected methods
var expected_methods = ["get_setting", "set_setting", "save_settings", "load_settings", "reset_settings_to_defaults"]
var expected_methods = [
"get_setting", "set_setting", "save_settings", "load_settings", "reset_settings_to_defaults"
]
TestHelper.assert_has_methods(settings_manager, expected_methods, "SettingsManager methods")
# Test default settings structure
var expected_defaults = ["master_volume", "music_volume", "sfx_volume", "language"]
for key in expected_defaults:
TestHelper.assert_has_key(settings_manager.default_settings, key, "Default setting key: " + key)
TestHelper.assert_has_key(
settings_manager.default_settings, key, "Default setting key: " + key
)
# Test getting settings
var master_volume = settings_manager.get_setting("master_volume")
TestHelper.assert_not_null(master_volume, "Can get master_volume setting")
TestHelper.assert_true(master_volume is float, "master_volume is float type")
func test_input_validation_security():
TestHelper.print_step("Input Validation Security")
@@ -94,7 +106,9 @@ func test_input_validation_security():
# Test valid volume range
var valid_volume = settings_manager.set_setting("master_volume", 0.5)
TestHelper.assert_true(valid_volume, "Valid volume values accepted")
TestHelper.assert_equal(0.5, settings_manager.get_setting("master_volume"), "Volume value set correctly")
TestHelper.assert_equal(
0.5, settings_manager.get_setting("master_volume"), "Volume value set correctly"
)
# Test string length validation for language
var long_language = "a".repeat(20) # Exceeds MAX_SETTING_STRING_LENGTH
@@ -109,11 +123,14 @@ func test_input_validation_security():
var valid_lang = settings_manager.set_setting("language", "en")
TestHelper.assert_true(valid_lang, "Valid language codes accepted")
func test_file_io_security():
TestHelper.print_step("File I/O Security")
# Test file size limits by creating oversized config
var oversized_config_path = TestHelper.create_temp_file("oversized_settings.cfg", "x".repeat(70000)) # > 64KB
var oversized_config_path = TestHelper.create_temp_file(
"oversized_settings.cfg", "x".repeat(70000)
) # > 64KB
temp_files.append(oversized_config_path)
# Test that normal save/load operations work
@@ -127,21 +144,30 @@ func test_file_io_security():
# Test that settings file exists after save
TestHelper.assert_file_exists("user://settings.cfg", "Settings file created after save")
func test_json_parsing_security():
TestHelper.print_step("JSON Parsing Security")
# Create invalid languages.json for testing
var invalid_json_path = TestHelper.create_temp_file("invalid_languages.json", TestHelper.create_invalid_json())
var invalid_json_path = TestHelper.create_temp_file(
"invalid_languages.json", TestHelper.create_invalid_json()
)
temp_files.append(invalid_json_path)
# Create oversized JSON file
var large_json_content = '{"languages": {"' + "x".repeat(70000) + '": "test"}}'
var oversized_json_path = TestHelper.create_temp_file("oversized_languages.json", large_json_content)
var oversized_json_path = TestHelper.create_temp_file(
"oversized_languages.json", large_json_content
)
temp_files.append(oversized_json_path)
# Test that SettingsManager handles invalid JSON gracefully
# This should fall back to default languages
TestHelper.assert_true(settings_manager.languages_data.has("languages"), "Default languages loaded on JSON parse failure")
TestHelper.assert_true(
settings_manager.languages_data.has("languages"),
"Default languages loaded on JSON parse failure"
)
func test_language_validation():
TestHelper.print_step("Language Validation")
@@ -164,6 +190,7 @@ func test_language_validation():
var null_result = settings_manager.set_setting("language", null)
TestHelper.assert_false(null_result, "Null language rejected")
func test_volume_validation():
TestHelper.print_step("Volume Validation")
@@ -171,16 +198,29 @@ func test_volume_validation():
for setting in volume_settings:
# Test boundary values
TestHelper.assert_true(settings_manager.set_setting(setting, 0.0), "Volume 0.0 accepted for " + setting)
TestHelper.assert_true(settings_manager.set_setting(setting, 1.0), "Volume 1.0 accepted for " + setting)
TestHelper.assert_true(
settings_manager.set_setting(setting, 0.0), "Volume 0.0 accepted for " + setting
)
TestHelper.assert_true(
settings_manager.set_setting(setting, 1.0), "Volume 1.0 accepted for " + setting
)
# Test out of range values
TestHelper.assert_false(settings_manager.set_setting(setting, -0.1), "Negative volume rejected for " + setting)
TestHelper.assert_false(settings_manager.set_setting(setting, 1.1), "Volume > 1.0 rejected for " + setting)
TestHelper.assert_false(
settings_manager.set_setting(setting, -0.1), "Negative volume rejected for " + setting
)
TestHelper.assert_false(
settings_manager.set_setting(setting, 1.1), "Volume > 1.0 rejected for " + setting
)
# Test invalid types
TestHelper.assert_false(settings_manager.set_setting(setting, "0.5"), "String volume rejected for " + setting)
TestHelper.assert_false(settings_manager.set_setting(setting, null), "Null volume rejected for " + setting)
TestHelper.assert_false(
settings_manager.set_setting(setting, "0.5"), "String volume rejected for " + setting
)
TestHelper.assert_false(
settings_manager.set_setting(setting, null), "Null volume rejected for " + setting
)
func test_error_handling_and_recovery():
TestHelper.print_step("Error Handling and Recovery")
@@ -198,12 +238,23 @@ func test_error_handling_and_recovery():
# Verify defaults are loaded
var default_volume = settings_manager.default_settings["master_volume"]
TestHelper.assert_equal(default_volume, settings_manager.get_setting("master_volume"), "Reset to defaults works correctly")
TestHelper.assert_equal(
default_volume,
settings_manager.get_setting("master_volume"),
"Reset to defaults works correctly"
)
# Test fallback language loading
TestHelper.assert_true(settings_manager.languages_data.has("languages"), "Fallback languages loaded")
TestHelper.assert_has_key(settings_manager.languages_data["languages"], "en", "English fallback language available")
TestHelper.assert_has_key(settings_manager.languages_data["languages"], "ru", "Russian fallback language available")
TestHelper.assert_true(
settings_manager.languages_data.has("languages"), "Fallback languages loaded"
)
TestHelper.assert_has_key(
settings_manager.languages_data["languages"], "en", "English fallback language available"
)
TestHelper.assert_has_key(
settings_manager.languages_data["languages"], "ru", "Russian fallback language available"
)
func test_reset_functionality():
TestHelper.print_step("Reset Functionality")
@@ -216,12 +267,21 @@ func test_reset_functionality():
settings_manager.reset_settings_to_defaults()
# Verify reset worked
TestHelper.assert_equal(settings_manager.default_settings["master_volume"], settings_manager.get_setting("master_volume"), "Volume reset to default")
TestHelper.assert_equal(settings_manager.default_settings["language"], settings_manager.get_setting("language"), "Language reset to default")
TestHelper.assert_equal(
settings_manager.default_settings["master_volume"],
settings_manager.get_setting("master_volume"),
"Volume reset to default"
)
TestHelper.assert_equal(
settings_manager.default_settings["language"],
settings_manager.get_setting("language"),
"Language reset to default"
)
# Test that reset saves automatically
TestHelper.assert_file_exists("user://settings.cfg", "Settings file exists after reset")
func test_performance_benchmarks():
TestHelper.print_step("Performance Benchmarks")
@@ -241,6 +301,7 @@ func test_performance_benchmarks():
settings_manager.set_setting("master_volume", 0.5)
TestHelper.end_performance_test("validation", 50.0, "100 validations within 50ms")
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -253,4 +314,4 @@ func cleanup_tests():
for temp_file in temp_files:
TestHelper.cleanup_temp_file(temp_file)
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")

View File

@@ -10,6 +10,7 @@ var tile_scene: PackedScene
var tile_instance: Node2D
var test_viewport: SubViewport
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -20,6 +21,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Tile Component")
@@ -43,6 +45,7 @@ func run_tests():
TestHelper.print_test_footer("Tile Component")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
@@ -65,6 +68,7 @@ func setup_test_environment():
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
@@ -73,16 +77,31 @@ func test_basic_functionality():
return
# Test that Tile has expected properties
var expected_properties = ["tile_type", "grid_position", "is_selected", "is_highlighted", "original_scale", "active_gem_types"]
var expected_properties = [
"tile_type",
"grid_position",
"is_selected",
"is_highlighted",
"original_scale",
"active_gem_types"
]
for prop in expected_properties:
TestHelper.assert_true(prop in tile_instance, "Tile has property: " + prop)
# Test that Tile has expected methods
var expected_methods = ["set_active_gem_types", "get_active_gem_count", "add_gem_type", "remove_gem_type", "force_reset_visual_state"]
var expected_methods = [
"set_active_gem_types",
"get_active_gem_count",
"add_gem_type",
"remove_gem_type",
"force_reset_visual_state"
]
TestHelper.assert_has_methods(tile_instance, expected_methods, "Tile component methods")
# Test signals
TestHelper.assert_true(tile_instance.has_signal("tile_selected"), "Tile has tile_selected signal")
TestHelper.assert_true(
tile_instance.has_signal("tile_selected"), "Tile has tile_selected signal"
)
# Test sprite reference
TestHelper.assert_not_null(tile_instance.sprite, "Sprite node is available")
@@ -91,6 +110,7 @@ func test_basic_functionality():
# Test group membership
TestHelper.assert_true(tile_instance.is_in_group("tiles"), "Tile is in 'tiles' group")
func test_tile_constants():
TestHelper.print_step("Tile Constants")
@@ -102,8 +122,12 @@ func test_tile_constants():
# Test all_gem_textures array
TestHelper.assert_not_null(tile_instance.all_gem_textures, "All gem textures array exists")
TestHelper.assert_true(tile_instance.all_gem_textures is Array, "All gem textures is Array type")
TestHelper.assert_equal(8, tile_instance.all_gem_textures.size(), "All gem textures has expected count")
TestHelper.assert_true(
tile_instance.all_gem_textures is Array, "All gem textures is Array type"
)
TestHelper.assert_equal(
8, tile_instance.all_gem_textures.size(), "All gem textures has expected count"
)
# Test that all gem textures are valid
for i in range(tile_instance.all_gem_textures.size()):
@@ -111,6 +135,7 @@ func test_tile_constants():
TestHelper.assert_not_null(texture, "Gem texture %d is not null" % i)
TestHelper.assert_true(texture is Texture2D, "Gem texture %d is Texture2D type" % i)
func test_texture_management():
TestHelper.print_step("Texture Management")
@@ -119,8 +144,12 @@ func test_texture_management():
# Test default gem types initialization
TestHelper.assert_not_null(tile_instance.active_gem_types, "Active gem types is initialized")
TestHelper.assert_true(tile_instance.active_gem_types is Array, "Active gem types is Array type")
TestHelper.assert_true(tile_instance.active_gem_types.size() > 0, "Active gem types has content")
TestHelper.assert_true(
tile_instance.active_gem_types is Array, "Active gem types is Array type"
)
TestHelper.assert_true(
tile_instance.active_gem_types.size() > 0, "Active gem types has content"
)
# Test texture assignment for valid tile types
var original_type = tile_instance.tile_type
@@ -130,11 +159,14 @@ func test_texture_management():
TestHelper.assert_equal(i, tile_instance.tile_type, "Tile type set correctly to %d" % i)
if tile_instance.sprite:
TestHelper.assert_not_null(tile_instance.sprite.texture, "Sprite texture assigned for type %d" % i)
TestHelper.assert_not_null(
tile_instance.sprite.texture, "Sprite texture assigned for type %d" % i
)
# Restore original type
tile_instance.tile_type = original_type
func test_gem_type_management():
TestHelper.print_step("Gem Type Management")
@@ -147,17 +179,23 @@ func test_gem_type_management():
# Test set_active_gem_types with valid array
var test_gems = [0, 1, 2]
tile_instance.set_active_gem_types(test_gems)
TestHelper.assert_equal(3, tile_instance.get_active_gem_count(), "Active gem count set correctly")
TestHelper.assert_equal(
3, tile_instance.get_active_gem_count(), "Active gem count set correctly"
)
# Test add_gem_type
var add_result = tile_instance.add_gem_type(3)
TestHelper.assert_true(add_result, "Valid gem type added successfully")
TestHelper.assert_equal(4, tile_instance.get_active_gem_count(), "Gem count increased after addition")
TestHelper.assert_equal(
4, tile_instance.get_active_gem_count(), "Gem count increased after addition"
)
# Test adding duplicate gem type
var duplicate_result = tile_instance.add_gem_type(3)
TestHelper.assert_false(duplicate_result, "Duplicate gem type addition rejected")
TestHelper.assert_equal(4, tile_instance.get_active_gem_count(), "Gem count unchanged after duplicate")
TestHelper.assert_equal(
4, tile_instance.get_active_gem_count(), "Gem count unchanged after duplicate"
)
# Test add_gem_type with invalid index
var invalid_add = tile_instance.add_gem_type(99)
@@ -166,7 +204,9 @@ func test_gem_type_management():
# Test remove_gem_type
var remove_result = tile_instance.remove_gem_type(3)
TestHelper.assert_true(remove_result, "Valid gem type removed successfully")
TestHelper.assert_equal(3, tile_instance.get_active_gem_count(), "Gem count decreased after removal")
TestHelper.assert_equal(
3, tile_instance.get_active_gem_count(), "Gem count decreased after removal"
)
# Test removing non-existent gem type
var nonexistent_remove = tile_instance.remove_gem_type(99)
@@ -181,6 +221,7 @@ func test_gem_type_management():
# Restore original state
tile_instance.set_active_gem_types(original_gem_types)
func test_visual_feedback_system():
TestHelper.print_step("Visual Feedback System")
@@ -201,7 +242,10 @@ func test_visual_feedback_system():
if tile_instance.sprite:
# Test that modulate is brighter when selected
var modulate = tile_instance.sprite.modulate
TestHelper.assert_true(modulate.r > 1.0 or modulate.g > 1.0 or modulate.b > 1.0, "Selected tile has brighter modulate")
TestHelper.assert_true(
modulate.r > 1.0 or modulate.g > 1.0 or modulate.b > 1.0,
"Selected tile has brighter modulate"
)
# Test highlight visual feedback
tile_instance.is_selected = false
@@ -226,6 +270,7 @@ func test_visual_feedback_system():
tile_instance.is_selected = original_selected
tile_instance.is_highlighted = original_highlighted
func test_state_management():
TestHelper.print_step("State Management")
@@ -234,8 +279,12 @@ func test_state_management():
# Test initial state
TestHelper.assert_true(tile_instance.tile_type >= 0, "Initial tile type is non-negative")
TestHelper.assert_true(tile_instance.grid_position is Vector2i, "Grid position is Vector2i type")
TestHelper.assert_true(tile_instance.original_scale is Vector2, "Original scale is Vector2 type")
TestHelper.assert_true(
tile_instance.grid_position is Vector2i, "Grid position is Vector2i type"
)
TestHelper.assert_true(
tile_instance.original_scale is Vector2, "Original scale is Vector2 type"
)
# Test tile type bounds checking
var original_type = tile_instance.tile_type
@@ -247,11 +296,15 @@ func test_state_management():
TestHelper.assert_equal(max_valid_type, tile_instance.tile_type, "Valid tile type accepted")
# Test state consistency
TestHelper.assert_true(tile_instance.tile_type < tile_instance.active_gem_types.size(), "Tile type within active gems range")
TestHelper.assert_true(
tile_instance.tile_type < tile_instance.active_gem_types.size(),
"Tile type within active gems range"
)
# Restore original type
tile_instance.tile_type = original_type
func test_input_validation():
TestHelper.print_step("Input Validation")
@@ -262,16 +315,22 @@ func test_input_validation():
var original_gems = tile_instance.active_gem_types.duplicate()
tile_instance.set_active_gem_types([])
# Should fall back to defaults or maintain previous state
TestHelper.assert_true(tile_instance.get_active_gem_count() > 0, "Empty gem array handled gracefully")
TestHelper.assert_true(
tile_instance.get_active_gem_count() > 0, "Empty gem array handled gracefully"
)
# Test null gem types array
tile_instance.set_active_gem_types(null)
TestHelper.assert_true(tile_instance.get_active_gem_count() > 0, "Null gem array handled gracefully")
TestHelper.assert_true(
tile_instance.get_active_gem_count() > 0, "Null gem array handled gracefully"
)
# Test invalid gem indices in array
tile_instance.set_active_gem_types([0, 1, 99, 2]) # 99 is invalid
# Should use fallback or filter invalid indices
TestHelper.assert_true(tile_instance.get_active_gem_count() > 0, "Invalid gem indices handled gracefully")
TestHelper.assert_true(
tile_instance.get_active_gem_count() > 0, "Invalid gem indices handled gracefully"
)
# Test negative gem indices
var negative_add = tile_instance.add_gem_type(-1)
@@ -284,6 +343,7 @@ func test_input_validation():
# Restore original state
tile_instance.set_active_gem_types(original_gems)
func test_scaling_and_sizing():
TestHelper.print_step("Scaling and Sizing")
@@ -300,7 +360,9 @@ func test_scaling_and_sizing():
var texture_size = tile_instance.sprite.texture.get_size()
var scaled_size = texture_size * tile_instance.original_scale
var max_dimension = max(scaled_size.x, scaled_size.y)
TestHelper.assert_true(max_dimension <= tile_instance.TILE_SIZE + 1, "Scaled tile fits within TILE_SIZE")
TestHelper.assert_true(
max_dimension <= tile_instance.TILE_SIZE + 1, "Scaled tile fits within TILE_SIZE"
)
# Test scale animation for visual feedback
var original_scale = tile_instance.sprite.scale if tile_instance.sprite else Vector2.ONE
@@ -312,13 +374,16 @@ func test_scaling_and_sizing():
if tile_instance.sprite:
var selected_scale = tile_instance.sprite.scale
TestHelper.assert_true(selected_scale.x >= original_scale.x, "Selected tile scale is larger or equal")
TestHelper.assert_true(
selected_scale.x >= original_scale.x, "Selected tile scale is larger or equal"
)
# Reset to normal
tile_instance.is_selected = false
await process_frame
await process_frame
func test_memory_safety():
TestHelper.print_step("Memory Safety")
@@ -344,12 +409,18 @@ func test_memory_safety():
TestHelper.assert_true(is_instance_valid(tile_instance.sprite), "Sprite instance is valid")
# Test gem types array integrity
TestHelper.assert_true(tile_instance.active_gem_types is Array, "Active gem types maintains Array type")
TestHelper.assert_true(
tile_instance.active_gem_types is Array, "Active gem types maintains Array type"
)
# Test that gem indices are within bounds
for gem_index in tile_instance.active_gem_types:
TestHelper.assert_true(gem_index >= 0, "Gem index is non-negative")
TestHelper.assert_true(gem_index < tile_instance.all_gem_textures.size(), "Gem index within texture array bounds")
TestHelper.assert_true(
gem_index < tile_instance.all_gem_textures.size(),
"Gem index within texture array bounds"
)
func test_error_handling():
TestHelper.print_step("Error Handling")
@@ -390,7 +461,11 @@ func test_error_handling():
tile_instance.set_active_gem_types(large_gem_types)
# Should fall back to safe defaults
TestHelper.assert_true(tile_instance.get_active_gem_count() <= tile_instance.all_gem_textures.size(), "Large gem array handled safely")
TestHelper.assert_true(
tile_instance.get_active_gem_count() <= tile_instance.all_gem_textures.size(),
"Large gem array handled safely"
)
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -406,4 +481,4 @@ func cleanup_tests():
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")

View File

@@ -11,6 +11,7 @@ var stepper_instance: Control
var test_viewport: SubViewport
var original_language: String
func _initialize():
# Wait for autoloads to initialize
await process_frame
@@ -21,6 +22,7 @@ func _initialize():
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("ValueStepper Component")
@@ -47,6 +49,7 @@ func run_tests():
TestHelper.print_test_footer("ValueStepper Component")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
@@ -69,6 +72,7 @@ func setup_test_environment():
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
@@ -77,16 +81,30 @@ func test_basic_functionality():
return
# Test that ValueStepper has expected properties
var expected_properties = ["data_source", "custom_format_function", "values", "display_names", "current_index"]
var expected_properties = [
"data_source", "custom_format_function", "values", "display_names", "current_index"
]
for prop in expected_properties:
TestHelper.assert_true(prop in stepper_instance, "ValueStepper has property: " + prop)
# Test that ValueStepper has expected methods
var expected_methods = ["change_value", "setup_custom_values", "get_current_value", "set_current_value", "set_highlighted", "handle_input_action", "get_control_name"]
TestHelper.assert_has_methods(stepper_instance, expected_methods, "ValueStepper component methods")
var expected_methods = [
"change_value",
"setup_custom_values",
"get_current_value",
"set_current_value",
"set_highlighted",
"handle_input_action",
"get_control_name"
]
TestHelper.assert_has_methods(
stepper_instance, expected_methods, "ValueStepper component methods"
)
# Test signals
TestHelper.assert_true(stepper_instance.has_signal("value_changed"), "ValueStepper has value_changed signal")
TestHelper.assert_true(
stepper_instance.has_signal("value_changed"), "ValueStepper has value_changed signal"
)
# Test UI components
TestHelper.assert_not_null(stepper_instance.left_button, "Left button is available")
@@ -98,6 +116,7 @@ func test_basic_functionality():
TestHelper.assert_true(stepper_instance.right_button is Button, "Right button is Button type")
TestHelper.assert_true(stepper_instance.value_display is Label, "Value display is Label type")
func test_data_source_loading():
TestHelper.print_step("Data Source Loading")
@@ -105,7 +124,9 @@ func test_data_source_loading():
return
# Test default language data source
TestHelper.assert_equal("language", stepper_instance.data_source, "Default data source is language")
TestHelper.assert_equal(
"language", stepper_instance.data_source, "Default data source is language"
)
# Test that values are loaded
TestHelper.assert_not_null(stepper_instance.values, "Values array is initialized")
@@ -116,14 +137,24 @@ func test_data_source_loading():
# Test that language data is loaded correctly
if stepper_instance.data_source == "language":
TestHelper.assert_true(stepper_instance.values.size() > 0, "Language values loaded")
TestHelper.assert_true(stepper_instance.display_names.size() > 0, "Language display names loaded")
TestHelper.assert_equal(stepper_instance.values.size(), stepper_instance.display_names.size(), "Values and display names arrays have same size")
TestHelper.assert_true(
stepper_instance.display_names.size() > 0, "Language display names loaded"
)
TestHelper.assert_equal(
stepper_instance.values.size(),
stepper_instance.display_names.size(),
"Values and display names arrays have same size"
)
# Test that current language is properly selected
var current_lang = root.get_node("SettingsManager").get_setting("language")
var expected_index = stepper_instance.values.find(current_lang)
if expected_index >= 0:
TestHelper.assert_equal(expected_index, stepper_instance.current_index, "Current language index set correctly")
TestHelper.assert_equal(
expected_index,
stepper_instance.current_index,
"Current language index set correctly"
)
# Test resolution data source
var resolution_stepper = stepper_scene.instantiate()
@@ -132,7 +163,9 @@ func test_data_source_loading():
await process_frame
TestHelper.assert_true(resolution_stepper.values.size() > 0, "Resolution values loaded")
TestHelper.assert_contains(resolution_stepper.values, "1920x1080", "Resolution data contains expected value")
TestHelper.assert_contains(
resolution_stepper.values, "1920x1080", "Resolution data contains expected value"
)
resolution_stepper.queue_free()
@@ -143,11 +176,14 @@ func test_data_source_loading():
await process_frame
TestHelper.assert_true(difficulty_stepper.values.size() > 0, "Difficulty values loaded")
TestHelper.assert_contains(difficulty_stepper.values, "normal", "Difficulty data contains expected value")
TestHelper.assert_contains(
difficulty_stepper.values, "normal", "Difficulty data contains expected value"
)
TestHelper.assert_equal(1, difficulty_stepper.current_index, "Difficulty defaults to normal")
difficulty_stepper.queue_free()
func test_value_navigation():
TestHelper.print_step("Value Navigation")
@@ -167,23 +203,30 @@ func test_value_navigation():
# Test backward navigation
stepper_instance.change_value(-1)
var back_value = stepper_instance.get_current_value()
TestHelper.assert_equal(initial_value, back_value, "Backward navigation returns to original value")
TestHelper.assert_equal(
initial_value, back_value, "Backward navigation returns to original value"
)
# Test wrap-around forward
var max_index = stepper_instance.values.size() - 1
stepper_instance.current_index = max_index
stepper_instance.change_value(1)
TestHelper.assert_equal(0, stepper_instance.current_index, "Forward navigation wraps to beginning")
TestHelper.assert_equal(
0, stepper_instance.current_index, "Forward navigation wraps to beginning"
)
# Test wrap-around backward
stepper_instance.current_index = 0
stepper_instance.change_value(-1)
TestHelper.assert_equal(max_index, stepper_instance.current_index, "Backward navigation wraps to end")
TestHelper.assert_equal(
max_index, stepper_instance.current_index, "Backward navigation wraps to end"
)
# Restore original state
stepper_instance.current_index = original_index
stepper_instance._update_display()
func test_custom_values():
TestHelper.print_step("Custom Values")
@@ -202,27 +245,41 @@ func test_custom_values():
TestHelper.assert_equal(3, stepper_instance.values.size(), "Custom values set correctly")
TestHelper.assert_equal("apple", stepper_instance.values[0], "First custom value correct")
TestHelper.assert_equal(0, stepper_instance.current_index, "Index reset to 0 for custom values")
TestHelper.assert_equal("apple", stepper_instance.get_current_value(), "Current value matches first custom value")
TestHelper.assert_equal(
"apple", stepper_instance.get_current_value(), "Current value matches first custom value"
)
# Test custom values with display names
var custom_display_names = ["Red Apple", "Yellow Banana", "Red Cherry"]
stepper_instance.setup_custom_values(custom_values, custom_display_names)
TestHelper.assert_equal(3, stepper_instance.display_names.size(), "Custom display names set correctly")
TestHelper.assert_equal("Red Apple", stepper_instance.display_names[0], "First display name correct")
TestHelper.assert_equal(
3, stepper_instance.display_names.size(), "Custom display names set correctly"
)
TestHelper.assert_equal(
"Red Apple", stepper_instance.display_names[0], "First display name correct"
)
# Test navigation with custom values
stepper_instance.change_value(1)
TestHelper.assert_equal("banana", stepper_instance.get_current_value(), "Navigation works with custom values")
TestHelper.assert_equal(
"banana", stepper_instance.get_current_value(), "Navigation works with custom values"
)
# Test set_current_value
stepper_instance.set_current_value("cherry")
TestHelper.assert_equal("cherry", stepper_instance.get_current_value(), "set_current_value works correctly")
TestHelper.assert_equal(2, stepper_instance.current_index, "Index updated correctly by set_current_value")
TestHelper.assert_equal(
"cherry", stepper_instance.get_current_value(), "set_current_value works correctly"
)
TestHelper.assert_equal(
2, stepper_instance.current_index, "Index updated correctly by set_current_value"
)
# Test invalid value
stepper_instance.set_current_value("grape")
TestHelper.assert_equal("cherry", stepper_instance.get_current_value(), "Invalid value doesn't change current value")
TestHelper.assert_equal(
"cherry", stepper_instance.get_current_value(), "Invalid value doesn't change current value"
)
# Restore original state
stepper_instance.values = original_values
@@ -230,6 +287,7 @@ func test_custom_values():
stepper_instance.current_index = original_index
stepper_instance._update_display()
func test_input_handling():
TestHelper.print_step("Input Handling")
@@ -242,12 +300,18 @@ func test_input_handling():
# Test left input action
var left_handled = stepper_instance.handle_input_action("move_left")
TestHelper.assert_true(left_handled, "Left input action handled")
TestHelper.assert_not_equal(original_value, stepper_instance.get_current_value(), "Left action changes value")
TestHelper.assert_not_equal(
original_value, stepper_instance.get_current_value(), "Left action changes value"
)
# Test right input action
var right_handled = stepper_instance.handle_input_action("move_right")
TestHelper.assert_true(right_handled, "Right input action handled")
TestHelper.assert_equal(original_value, stepper_instance.get_current_value(), "Right action returns to original value")
TestHelper.assert_equal(
original_value,
stepper_instance.get_current_value(),
"Right action returns to original value"
)
# Test invalid input action
var invalid_handled = stepper_instance.handle_input_action("invalid_action")
@@ -257,12 +321,19 @@ func test_input_handling():
if stepper_instance.left_button:
var before_left = stepper_instance.get_current_value()
stepper_instance._on_left_button_pressed()
TestHelper.assert_not_equal(before_left, stepper_instance.get_current_value(), "Left button press changes value")
TestHelper.assert_not_equal(
before_left, stepper_instance.get_current_value(), "Left button press changes value"
)
if stepper_instance.right_button:
var before_right = stepper_instance.get_current_value()
stepper_instance._on_right_button_pressed()
TestHelper.assert_equal(original_value, stepper_instance.get_current_value(), "Right button press returns to original")
TestHelper.assert_equal(
original_value,
stepper_instance.get_current_value(),
"Right button press returns to original"
)
func test_visual_feedback():
TestHelper.print_step("Visual Feedback")
@@ -277,13 +348,19 @@ func test_visual_feedback():
# Test highlighting
stepper_instance.set_highlighted(true)
TestHelper.assert_true(stepper_instance.is_highlighted, "Highlighted state set correctly")
TestHelper.assert_true(stepper_instance.scale.x > original_scale.x, "Scale increased when highlighted")
TestHelper.assert_true(
stepper_instance.scale.x > original_scale.x, "Scale increased when highlighted"
)
# Test unhighlighting
stepper_instance.set_highlighted(false)
TestHelper.assert_false(stepper_instance.is_highlighted, "Highlighted state cleared correctly")
TestHelper.assert_equal(original_scale, stepper_instance.scale, "Scale restored when unhighlighted")
TestHelper.assert_equal(original_modulate, stepper_instance.modulate, "Modulate restored when unhighlighted")
TestHelper.assert_equal(
original_scale, stepper_instance.scale, "Scale restored when unhighlighted"
)
TestHelper.assert_equal(
original_modulate, stepper_instance.modulate, "Modulate restored when unhighlighted"
)
# Test display update
if stepper_instance.value_display:
@@ -291,6 +368,7 @@ func test_visual_feedback():
TestHelper.assert_true(current_text.length() > 0, "Value display has text content")
TestHelper.assert_not_equal("N/A", current_text, "Value display shows valid content")
func test_settings_integration():
TestHelper.print_step("Settings Integration")
@@ -321,6 +399,7 @@ func test_settings_integration():
# Restore original language
root.get_node("SettingsManager").set_setting("language", original_lang)
func test_boundary_conditions():
TestHelper.print_step("Boundary Conditions")
@@ -333,7 +412,9 @@ func test_boundary_conditions():
test_viewport.add_child(empty_stepper)
await process_frame
TestHelper.assert_equal("", empty_stepper.get_current_value(), "Empty values array returns empty string")
TestHelper.assert_equal(
"", empty_stepper.get_current_value(), "Empty values array returns empty string"
)
# Test change_value with empty array
empty_stepper.change_value(1) # Should not crash
@@ -346,17 +427,22 @@ func test_boundary_conditions():
# Test negative index handling
stepper_instance.current_index = -1
stepper_instance._update_display()
TestHelper.assert_equal("N/A", stepper_instance.value_display.text, "Negative index shows N/A")
TestHelper.assert_equal(
"N/A", stepper_instance.value_display.text, "Negative index shows N/A"
)
# Test out-of-bounds index handling
stepper_instance.current_index = stepper_instance.values.size()
stepper_instance._update_display()
TestHelper.assert_equal("N/A", stepper_instance.value_display.text, "Out-of-bounds index shows N/A")
TestHelper.assert_equal(
"N/A", stepper_instance.value_display.text, "Out-of-bounds index shows N/A"
)
# Restore valid index
stepper_instance.current_index = 0
stepper_instance._update_display()
func test_error_handling():
TestHelper.print_step("Error Handling")
@@ -376,7 +462,9 @@ func test_error_handling():
# Test get_control_name
var control_name = stepper_instance.get_control_name()
TestHelper.assert_true(control_name.ends_with("_stepper"), "Control name has correct suffix")
TestHelper.assert_true(control_name.begins_with(stepper_instance.data_source), "Control name includes data source")
TestHelper.assert_true(
control_name.begins_with(stepper_instance.data_source), "Control name includes data source"
)
# Test custom values with mismatched arrays
var values_3 = ["a", "b", "c"]
@@ -385,12 +473,17 @@ func test_error_handling():
# Should handle gracefully - display_names should be duplicated from values
TestHelper.assert_equal(3, stepper_instance.values.size(), "Values array size preserved")
TestHelper.assert_equal(2, stepper_instance.display_names.size(), "Display names size preserved as provided")
TestHelper.assert_equal(
2, stepper_instance.display_names.size(), "Display names size preserved as provided"
)
# Test navigation with mismatched arrays
stepper_instance.current_index = 2 # Index where display_names doesn't exist
stepper_instance._update_display()
TestHelper.assert_equal("c", stepper_instance.value_display.text, "Falls back to value when display name missing")
TestHelper.assert_equal(
"c", stepper_instance.value_display.text, "Falls back to value when display name missing"
)
func cleanup_tests():
TestHelper.print_step("Cleanup")
@@ -409,4 +502,4 @@ func cleanup_tests():
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")
TestHelper.assert_true(true, "Test cleanup completed")