Compare commits

...

4 Commits

62 changed files with 5840 additions and 663 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

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
# Generated files
*.tmp
*.import~
test_results.txt

View File

@@ -1,2 +1,9 @@
- The documentation of the project is located in docs/ directory.
So the docs\CLAUDE.md does. Get it in context before doing anything else.
- 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;
- Use static data types;
- Keep documentation up to date;
- Always run gdlint, gdformat and run tests;

View File

@@ -1,10 +1,10 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Guidance for Claude Code (claude.ai/code) when working with this repository.
## Project Overview
"Skelly" is a Godot 4.4 mobile game project featuring multiple gameplay modes within a unified game framework. The project currently supports match-3 puzzle gameplay with planned support for clickomania gameplay. It includes a modular gameplay system, menu system, settings management, audio handling, localization support, and a comprehensive debug system.
"Skelly" is a Godot 4.4 mobile game project with multiple gameplay modes. Supports match-3 puzzle gameplay with planned clickomania gameplay. Includes modular gameplay system, menu system, settings management, audio handling, localization support, and debug system.
**For detailed project architecture, see `docs/MAP.md`**
@@ -41,48 +41,60 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- New translations: Add to `project.godot` internationalization section
### Asset Management
- **CRITICAL**: Every asset must be documented in `assets/sources.yaml` before committing
- **Document every asset** in `assets/sources.yaml` before committing
- Include source, license, attribution, modifications, and usage information
- Verify license compatibility with project requirements
- Commit asset files and sources.yaml together in the same commit
- Verify license compatibility
- Commit asset files and sources.yaml together
## Key Development Guidelines
### Code Quality & Safety Standards
- **Memory Management**: Always use `queue_free()` instead of `free()` for node cleanup
- **Input Validation**: Validate all user inputs with bounds checking and type validation
- **Error Handling**: Implement comprehensive error handling with fallback mechanisms
- **Memory Management**: Use `queue_free()` instead of `free()`
- **Input Validation**: Validate user inputs with bounds checking and type validation
- **Error Handling**: Implement error handling with fallback mechanisms
- **Race Condition Prevention**: Use state flags to prevent concurrent operations
- **No Global State**: Avoid static variables; use instance-based architecture for testability
### Scene Management
- **ALWAYS** use `GameManager` for scene transitions - never call `get_tree().change_scene_to_file()` directly
- Scene paths are defined as constants in GameManager
- Error handling is built into GameManager for failed scene loads
- **Use `GameManager` for all scene transitions** - never call `get_tree().change_scene_to_file()` directly
- Scene paths defined as constants in GameManager
- Error handling built into GameManager for failed scene loads
- Use `GameManager.start_game_with_mode(mode)` to launch specific gameplay modes
- Supported gameplay modes: "match3", "clickomania" (validated with whitelist)
- Supported modes: "match3", "clickomania" (validated with whitelist)
- GameManager prevents concurrent scene changes with `is_changing_scene` protection
### Autoload Usage
- Use autoloads for global state management only
- Prefer signals over direct access for loose coupling
- Don't access autoloads from deeply nested components
- **SettingsManager**: Features comprehensive input validation and error recovery
- **SaveManager**: Save system with tamper detection, race condition protection, and permissive validation
- **SettingsManager**: Features input validation, NaN/Infinity checks, and security hardening
- **GameManager**: Protected against race conditions with state management
### Save System Security & Data Integrity
- **SaveManager implements security standards** for data protection
- **Tamper Detection**: Deterministic checksums detect save file modification or corruption
- **Race Condition Protection**: Save operation locking prevents concurrent conflicts
- **Permissive Validation**: Auto-repair system fixes corrupted data instead of rejecting saves
- **Type Safety**: NaN/Infinity/bounds checking for numeric values
- **Memory Protection**: File size limits prevent memory exhaustion attacks
- **Version Migration**: Backward-compatible system handles save format upgrades
- **Error Recovery**: Multi-layered backup and fallback systems ensure no data loss
- **Security Logging**: All save operations logged for monitoring and debugging
### Debug System Integration
- Connect to `DebugManager.debug_ui_toggled` signal for debug UI visibility
- Use F12 key for global debug toggle
- Remove debug prints before committing unless permanently useful
### Logging System Usage
- **CRITICAL**: ALL print() and push_error() statements have been migrated to DebugManager
- **ALWAYS** use `DebugManager` logging functions instead of `print()`, `push_error()`, etc.
- Use appropriate log levels: INFO for general messages, WARN for issues, ERROR for failures
- Include meaningful categories to organize log output, eg: `"GameManager"`, `"Match3"`, `"Settings"`, `"DebugMenu"`
- Leverage structured logging for better debugging and production monitoring
- **All print() and push_error() statements migrated to DebugManager**
- Use `DebugManager` logging functions instead of `print()`, `push_error()`, etc.
- Use log levels: INFO for general messages, WARN for issues, ERROR for failures
- Include categories to organize log output: `"GameManager"`, `"Match3"`, `"Settings"`, `"DebugMenu"`
- Use structured logging for better debugging and production monitoring
- Use `DebugManager.set_log_level()` to control verbosity during development and testing
- The logging system provides unified output across all game systems
- Logging system provides unified output across all game systems
## Important File References
@@ -95,10 +107,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
### Key Scripts to Understand
- `src/autoloads/GameManager.gd` - Scene transition patterns with race condition protection
- `src/autoloads/SettingsManager.gd` - Settings management with comprehensive error handling
- `src/autoloads/SaveManager.gd` - **Save system with security features**
- `src/autoloads/SettingsManager.gd` - Settings management with input validation and security
- `src/autoloads/DebugManager.gd` - Debug system integration
- `scenes/game/game.gd` - Main game scene with modular gameplay system
- `scenes/game/gameplays/match3_gameplay.gd` - Memory-safe Match-3 implementation with input validation
- `scenes/game/gameplays/match3_gameplay.gd` - Match-3 implementation with input validation
- `scenes/game/gameplays/tile.gd` - Instance-based tile behavior without global state
- `scenes/ui/DebugMenuBase.gd` - Unified debug menu base class
- `scenes/ui/SettingsMenu.gd` - Settings UI with input validation
@@ -108,18 +121,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Workflow
### Before Making Changes
1. Check `docs/MAP.md` for architecture understanding
1. Check `docs/MAP.md` for architecture
2. Review `docs/CODE_OF_CONDUCT.md` for coding standards
3. Understand existing patterns before implementing new features
3. Understand existing patterns before implementing features
4. If adding assets, prepare `assets/sources.yaml` documentation
### Testing Changes
- Run project with F5 in Godot Editor
- Test debug UI with F12 toggle
- Verify scene transitions work correctly
- Verify scene transitions work
- Check mobile compatibility if UI changes made
- Use relevant test scripts from `tests/` directory to validate system functionality
- Run `test_logging.gd` after making changes to the logging system
- Use test scripts from `tests/` directory to validate functionality
- Run `test_logging.gd` after logging system changes
- **Save system testing**: Run save/load test suites after SaveManager changes
- **Checksum validation**: Test `test_checksum_issue.gd` to verify deterministic checksums
- **Migration compatibility**: Run `test_migration_compatibility.gd` for version upgrades
### Common Implementation Patterns
- **Scene transitions**: Use `GameManager.start_game_with_mode()` with built-in validation
@@ -127,22 +143,23 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Logging**: Use `DebugManager.log_*()` functions with appropriate levels and categories
- **Gameplay modes**: Implement in `scenes/game/gameplays/` directory following modular pattern
- **Scoring system**: Connect `score_changed` signal from gameplay to main game scene
- **Settings**: Use `SettingsManager` with automatic input validation and error recovery
- **Save/Load operations**: Use `SaveManager` with security and validation
- **Settings**: Use `SettingsManager` with input validation, NaN/Infinity checks, and security hardening
- **Audio**: Use `AudioManager` for music and sound effects
- **Localization**: Use `LocalizationManager` for language switching
- **UI Components**: Extend `DebugMenuBase` for debug menus to avoid code duplication
- **Value Selection**: Use `ValueStepper` component for discrete option selection (language, resolution, difficulty)
- **Memory Management**: Use `queue_free()` and await frame completion for safe cleanup
- **Input Validation**: Always validate user inputs with type checking and bounds validation
- **Input Validation**: Validate user inputs with type checking and bounds validation
### Logging Best Practices
```gdscript
# Good logging practices
# Good logging
DebugManager.log_info("Scene transition completed", "GameManager")
DebugManager.log_warn("Settings file not found, using defaults", "Settings")
DebugManager.log_error("Failed to load audio resource: " + audio_path, "AudioManager")
# Avoid these patterns
# Avoid
print("debug") # Use structured logging instead
push_error("error") # Use DebugManager.log_error() with category
```

View File

@@ -2,19 +2,19 @@
## Overview
This document establishes coding standards and development practices for the Skelly project. These guidelines are designed to help junior developers contribute effectively while maintaining code quality and project consistency.
Coding standards and development practices for the Skelly project. These guidelines help developers contribute effectively while maintaining code quality and project consistency.
## Core Principles
### 1. Code Clarity Over Cleverness
- Write code that is easy to read and understand
- Write code that is easy to read
- Use descriptive variable and function names
- Prefer explicit code over implicit or "clever" solutions
- Prefer explicit code over "clever" solutions
- Comment complex logic and business rules
### 2. Consistency First
- Follow existing code patterns in the project
- Use the same naming conventions throughout
- Follow existing code patterns
- Use same naming conventions throughout
- Maintain consistent indentation and formatting
- Follow Godot's GDScript style guide
@@ -22,7 +22,7 @@ This document establishes coding standards and development practices for the Ske
- Make small, focused commits
- Test changes before committing
- Don't break existing functionality
- Use the debug system to verify your changes
- Use debug system to verify changes
## GDScript Coding Standards
@@ -91,7 +91,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
## Project-Specific Guidelines
### Scene Management
- All scene transitions MUST go through `GameManager`
- All scene transitions go through `GameManager`
- Never use `get_tree().change_scene_to_file()` directly
- Define scene paths as constants in GameManager
@@ -100,7 +100,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
GameManager.start_match3_game()
# ❌ Wrong
GameManager.start_match3_game() # Use GameManager instead of direct scene loading
get_tree().change_scene_to_file("res://scenes/game.tscn")
```
### Autoload Usage
@@ -142,9 +142,9 @@ print(some_variable) # No context, use proper log level
```
### Logging Standards
- **ALWAYS** use `DebugManager.log_*()` functions instead of `print()` or `push_error()`
- Choose appropriate log levels based on message importance and audience
- Include meaningful categories to organize log output by system/component
- Use `DebugManager.log_*()` functions instead of `print()` or `push_error()`
- Choose log levels based on message importance and audience
- Include categories to organize log output by system/component
- Format messages with clear, descriptive text and relevant context
```gdscript
@@ -160,11 +160,11 @@ if debug_mode: print("debug info") # Use DebugManager.log_debug()
```
### Asset Management
- **MANDATORY**: Every asset added to the project must be documented in `assets/sources.yaml`
- Include complete source information, license details, and attribution requirements
- Document any modifications made to original assets
- Verify license compatibility with project usage before adding assets
- Update sources.yaml in the same commit as adding the asset
- **Document every asset** in `assets/sources.yaml`
- Include source information, license details, and attribution
- Document modifications made to original assets
- Verify license compatibility before adding assets
- Update sources.yaml in same commit as adding asset
```gdscript
# ✅ Correct asset addition workflow
@@ -184,13 +184,13 @@ if debug_mode: print("debug info") # Use DebugManager.log_debug()
```
### Error Handling
- Always check if resources loaded successfully
- Check if resources loaded successfully
- Use `DebugManager.log_error()` for critical failures
- Provide fallback behavior when possible
- Include meaningful context in error messages
```gdscript
# ✅ Correct error handling with structured logging
# Good error handling with structured logging
func load_scene(path: String) -> void:
var packed_scene := load(path)
if not packed_scene or not packed_scene is PackedScene:
@@ -209,12 +209,12 @@ func load_scene(path: String) -> void:
- Add body if needed for complex changes
```bash
# Good commit messages
# Good commit messages
Add gem pool management to match-3 system
Fix debug UI visibility toggle issue
Update documentation for new debug system
# Bad commit messages
# Bad commit messages
fix bug
update
wip
@@ -253,7 +253,7 @@ wip
### Manual Testing Requirements
- Test in Godot editor with F5 run
- Verify debug UI works with F12 toggle
- Check scene transitions work correctly
- Check scene transitions work
- Test on different screen sizes (mobile target)
- Verify audio and settings integration
@@ -261,53 +261,53 @@ wip
- Ensure debug panels appear/disappear correctly
- Test all debug buttons and controls
- Verify debug state persists across scene changes
- Check that debug code doesn't affect release builds
- Check debug code doesn't affect release builds
## Common Mistakes to Avoid
### Architecture Violations
```gdscript
# Don't bypass GameManager
# Don't bypass GameManager
get_tree().change_scene_to_file("some_scene.tscn")
# Don't hardcode paths
# Don't hardcode paths
var tile = load("res://scenes/game/gameplays/tile.tscn")
# Don't ignore null checks
# Don't ignore null checks
var node = get_node("SomeNode")
node.do_something() # Could crash if node doesn't exist
# Don't create global state in random scripts
# Don't create global state in random scripts
# Use autoloads instead
```
### Asset Management Violations
```gdscript
# Don't add assets without documentation
# Don't add assets without documentation
# Adding audio/new_music.mp3 without updating sources.yaml
# Don't use assets without verifying licenses
# Don't use assets without verifying licenses
# Using copyrighted music without permission
# Don't modify assets without documenting changes
# Don't modify assets without documenting changes
# Editing sprites without noting modifications in sources.yaml
# Don't commit assets and documentation separately
# Don't commit assets and documentation separately
git add assets/sprites/new_sprite.png
git commit -m "add sprite" # Missing sources.yaml update
# Correct approach
# Correct approach
git add assets/sprites/new_sprite.png assets/sources.yaml
git commit -m "add new sprite with attribution"
```
### Performance Issues
```gdscript
# Don't search nodes repeatedly
# Don't search nodes repeatedly
func _process(delta):
var ui = get_node("UI") # Expensive every frame
# Cache node references
# Cache node references
@onready var ui = $UI
func _process(delta):
ui.update_display() # Much better
@@ -315,11 +315,11 @@ func _process(delta):
### Debug System Misuse
```gdscript
# Don't create separate debug systems
# Don't create separate debug systems
var my_debug_enabled = false
print("debug: " + some_info) # Don't use plain print()
# Use the global debug and logging systems
# Use the global debug and logging systems
if DebugManager.is_debug_enabled():
show_debug_info()
DebugManager.log_debug("Debug information: " + some_info, "MyComponent")

View File

@@ -1,7 +1,7 @@
# Skelly - Project Structure Map
## Overview
Skelly is a Godot 4.4 game project featuring multiple gameplay modes with skeleton character themes. The project supports match-3 puzzle gameplay with planned clickomania gameplay through a modular gameplay architecture. It follows a modular structure with clear separation between scenes, autoloads, assets, and data.
Skelly is a Godot 4.4 game project featuring multiple gameplay modes. The project supports match-3 puzzle gameplay with planned clickomania gameplay through a modular gameplay architecture. It follows a modular structure with clear separation between scenes, autoloads, assets, and data.
## Project Root Structure
@@ -25,43 +25,8 @@ skelly/
### Autoloads (Global Singletons)
Located in `src/autoloads/`, these scripts are automatically loaded when the game starts:
1. **SettingsManager** (`src/autoloads/SettingsManager.gd`)
- Manages game settings and user preferences with comprehensive error handling
- Robust configuration file I/O with fallback mechanisms
- Input validation for all setting values and range checking
- JSON parsing with detailed error recovery and default language fallback
- Provides language selection functionality with validation
- Dependencies: `localization/languages.json`
2. **AudioManager** (`src/autoloads/AudioManager.gd`)
- Controls music and sound effects
- Manages audio bus configuration
- Uses: `data/default_bus_layout.tres`
3. **GameManager** (`src/autoloads/GameManager.gd`)
- Central game state management and gameplay mode coordination with race condition protection
- Safe scene transitions with concurrent change prevention and validation
- Gameplay mode selection and launching with input validation (match3, clickomania)
- Error handling for scene loading failures and fallback mechanisms
- Navigation flow control with state protection
- References: main.tscn, game.tscn and individual gameplay scenes
4. **LocalizationManager** (`src/autoloads/LocalizationManager.gd`)
- Language switching functionality
- Works with Godot's built-in internationalization system
- Uses translation files in `localization/`
5. **DebugManager** (`src/autoloads/DebugManager.gd`)
- Global debug state management and centralized logging system
- Debug UI visibility control
- F12 toggle functionality
- Signal-based debug system
- Structured logging with configurable log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Timestamp-based log formatting with category support
- Runtime log level filtering for development and production builds
6. **SaveManager** (`src/autoloads/SaveManager.gd`)
- Persistent game data management with comprehensive validation
1. **SaveManager** (`src/autoloads/SaveManager.gd`)
- Persistent game data management with validation
- High score tracking and current score management
- Game statistics (games played, total score)
- Grid state persistence for match-3 gameplay continuity
@@ -70,12 +35,48 @@ Located in `src/autoloads/`, these scripts are automatically loaded when the gam
- Robust error handling with backup restoration capabilities
- Uses: `user://savegame.save` for persistent storage
2. **SettingsManager** (`src/autoloads/SettingsManager.gd`)
- Manages game settings and user preferences
- Configuration file I/O
- input validation
- JSON parsing
- Provides language selection functionality
- Dependencies: `localization/languages.json`
3. **AudioManager** (`src/autoloads/AudioManager.gd`)
- Controls music and sound effects
- Manages audio bus configuration
- Uses: `data/default_bus_layout.tres`
4. **GameManager** (`src/autoloads/GameManager.gd`)
- Game state management and gameplay mode coordination with race condition protection
- Scene transitions with concurrent change prevention and validation
- Gameplay mode selection and launching with input validation (match3, clickomania)
- Error handling for scene loading failures and fallback mechanisms
- Navigation flow control with state protection
- References: main.tscn, game.tscn and individual gameplay scenes
5. **LocalizationManager** (`src/autoloads/LocalizationManager.gd`)
- Language switching functionality
- Works with Godot's built-in internationalization system
- Uses translation files in `localization/`
6. **DebugManager** (`src/autoloads/DebugManager.gd`)
- Global debug state management and centralized logging system
- Debug UI visibility control
- F12 toggle functionality
- Signal-based debug system
- Structured logging with configurable log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Timestamp-based log formatting with category support
- Runtime log level filtering
## Scene Hierarchy & Flow
### Main Scenes
```
main.tscn (Entry Point)
├── PressAnyKeyScreen.tscn
├── SplashScreen.tscn
├── MainMenu.tscn
└── SettingsMenu.tscn
@@ -89,11 +90,11 @@ game.tscn (Gameplay Container)
### Game Flow
1. **Main Scene** (`scenes/main/main.tscn` + `Main.gd`)
- Application entry point
- Manages "Press Any Key" screen
- Manages splash screen
- Transitions to main menu
- Dynamic menu loading system
2. **Press Any Key Screen** (`scenes/main/PressAnyKeyScreen.tscn` + `PressAnyKeyScreen.gd`)
2. **Splash Screen** (`scenes/main/SplashScreen.tscn` + `SplashScreen.gd`)
- Initial splash screen
- Input detection for any key/button
- Signals to main scene for transition
@@ -130,12 +131,12 @@ scenes/ui/
└── SettingsMenu.tscn + SettingsMenu.gd # With comprehensive input validation
```
**Code Quality Improvements:**
**Quality Improvements:**
- **ValueStepper Component**: Reusable arrow-based selector for discrete values (language, resolution, difficulty)
- **DebugMenuBase.gd**: Eliminates 90% code duplication between debug menu classes
- **Input Validation**: All user inputs are validated and sanitized before processing
- **Error Recovery**: Robust error handling with fallback mechanisms throughout UI
- **Navigation Support**: Full gamepad/keyboard navigation across all menus
- **Input Validation**: User inputs are validated and sanitized before processing
- **Error Recovery**: Error handling with fallback mechanisms throughout UI
- **Navigation Support**: Gamepad/keyboard navigation across menus
## Modular Gameplay System
@@ -152,12 +153,12 @@ The game now uses a modular gameplay architecture where different game modes can
#### Match-3 Mode (`scenes/game/gameplays/match3_gameplay.tscn`)
1. **Match3 Controller** (`scenes/game/gameplays/match3_gameplay.gd`)
- Grid management (8x8 default) with memory-safe node cleanup
- Match detection algorithms with bounds checking and null validation
- Tile dropping and refilling with proper signal connections
- Match detection algorithms with bounds checking and validation
- Tile dropping and refilling with signal connections
- Gem pool management (3-8 gem types) with instance-based architecture
- Debug UI integration with input validation
- Debug UI integration with validation
- Score reporting via `score_changed` signal
- **Memory Safety**: Uses `queue_free()` with proper frame waiting to prevent crashes
- **Memory Safety**: Uses `queue_free()` with frame waiting to prevent crashes
- **Gem Movement System**: Keyboard and gamepad input for tile selection and swapping
- State machine: WAITING → SELECTING → SWAPPING → PROCESSING
- Adjacent tile validation (horizontal/vertical neighbors only)
@@ -166,29 +167,29 @@ The game now uses a modular gameplay architecture where different game modes can
- Cursor-based navigation with visual highlighting and bounds checking
2. **Tile System** (`scenes/game/gameplays/tile.gd` + `Tile.tscn`)
- Individual tile behavior with instance-based architecture (no global state)
- Gem type management with input validation and bounds checking
- Visual representation with scaling and color modulation
- Tile behavior with instance-based architecture (no global state)
- Gem type management with validation and bounds checking
- Visual representation with scaling and color
- Group membership for coordination
- **Visual Feedback System**: Multi-state display for game interaction
- Selection visual feedback (scale and color modulation)
- State management (normal, highlighted, selected)
- Signal-based communication with gameplay controller
- Smooth animations with Tween system
- **Memory Safety**: Proper resource management and cleanup
- **Memory Safety**: Resource management and cleanup
#### Clickomania Mode (`scenes/game/gameplays/clickomania_gameplay.tscn`)
- Planned implementation for clickomania-style gameplay
- Will integrate with same scoring and UI systems as match-3
### Debug System
- Global debug state via DebugManager with proper initialization
- Debug toggle available on all major scenes (MainMenu, SettingsMenu, PressAnyKeyScreen, Game)
- Global debug state via DebugManager with initialization
- Debug toggle available on all major scenes (MainMenu, SettingsMenu, SplashScreen, Game)
- Match-3 specific debug UI panel with gem count controls and difficulty presets
- Gem count controls (+/- buttons) with difficulty presets (Easy: 3, Normal: 5, Hard: 8)
- Board reroll functionality for testing
- F12 toggle support across all scenes
- Debug prints reduced in production code
- Fewer debug prints in production code
## Asset Organization
@@ -262,8 +263,12 @@ sprites:
### Testing & Validation (`tests/`)
- `test_logging.gd` - DebugManager logging system validation
- **`test_checksum_issue.gd`** - SaveManager checksum validation and deterministic hashing
- **`test_migration_compatibility.gd`** - SaveManager version migration and backward compatibility
- **`test_save_system_integration.gd`** - Complete save/load workflow integration testing
- **`test_checksum_fix_verification.gd`** - JSON serialization checksum fix verification
- `README.md` - Brief directory overview (see docs/TESTING.md for full guidelines)
- Future test scripts for individual components and integration testing
- Comprehensive test scripts for save system security and data integrity validation
- Temporary test utilities for development and debugging
### Project Configuration
@@ -277,7 +282,7 @@ sprites:
### Signal Connections
```
PressAnyKeyScreen --[any_key_pressed]--> Main
SplashScreen --[any_key_pressed]--> Main
MainMenu --[open_settings]--> Main
SettingsMenu --[back_to_main_menu]--> Main
DebugManager --[debug_toggled]--> All scenes with DebugToggle
@@ -334,7 +339,7 @@ DebugManager.log_error("Invalid scene path provided", "GameManager")
# - Settings: Settings management, language changes
# - Game: Main game scene, mode switching
# - MainMenu: Main menu interactions
# - PressAnyKey: Press any key screen
# - SplashScreen: Splash screen
# - Clickomania: Clickomania gameplay mode
# - DebugMenu: Debug menu operations
```

View File

@@ -1,10 +1,10 @@
# Tests Directory
This directory contains test scripts and utilities for validating various systems and components in the Skelly project.
Test scripts and utilities for validating Skelly project systems.
## Overview
The `tests/` directory is designed to house:
The `tests/` directory contains:
- System validation scripts
- Component testing utilities
- Integration tests
@@ -14,14 +14,14 @@ The `tests/` directory is designed to house:
## Current Test Files
### `test_logging.gd`
Comprehensive test script for the DebugManager logging system.
Test script for DebugManager logging system.
**Features:**
- Tests all log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Validates log level filtering functionality
- Tests category-based logging organization
- Validates log level filtering
- Tests category-based logging
- Verifies debug mode integration
- Demonstrates proper logging usage patterns
- Demonstrates logging usage patterns
**Usage:**
```gdscript
@@ -37,15 +37,15 @@ add_child(test_script)
```
**Expected Output:**
The script will output formatted log messages demonstrating:
- Proper timestamp formatting
- Log level filtering behavior
Formatted log messages showing:
- Timestamp formatting
- Log level filtering
- Category organization
- Debug mode dependency for TRACE/DEBUG levels
## Adding New Tests
When creating new test files, follow these conventions:
Follow these conventions for new test files:
### File Naming
- Use descriptive names starting with `test_`
@@ -87,33 +87,37 @@ func test_error_conditions():
### Testing Guidelines
1. **Independence**: Each test should be self-contained and not depend on other tests
2. **Cleanup**: Restore original state after testing (settings, debug modes, etc.)
3. **Clear Output**: Use descriptive print statements to show test progress
4. **Error Handling**: Test both success and failure conditions
5. **Documentation**: Include comments explaining complex test scenarios
1. **Independence**: Each test is self-contained
2. **Cleanup**: Restore original state after testing
3. **Clear Output**: Use descriptive print statements
4. **Error Handling**: Test success and failure conditions
5. **Documentation**: Comment complex test scenarios
### Integration with Main Project
- **Temporary Usage**: Test files are meant to be added temporarily during development
- **Not in Production**: These files should not be included in release builds
- **Temporary Usage**: Add test files temporarily during development
- **Not in Production**: Exclude from release builds
- **Autoload Testing**: Add to autoloads temporarily for automatic execution
- **Manual Testing**: Run individually when testing specific components
- **Manual Testing**: Run individually for specific components
## Test Categories
### System Tests
Test core autoload managers and global systems:
- `test_logging.gd` - DebugManager logging system
- Future: `test_settings.gd` - SettingsManager functionality
- Future: `test_audio.gd` - AudioManager functionality
- Future: `test_scene_management.gd` - GameManager transitions
- `test_checksum_issue.gd` - SaveManager checksum validation and deterministic hashing
- `test_migration_compatibility.gd` - SaveManager version migration and backward compatibility
- `test_save_system_integration.gd` - Complete save/load workflow integration testing
- `test_checksum_fix_verification.gd` - Verification of JSON serialization checksum fixes
- `test_settings_manager.gd` - SettingsManager security validation, input validation, and error handling
- `test_game_manager.gd` - GameManager scene transitions, race condition protection, and input validation
- `test_audio_manager.gd` - AudioManager functionality, resource loading, and volume management
### Component Tests
Test individual game components:
- Future: `test_match3.gd` - Match-3 gameplay mechanics
- Future: `test_tile_system.gd` - Tile behavior and interactions
- Future: `test_ui_components.gd` - Menu and UI functionality
- `test_match3_gameplay.gd` - Match-3 gameplay mechanics, grid management, and match detection
- `test_tile.gd` - Tile component behavior, visual feedback, and memory safety
- `test_value_stepper.gd` - ValueStepper UI component functionality and settings integration
### Integration Tests
Test system interactions and workflows:
@@ -121,36 +125,141 @@ Test system interactions and workflows:
- Future: `test_debug_system.gd` - Debug UI integration
- Future: `test_localization.gd` - Language switching and translations
## Save System Testing Protocols
SaveManager implements security features requiring testing for modifications.
### Critical Test Suites
#### **`test_checksum_issue.gd`** - Checksum Validation
**Tests**: Checksum generation, JSON serialization consistency, save/load cycles
**Usage**: Run after checksum algorithm changes
#### **`test_migration_compatibility.gd`** - Version Migration
**Tests**: Backward compatibility, missing field addition, data structure normalization
**Usage**: Test save format upgrades
#### **`test_save_system_integration.gd`** - End-to-End Integration
**Tests**: Save/load workflow, grid state serialization, race condition prevention
**Usage**: Run after SaveManager modifications
#### **`test_checksum_fix_verification.gd`** - JSON Serialization Fix
**Tests**: Checksum consistency, int/float conversion, type safety validation
**Usage**: Test JSON type conversion fixes
### Save System Security Testing
#### **Required Tests Before SaveManager Changes**
1. Run 4 save system test suites
2. Test tamper detection by modifying save files
3. Validate error recovery by corrupting files
4. Check race condition protection
5. Verify permissive validation
#### **Performance Benchmarks**
- Checksum calculation: < 1ms
- Memory usage: File size limits prevent exhaustion
- Error recovery: Never crash regardless of corruption
- Data preservation: User scores survive migration
#### **Test Sequence After Modifications**
1. `test_checksum_issue.gd` - Verify checksum consistency
2. `test_migration_compatibility.gd` - Check version upgrades
3. `test_save_system_integration.gd` - Validate workflow
4. Manual testing with corrupted files
5. Performance validation
**Failure Response**: Test failure indicates corruption risk. Do not commit until all tests pass.
## Running Tests
### During Development
1. Copy or symlink the test file to your scene
2. Add as a child node or autoload temporarily
3. Run the project and observe console output
4. Remove from project when testing is complete
### Manual Test Execution
### Automated Testing
While Godot doesn't have built-in unit testing, these scripts provide:
- Consistent validation approach
- Repeatable test scenarios
- Clear pass/fail output
- System behavior documentation
#### **Direct Script Execution (Recommended)**
```bash
# Run specific test
godot --headless --script tests/test_checksum_issue.gd
# Run all save system tests
godot --headless --script tests/test_checksum_issue.gd
godot --headless --script tests/test_migration_compatibility.gd
godot --headless --script tests/test_save_system_integration.gd
```
#### **Other Methods**
- **Temporary Autoload**: Add to project.godot autoloads temporarily, run with F5
- **Scene-based**: Create temporary scene, add test script as child, run with F6
- **Editor**: Open test file, attach to scene, run with F6
### Automated Test Execution
Use provided scripts `run_tests.bat` (Windows) or `run_tests.sh` (Linux/Mac) to run all tests sequentially.
For CI/CD integration:
```yaml
- name: Run Test Suite
run: |
godot --headless --script tests/test_checksum_issue.gd
godot --headless --script tests/test_migration_compatibility.gd
# Add other tests as needed
```
### Expected Test Output
#### **Successful Test Run:**
```
=== Testing Checksum Issue Fix ===
Testing checksum consistency across save/load cycles...
✅ SUCCESS: Checksums are deterministic
✅ SUCCESS: JSON serialization doesn't break checksums
✅ SUCCESS: Save/load cycle maintains checksum integrity
=== Test Complete ===
```
#### **Failed Test Run:**
```
=== Testing Checksum Issue Fix ===
Testing checksum consistency across save/load cycles...
❌ FAILURE: Checksum mismatch detected
Expected: 1234567890
Got: 9876543210
=== Test Failed ===
```
### Test Execution Best Practices
**Before**: Remove existing save files, verify autoloads configured, run one test at a time
**During**: Monitor console output, note timing (tests complete within seconds)
**After**: Clean up temporary files, document issues
### Troubleshooting
**Common Issues:**
- Permission errors: Run with elevated permissions if needed
- Missing dependencies: Ensure autoloads configured
- Timeout issues: Add timeout for hung tests
- Path issues: Use absolute paths if relative paths fail
### Performance Benchmarks
Expected execution times: Individual tests < 5 seconds, total suite < 35 seconds.
If tests take longer, investigate file I/O issues, memory leaks, infinite loops, or external dependencies.
## Best Practices
1. **Document Expected Behavior**: Include comments about what should happen
2. **Test Boundary Conditions**: Include edge cases and error conditions
3. **Measure Performance**: Add timing for performance-critical components
4. **Visual Validation**: For UI components, include visual checks
5. **Cleanup After Tests**: Restore initial state to avoid side effects
1. Document expected behavior
2. Test boundary conditions and edge cases
3. Measure performance for critical components
4. Include visual validation for UI components
5. Cleanup after tests
## Contributing
When adding new test files:
1. Follow the naming and structure conventions
2. Update this README with new test descriptions
When adding test files:
1. Follow naming and structure conventions
2. Update this README with test descriptions
3. Ensure tests are self-contained and documented
4. Test both success and failure scenarios
5. Include performance considerations where relevant
4. Test success and failure scenarios
This testing approach helps maintain code quality and provides validation tools for system changes and refactoring.
This testing approach maintains code quality and provides validation tools for system changes.

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

View File

@@ -14,6 +14,8 @@ config/name="Skelly"
run/main_scene="res://scenes/main/main.tscn"
config/features=PackedStringArray("4.4", "Mobile")
config/icon="res://icon.svg"
boot_splash/handheld/orientation=0
boot_splash/stretch/aspect="keep"
[audio]
@@ -27,9 +29,36 @@ GameManager="*res://src/autoloads/GameManager.gd"
LocalizationManager="*res://src/autoloads/LocalizationManager.gd"
DebugManager="*res://src/autoloads/DebugManager.gd"
SaveManager="*res://src/autoloads/SaveManager.gd"
UIConstants="*res://src/autoloads/UIConstants.gd"
[display]
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="canvas_items"
window/handheld/orientation=4
[input]
ui_pause={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
]
}
any_key={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(165, 16),"global_position":Vector2(174, 64),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
]
}
ui_menu_toggle={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
]
}
action_south={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
@@ -178,6 +207,12 @@ quit_game={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194335,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
ui_back={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
]
}
[internationalization]

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
)

116
run_tests.bat Normal file
View File

@@ -0,0 +1,116 @@
@echo off
setlocal enabledelayedexpansion
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
set failed_tests=0
echo.
echo Discovered test files:
call :discover_tests "tests" ""
call :discover_tests "tests\unit" "Unit: "
call :discover_tests "tests\integration" "Integration: "
echo.
echo Starting test execution...
echo.
call :run_tests "tests" ""
call :run_tests "tests\unit" "Unit: "
call :run_tests "tests\integration" "Integration: "
set /a passed_tests=total_tests-failed_tests
echo ================================
echo Test Execution Summary
echo ================================
echo Total Tests Run: !total_tests!
echo Tests Passed: !passed_tests!
echo Tests Failed: !failed_tests!
if !failed_tests! equ 0 (
echo ✅ ALL TESTS PASSED!
) else (
echo!failed_tests! TEST(S) FAILED
)
pause
goto :eof
:discover_tests
set "test_dir=%~1"
set "prefix=%~2"
if exist "%test_dir%\" (
for %%f in ("%test_dir%\test_*.gd") do (
call :format_test_name "%%~nf" test_name
echo %prefix%!test_name!: %%f
)
)
goto :eof
:run_tests
set "test_dir=%~1"
set "prefix=%~2"
if exist "%test_dir%\" (
for %%f in ("%test_dir%\test_*.gd") do (
call :format_test_name "%%~nf" test_name
call :run_single_test "%%f" "%prefix%!test_name!"
)
)
goto :eof
:format_test_name
set "filename=%~1"
set "result=%filename:test_=%"
set "%~2=%result:_= %"
goto :eof
:run_single_test
set "test_file=%~1"
set "test_name=%~2"
echo.
echo === %test_name% ===
echo Running: %test_file%
REM Run the test and capture the exit code
godot --headless --script "%test_file%" >temp_test_output.txt 2>&1
set test_exit_code=!errorlevel!
REM Display results based on exit code
if !test_exit_code! equ 0 (
echo PASSED: %test_name%
) else (
echo FAILED: %test_name%
set /a failed_tests+=1
)
set /a total_tests+=1
REM Clean up temporary file
if exist temp_test_output.txt del temp_test_output.txt
echo.
goto :eof

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,8 +111,12 @@ 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("action_south") and Input.is_action_pressed("action_north"):
if event.is_action_pressed("ui_back"):
# Handle gamepad/keyboard back action - same as back button
_on_back_button_pressed()
elif event.is_action_pressed("action_south") and Input.is_action_pressed("action_north"):
# Debug: Switch to clickomania when primary+secondary actions pressed together
if current_gameplay_mode == "match3":
set_gameplay_mode("clickomania")

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=4 format=3 uid="uid://dmwkyeq2l7u04"]
[ext_resource type="Script" uid="uid://b16jnk7w22mb" path="res://scenes/game/game.gd" id="1_uwrxv"]
[ext_resource type="Script" uid="uid://bs4veuda3h358" path="res://scenes/game/game.gd" id="1_uwrxv"]
[ext_resource type="PackedScene" path="res://scenes/ui/DebugToggle.tscn" id="3_debug"]
[ext_resource type="Texture2D" uid="uid://c8y6tlvcgh2gn" path="res://assets/textures/backgrounds/beanstalk-dark.webp" id="5_background"]
@@ -21,6 +21,7 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("5_background")
expand_mode = 1
stretch_mode = 1
[node name="UI" type="Control" parent="."]
@@ -53,7 +54,9 @@ grow_vertical = 2
[node name="BackButtonContainer" type="Control" parent="."]
layout_mode = 1
anchors_preset = 0
anchors_preset = 1
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 10.0
offset_top = 10.0
offset_right = 55.0

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,31 +27,35 @@ 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():
# Generate unique instance ID for debugging
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")
grid_initialized = true
# Always calculate grid layout first
# Calculate grid layout
_calculate_grid_layout()
# Try to load saved state first, otherwise use default initialization
# Try to load saved state, otherwise use default
var loaded_saved_state = await load_saved_state()
if not loaded_saved_state:
DebugManager.log_info("No saved state found, using default grid initialization", "Match3")
@@ -66,12 +65,13 @@ func _ready():
DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3")
# Emit signal to notify UI components (like debug menu) that grid state is fully loaded
# Notify UI that grid state is loaded
grid_state_loaded.emit(GRID_SIZE, TILE_TYPES)
# Debug: Check scene tree structure immediately
# 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
@@ -82,16 +82,16 @@ func _calculate_grid_layout():
var max_tile_height = available_height / GRID_SIZE.y
tile_size = min(max_tile_width, max_tile_height)
# Align grid to left side with configurable margins
# 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)
@@ -107,17 +107,24 @@ func _initialize_grid():
# Set gem types for this tile
tile.set_active_gem_types(gem_indices)
# Set tile type after adding to scene tree so sprite reference is available
# Set tile type after adding to scene tree
var new_type = randi() % TILE_TYPES
tile.tile_type = new_type
# Connect tile signals
tile.tile_selected.connect(_on_tile_selected)
DebugManager.log_debug("Created tile at grid(%d,%d) world_pos(%s) with type %d" % [x, y, tile_position, new_type], "Match3")
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:
# Comprehensive bounds and null checks
# Bounds and null checks
if not _is_valid_grid_position(pos):
return false
@@ -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,7 +150,8 @@ func _has_match_at(pos: Vector2i) -> bool:
var matches_vertical = _get_match_line(pos, Vector2i(0, 1))
return matches_vertical.size() >= 3
# Fixed: Add missing function to check for any matches on the board
# Check for any matches on the board
func _check_for_matches() -> bool:
for y in range(GRID_SIZE.y):
for x in range(GRID_SIZE.x):
@@ -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
@@ -173,7 +188,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
var line = [start_tile]
var type = start_tile.tile_type
# Check in both directions separately with safety limits
# Check both directions with safety limits
for offset in [1, -1]:
var current = start + dir * offset
var steps = 0
@@ -194,8 +209,9 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
return line
func _clear_matches():
# Safety check for grid integrity
# Check grid integrity
if not _validate_grid_integrity():
DebugManager.log_error("Grid integrity check failed in _clear_matches", "Match3")
return
@@ -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,43 +1,95 @@
extends Control
@onready var press_any_key_screen = $PressAnyKeyScreen
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():
DebugManager.log_debug("Main scene ready", "Main")
press_any_key_screen.any_key_pressed.connect(_on_any_key_pressed)
func _on_any_key_pressed():
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() -> void:
# Wait for all nodes to be ready
await get_tree().process_frame
await get_tree().process_frame
# Try to find SplashScreen node
splash_screen = get_node_or_null("SplashScreen")
if not splash_screen:
DebugManager.log_warn("SplashScreen node not found, trying alternative methods", "Main")
# Try to find by class or group
var splash_nodes = get_tree().get_nodes_in_group("localizable")
for node in splash_nodes:
if node.scene_file_path.ends_with("SplashScreen.tscn"):
splash_screen = node
break
if splash_screen:
DebugManager.log_debug("SplashScreen node found: %s" % splash_screen.name, "Main")
# Try connecting to the signal if it exists
if splash_screen.has_signal("any_key_pressed"):
splash_screen.any_key_pressed.connect(_on_any_key_pressed)
DebugManager.log_debug("Connected to any_key_pressed signal", "Main")
else:
# Fallback: use input handling directly on the main scene
DebugManager.log_warn("Using fallback input handling", "Main")
_use_fallback_input_handling()
else:
DebugManager.log_error("Could not find SplashScreen node", "Main")
_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: 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() -> void:
DebugManager.log_debug("Transitioning to main menu", "Main")
press_any_key_screen.queue_free()
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

@@ -1,16 +0,0 @@
extends Control
signal any_key_pressed
func _ready():
DebugManager.log_debug("PressAnyKeyScreen ready", "PressAnyKey")
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):
DebugManager.log_debug("Action pressed: " + str(event), "PressAnyKey")
any_key_pressed.emit()
get_viewport().set_input_as_handled()
func update_text():
$PressKeyContainer/PressKeyLabel.text = tr("press_ok_continue")

View File

@@ -0,0 +1,27 @@
extends Control
signal any_key_pressed
func _ready() -> void:
DebugManager.log_debug("SplashScreen ready", "SplashScreen")
update_text()
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() -> void:
$SplashContainer/ContinueLabel.text = tr("press_ok_continue")

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=16 format=3 uid="uid://gbe1jarrwqsi"]
[ext_resource type="Script" uid="uid://cq7or0bcm2xfj" path="res://scenes/main/PressAnyKeyScreen.gd" id="1_0a4p2"]
[ext_resource type="Script" uid="uid://cq7or0bcm2xfj" path="res://scenes/main/SplashScreen.gd" id="1_0a4p2"]
[ext_resource type="Texture2D" uid="uid://bcr4bokw87m5n" path="res://assets/sprites/characters/skeleton/Skeleton Idle.png" id="2_rjjcb"]
[ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="3_debug"]
@@ -89,7 +89,7 @@ animations = [{
"speed": 5.0
}]
[node name="PressAnyKeyScreen" type="Control" groups=["localizable"]]
[node name="SplashScreen" type="Control" groups=["localizable"]]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -98,7 +98,7 @@ grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_0a4p2")
[node name="PressKeyContainer" type="VBoxContainer" parent="."]
[node name="SplashContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
@@ -113,24 +113,21 @@ grow_horizontal = 2
grow_vertical = 2
metadata/_edit_use_anchors_ = true
[node name="AspectRatioContainer" type="AspectRatioContainer" parent="PressKeyContainer"]
[node name="SpriteContainer" type="Control" parent="SplashContainer"]
custom_minimum_size = Vector2(30, 32)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 0
alignment_horizontal = 0
alignment_vertical = 0
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="PressKeyContainer/AspectRatioContainer"]
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="SplashContainer/SpriteContainer"]
sprite_frames = SubResource("SpriteFrames_wtrhp")
autoplay = "default"
offset = Vector2(0, -30)
[node name="TitleLabel" type="Label" parent="PressKeyContainer"]
[node name="TitleLabel" type="Label" parent="SplashContainer"]
layout_mode = 2
text = "Skelly"
horizontal_alignment = 1
[node name="PressKeyLabel" type="Label" parent="PressKeyContainer"]
[node name="ContinueLabel" type="Label" parent="SplashContainer"]
layout_mode = 2
text = "`press_ok_continue`"

View File

@@ -1,7 +1,7 @@
[gd_scene load_steps=5 format=3 uid="uid://ci2gk11211n0d"]
[ext_resource type="Script" uid="uid://rvuchiy0guv3" path="res://scenes/main/Main.gd" id="1_0wfyh"]
[ext_resource type="PackedScene" uid="uid://gbe1jarrwqsi" path="res://scenes/main/PressAnyKeyScreen.tscn" id="1_o5qli"]
[ext_resource type="PackedScene" uid="uid://gbe1jarrwqsi" path="res://scenes/main/SplashScreen.tscn" id="1_o5qli"]
[ext_resource type="Texture2D" uid="uid://c8y6tlvcgh2gn" path="res://assets/textures/backgrounds/beanstalk-dark.webp" id="2_sugp2"]
[ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="4_v7g8d"]
@@ -22,9 +22,10 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("2_sugp2")
expand_mode = 1
stretch_mode = 1
[node name="PressAnyKeyScreen" parent="." instance=ExtResource("1_o5qli")]
[node name="SplashScreen" parent="." instance=ExtResource("1_o5qli")]
layout_mode = 1
[node name="DebugToggle" parent="." instance=ExtResource("4_v7g8d")]

20
scenes/ui/DebugButton.gd Normal file
View File

@@ -0,0 +1,20 @@
extends Control
@onready var button: Button = $Button
func _ready():
button.pressed.connect(_on_button_pressed)
DebugManager.debug_ui_toggled.connect(_on_debug_ui_toggled)
# Initialize with current debug UI state
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")

View File

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

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

@@ -1,7 +1,72 @@
[gd_scene load_steps=3 format=3 uid="uid://m8lf3eh3al5j"]
[gd_scene load_steps=13 format=3 uid="uid://m8lf3eh3al5j"]
[ext_resource type="Script" uid="uid://b2x0kw8f70s8q" path="res://scenes/ui/MainMenu.gd" id="1_b00nv"]
[ext_resource type="PackedScene" uid="uid://df2b4wn8j6cxl" path="res://scenes/ui/DebugToggle.tscn" id="2_debug"]
[ext_resource type="Texture2D" uid="uid://btfjyc4jfhiii" path="res://assets/sprites/characters/skeleton/Skeleton Hit.png" id="2_iwbf0"]
[sub_resource type="AtlasTexture" id="AtlasTexture_2ysvc"]
atlas = ExtResource("2_iwbf0")
region = Rect2(0, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_xpiny"]
atlas = ExtResource("2_iwbf0")
region = Rect2(30, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_bhu4a"]
atlas = ExtResource("2_iwbf0")
region = Rect2(60, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_e2per"]
atlas = ExtResource("2_iwbf0")
region = Rect2(90, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_7mi0g"]
atlas = ExtResource("2_iwbf0")
region = Rect2(120, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_nqjyj"]
atlas = ExtResource("2_iwbf0")
region = Rect2(150, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_7vr37"]
atlas = ExtResource("2_iwbf0")
region = Rect2(180, 0, 30, 32)
[sub_resource type="AtlasTexture" id="AtlasTexture_kncl5"]
atlas = ExtResource("2_iwbf0")
region = Rect2(210, 0, 30, 32)
[sub_resource type="SpriteFrames" id="SpriteFrames_clp4r"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_2ysvc")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xpiny")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_bhu4a")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_e2per")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_7mi0g")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_nqjyj")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_7vr37")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_kncl5")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[node name="MainMenu" type="Control"]
layout_mode = 3
@@ -13,6 +78,7 @@ grow_vertical = 2
script = ExtResource("1_b00nv")
[node name="MenuContainer" type="VBoxContainer" parent="."]
custom_minimum_size = Vector2(200, 100)
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
@@ -25,6 +91,17 @@ offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_use_anchors_ = true
[node name="SpriteContainer" type="Control" parent="MenuContainer"]
custom_minimum_size = Vector2(30, 32)
layout_mode = 2
size_flags_horizontal = 4
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="MenuContainer/SpriteContainer"]
sprite_frames = SubResource("SpriteFrames_clp4r")
autoplay = "default"
frame_progress = 0.574348
[node name="NewGameButton" type="Button" parent="MenuContainer"]
layout_mode = 2

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

@@ -16,7 +16,7 @@ signal value_changed(new_value: String, new_index: int)
@onready var right_button: Button = $RightButton
@onready var value_display: Label = $ValueDisplay
## The data source for values. Override this for custom implementations.
## The data source for values.
@export var data_source: String = "language"
## Custom display format function. Leave empty to use default.
@export var custom_format_function: String = ""
@@ -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,30 +6,37 @@ 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 - just load the game scene
# 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 for gameplay mode
# Input validation
if not gameplay_mode or gameplay_mode.is_empty():
DebugManager.log_error("Empty or null gameplay mode provided", "GameManager")
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
@@ -37,10 +44,13 @@ func start_game_with_mode(gameplay_mode: String) -> void:
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
return
# Validate gameplay mode against allowed values
# 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,11 +64,13 @@ 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
# Wait for scene to be properly instantiated and added to tree
# Wait for scene instantiation and tree addition
await get_tree().process_frame
await get_tree().process_frame # Additional frame for complete initialization
@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -2,27 +2,27 @@ extends Node
const LANGUAGES_JSON_PATH := "res://localization/languages.json"
const SETTINGS_FILE = "user://settings.cfg"
const MAX_JSON_FILE_SIZE = 65536 # 64KB limit for languages.json
const MAX_SETTING_STRING_LENGTH = 10 # Max length for string settings like language code
# 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)
@@ -37,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")
@@ -62,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()
@@ -81,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")
@@ -97,22 +118,51 @@ 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":
return value is float and value >= 0.0 and value <= 1.0
# Enhanced numeric validation with NaN/Infinity checks
if not (value is float or value is int):
return false
# Convert to float for validation
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"
)
return false
# Range validation
return float_value >= 0.0 and float_value <= 1.0
"language":
if not value is String:
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"
)
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"
)
return false
# Check if language is supported
if languages_data.has("languages"):
if languages_data.has("languages") and languages_data.languages is Dictionary:
return value in languages_data.languages
else:
# Fallback to basic validation if languages not loaded
@@ -120,26 +170,52 @@ func _validate_setting_value(key: String, value) -> bool:
# Default validation: accept if type matches default setting type
var default_value = default_settings.get(key)
if default_value == null:
DebugManager.log_warn("Unknown setting key in validation: %s" % key, "SettingsManager")
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"
)
file.close()
_load_default_languages()
return
if file_size == 0:
DebugManager.log_error("Languages.json file is empty", "SettingsManager")
file.close()
_load_default_languages()
return
@@ -148,14 +224,25 @@ 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
# Validate the JSON string is not empty
if json_string.is_empty():
DebugManager.log_error("Languages.json contains empty content", "SettingsManager")
_load_default_languages()
return
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
@@ -164,28 +251,82 @@ func load_languages():
_load_default_languages()
return
languages_data = json.data
if languages_data.has("languages") and languages_data.languages is Dictionary:
DebugManager.log_info("Languages loaded: " + str(languages_data.languages.keys()), "SettingsManager")
else:
DebugManager.log_warn("Languages.json missing 'languages' dictionary, using defaults", "SettingsManager")
# Validate the structure of the JSON data
if not _validate_languages_structure(json.data):
DebugManager.log_error("Languages.json structure validation failed", "SettingsManager")
_load_default_languages()
return
languages_data = json.data
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():
settings[key] = default_settings[key]
_apply_setting_side_effect(key, settings[key])
save_settings()
var save_success = save_settings()
if save_success:
DebugManager.log_info("Settings reset completed successfully", "SettingsManager")
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"):
DebugManager.log_error("Languages.json missing 'languages' key", "SettingsManager")
return false
var languages = data["languages"]
if not languages is Dictionary:
DebugManager.log_error("'languages' is not a dictionary", "SettingsManager")
return false
if languages.is_empty():
DebugManager.log_error("Languages dictionary is empty", "SettingsManager")
return false
# 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"
)
return false
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager")
return false
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"
)
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"
)
return false
return true

View File

@@ -0,0 +1,41 @@
extends Node
## UI Constants for the Skelly project
##
## Contains shared UI constants, sizes, colors, and other UI-related values
## to maintain consistency across the game interface.
# Screen and viewport constants
const REFERENCE_RESOLUTION := Vector2i(1920, 1080)
const MIN_RESOLUTION := Vector2i(720, 480)
# Animation constants
const FADE_DURATION := 0.3
const BUTTON_HOVER_SCALE := 1.1
const BUTTON_PRESS_SCALE := 0.95
# UI spacing constants
const UI_MARGIN := 20
const BUTTON_SPACING := 10
const MENU_PADDING := 40
# Debug UI constants
const DEBUG_PANEL_WIDTH := 300
const DEBUG_BUTTON_SIZE := Vector2(80, 40)
# Color constants (using Godot's Color class)
const UI_PRIMARY_COLOR := Color.WHITE
const UI_SECONDARY_COLOR := Color(0.8, 0.8, 0.8)
const UI_ACCENT_COLOR := Color(0.2, 0.6, 1.0)
const UI_WARNING_COLOR := Color(1.0, 0.6, 0.2)
const UI_ERROR_COLOR := Color(1.0, 0.2, 0.2)
# Font sizes (relative to default)
const FONT_SIZE_SMALL := 14
const FONT_SIZE_NORMAL := 18
const FONT_SIZE_LARGE := 24
const FONT_SIZE_TITLE := 32
func _ready():
DebugManager.log_info("UIConstants loaded successfully", "UIConstants")

View File

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

220
tests/helpers/TestHelper.gd Normal file
View File

@@ -0,0 +1,220 @@
class_name TestHelper
extends RefCounted
## Common test utilities and assertions for Skelly project testing
##
## Provides standardized testing functions, assertions, and utilities
## 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
## Performance tracking
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):
print("\n=== Testing %s ===" % test_name)
tests_run = 0
tests_passed = 0
tests_failed = 0
test_start_time = Time.get_unix_time_from_system()
## Print test section footer with results summary
static func print_test_footer(test_name: String):
var end_time = Time.get_unix_time_from_system()
var duration = end_time - test_start_time
print("\n--- %s Results ---" % test_name)
print("Tests Run: %d" % tests_run)
print("Passed: %d" % tests_passed)
print("Failed: %d" % tests_failed)
print("Duration: %.3f seconds" % duration)
if tests_failed == 0:
print("✅ All tests PASSED")
else:
print("%d tests FAILED" % tests_failed)
print("=== %s Complete ===" % test_name)
## Assert that a condition is true
static func assert_true(condition: bool, message: String = ""):
tests_run += 1
if condition:
tests_passed += 1
print("✅ PASS: %s" % message)
else:
tests_failed += 1
print("❌ FAIL: %s" % message)
## Assert that a condition is false
static func assert_false(condition: bool, message: String = ""):
assert_true(not condition, message)
## Assert that two values are equal
static func assert_equal(expected, actual, message: String = ""):
var condition = expected == actual
var full_message = message
if not full_message.is_empty():
full_message += " "
full_message += "(Expected: %s, Got: %s)" % [str(expected), str(actual)]
assert_true(condition, full_message)
## Assert that two values are not equal
static func assert_not_equal(expected, actual, message: String = ""):
var condition = expected != actual
var full_message = message
if not full_message.is_empty():
full_message += " "
full_message += "(Should not equal: %s, Got: %s)" % [str(expected), str(actual)]
assert_true(condition, full_message)
## Assert that a value is null
static func assert_null(value, message: String = ""):
assert_true(value == null, message + " (Should be null, got: %s)" % str(value))
## Assert that a value is not null
static func assert_not_null(value, message: String = ""):
assert_true(value != null, message + " (Should not be null)")
## Assert that a value is within a range
static func assert_in_range(value: float, min_val: float, max_val: float, message: String = ""):
var condition = value >= min_val and value <= max_val
var full_message = "%s (Value: %f, Range: %f-%f)" % [message, value, min_val, max_val]
assert_true(condition, full_message)
## Assert that two floating-point values are approximately equal (with tolerance)
static func assert_float_equal(expected: float, actual: float, tolerance: float = 0.0001, message: String = ""):
# Handle special cases: both infinity, both negative infinity, both NaN
if is_inf(expected) and is_inf(actual):
var condition = (expected > 0) == (actual > 0) # Same sign of infinity
var full_message = "%s (Both infinity values: Expected: %f, Got: %f)" % [message, expected, actual]
assert_true(condition, full_message)
return
if is_nan(expected) and is_nan(actual):
var full_message = "%s (Both NaN values: Expected: %f, Got: %f)" % [message, expected, actual]
assert_true(true, full_message) # Both NaN is considered equal
return
# Normal floating-point comparison
var difference = abs(expected - actual)
var condition = difference <= tolerance
var full_message = "%s (Expected: %f, Got: %f, Difference: %f, Tolerance: %f)" % [message, expected, actual, difference, tolerance]
assert_true(condition, full_message)
## Assert that an array contains a specific value
static func assert_contains(array: Array, value, message: String = ""):
var condition = value in array
var full_message = "%s (Array: %s, Looking for: %s)" % [message, str(array), str(value)]
assert_true(condition, full_message)
## Assert that an array does not contain a specific value
static func assert_not_contains(array: Array, value, message: String = ""):
var condition = not (value in array)
var full_message = "%s (Array: %s, Should not contain: %s)" % [message, str(array), str(value)]
assert_true(condition, full_message)
## Assert that a dictionary has a specific key
static func assert_has_key(dict: Dictionary, key, message: String = ""):
var condition = dict.has(key)
var full_message = "%s (Dictionary keys: %s, Looking for: %s)" % [message, str(dict.keys()), str(key)]
assert_true(condition, full_message)
## Assert that a file exists
static func assert_file_exists(path: String, message: String = ""):
var condition = FileAccess.file_exists(path)
var full_message = "%s (Path: %s)" % [message, path]
assert_true(condition, full_message)
## Assert that a file does not exist
static func assert_file_not_exists(path: String, message: String = ""):
var condition = not FileAccess.file_exists(path)
var full_message = "%s (Path: %s)" % [message, path]
assert_true(condition, full_message)
## Performance testing - start timing
static func start_performance_test(test_id: String):
performance_data[test_id] = Time.get_unix_time_from_system()
## Performance testing - end timing and validate
static func end_performance_test(test_id: String, max_duration_ms: float, message: String = ""):
if not performance_data.has(test_id):
assert_true(false, "Performance test '%s' was not started" % test_id)
return
var start_time = performance_data[test_id]
var end_time = Time.get_unix_time_from_system()
var duration_ms = (end_time - start_time) * 1000.0
var condition = duration_ms <= max_duration_ms
var full_message = "%s (Duration: %.2fms, Max: %.2fms)" % [message, duration_ms, max_duration_ms]
assert_true(condition, full_message)
performance_data.erase(test_id)
## Create a temporary test file with content
static func create_temp_file(filename: String, content: String = "") -> String:
var temp_path = "user://test_" + filename
var file = FileAccess.open(temp_path, FileAccess.WRITE)
if file:
file.store_string(content)
file.close()
return temp_path
## Clean up temporary test file
static func cleanup_temp_file(path: String):
if FileAccess.file_exists(path):
DirAccess.remove_absolute(path)
## Create invalid JSON content for testing
static func create_invalid_json() -> String:
return '{"invalid": json, missing_quotes: true, trailing_comma: true,}'
## Create valid test JSON content
static func create_valid_json() -> String:
return '{"test_key": "test_value", "test_number": 42, "test_bool": true}'
## Wait for a specific number of frames
static func wait_frames(frames: int, node: Node):
for i in range(frames):
await node.get_tree().process_frame
## Mock a simple function call counter
class MockCallCounter:
var call_count := 0
var last_args := []
func call_function(args: Array = []):
call_count += 1
last_args = args.duplicate()
func reset():
call_count = 0
last_args.clear()
## Create a mock call counter for testing
static func create_mock_counter() -> MockCallCounter:
return MockCallCounter.new()
## Validate that an object has expected properties
static func assert_has_properties(object: Object, properties: Array, message: String = ""):
for property in properties:
var condition = property in object
var full_message = "%s - Missing property: %s" % [message, property]
assert_true(condition, full_message)
## Validate that an object has expected methods
static func assert_has_methods(object: Object, methods: Array, message: String = ""):
for method in methods:
var condition = object.has_method(method)
var full_message = "%s - Missing method: %s" % [message, method]
assert_true(condition, full_message)
## Print a test step with consistent formatting
static func print_step(step_name: String):
print("\n--- Test: %s ---" % step_name)

View File

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

381
tests/test_audio_manager.gd Normal file
View File

@@ -0,0 +1,381 @@
extends SceneTree
## Comprehensive test suite for AudioManager
##
## Tests audio resource loading, stream configuration, volume management,
## audio bus configuration, and playback control functionality.
## Validates proper audio system initialization and error handling.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var audio_manager: Node
var original_music_volume: float
var original_sfx_volume: float
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("AudioManager")
# Get reference to AudioManager
audio_manager = root.get_node("AudioManager")
if not audio_manager:
TestHelper.assert_true(false, "AudioManager autoload not found")
TestHelper.print_test_footer("AudioManager")
return
# Store original settings for restoration
var settings_manager = root.get_node("SettingsManager")
original_music_volume = settings_manager.get_setting("music_volume")
original_sfx_volume = settings_manager.get_setting("sfx_volume")
# Run test suites
test_basic_functionality()
test_audio_constants()
test_audio_player_initialization()
test_stream_loading_and_validation()
test_audio_bus_configuration()
test_volume_management()
test_music_playback_control()
test_ui_sound_effects()
test_stream_loop_configuration()
test_error_handling()
# Cleanup and restore original state
cleanup_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"
)
# Test that AudioManager has expected methods
var expected_methods = ["update_music_volume", "play_ui_click"]
TestHelper.assert_has_methods(audio_manager, expected_methods, "AudioManager methods")
# 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"
)
func test_audio_constants():
TestHelper.print_step("Audio File Constants")
# Test path format validation
var music_path = audio_manager.MUSIC_PATH
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"
)
# Test file extensions
var valid_audio_extensions = [".wav", ".ogg", ".mp3"]
var music_has_valid_ext = false
var click_has_valid_ext = false
for ext in valid_audio_extensions:
if music_path.ends_with(ext):
music_has_valid_ext = true
if click_path.ends_with(ext):
click_has_valid_ext = true
TestHelper.assert_true(music_has_valid_ext, "Music file has valid audio extension")
TestHelper.assert_true(click_has_valid_ext, "Click sound has valid audio extension")
# Test that audio files exist
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"
)
# 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"
)
# 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"
)
func test_stream_loading_and_validation():
TestHelper.print_step("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"
)
# 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"
)
# Test stream resource loading directly
var loaded_music = load(audio_manager.MUSIC_PATH)
TestHelper.assert_not_null(loaded_music, "Music resource loads successfully")
TestHelper.assert_true(loaded_music is AudioStream, "Loaded music is AudioStream type")
var loaded_click = load(audio_manager.UI_CLICK_SOUND_PATH)
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")
# Test that required audio buses exist
var music_bus_index = AudioServer.get_bus_index("Music")
var sfx_bus_index = AudioServer.get_bus_index("SFX")
TestHelper.assert_true(music_bus_index >= 0, "Music audio bus exists")
TestHelper.assert_true(sfx_bus_index >= 0, "SFX audio bus exists")
# 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"
)
if sfx_bus_index >= 0:
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")
# Store original volume
var settings_manager = root.get_node("SettingsManager")
var original_volume = settings_manager.get_setting("music_volume")
var was_playing = audio_manager.music_player.playing
# 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"
)
# 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"
)
# 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"
)
# 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
)
# 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"
)
# Test playback state management
# Note: We test the control methods exist and can be called safely
var original_playing = audio_manager.music_player.playing
# Test that playback methods can be called without errors
if audio_manager.has_method("_start_music"):
# Method exists but is private - test that the logic is sound
TestHelper.assert_true(true, "Private _start_music method exists")
if audio_manager.has_method("_stop_music"):
# Method exists but is private - test that the logic is sound
TestHelper.assert_true(true, "Private _stop_music method exists")
# Test volume-based playback control
var settings_manager = root.get_node("SettingsManager")
var current_volume = settings_manager.get_setting("music_volume")
if current_volume > 0.0:
audio_manager.update_music_volume(current_volume)
TestHelper.assert_true(true, "Volume-based playback start works")
else:
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")
# Test UI click functionality
TestHelper.assert_not_null(audio_manager.ui_click_player, "UI click player exists")
TestHelper.assert_not_null(audio_manager.click_stream, "Click stream is loaded")
# Test that play_ui_click can be called safely
var original_stream = audio_manager.ui_click_player.stream
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"
)
# Test multiple rapid clicks (should not cause errors)
for i in range(3):
audio_manager.play_ui_click()
TestHelper.assert_true(true, "Rapid click %d handled safely" % (i + 1))
# Test click with null stream
var backup_stream = audio_manager.click_stream
audio_manager.click_stream = null
audio_manager.play_ui_click() # Should not crash
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")
# Test that music stream has loop configuration
var music_stream = audio_manager.music_player.stream
if music_stream:
if music_stream is AudioStreamWAV:
# For WAV files, check loop mode
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"
)
elif music_stream is AudioStreamOggVorbis:
# For OGG files, check loop property
var has_loop = "loop" in music_stream
TestHelper.assert_true(has_loop, "OGG stream has loop property")
if has_loop:
TestHelper.assert_true(music_stream.loop, "OGG stream loop enabled")
# 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")
# Test graceful handling of missing resources
# 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"
)
# 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"
)
# Test null stream handling in play_ui_click
var original_click_stream = audio_manager.click_stream
audio_manager.click_stream = null
# This should not crash
audio_manager.play_ui_click()
TestHelper.assert_true(true, "play_ui_click handles null stream gracefully")
# Restore original stream
audio_manager.click_stream = original_click_stream
# Test volume edge cases
audio_manager.update_music_volume(0.0)
TestHelper.assert_true(true, "Zero volume handled safely")
audio_manager.update_music_volume(1.0)
TestHelper.assert_true(true, "Maximum volume handled safely")
func cleanup_tests():
TestHelper.print_step("Cleanup")
# Restore original volume settings
var settings_manager = root.get_node("SettingsManager")
settings_manager.set_setting("music_volume", original_music_volume)
settings_manager.set_setting("sfx_volume", original_sfx_volume)
# Update AudioManager to original settings
audio_manager.update_music_volume(original_music_volume)
TestHelper.assert_true(true, "Test cleanup completed")

View File

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

311
tests/test_game_manager.gd Normal file
View File

@@ -0,0 +1,311 @@
extends SceneTree
## Test suite for GameManager
##
## Tests scene transitions, input validation, and gameplay modes.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var game_manager: Node
var original_scene: Node
var test_scenes_created: Array[String] = []
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("GameManager")
# Get reference to GameManager
game_manager = root.get_node("GameManager")
if not game_manager:
TestHelper.assert_true(false, "GameManager autoload not found")
TestHelper.print_test_footer("GameManager")
return
# Store original scene reference
original_scene = current_scene
# Run test suites
test_basic_functionality()
test_scene_constants()
test_input_validation()
test_race_condition_protection()
test_gameplay_mode_validation()
test_scene_transition_safety()
test_error_handling()
test_scene_method_validation()
test_pending_mode_management()
# Cleanup
cleanup_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"
)
# 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"
]
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_false(game_manager.is_changing_scene, "Initial scene change flag")
func test_scene_constants():
TestHelper.print_step("Scene Path Constants")
# Test that scene path constants are defined and valid
TestHelper.assert_true("GAME_SCENE_PATH" in game_manager, "GAME_SCENE_PATH constant exists")
TestHelper.assert_true("MAIN_SCENE_PATH" in game_manager, "MAIN_SCENE_PATH constant exists")
# Test path format validation
var game_path = game_manager.GAME_SCENE_PATH
var main_path = game_manager.MAIN_SCENE_PATH
TestHelper.assert_true(game_path.begins_with("res://"), "Game scene path uses res:// protocol")
TestHelper.assert_true(game_path.ends_with(".tscn"), "Game scene path has .tscn extension")
TestHelper.assert_true(main_path.begins_with("res://"), "Main scene path uses res:// protocol")
TestHelper.assert_true(main_path.ends_with(".tscn"), "Main scene path has .tscn extension")
# Test that scene files exist
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")
# Store original state
var original_changing = game_manager.is_changing_scene
var original_mode = game_manager.pending_gameplay_mode
# 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"
)
# 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"
)
# 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"
)
# 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"
)
func test_race_condition_protection():
TestHelper.print_step("Race Condition Protection")
# Store original state
var original_mode = game_manager.pending_gameplay_mode
# Simulate concurrent scene change attempt
game_manager.is_changing_scene = true
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_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"
)
# Reset state
game_manager.is_changing_scene = false
func test_gameplay_mode_validation():
TestHelper.print_step("Gameplay Mode Validation")
# Test valid modes
var valid_modes = ["match3", "clickomania"]
for mode in valid_modes:
var original_changing = game_manager.is_changing_scene
# We'll test the validation logic without actually changing scenes
# by checking if the function would accept the mode
# Create a temporary mock to test validation
var test_mode_valid = mode in ["match3", "clickomania"]
TestHelper.assert_true(test_mode_valid, "Valid mode accepted: " + mode)
# Test whitelist enforcement
var invalid_modes = ["puzzle", "arcade", "adventure", "rpg", "action"]
for mode in invalid_modes:
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")
# Test scene loading validation (without actually changing scenes)
var game_scene_path = game_manager.GAME_SCENE_PATH
var main_scene_path = game_manager.MAIN_SCENE_PATH
# Test scene resource loading
var game_scene = load(game_scene_path)
TestHelper.assert_not_null(game_scene, "Game scene resource loads successfully")
TestHelper.assert_true(game_scene is PackedScene, "Game scene is PackedScene type")
var main_scene = load(main_scene_path)
TestHelper.assert_not_null(main_scene, "Main scene resource loads successfully")
TestHelper.assert_true(main_scene is PackedScene, "Main scene is PackedScene type")
# Test that current scene exists
TestHelper.assert_not_null(current_scene, "Current scene exists")
func test_error_handling():
TestHelper.print_step("Error Handling")
# Store original state
var original_changing = game_manager.is_changing_scene
var original_mode = game_manager.pending_gameplay_mode
# Test error recovery - verify state is properly reset on errors
# Since we can't easily trigger scene loading errors in tests,
# we'll verify the error handling patterns are in place
# 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"
)
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"
)
func test_scene_method_validation():
TestHelper.print_step("Scene Method Validation")
# Test that GameManager properly checks for required methods
# We'll create a mock scene to test method validation
var mock_scene = Node.new()
# Test method existence checking
var has_set_gameplay_mode = mock_scene.has_method("set_gameplay_mode")
var has_set_global_score = mock_scene.has_method("set_global_score")
var has_get_global_score = mock_scene.has_method("get_global_score")
TestHelper.assert_false(has_set_gameplay_mode, "Mock scene lacks set_gameplay_mode method")
TestHelper.assert_false(has_set_global_score, "Mock scene lacks set_global_score method")
TestHelper.assert_false(has_get_global_score, "Mock scene lacks get_global_score method")
# Clean up mock scene
mock_scene.queue_free()
func test_pending_mode_management():
TestHelper.print_step("Pending Mode Management")
# Store original mode
var original_mode = game_manager.pending_gameplay_mode
# Test that pending mode is properly set for valid inputs
# We'll manually set the pending mode to test the logic
var test_mode = "clickomania"
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"
)
# Test mode preservation during errors
game_manager.pending_gameplay_mode = "match3"
var preserved_mode = game_manager.pending_gameplay_mode
# 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"
)
# Restore original mode
game_manager.pending_gameplay_mode = original_mode
func cleanup_tests():
TestHelper.print_step("Cleanup")
# Reset GameManager state
game_manager.is_changing_scene = false
game_manager.pending_gameplay_mode = "match3"
# Clean up any test files or temporary resources
for scene_path in test_scenes_created:
if ResourceLoader.exists(scene_path):
# Note: Can't actually delete from res:// in tests, just track for manual cleanup
pass
TestHelper.assert_true(true, "Test cleanup completed")

View File

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

View File

@@ -1,106 +1,117 @@
extends Node
extends SceneTree
# Test script for the DebugManager logging system
# Test script for the debug_manager logging system
# This script validates all log levels, filtering, and formatting functionality
# Usage: Add to scene or autoload temporarily to run tests
func _ready():
# Wait a frame for DebugManager to initialize
await get_tree().process_frame
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 ===")
# Get DebugManager reference once
var debug_manager = root.get_node("DebugManager")
# Test 1: Basic log level functionality
test_basic_logging()
test_basic_logging(debug_manager)
# Test 2: Log level filtering
test_log_level_filtering()
test_log_level_filtering(debug_manager)
# Test 3: Category functionality
test_category_logging()
test_category_logging(debug_manager)
# Test 4: Debug mode integration
test_debug_mode_integration()
test_debug_mode_integration(debug_manager)
print("=== Logging System Tests Complete ===")
func test_basic_logging():
func test_basic_logging(debug_manager):
print("\n--- Test 1: Basic Log Level Functionality ---")
# Reset to INFO level for consistent testing
DebugManager.set_log_level(DebugManager.LogLevel.INFO)
debug_manager.set_log_level(debug_manager.LogLevel.INFO)
DebugManager.log_trace("TRACE: This should not appear (below INFO level)")
DebugManager.log_debug("DEBUG: This should not appear (below INFO level)")
DebugManager.log_info("INFO: This message should appear")
DebugManager.log_warn("WARN: This warning should appear")
DebugManager.log_error("ERROR: This error should appear")
DebugManager.log_fatal("FATAL: This fatal error should appear")
debug_manager.log_trace("TRACE: This should not appear (below INFO level)")
debug_manager.log_debug("DEBUG: This should not appear (below INFO level)")
debug_manager.log_info("INFO: This message should appear")
debug_manager.log_warn("WARN: This warning should appear")
debug_manager.log_error("ERROR: This error should appear")
debug_manager.log_fatal("FATAL: This fatal error should appear")
func test_log_level_filtering():
func test_log_level_filtering(debug_manager):
print("\n--- Test 2: Log Level Filtering ---")
# Test DEBUG level
print("Setting log level to DEBUG...")
DebugManager.set_log_level(DebugManager.LogLevel.DEBUG)
DebugManager.log_trace("TRACE: Should not appear (below DEBUG)")
DebugManager.log_debug("DEBUG: Should appear with debug enabled")
DebugManager.log_info("INFO: Should appear")
debug_manager.set_log_level(debug_manager.LogLevel.DEBUG)
debug_manager.log_trace("TRACE: Should not appear (below DEBUG)")
debug_manager.log_debug("DEBUG: Should appear with debug enabled")
debug_manager.log_info("INFO: Should appear")
# Test ERROR level (very restrictive)
print("Setting log level to ERROR...")
DebugManager.set_log_level(DebugManager.LogLevel.ERROR)
DebugManager.log_debug("DEBUG: Should not appear (below ERROR)")
DebugManager.log_warn("WARN: Should not appear (below ERROR)")
DebugManager.log_error("ERROR: Should appear")
DebugManager.log_fatal("FATAL: Should appear")
debug_manager.set_log_level(debug_manager.LogLevel.ERROR)
debug_manager.log_debug("DEBUG: Should not appear (below ERROR)")
debug_manager.log_warn("WARN: Should not appear (below ERROR)")
debug_manager.log_error("ERROR: Should appear")
debug_manager.log_fatal("FATAL: Should appear")
# Reset to INFO for remaining tests
DebugManager.set_log_level(DebugManager.LogLevel.INFO)
debug_manager.set_log_level(debug_manager.LogLevel.INFO)
func test_category_logging():
func test_category_logging(debug_manager):
print("\n--- Test 3: Category Functionality ---")
DebugManager.log_info("Message without category")
DebugManager.log_info("Message with TEST category", "TEST")
DebugManager.log_info("Message with LOGGING category", "LOGGING")
DebugManager.log_warn("Warning with VALIDATION category", "VALIDATION")
DebugManager.log_error("Error with SYSTEM category", "SYSTEM")
debug_manager.log_info("Message without category")
debug_manager.log_info("Message with TEST category", "TEST")
debug_manager.log_info("Message with LOGGING category", "LOGGING")
debug_manager.log_warn("Warning with VALIDATION category", "VALIDATION")
debug_manager.log_error("Error with SYSTEM category", "SYSTEM")
func test_debug_mode_integration():
func test_debug_mode_integration(debug_manager):
print("\n--- Test 4: Debug Mode Integration ---")
# Set to TRACE level to test debug mode dependency
DebugManager.set_log_level(DebugManager.LogLevel.TRACE)
debug_manager.set_log_level(debug_manager.LogLevel.TRACE)
var original_debug_state = DebugManager.is_debug_enabled()
var original_debug_state = debug_manager.is_debug_enabled()
# Test with debug mode OFF
DebugManager.set_debug_enabled(false)
debug_manager.set_debug_enabled(false)
print("Debug mode OFF - TRACE and DEBUG should not appear:")
DebugManager.log_trace("TRACE: Should NOT appear (debug mode OFF)")
DebugManager.log_debug("DEBUG: Should NOT appear (debug mode OFF)")
DebugManager.log_info("INFO: Should appear regardless of debug mode")
debug_manager.log_trace("TRACE: Should NOT appear (debug mode OFF)")
debug_manager.log_debug("DEBUG: Should NOT appear (debug mode OFF)")
debug_manager.log_info("INFO: Should appear regardless of debug mode")
# Test with debug mode ON
DebugManager.set_debug_enabled(true)
debug_manager.set_debug_enabled(true)
print("Debug mode ON - TRACE and DEBUG should appear:")
DebugManager.log_trace("TRACE: Should appear (debug mode ON)")
DebugManager.log_debug("DEBUG: Should appear (debug mode ON)")
DebugManager.log_info("INFO: Should still appear")
debug_manager.log_trace("TRACE: Should appear (debug mode ON)")
debug_manager.log_debug("DEBUG: Should appear (debug mode ON)")
debug_manager.log_info("INFO: Should still appear")
# Restore original debug state
DebugManager.set_debug_enabled(original_debug_state)
DebugManager.set_log_level(DebugManager.LogLevel.INFO)
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():
func test_log_level_enum(debug_manager):
print("\n--- Log Level Enum Values ---")
print("TRACE: ", DebugManager.LogLevel.TRACE)
print("DEBUG: ", DebugManager.LogLevel.DEBUG)
print("INFO: ", DebugManager.LogLevel.INFO)
print("WARN: ", DebugManager.LogLevel.WARN)
print("ERROR: ", DebugManager.LogLevel.ERROR)
print("FATAL: ", DebugManager.LogLevel.FATAL)
print("TRACE: ", debug_manager.LogLevel.TRACE)
print("DEBUG: ", debug_manager.LogLevel.DEBUG)
print("INFO: ", debug_manager.LogLevel.INFO)
print("WARN: ", debug_manager.LogLevel.WARN)
print("ERROR: ", debug_manager.LogLevel.ERROR)
print("FATAL: ", debug_manager.LogLevel.FATAL)

View File

@@ -0,0 +1,449 @@
extends SceneTree
## Test suite for Match3Gameplay
##
## Tests grid initialization, match detection, and scoring system.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var match3_scene: PackedScene
var match3_instance: Node2D
var test_viewport: SubViewport
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Match3 Gameplay")
# Setup test environment
setup_test_environment()
# Run test suites
test_basic_functionality()
test_constants_and_safety_limits()
test_grid_initialization()
test_grid_layout_calculation()
test_state_management()
test_match_detection()
test_scoring_system()
test_input_validation()
test_memory_safety()
test_performance_requirements()
# Cleanup
cleanup_tests()
TestHelper.print_test_footer("Match3 Gameplay")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
# Load Match3 scene
match3_scene = load("res://scenes/game/gameplays/match3_gameplay.tscn")
TestHelper.assert_not_null(match3_scene, "Match3 scene loads successfully")
# Create test viewport for isolated testing
test_viewport = SubViewport.new()
test_viewport.size = Vector2i(800, 600)
root.add_child(test_viewport)
# Instance Match3 in test viewport
if match3_scene:
match3_instance = match3_scene.instantiate()
test_viewport.add_child(match3_instance)
TestHelper.assert_not_null(match3_instance, "Match3 instance created successfully")
# Wait for initialization
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
if not match3_instance:
TestHelper.assert_true(false, "Match3 instance not available for testing")
return
# Test that Match3 has expected properties
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"
]
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"
)
func test_constants_and_safety_limits():
TestHelper.print_step("Constants and Safety Limits")
if not match3_instance:
return
# 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("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(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"
)
# 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"
)
func test_grid_initialization():
TestHelper.print_step("Grid Initialization")
if not match3_instance:
return
# Test grid structure
TestHelper.assert_not_null(match3_instance.grid, "Grid array is initialized")
TestHelper.assert_true(match3_instance.grid is Array, "Grid is Array type")
# Test grid dimensions
var expected_height = match3_instance.GRID_SIZE.y
var expected_width = match3_instance.GRID_SIZE.x
TestHelper.assert_equal(expected_height, match3_instance.grid.size(), "Grid has correct height")
# 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
)
# Test tiles are properly instantiated
var tile_count = 0
var valid_tile_count = 0
for y in range(match3_instance.grid.size()):
for x in range(match3_instance.grid[y].size()):
var tile = match3_instance.grid[y][x]
tile_count += 1
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]
)
# 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_equal(tile_count, valid_tile_count, "All grid positions have valid tiles")
func test_grid_layout_calculation():
TestHelper.print_step("Grid Layout Calculation")
if not match3_instance:
return
# 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)"
)
# Test grid offset
TestHelper.assert_not_null(match3_instance.grid_offset, "Grid offset is set")
TestHelper.assert_true(match3_instance.grid_offset.x >= 0, "Grid offset X is non-negative")
TestHelper.assert_true(match3_instance.grid_offset.y >= 0, "Grid offset Y is non-negative")
# 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(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")
if not match3_instance:
return
# Test GameState enum exists and has expected values
var game_state_class = match3_instance.get_script().get_global_class()
TestHelper.assert_true("GameState" in match3_instance, "GameState enum accessible")
# Test current state is valid
TestHelper.assert_not_null(match3_instance.current_state, "Current state is set")
# Test initialization flags
TestHelper.assert_true("grid_initialized" in match3_instance, "Grid initialized flag exists")
TestHelper.assert_true(match3_instance.grid_initialized, "Grid is marked as initialized")
# 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"
)
func test_match_detection():
TestHelper.print_step("Match Detection Logic")
if not match3_instance:
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"
)
# Test boundary checking with invalid positions
var invalid_positions = [
Vector2i(-1, 0),
Vector2i(0, -1),
Vector2i(match3_instance.GRID_SIZE.x, 0),
Vector2i(0, match3_instance.GRID_SIZE.y),
Vector2i(100, 100)
]
for pos in invalid_positions:
var result = match3_instance._has_match_at(pos)
TestHelper.assert_false(result, "Invalid position (%d,%d) returns false" % [pos.x, pos.y])
# Test valid positions don't crash
for y in range(min(3, match3_instance.GRID_SIZE.y)):
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]
)
func test_scoring_system():
TestHelper.print_step("Scoring System")
if not match3_instance:
return
# Test scoring formula constants and logic
# 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"
)
# Test that 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, 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]
var calculated_score: int
if match_size == 3:
calculated_score = 3
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
)
func test_input_validation():
TestHelper.print_step("Input Validation")
if not match3_instance:
return
# 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"
)
# 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"
)
# 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"
)
func test_memory_safety():
TestHelper.print_step("Memory Safety")
if not match3_instance:
return
# Test grid integrity validation
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"
)
# Test position validation
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")
if not match3_instance:
return
# Test grid size is within performance limits
var total_tiles = match3_instance.GRID_SIZE.x * match3_instance.GRID_SIZE.y
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"
)
# 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"
)
# Test grid initialization performance
TestHelper.start_performance_test("grid_access")
for y in range(min(5, match3_instance.grid.size())):
for x in range(min(5, match3_instance.grid[y].size())):
var tile = match3_instance.grid[y][x]
if tile and "tile_type" in tile:
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")
# Clean up Match3 instance
if match3_instance and is_instance_valid(match3_instance):
match3_instance.queue_free()
# Clean up test viewport
if test_viewport and is_instance_valid(test_viewport):
test_viewport.queue_free()
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")

View File

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

View File

@@ -0,0 +1,163 @@
extends SceneTree
# Test to verify that existing save files with old checksum format can be migrated
# This ensures backward compatibility with the checksum fix
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# 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 = {
"_version": 1,
"high_score": 150,
"current_score": 0,
"games_played": 5,
"total_score": 450,
"grid_state":
{
"grid_size": {"x": 8, "y": 8},
"tile_types_count": 5,
"active_gem_types": [0, 1, 2, 3, 4],
"grid_layout": []
}
}
# Create old checksum (without normalization)
var old_checksum = _calculate_old_checksum(old_save_data)
old_save_data["_checksum"] = old_checksum
print("Old checksum format: %s" % old_checksum)
# Simulate JSON round-trip (causes the type conversion issue)
var json_string = JSON.stringify(old_save_data)
var json = JSON.new()
json.parse(json_string)
var loaded_data = json.data
# Calculate new checksum with fixed algorithm
var new_checksum = _calculate_new_checksum(loaded_data)
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"
)
print("Old checksum: %s" % old_checksum)
print("New checksum: %s" % new_checksum)
TestHelper.print_step("New System Self-Consistency")
# Remove old checksum and recalculate
loaded_data.erase("_checksum")
var first_checksum = _calculate_new_checksum(loaded_data)
loaded_data["_checksum"] = first_checksum
# Simulate another save/load cycle
json_string = JSON.stringify(loaded_data)
json = JSON.new()
json.parse(json_string)
var reloaded_data = json.data
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"
)
print("Consistent checksum: %s" % first_checksum)
TestHelper.print_step("Migration Strategy Verification")
TestHelper.assert_true(true, "Version-based checksum handling implemented")
print("✓ Files without _checksum: Allow (backward 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)
var data_copy = data.duplicate(true)
data_copy.erase("_checksum")
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
var data_copy = 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)
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()
keys.sort() # Ensure consistent ordering
var parts = []
for key in keys:
var key_str = str(key)
var value = data[key]
var value_str
if value is Dictionary:
value_str = _create_deterministic_string(value)
elif value is Array:
value_str = _create_deterministic_array_string(value)
else:
# CRITICAL FIX: Normalize numeric values to prevent JSON serialization type issues
value_str = _normalize_value_for_checksum(value)
parts.append(key_str + ":" + value_str)
return "{" + ",".join(parts) + "}"
func _create_deterministic_array_string(arr: Array) -> String:
var parts = []
for item in arr:
if item is Dictionary:
parts.append(_create_deterministic_string(item))
elif item is Array:
parts.append(_create_deterministic_array_string(item))
else:
# CRITICAL FIX: Normalize array values for consistent checksum
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
This prevents JSON serialization type conversion from breaking checksums
"""
if value == null:
return "null"
elif value is bool:
return str(value)
elif value is int or value is float:
# Convert all numeric values to integers if they are whole numbers
# This prevents float/int type conversion issues after JSON serialization
if value is float and value == floor(value):
return str(int(value))
else:
return str(value)
else:
return str(value)

View File

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

View File

@@ -0,0 +1,191 @@
extends SceneTree
## Test suite for Scene Validation
##
## Validates all .tscn files in the project for loading and instantiation errors.
## Provides comprehensive scene validation to catch issues before runtime.
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
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Scene Validation")
# Run test suites
test_scene_discovery()
test_scene_loading()
test_scene_instantiation()
test_critical_scenes()
# Print final summary
print_validation_summary()
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/"]
for directory in scene_directories:
discover_scenes_in_directory(directory)
TestHelper.assert_true(discovered_scenes.size() > 0, "Found scenes in project")
print("Discovered %d scene files" % discovered_scenes.size())
# List discovered scenes for reference
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:
print("Warning: Could not access directory: %s" % directory_path)
return
dir.list_dir_begin()
var file_name = dir.get_next()
while file_name != "":
var full_path = directory_path.path_join(file_name)
if dir.current_is_dir() and not file_name.begins_with("."):
# Recursively search subdirectories
discover_scenes_in_directory(full_path)
elif file_name.ends_with(".tscn"):
# Add scene file to discovery list
discovered_scenes.append(full_path)
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()
# Check if resource exists
if not ResourceLoader.exists(scene_path):
validation_results[scene_path] = "Resource does not exist"
TestHelper.assert_false(true, "%s - Resource does not exist" % scene_name)
return
# Attempt to load the scene
var packed_scene = load(scene_path)
if not packed_scene:
validation_results[scene_path] = "Failed to load scene"
TestHelper.assert_false(true, "%s - Failed to load scene" % scene_name)
return
if not packed_scene is PackedScene:
validation_results[scene_path] = "Resource is not a PackedScene"
TestHelper.assert_false(true, "%s - Resource is not a PackedScene" % scene_name)
return
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")
for scene_path in discovered_scenes:
# Only test instantiation for scenes that loaded successfully
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()
# Load the scene (we know it loads from previous test)
var packed_scene = load(scene_path)
# Attempt to instantiate
var scene_instance = packed_scene.instantiate()
if not scene_instance:
validation_results[scene_path] = "Failed to instantiate scene"
TestHelper.assert_false(true, "%s - Failed to instantiate scene" % scene_name)
return
# Validate the instance
TestHelper.assert_not_null(
scene_instance, "%s - Scene instantiation creates valid node" % scene_name
)
# Clean up the instance
scene_instance.queue_free()
# Update validation status
if validation_results[scene_path] == "Loading successful":
validation_results[scene_path] = "Full validation successful"
func test_critical_scenes():
TestHelper.print_step("Critical Scene Validation")
# Define critical scenes that must work
var critical_scenes = [
"res://scenes/main/main.tscn",
"res://scenes/game/game.tscn",
"res://scenes/ui/MainMenu.tscn",
"res://scenes/game/gameplays/match3_gameplay.tscn"
]
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()
)
else:
TestHelper.assert_false(true, "Critical scene missing: %s" % scene_path)
func print_validation_summary():
print("\n=== Scene Validation Summary ===")
var total_scenes = discovered_scenes.size()
var successful_scenes = 0
var failed_scenes = 0
for scene_path in discovered_scenes:
var status = validation_results.get(scene_path, "Not tested")
if status == "Full validation successful" or status == "Loading successful":
successful_scenes += 1
else:
failed_scenes += 1
print("%s: %s" % [scene_path.get_file(), status])
print("\nTotal Scenes: %d" % total_scenes)
print("Successful: %d" % successful_scenes)
print("Failed: %d" % failed_scenes)
if failed_scenes == 0:
print("✅ All scenes passed validation!")
else:
print("%d scene(s) failed validation" % failed_scenes)

View File

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

View File

@@ -0,0 +1,317 @@
extends SceneTree
## Test suite for SettingsManager
##
## Tests input validation, file I/O, and error handling.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var settings_manager: Node
var original_settings: Dictionary
var temp_files: Array[String] = []
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("SettingsManager")
# Get reference to SettingsManager
settings_manager = root.get_node("SettingsManager")
if not settings_manager:
TestHelper.assert_true(false, "SettingsManager autoload not found")
TestHelper.print_test_footer("SettingsManager")
return
# Store original settings for restoration
original_settings = settings_manager.settings.duplicate(true)
# Run test suites
test_basic_functionality()
test_input_validation_security()
test_file_io_security()
test_json_parsing_security()
test_language_validation()
test_volume_validation()
test_error_handling_and_recovery()
test_reset_functionality()
test_performance_benchmarks()
# Cleanup and restore original state
cleanup_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"
)
# Test that SettingsManager has expected methods
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
)
# 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")
# Test NaN validation
var nan_result = settings_manager.set_setting("master_volume", NAN)
TestHelper.assert_false(nan_result, "NaN values rejected for volume settings")
# Test Infinity validation
var inf_result = settings_manager.set_setting("master_volume", INF)
TestHelper.assert_false(inf_result, "Infinity values rejected for volume settings")
# Test negative infinity validation
var neg_inf_result = settings_manager.set_setting("master_volume", -INF)
TestHelper.assert_false(neg_inf_result, "Negative infinity values rejected")
# Test range validation for volumes
var negative_volume = settings_manager.set_setting("master_volume", -0.5)
TestHelper.assert_false(negative_volume, "Negative volume values rejected")
var excessive_volume = settings_manager.set_setting("master_volume", 1.5)
TestHelper.assert_false(excessive_volume, "Volume values > 1.0 rejected")
# 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"
)
# Test string length validation for language
var long_language = "a".repeat(20) # Exceeds MAX_SETTING_STRING_LENGTH
var long_lang_result = settings_manager.set_setting("language", long_language)
TestHelper.assert_false(long_lang_result, "Excessively long language codes rejected")
# Test invalid characters in language code
var invalid_chars = settings_manager.set_setting("language", "en<script>")
TestHelper.assert_false(invalid_chars, "Language codes with invalid characters rejected")
# Test valid language code
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
temp_files.append(oversized_config_path)
# Test that normal save/load operations work
var save_result = settings_manager.save_settings()
TestHelper.assert_true(save_result, "Normal settings save succeeds")
# Test loading with backup scenario
settings_manager.load_settings()
TestHelper.assert_not_null(settings_manager.settings, "Settings loaded successfully")
# 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()
)
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
)
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"
)
func test_language_validation():
TestHelper.print_step("Language Validation")
# Test supported languages
var supported_langs = ["en", "ru"]
for lang in supported_langs:
var result = settings_manager.set_setting("language", lang)
TestHelper.assert_true(result, "Supported language accepted: " + lang)
# Test unsupported language
var unsupported_result = settings_manager.set_setting("language", "xyz")
TestHelper.assert_false(unsupported_result, "Unsupported language rejected")
# Test empty language
var empty_result = settings_manager.set_setting("language", "")
TestHelper.assert_false(empty_result, "Empty language rejected")
# Test null language
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")
var volume_settings = ["master_volume", "music_volume", "sfx_volume"]
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
)
# 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
)
# 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
)
func test_error_handling_and_recovery():
TestHelper.print_step("Error Handling and Recovery")
# Test unknown setting key
var unknown_result = settings_manager.set_setting("unknown_setting", "value")
TestHelper.assert_false(unknown_result, "Unknown setting keys rejected")
# Test recovery from corrupted settings
# Save current state
var current_volume = settings_manager.get_setting("master_volume")
# Reset settings
settings_manager.reset_settings_to_defaults()
# 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"
)
# 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"
)
func test_reset_functionality():
TestHelper.print_step("Reset Functionality")
# Modify settings
settings_manager.set_setting("master_volume", 0.8)
settings_manager.set_setting("language", "ru")
# Reset to defaults
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"
)
# 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")
# Test settings load performance
TestHelper.start_performance_test("load_settings")
settings_manager.load_settings()
TestHelper.end_performance_test("load_settings", 100.0, "Settings load within 100ms")
# Test settings save performance
TestHelper.start_performance_test("save_settings")
settings_manager.save_settings()
TestHelper.end_performance_test("save_settings", 50.0, "Settings save within 50ms")
# Test validation performance
TestHelper.start_performance_test("validation")
for i in range(100):
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")
# Restore original settings
if original_settings:
settings_manager.settings = original_settings.duplicate(true)
settings_manager.save_settings()
# Clean up temporary files
for temp_file in temp_files:
TestHelper.cleanup_temp_file(temp_file)
TestHelper.assert_true(true, "Test cleanup completed")

View File

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

484
tests/test_tile.gd Normal file
View File

@@ -0,0 +1,484 @@
extends SceneTree
## Test suite for Tile component
##
## Tests tile initialization, texture management, and gem types.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var tile_scene: PackedScene
var tile_instance: Node2D
var test_viewport: SubViewport
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("Tile Component")
# Setup test environment
setup_test_environment()
# Run test suites
test_basic_functionality()
test_tile_constants()
test_texture_management()
test_gem_type_management()
test_visual_feedback_system()
test_state_management()
test_input_validation()
test_scaling_and_sizing()
test_memory_safety()
test_error_handling()
# Cleanup
cleanup_tests()
TestHelper.print_test_footer("Tile Component")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
# Load Tile scene
tile_scene = load("res://scenes/game/gameplays/tile.tscn")
TestHelper.assert_not_null(tile_scene, "Tile scene loads successfully")
# Create test viewport for isolated testing
test_viewport = SubViewport.new()
test_viewport.size = Vector2i(400, 400)
root.add_child(test_viewport)
# Instance Tile in test viewport
if tile_scene:
tile_instance = tile_scene.instantiate()
test_viewport.add_child(tile_instance)
TestHelper.assert_not_null(tile_instance, "Tile instance created successfully")
# Wait for initialization
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
if not tile_instance:
TestHelper.assert_true(false, "Tile instance not available for testing")
return
# Test that Tile has expected properties
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"
]
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"
)
# Test sprite reference
TestHelper.assert_not_null(tile_instance.sprite, "Sprite node is available")
TestHelper.assert_true(tile_instance.sprite is Sprite2D, "Sprite is Sprite2D type")
# 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")
if not tile_instance:
return
# Test TILE_SIZE constant
TestHelper.assert_equal(48, tile_instance.TILE_SIZE, "TILE_SIZE constant is correct")
# 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"
)
# Test that all gem textures are valid
for i in range(tile_instance.all_gem_textures.size()):
var texture = tile_instance.all_gem_textures[i]
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")
if not tile_instance:
return
# 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"
)
# Test texture assignment for valid tile types
var original_type = tile_instance.tile_type
for i in range(min(3, tile_instance.active_gem_types.size())):
tile_instance.tile_type = i
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
)
# Restore original type
tile_instance.tile_type = original_type
func test_gem_type_management():
TestHelper.print_step("Gem Type Management")
if not tile_instance:
return
# Store original state
var original_gem_types = tile_instance.active_gem_types.duplicate()
# 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"
)
# 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"
)
# 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"
)
# Test add_gem_type with invalid index
var invalid_add = tile_instance.add_gem_type(99)
TestHelper.assert_false(invalid_add, "Invalid gem index addition rejected")
# 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"
)
# Test removing non-existent gem type
var nonexistent_remove = tile_instance.remove_gem_type(99)
TestHelper.assert_false(nonexistent_remove, "Non-existent gem type removal rejected")
# Test minimum gem types protection
tile_instance.set_active_gem_types([0, 1]) # Set to minimum
var protected_remove = tile_instance.remove_gem_type(0)
TestHelper.assert_false(protected_remove, "Minimum gem types protection active")
TestHelper.assert_equal(2, tile_instance.get_active_gem_count(), "Minimum gem count preserved")
# Restore original state
tile_instance.set_active_gem_types(original_gem_types)
func test_visual_feedback_system():
TestHelper.print_step("Visual Feedback System")
if not tile_instance:
return
# Store original state
var original_selected = tile_instance.is_selected
var original_highlighted = tile_instance.is_highlighted
# Test selection visual feedback
tile_instance.is_selected = true
TestHelper.assert_true(tile_instance.is_selected, "Selection state set correctly")
# Wait for potential animation
await process_frame
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"
)
# Test highlight visual feedback
tile_instance.is_selected = false
tile_instance.is_highlighted = true
TestHelper.assert_true(tile_instance.is_highlighted, "Highlight state set correctly")
# Wait for potential animation
await process_frame
# Test normal state
tile_instance.is_highlighted = false
TestHelper.assert_false(tile_instance.is_highlighted, "Normal state restored")
# Test force reset
tile_instance.is_selected = true
tile_instance.is_highlighted = true
tile_instance.force_reset_visual_state()
TestHelper.assert_false(tile_instance.is_selected, "Force reset clears selection")
TestHelper.assert_false(tile_instance.is_highlighted, "Force reset clears highlight")
# Restore original state
tile_instance.is_selected = original_selected
tile_instance.is_highlighted = original_highlighted
func test_state_management():
TestHelper.print_step("State Management")
if not tile_instance:
return
# 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"
)
# Test tile type bounds checking
var original_type = tile_instance.tile_type
var max_valid_type = tile_instance.active_gem_types.size() - 1
# Test valid tile type
if max_valid_type >= 0:
tile_instance.tile_type = max_valid_type
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"
)
# Restore original type
tile_instance.tile_type = original_type
func test_input_validation():
TestHelper.print_step("Input Validation")
if not tile_instance:
return
# Test empty gem types array
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"
)
# 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"
)
# 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"
)
# Test negative gem indices
var negative_add = tile_instance.add_gem_type(-1)
TestHelper.assert_false(negative_add, "Negative gem index rejected")
# Test out-of-bounds gem indices
var oob_add = tile_instance.add_gem_type(tile_instance.all_gem_textures.size())
TestHelper.assert_false(oob_add, "Out-of-bounds gem index rejected")
# Restore original state
tile_instance.set_active_gem_types(original_gems)
func test_scaling_and_sizing():
TestHelper.print_step("Scaling and Sizing")
if not tile_instance:
return
# Test original scale calculation
TestHelper.assert_not_null(tile_instance.original_scale, "Original scale is calculated")
TestHelper.assert_true(tile_instance.original_scale.x > 0, "Original scale X is positive")
TestHelper.assert_true(tile_instance.original_scale.y > 0, "Original scale Y is positive")
# Test that tile size is respected
if tile_instance.sprite and tile_instance.sprite.texture:
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"
)
# Test scale animation for visual feedback
var original_scale = tile_instance.sprite.scale if tile_instance.sprite else Vector2.ONE
# Test selection scaling
tile_instance.is_selected = true
await process_frame
await process_frame # Wait for animation
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"
)
# Reset to normal
tile_instance.is_selected = false
await process_frame
await process_frame
func test_memory_safety():
TestHelper.print_step("Memory Safety")
if not tile_instance:
return
# Test sprite null checking
var original_sprite = tile_instance.sprite
tile_instance.sprite = null
# These operations should not crash
tile_instance._set_tile_type(0)
tile_instance._update_visual_feedback()
tile_instance.force_reset_visual_state()
TestHelper.assert_true(true, "Null sprite operations handled safely")
# Restore sprite
tile_instance.sprite = original_sprite
# Test valid instance checking in visual updates
if tile_instance.sprite:
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"
)
# 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"
)
func test_error_handling():
TestHelper.print_step("Error Handling")
if not tile_instance:
return
# Test graceful handling of missing sprite during initialization
var backup_sprite = tile_instance.sprite
tile_instance.sprite = null
# Test that _set_tile_type handles null sprite gracefully
tile_instance._set_tile_type(0)
TestHelper.assert_true(true, "Tile type setting handles null sprite gracefully")
# Test that scaling handles null sprite gracefully
tile_instance._scale_sprite_to_fit()
TestHelper.assert_true(true, "Sprite scaling handles null sprite gracefully")
# Restore sprite
tile_instance.sprite = backup_sprite
# Test invalid tile type handling
var original_type = tile_instance.tile_type
tile_instance._set_tile_type(-1) # Invalid negative type
tile_instance._set_tile_type(999) # Invalid large type
# Should not crash and should maintain reasonable state
TestHelper.assert_true(true, "Invalid tile types handled gracefully")
# Restore original type
tile_instance.tile_type = original_type
# Test array bounds protection
var large_gem_types = []
for i in range(50): # Create array larger than texture array
large_gem_types.append(i)
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"
)
func cleanup_tests():
TestHelper.print_step("Cleanup")
# Clean up tile instance
if tile_instance and is_instance_valid(tile_instance):
tile_instance.queue_free()
# Clean up test viewport
if test_viewport and is_instance_valid(test_viewport):
test_viewport.queue_free()
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")

1
tests/test_tile.gd.uid Normal file
View File

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

505
tests/test_value_stepper.gd Normal file
View File

@@ -0,0 +1,505 @@
extends SceneTree
## Test suite for ValueStepper component
##
## Tests data loading, value navigation, and input handling.
const TestHelper = preload("res://tests/helpers/TestHelper.gd")
var stepper_scene: PackedScene
var stepper_instance: Control
var test_viewport: SubViewport
var original_language: String
func _initialize():
# Wait for autoloads to initialize
await process_frame
await process_frame
run_tests()
# Exit after tests complete
quit()
func run_tests():
TestHelper.print_test_header("ValueStepper Component")
# Store original settings
var settings_manager = root.get_node("SettingsManager")
original_language = settings_manager.get_setting("language")
# Setup test environment
setup_test_environment()
# Run test suites
test_basic_functionality()
test_data_source_loading()
test_value_navigation()
test_custom_values()
test_input_handling()
test_visual_feedback()
test_settings_integration()
test_boundary_conditions()
test_error_handling()
# Cleanup
cleanup_tests()
TestHelper.print_test_footer("ValueStepper Component")
func setup_test_environment():
TestHelper.print_step("Test Environment Setup")
# Load ValueStepper scene
stepper_scene = load("res://scenes/ui/components/ValueStepper.tscn")
TestHelper.assert_not_null(stepper_scene, "ValueStepper scene loads successfully")
# Create test viewport for isolated testing
test_viewport = SubViewport.new()
test_viewport.size = Vector2i(400, 200)
root.add_child(test_viewport)
# Instance ValueStepper in test viewport
if stepper_scene:
stepper_instance = stepper_scene.instantiate()
test_viewport.add_child(stepper_instance)
TestHelper.assert_not_null(stepper_instance, "ValueStepper instance created successfully")
# Wait for initialization
await process_frame
await process_frame
func test_basic_functionality():
TestHelper.print_step("Basic Functionality")
if not stepper_instance:
TestHelper.assert_true(false, "ValueStepper instance not available for testing")
return
# Test that ValueStepper has expected properties
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"
)
# Test signals
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")
TestHelper.assert_not_null(stepper_instance.right_button, "Right button is available")
TestHelper.assert_not_null(stepper_instance.value_display, "Value display label is available")
# Test UI component types
TestHelper.assert_true(stepper_instance.left_button is Button, "Left button is Button type")
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")
if not stepper_instance:
return
# Test default language data source
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")
TestHelper.assert_not_null(stepper_instance.display_names, "Display names array is initialized")
TestHelper.assert_true(stepper_instance.values is Array, "Values is Array type")
TestHelper.assert_true(stepper_instance.display_names is Array, "Display names is Array type")
# 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"
)
# 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"
)
# Test resolution data source
var resolution_stepper = stepper_scene.instantiate()
resolution_stepper.data_source = "resolution"
test_viewport.add_child(resolution_stepper)
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"
)
resolution_stepper.queue_free()
# Test difficulty data source
var difficulty_stepper = stepper_scene.instantiate()
difficulty_stepper.data_source = "difficulty"
test_viewport.add_child(difficulty_stepper)
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_equal(1, difficulty_stepper.current_index, "Difficulty defaults to normal")
difficulty_stepper.queue_free()
func test_value_navigation():
TestHelper.print_step("Value Navigation")
if not stepper_instance:
return
# Store original state
var original_index = stepper_instance.current_index
var original_value = stepper_instance.get_current_value()
# Test forward navigation
var initial_value = stepper_instance.get_current_value()
stepper_instance.change_value(1)
var next_value = stepper_instance.get_current_value()
TestHelper.assert_not_equal(initial_value, next_value, "Forward navigation changes value")
# 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"
)
# 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"
)
# 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"
)
# Restore original state
stepper_instance.current_index = original_index
stepper_instance._update_display()
func test_custom_values():
TestHelper.print_step("Custom Values")
if not stepper_instance:
return
# Store original state
var original_values = stepper_instance.values.duplicate()
var original_display_names = stepper_instance.display_names.duplicate()
var original_index = stepper_instance.current_index
# Test custom values without display names
var custom_values = ["apple", "banana", "cherry"]
stepper_instance.setup_custom_values(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"
)
# 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"
)
# Test navigation with custom values
stepper_instance.change_value(1)
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"
)
# 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"
)
# Restore original state
stepper_instance.values = original_values
stepper_instance.display_names = original_display_names
stepper_instance.current_index = original_index
stepper_instance._update_display()
func test_input_handling():
TestHelper.print_step("Input Handling")
if not stepper_instance:
return
# Store original state
var original_value = stepper_instance.get_current_value()
# 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"
)
# 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"
)
# Test invalid input action
var invalid_handled = stepper_instance.handle_input_action("invalid_action")
TestHelper.assert_false(invalid_handled, "Invalid input action not handled")
# Test button press simulation
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"
)
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"
)
func test_visual_feedback():
TestHelper.print_step("Visual Feedback")
if not stepper_instance:
return
# Store original visual properties
var original_scale = stepper_instance.scale
var original_modulate = stepper_instance.modulate
# 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"
)
# 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"
)
# Test display update
if stepper_instance.value_display:
var current_text = stepper_instance.value_display.text
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")
if not stepper_instance or stepper_instance.data_source != "language":
return
# Store original language
var original_lang = root.get_node("SettingsManager").get_setting("language")
# Test that changing language stepper updates settings
var available_languages = stepper_instance.values
if available_languages.size() > 1:
# Find a different language
var target_lang = null
for lang in available_languages:
if lang != original_lang:
target_lang = lang
break
if target_lang:
stepper_instance.set_current_value(target_lang)
stepper_instance._apply_value_change(target_lang, stepper_instance.current_index)
# Verify setting was updated
var updated_lang = root.get_node("SettingsManager").get_setting("language")
TestHelper.assert_equal(target_lang, updated_lang, "Language setting updated correctly")
# Restore original language
root.get_node("SettingsManager").set_setting("language", original_lang)
func test_boundary_conditions():
TestHelper.print_step("Boundary Conditions")
if not stepper_instance:
return
# Test empty values array
var empty_stepper = stepper_scene.instantiate()
empty_stepper.data_source = "unknown" # Will result in empty arrays
test_viewport.add_child(empty_stepper)
await process_frame
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
TestHelper.assert_true(true, "change_value handles empty array gracefully")
empty_stepper.queue_free()
# Test index bounds
if stepper_instance.values.size() > 0:
# 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"
)
# 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"
)
# Restore valid index
stepper_instance.current_index = 0
stepper_instance._update_display()
func test_error_handling():
TestHelper.print_step("Error Handling")
if not stepper_instance:
return
# Test unknown data source
var unknown_stepper = stepper_scene.instantiate()
unknown_stepper.data_source = "invalid_source"
test_viewport.add_child(unknown_stepper)
await process_frame
# Should not crash and should handle gracefully
TestHelper.assert_true(true, "Unknown data source handled gracefully")
unknown_stepper.queue_free()
# 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"
)
# Test custom values with mismatched arrays
var values_3 = ["a", "b", "c"]
var names_2 = ["A", "B"]
stepper_instance.setup_custom_values(values_3, names_2)
# 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"
)
# 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"
)
func cleanup_tests():
TestHelper.print_step("Cleanup")
# Restore original language setting
root.get_node("SettingsManager").set_setting("language", original_language)
# Clean up stepper instance
if stepper_instance and is_instance_valid(stepper_instance):
stepper_instance.queue_free()
# Clean up test viewport
if test_viewport and is_instance_valid(test_viewport):
test_viewport.queue_free()
# Wait for cleanup
await process_frame
TestHelper.assert_true(true, "Test cleanup completed")

View File

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