Compare commits
23 Commits
e2e49f89ce
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| d1761a2464 | |||
| ffd88c02e1 | |||
| bd9b7c009a | |||
| e61ab94935 | |||
| 9150622e74 | |||
| 501cad6175 | |||
| 5275c5ca94 | |||
| 61951a047b | |||
| 5f6a3ae175 | |||
| 40c06ae249 | |||
| 1189ce0931 | |||
| ff04b6ee22 | |||
| ff0a4fefe1 | |||
| 666823c641 | |||
| 02f2bb2703 | |||
| 38e85c2a24 | |||
| e31278e389 | |||
| 024343db19 | |||
| ad7a2575da | |||
| 26991ce61a | |||
| 8ded8c81ee | |||
| eb99c6a18e | |||
| c1f3f9f708 |
@@ -4,6 +4,9 @@
|
||||
"WebSearch",
|
||||
"Bash(find:*)",
|
||||
"Bash(godot:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(git mv:*)",
|
||||
"Bash(dir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Maximum line length (default is 100)
|
||||
# Godot's style guide recommends keeping lines under 100 characters
|
||||
line_length = 100
|
||||
line_length = 80
|
||||
|
||||
# Whether to use tabs or spaces for indentation
|
||||
# Godot uses tabs by default
|
||||
|
||||
498
.gitea/workflows/build.yml
Normal file
498
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,498 @@
|
||||
name: Build Game
|
||||
|
||||
# Build pipeline for creating game executables across multiple platforms
|
||||
#
|
||||
# Features:
|
||||
# - Manual trigger with individual platform checkboxes
|
||||
# - Tag-based automatic builds for releases
|
||||
# - Multi-platform builds (Windows, Linux, macOS, Android)
|
||||
# - Artifact storage for one week
|
||||
# - Configurable build options
|
||||
|
||||
on:
|
||||
# Manual trigger with platform selection
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_windows:
|
||||
description: 'Build for Windows'
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
build_linux:
|
||||
description: 'Build for Linux'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_macos:
|
||||
description: 'Build for macOS'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_android:
|
||||
description: 'Build for Android'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
build_type:
|
||||
description: 'Build type'
|
||||
required: true
|
||||
default: 'release'
|
||||
type: debug
|
||||
options:
|
||||
- release
|
||||
- debug
|
||||
version_override:
|
||||
description: 'Override version (optional)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# Automatic trigger on git tags (for releases)
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Version tags (v1.0.0, v2.1.0, etc.)
|
||||
- 'release-*' # Release tags
|
||||
|
||||
env:
|
||||
GODOT_VERSION: "4.4.1"
|
||||
PROJECT_NAME: "Skelly"
|
||||
BUILD_DIR: "builds"
|
||||
|
||||
jobs:
|
||||
# Preparation job - determines build configuration
|
||||
prepare:
|
||||
name: Prepare Build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
platforms: ${{ steps.config.outputs.platforms }}
|
||||
build_type: ${{ steps.config.outputs.build_type }}
|
||||
version: ${{ steps.config.outputs.version }}
|
||||
artifact_name: ${{ steps.config.outputs.artifact_name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure build parameters
|
||||
id: config
|
||||
run: |
|
||||
# Determine platforms to build
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
# Build platforms array from individual checkboxes
|
||||
platforms=""
|
||||
if [[ "${{ github.event.inputs.build_windows }}" == "true" ]]; then
|
||||
platforms="${platforms}windows,"
|
||||
fi
|
||||
if [[ "${{ github.event.inputs.build_linux }}" == "true" ]]; then
|
||||
platforms="${platforms}linux,"
|
||||
fi
|
||||
if [[ "${{ github.event.inputs.build_macos }}" == "true" ]]; then
|
||||
platforms="${platforms}macos,"
|
||||
fi
|
||||
if [[ "${{ github.event.inputs.build_android }}" == "true" ]]; then
|
||||
platforms="${platforms}android,"
|
||||
fi
|
||||
# Remove trailing comma
|
||||
platforms="${platforms%,}"
|
||||
|
||||
build_type="${{ github.event.inputs.build_type }}"
|
||||
version_override="${{ github.event.inputs.version_override }}"
|
||||
else
|
||||
# Tag-triggered build - build all platforms
|
||||
platforms="windows,linux,macos,android"
|
||||
build_type="release"
|
||||
version_override=""
|
||||
fi
|
||||
|
||||
# Determine version
|
||||
if [[ -n "$version_override" ]]; then
|
||||
version="$version_override"
|
||||
elif [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
version="${{ github.ref_name }}"
|
||||
else
|
||||
# Generate version from git info
|
||||
commit_short=$(git rev-parse --short HEAD)
|
||||
branch_name="${{ github.ref_name }}"
|
||||
timestamp=$(date +%Y%m%d-%H%M)
|
||||
version="${branch_name}-${commit_short}-${timestamp}"
|
||||
fi
|
||||
|
||||
# Create artifact name
|
||||
artifact_name="${{ env.PROJECT_NAME }}-${version}-${build_type}"
|
||||
|
||||
echo "platforms=${platforms}" >> $GITHUB_OUTPUT
|
||||
echo "build_type=${build_type}" >> $GITHUB_OUTPUT
|
||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
||||
echo "artifact_name=${artifact_name}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "🔧 Build Configuration:"
|
||||
echo " Platforms: ${platforms}"
|
||||
echo " Build Type: ${build_type}"
|
||||
echo " Version: ${version}"
|
||||
echo " Artifact: ${artifact_name}"
|
||||
|
||||
# Setup export templates (shared across all platform builds)
|
||||
setup-templates:
|
||||
name: Setup Export Templates
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare
|
||||
|
||||
steps:
|
||||
- name: Cache export templates
|
||||
id: cache-templates
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/share/godot/export_templates
|
||||
key: godot-templates-${{ env.GODOT_VERSION }}
|
||||
restore-keys: |
|
||||
godot-templates-
|
||||
|
||||
- name: Setup Godot
|
||||
if: steps.cache-templates.outputs.cache-hit != 'true'
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: ${{ env.GODOT_VERSION }}
|
||||
use-dotnet: false
|
||||
|
||||
- name: Install export templates
|
||||
if: steps.cache-templates.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "📦 Installing Godot export templates..."
|
||||
mkdir -p ~/.local/share/godot/export_templates/${{ env.GODOT_VERSION }}.stable
|
||||
wget -q https://github.com/godotengine/godot/releases/download/${{ env.GODOT_VERSION }}-stable/Godot_v${{ env.GODOT_VERSION }}-stable_export_templates.tpz
|
||||
unzip -q Godot_v${{ env.GODOT_VERSION }}-stable_export_templates.tpz
|
||||
mv templates/* ~/.local/share/godot/export_templates/${{ env.GODOT_VERSION }}.stable/
|
||||
echo "✅ Export templates installed successfully"
|
||||
ls -la ~/.local/share/godot/export_templates/${{ env.GODOT_VERSION }}.stable/
|
||||
|
||||
- name: Verify templates cache
|
||||
run: |
|
||||
echo "🔍 Verifying export templates are available:"
|
||||
ls -la ~/.local/share/godot/export_templates/${{ env.GODOT_VERSION }}.stable/
|
||||
|
||||
# Windows build job
|
||||
build-windows:
|
||||
name: Build Windows
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, setup-templates]
|
||||
if: contains(needs.prepare.outputs.platforms, 'windows')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: ${{ env.GODOT_VERSION }}
|
||||
use-dotnet: false
|
||||
|
||||
- name: Restore export templates cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/share/godot/export_templates
|
||||
key: godot-templates-${{ env.GODOT_VERSION }}
|
||||
restore-keys: |
|
||||
godot-templates-
|
||||
|
||||
- name: Create build directory
|
||||
run: mkdir -p ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Import project assets
|
||||
run: |
|
||||
echo "📦 Importing project assets..."
|
||||
godot --headless --verbose --editor --quit || true
|
||||
sleep 2
|
||||
|
||||
- name: Build Windows executable
|
||||
run: |
|
||||
echo "🏗️ Building Windows executable..."
|
||||
godot --headless --verbose --export-${{ needs.prepare.outputs.build_type }} "Windows Desktop" \
|
||||
${{ env.BUILD_DIR }}/skelly-windows-${{ needs.prepare.outputs.version }}.exe
|
||||
|
||||
# Verify build output
|
||||
if [[ -f "${{ env.BUILD_DIR }}/skelly-windows-${{ needs.prepare.outputs.version }}.exe" ]]; then
|
||||
echo "✅ Windows build successful"
|
||||
ls -la ${{ env.BUILD_DIR }}/
|
||||
else
|
||||
echo "❌ Windows build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ needs.prepare.outputs.artifact_name }}-windows
|
||||
path: ${{ env.BUILD_DIR }}/skelly-windows-${{ needs.prepare.outputs.version }}.exe
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
# Linux build job
|
||||
build-linux:
|
||||
name: Build Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, setup-templates]
|
||||
if: contains(needs.prepare.outputs.platforms, 'linux')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: ${{ env.GODOT_VERSION }}
|
||||
use-dotnet: false
|
||||
|
||||
- name: Restore export templates cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/share/godot/export_templates
|
||||
key: godot-templates-${{ env.GODOT_VERSION }}
|
||||
restore-keys: |
|
||||
godot-templates-
|
||||
|
||||
- name: Create build directory
|
||||
run: mkdir -p ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Import project assets
|
||||
run: |
|
||||
echo "📦 Importing project assets..."
|
||||
godot --headless --verbose --editor --quit || true
|
||||
sleep 2
|
||||
|
||||
- name: Build Linux executable
|
||||
run: |
|
||||
echo "🏗️ Building Linux executable..."
|
||||
godot --headless --verbose --export-${{ needs.prepare.outputs.build_type }} "Linux" \
|
||||
${{ env.BUILD_DIR }}/skelly-linux-${{ needs.prepare.outputs.version }}.x86_64
|
||||
|
||||
# Make executable
|
||||
chmod +x ${{ env.BUILD_DIR }}/skelly-linux-${{ needs.prepare.outputs.version }}.x86_64
|
||||
|
||||
# Verify build output
|
||||
if [[ -f "${{ env.BUILD_DIR }}/skelly-linux-${{ needs.prepare.outputs.version }}.x86_64" ]]; then
|
||||
echo "✅ Linux build successful"
|
||||
ls -la ${{ env.BUILD_DIR }}/
|
||||
else
|
||||
echo "❌ Linux build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Linux build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ needs.prepare.outputs.artifact_name }}-linux
|
||||
path: ${{ env.BUILD_DIR }}/skelly-linux-${{ needs.prepare.outputs.version }}.x86_64
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
# macOS build job
|
||||
build-macos:
|
||||
name: Build macOS
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, setup-templates]
|
||||
if: contains(needs.prepare.outputs.platforms, 'macos')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: ${{ env.GODOT_VERSION }}
|
||||
use-dotnet: false
|
||||
|
||||
- name: Restore export templates cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/share/godot/export_templates
|
||||
key: godot-templates-${{ env.GODOT_VERSION }}
|
||||
restore-keys: |
|
||||
godot-templates-
|
||||
|
||||
- name: Create build directory
|
||||
run: mkdir -p ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Import project assets
|
||||
run: |
|
||||
echo "📦 Importing project assets..."
|
||||
godot --headless --verbose --editor --quit || true
|
||||
sleep 2
|
||||
|
||||
- name: Build macOS application
|
||||
run: |
|
||||
echo "🏗️ Building macOS application..."
|
||||
godot --headless --verbose --export-${{ needs.prepare.outputs.build_type }} "macOS" \
|
||||
${{ env.BUILD_DIR }}/skelly-macos-${{ needs.prepare.outputs.version }}.zip
|
||||
|
||||
# Verify build output
|
||||
if [[ -f "${{ env.BUILD_DIR }}/skelly-macos-${{ needs.prepare.outputs.version }}.zip" ]]; then
|
||||
echo "✅ macOS build successful"
|
||||
ls -la ${{ env.BUILD_DIR }}/
|
||||
else
|
||||
echo "❌ macOS build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload macOS build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ needs.prepare.outputs.artifact_name }}-macos
|
||||
path: ${{ env.BUILD_DIR }}/skelly-macos-${{ needs.prepare.outputs.version }}.zip
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
# Android build job
|
||||
build-android:
|
||||
name: Build Android
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, setup-templates]
|
||||
if: contains(needs.prepare.outputs.platforms, 'android')
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
api-level: 33
|
||||
build-tools: 33.0.0
|
||||
|
||||
- name: Setup Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: ${{ env.GODOT_VERSION }}
|
||||
use-dotnet: false
|
||||
|
||||
- name: Restore export templates cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.local/share/godot/export_templates
|
||||
key: godot-templates-${{ env.GODOT_VERSION }}
|
||||
restore-keys: |
|
||||
godot-templates-
|
||||
|
||||
- name: Create build directory
|
||||
run: mkdir -p ${{ env.BUILD_DIR }}
|
||||
|
||||
- name: Import project assets
|
||||
run: |
|
||||
echo "📦 Importing project assets..."
|
||||
godot --headless --verbose --editor --quit || true
|
||||
sleep 2
|
||||
|
||||
- name: Build Android APK
|
||||
run: |
|
||||
echo "🏗️ Building Android APK..."
|
||||
|
||||
# Set ANDROID_HOME if not already set
|
||||
export ANDROID_HOME=${ANDROID_HOME:-$ANDROID_SDK_ROOT}
|
||||
|
||||
godot --headless --verbose --export-${{ needs.prepare.outputs.build_type }} "Android" \
|
||||
${{ env.BUILD_DIR }}/skelly-android-${{ needs.prepare.outputs.version }}.apk
|
||||
|
||||
# Verify build output
|
||||
if [[ -f "${{ env.BUILD_DIR }}/skelly-android-${{ needs.prepare.outputs.version }}.apk" ]]; then
|
||||
echo "✅ Android build successful"
|
||||
ls -la ${{ env.BUILD_DIR }}/
|
||||
|
||||
# Show APK info
|
||||
echo "📱 APK Information:"
|
||||
file ${{ env.BUILD_DIR }}/skelly-android-${{ needs.prepare.outputs.version }}.apk
|
||||
else
|
||||
echo "❌ Android build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Android build
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ needs.prepare.outputs.artifact_name }}-android
|
||||
path: ${{ env.BUILD_DIR }}/skelly-android-${{ needs.prepare.outputs.version }}.apk
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
# Summary job - creates release summary
|
||||
summary:
|
||||
name: Build Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, setup-templates, build-windows, build-linux, build-macos, build-android]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Generate build summary
|
||||
run: |
|
||||
echo "🎮 Build Summary for ${{ needs.prepare.outputs.artifact_name }}"
|
||||
echo "=================================="
|
||||
echo ""
|
||||
echo "📋 Configuration:"
|
||||
echo " Version: ${{ needs.prepare.outputs.version }}"
|
||||
echo " Build Type: ${{ needs.prepare.outputs.build_type }}"
|
||||
echo " Platforms: ${{ needs.prepare.outputs.platforms }}"
|
||||
echo ""
|
||||
echo "📊 Build Results:"
|
||||
|
||||
platforms="${{ needs.prepare.outputs.platforms }}"
|
||||
|
||||
if [[ "$platforms" == *"windows"* ]]; then
|
||||
windows_status="${{ needs.build-windows.result }}"
|
||||
echo " 🪟 Windows: $windows_status"
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"linux"* ]]; then
|
||||
linux_status="${{ needs.build-linux.result }}"
|
||||
echo " 🐧 Linux: $linux_status"
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"macos"* ]]; then
|
||||
macos_status="${{ needs.build-macos.result }}"
|
||||
echo " 🍎 macOS: $macos_status"
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"android"* ]]; then
|
||||
android_status="${{ needs.build-android.result }}"
|
||||
echo " 🤖 Android: $android_status"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📦 Artifacts are available for 7 days"
|
||||
echo "🔗 Download from: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
- name: Check overall build status
|
||||
run: |
|
||||
# Check if any required builds failed
|
||||
platforms="${{ needs.prepare.outputs.platforms }}"
|
||||
failed_builds=()
|
||||
|
||||
if [[ "$platforms" == *"windows"* ]] && [[ "${{ needs.build-windows.result }}" != "success" ]]; then
|
||||
failed_builds+=("Windows")
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"linux"* ]] && [[ "${{ needs.build-linux.result }}" != "success" ]]; then
|
||||
failed_builds+=("Linux")
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"macos"* ]] && [[ "${{ needs.build-macos.result }}" != "success" ]]; then
|
||||
failed_builds+=("macOS")
|
||||
fi
|
||||
|
||||
if [[ "$platforms" == *"android"* ]] && [[ "${{ needs.build-android.result }}" != "success" ]]; then
|
||||
failed_builds+=("Android")
|
||||
fi
|
||||
|
||||
if [[ ${#failed_builds[@]} -gt 0 ]]; then
|
||||
echo "❌ Build failed for: ${failed_builds[*]}"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All builds completed successfully!"
|
||||
fi
|
||||
304
.gitea/workflows/ci.yml
Normal file
304
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,304 @@
|
||||
name: Continuous Integration
|
||||
|
||||
# CI pipeline for the Skelly Godot project
|
||||
#
|
||||
# Code quality checks (formatting, linting, testing) run as independent jobs
|
||||
# in parallel. Uses tools/run_development.py for consistency with local development.
|
||||
#
|
||||
# Features:
|
||||
# - Independent job execution (no dependencies between format/lint/test)
|
||||
# - Automatic code formatting with commit back to branch
|
||||
# - Error reporting and PR comments
|
||||
# - Manual execution with selective step skipping
|
||||
|
||||
on:
|
||||
# Trigger on push to any branch - only when relevant files change
|
||||
push:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- '**/*.gd' # Any GDScript file
|
||||
- '.gdlintrc' # Linting configuration
|
||||
- '.gdformatrc' # Formatting configuration
|
||||
- 'tools/run_development.py' # Development workflow script
|
||||
- '.gitea/workflows/ci.yml' # This workflow file
|
||||
|
||||
# Trigger on pull requests - same file filters as push
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- '**/*.gd'
|
||||
- '.gdlintrc'
|
||||
- '.gdformatrc'
|
||||
- 'tools/run_development.py'
|
||||
- '.gitea/workflows/ci.yml'
|
||||
|
||||
# Allow manual triggering with optional step skipping
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_format:
|
||||
description: 'Skip code formatting'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
skip_lint:
|
||||
description: 'Skip code linting'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
skip_tests:
|
||||
description: 'Skip test execution'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: Code Formatting
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: ${{ always() && github.event.inputs.skip_format != 'true' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade "setuptools<81"
|
||||
pip install gdtoolkit==4
|
||||
|
||||
- name: Run code formatting
|
||||
id: format
|
||||
run: |
|
||||
echo "🎨 Running GDScript formatting..."
|
||||
python tools/run_development.py --format --silent --yaml > format_results.yaml
|
||||
|
||||
- name: Upload formatting results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: format-results
|
||||
path: |
|
||||
format_results.yaml
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
- name: Check for formatting changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "📝 No formatting changes detected"
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📝 Formatting changes detected"
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "🔍 Changed files:"
|
||||
git diff --name-only
|
||||
echo ""
|
||||
echo "📊 Diff summary:"
|
||||
git diff --stat
|
||||
fi
|
||||
|
||||
- name: Commit and push formatting changes
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
echo "💾 Committing formatting changes..."
|
||||
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea.local"
|
||||
|
||||
git add -A
|
||||
|
||||
commit_message="🎨 Auto-format GDScript code
|
||||
|
||||
Automated formatting applied by tools/run_development.py
|
||||
|
||||
🤖 Generated by Gitea Actions
|
||||
Workflow: ${{ github.workflow }}
|
||||
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
git commit -m "$commit_message"
|
||||
|
||||
target_branch="${{ github.event.pull_request.head.ref || github.ref_name }}"
|
||||
echo "📤 Pushing changes to branch: $target_branch"
|
||||
git push origin HEAD:"$target_branch"
|
||||
|
||||
echo "✅ Formatting changes pushed successfully!"
|
||||
|
||||
lint:
|
||||
name: Code Quality Check
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.skip_lint != 'true' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade "setuptools<81"
|
||||
pip install gdtoolkit==4
|
||||
|
||||
- name: Run linting
|
||||
id: lint
|
||||
run: |
|
||||
echo "🔍 Running GDScript linting..."
|
||||
python tools/run_development.py --lint --silent --yaml > lint_results.yaml
|
||||
|
||||
- name: Upload linting results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: lint-results
|
||||
path: |
|
||||
lint_results.yaml
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
test:
|
||||
name: Test Execution
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.skip_tests != 'true' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade "setuptools<81"
|
||||
pip install gdtoolkit==4
|
||||
|
||||
- name: Set up Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: 4.3.0
|
||||
use-dotnet: false
|
||||
|
||||
- name: Run tests
|
||||
id: test
|
||||
run: |
|
||||
echo "🧪 Running GDScript tests..."
|
||||
python tools/run_development.py --test --silent --yaml > test_results.yaml
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
test_results.yaml
|
||||
retention-days: 7
|
||||
compression-level: 0
|
||||
|
||||
summary:
|
||||
name: CI Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [format, lint, test]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Set workflow status
|
||||
id: status
|
||||
run: |
|
||||
format_status="${{ needs.format.result }}"
|
||||
lint_status="${{ needs.lint.result }}"
|
||||
test_status="${{ needs.test.result }}"
|
||||
|
||||
echo "📊 Workflow Results:"
|
||||
echo "🎨 Format: $format_status"
|
||||
echo "🔍 Lint: $lint_status"
|
||||
echo "🧪 Test: $test_status"
|
||||
|
||||
if [[ "$format_status" == "success" && "$lint_status" == "success" && ("$test_status" == "success" || "$test_status" == "skipped") ]]; then
|
||||
echo "overall_status=success" >> $GITHUB_OUTPUT
|
||||
echo "✅ All CI checks passed!"
|
||||
else
|
||||
echo "overall_status=failure" >> $GITHUB_OUTPUT
|
||||
echo "❌ Some CI checks failed"
|
||||
fi
|
||||
|
||||
- name: Comment on PR (if applicable)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const formatStatus = '${{ needs.format.result }}';
|
||||
const lintStatus = '${{ needs.lint.result }}';
|
||||
const testStatus = '${{ needs.test.result }}';
|
||||
const overallStatus = '${{ steps.status.outputs.overall_status }}';
|
||||
|
||||
const getStatusEmoji = (status) => {
|
||||
switch(status) {
|
||||
case 'success': return '✅';
|
||||
case 'failure': return '❌';
|
||||
case 'skipped': return '⏭️';
|
||||
default: return '⚠️';
|
||||
}
|
||||
};
|
||||
|
||||
const message = `## 🤖 CI Pipeline Results
|
||||
|
||||
| Step | Status | Result |
|
||||
|------|--------|--------|
|
||||
| 🎨 Formatting | ${getStatusEmoji(formatStatus)} | ${formatStatus} |
|
||||
| 🔍 Linting | ${getStatusEmoji(lintStatus)} | ${lintStatus} |
|
||||
| 🧪 Testing | ${getStatusEmoji(testStatus)} | ${testStatus} |
|
||||
|
||||
**Overall Status:** ${getStatusEmoji(overallStatus)} ${overallStatus.toUpperCase()}
|
||||
|
||||
${overallStatus === 'success'
|
||||
? '🎉 All checks passed! This PR is ready for review.'
|
||||
: '⚠️ Some checks failed. Please review the workflow logs and fix any issues.'}
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: message
|
||||
});
|
||||
|
||||
- name: Set final exit code
|
||||
run: |
|
||||
if [[ "${{ steps.status.outputs.overall_status }}" == "success" ]]; then
|
||||
echo "🎉 CI Pipeline completed successfully!"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ CI Pipeline failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,278 +0,0 @@
|
||||
name: GDScript Auto-Formatting
|
||||
|
||||
on:
|
||||
# Trigger on pull requests to main branch
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- '**/*.gd'
|
||||
- '.gdformatrc'
|
||||
- '.gitea/workflows/gdformat.yml'
|
||||
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_branch:
|
||||
description: 'Target branch to format (leave empty for current branch)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
gdformat:
|
||||
name: Auto-Format GDScript Code
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Grant write permissions for pushing changes
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Use the PR head ref for pull requests, or current branch for manual runs
|
||||
ref: ${{ github.event.pull_request.head.ref || github.ref }}
|
||||
# Need token with write permissions to push back
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade "setuptools<81"
|
||||
pip install gdtoolkit==4
|
||||
|
||||
- name: Verify gdformat installation
|
||||
run: |
|
||||
gdformat --version
|
||||
echo "✅ gdformat installed successfully"
|
||||
|
||||
- name: Get target branch info
|
||||
id: branch-info
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
target_branch="${{ github.event.pull_request.head.ref }}"
|
||||
echo "🔄 Processing PR branch: $target_branch"
|
||||
elif [[ -n "${{ github.event.inputs.target_branch }}" ]]; then
|
||||
target_branch="${{ github.event.inputs.target_branch }}"
|
||||
echo "🎯 Manual target branch: $target_branch"
|
||||
git checkout "$target_branch" || (echo "❌ Branch not found: $target_branch" && exit 1)
|
||||
else
|
||||
target_branch="${{ github.ref_name }}"
|
||||
echo "📍 Current branch: $target_branch"
|
||||
fi
|
||||
echo "target_branch=$target_branch" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Count GDScript files
|
||||
id: count-files
|
||||
run: |
|
||||
file_count=$(find . -name "*.gd" -not -path "./.git/*" | wc -l)
|
||||
echo "file_count=$file_count" >> $GITHUB_OUTPUT
|
||||
echo "📊 Found $file_count GDScript files to format"
|
||||
|
||||
- name: Run GDScript formatting
|
||||
id: format-files
|
||||
run: |
|
||||
echo "🎨 Starting GDScript formatting..."
|
||||
echo "================================"
|
||||
|
||||
# Initialize counters
|
||||
total_files=0
|
||||
formatted_files=0
|
||||
skipped_files=0
|
||||
failed_files=0
|
||||
|
||||
# Track if any files were actually changed
|
||||
files_changed=false
|
||||
|
||||
# Find all .gd files except TestHelper.gd (static var syntax incompatibility)
|
||||
while IFS= read -r -d '' file; do
|
||||
filename=$(basename "$file")
|
||||
|
||||
# Skip TestHelper.gd due to static var syntax incompatibility with gdformat
|
||||
if [[ "$filename" == "TestHelper.gd" ]]; then
|
||||
echo "⚠️ Skipping $file (static var syntax not supported by gdformat)"
|
||||
((total_files++))
|
||||
((skipped_files++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "🎨 Formatting: $file"
|
||||
((total_files++))
|
||||
|
||||
# Get file hash before formatting
|
||||
before_hash=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
|
||||
# Run gdformat
|
||||
if gdformat "$file" 2>/dev/null; then
|
||||
# Get file hash after formatting
|
||||
after_hash=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
|
||||
if [[ "$before_hash" != "$after_hash" ]]; then
|
||||
echo "✅ Formatted (changes applied)"
|
||||
files_changed=true
|
||||
else
|
||||
echo "✅ Already formatted"
|
||||
fi
|
||||
((formatted_files++))
|
||||
else
|
||||
echo "❌ Failed to format"
|
||||
((failed_files++))
|
||||
fi
|
||||
|
||||
done < <(find . -name "*.gd" -not -path "./.git/*" -print0)
|
||||
|
||||
# Print summary
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "📋 Formatting Summary"
|
||||
echo "================================"
|
||||
echo "📊 Total files: $total_files"
|
||||
echo "✅ Successfully formatted: $formatted_files"
|
||||
echo "⚠️ Skipped files: $skipped_files"
|
||||
echo "❌ Failed files: $failed_files"
|
||||
echo ""
|
||||
|
||||
# Export results for next step
|
||||
echo "files_changed=$files_changed" >> $GITHUB_OUTPUT
|
||||
echo "total_files=$total_files" >> $GITHUB_OUTPUT
|
||||
echo "formatted_files=$formatted_files" >> $GITHUB_OUTPUT
|
||||
echo "failed_files=$failed_files" >> $GITHUB_OUTPUT
|
||||
|
||||
# Exit with error if any files failed
|
||||
if [[ $failed_files -gt 0 ]]; then
|
||||
echo "❌ Formatting FAILED - $failed_files file(s) could not be formatted"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All files processed successfully!"
|
||||
fi
|
||||
|
||||
- name: Check for changes
|
||||
id: check-changes
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "📝 No formatting changes detected"
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📝 Formatting changes detected"
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Show what changed
|
||||
echo "🔍 Changed files:"
|
||||
git diff --name-only
|
||||
echo ""
|
||||
echo "📊 Diff summary:"
|
||||
git diff --stat
|
||||
fi
|
||||
|
||||
- name: Commit and push changes
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
echo "💾 Committing formatting changes..."
|
||||
|
||||
# Configure git
|
||||
git config user.name "Gitea Actions"
|
||||
git config user.email "actions@gitea.local"
|
||||
|
||||
# Add all changed files
|
||||
git add -A
|
||||
|
||||
# Create commit with detailed message
|
||||
commit_message="🎨 Auto-format GDScript code
|
||||
|
||||
Automated formatting applied by gdformat workflow
|
||||
|
||||
📊 Summary:
|
||||
- Total files processed: ${{ steps.format-files.outputs.total_files }}
|
||||
- Successfully formatted: ${{ steps.format-files.outputs.formatted_files }}
|
||||
- Files with changes: $(git diff --cached --name-only | wc -l)
|
||||
|
||||
🤖 Generated by Gitea Actions
|
||||
Workflow: ${{ github.workflow }}
|
||||
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
git commit -m "$commit_message"
|
||||
|
||||
# Push changes back to the branch
|
||||
target_branch="${{ steps.branch-info.outputs.target_branch }}"
|
||||
echo "📤 Pushing changes to branch: $target_branch"
|
||||
|
||||
git push origin HEAD:"$target_branch"
|
||||
|
||||
echo "✅ Changes pushed successfully!"
|
||||
|
||||
- name: Summary comment (PR only)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const hasChanges = '${{ steps.check-changes.outputs.has_changes }}' === 'true';
|
||||
const totalFiles = '${{ steps.format-files.outputs.total_files }}';
|
||||
const formattedFiles = '${{ steps.format-files.outputs.formatted_files }}';
|
||||
const failedFiles = '${{ steps.format-files.outputs.failed_files }}';
|
||||
|
||||
let message;
|
||||
if (hasChanges) {
|
||||
message = `🎨 **GDScript Auto-Formatting Complete**
|
||||
|
||||
✅ Code has been automatically formatted and pushed to this branch.
|
||||
|
||||
📊 **Summary:**
|
||||
- Total files processed: ${totalFiles}
|
||||
- Successfully formatted: ${formattedFiles}
|
||||
- Files with changes applied: ${hasChanges ? 'Yes' : 'No'}
|
||||
|
||||
🔄 **Next Steps:**
|
||||
The latest commit contains the formatted code. You may need to pull the changes locally.
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
|
||||
} else {
|
||||
message = `🎨 **GDScript Formatting Check**
|
||||
|
||||
✅ All GDScript files are already properly formatted!
|
||||
|
||||
📊 **Summary:**
|
||||
- Total files checked: ${totalFiles}
|
||||
- Files needing formatting: 0
|
||||
|
||||
🎉 No changes needed - code style is consistent.
|
||||
|
||||
[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})`;
|
||||
}
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: message
|
||||
});
|
||||
|
||||
- name: Upload formatting artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gdformat-results
|
||||
path: |
|
||||
**/*.gd
|
||||
retention-days: 7
|
||||
|
||||
- name: Workflow completion status
|
||||
run: |
|
||||
echo "🎉 GDScript formatting workflow completed!"
|
||||
echo ""
|
||||
echo "📋 Final Status:"
|
||||
if [[ "${{ steps.format-files.outputs.failed_files }}" != "0" ]]; then
|
||||
echo "❌ Some files failed to format"
|
||||
exit 1
|
||||
elif [[ "${{ steps.check-changes.outputs.has_changes }}" == "true" ]]; then
|
||||
echo "✅ Code formatted and changes pushed"
|
||||
else
|
||||
echo "✅ Code already properly formatted"
|
||||
fi
|
||||
@@ -1,147 +0,0 @@
|
||||
name: GDScript Linting
|
||||
|
||||
on:
|
||||
# Trigger on push to any branch
|
||||
push:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- '**/*.gd'
|
||||
- '.gdlintrc'
|
||||
- '.gitea/workflows/gdlint.yml'
|
||||
|
||||
# Trigger on pull requests
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- '**/*.gd'
|
||||
- '.gdlintrc'
|
||||
- '.gitea/workflows/gdlint.yml'
|
||||
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
gdlint:
|
||||
name: GDScript Code Quality Check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade "setuptools<81"
|
||||
pip install gdtoolkit==4
|
||||
|
||||
- name: Verify gdlint installation
|
||||
run: |
|
||||
gdlint --version
|
||||
echo "✅ gdlint installed successfully"
|
||||
|
||||
- name: Count GDScript files
|
||||
id: count-files
|
||||
run: |
|
||||
file_count=$(find . -name "*.gd" -not -path "./.git/*" | wc -l)
|
||||
echo "file_count=$file_count" >> $GITHUB_OUTPUT
|
||||
echo "📊 Found $file_count GDScript files to lint"
|
||||
|
||||
- name: Run GDScript linting
|
||||
run: |
|
||||
echo "🔍 Starting GDScript linting..."
|
||||
echo "================================"
|
||||
|
||||
# Initialize counters
|
||||
total_files=0
|
||||
clean_files=0
|
||||
warning_files=0
|
||||
error_files=0
|
||||
|
||||
# Find all .gd files except TestHelper.gd (static var syntax incompatibility)
|
||||
while IFS= read -r -d '' file; do
|
||||
filename=$(basename "$file")
|
||||
|
||||
# Skip TestHelper.gd due to static var syntax incompatibility with gdlint
|
||||
if [[ "$filename" == "TestHelper.gd" ]]; then
|
||||
echo "⚠️ Skipping $file (static var syntax not supported by gdlint)"
|
||||
((total_files++))
|
||||
((clean_files++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "🔍 Linting: $file"
|
||||
((total_files++))
|
||||
|
||||
# Run gdlint and capture output
|
||||
if output=$(gdlint "$file" 2>&1); then
|
||||
if [[ -z "$output" ]]; then
|
||||
echo "✅ Clean"
|
||||
((clean_files++))
|
||||
else
|
||||
echo "⚠️ Warnings found:"
|
||||
echo "$output"
|
||||
((warning_files++))
|
||||
fi
|
||||
else
|
||||
echo "❌ Errors found:"
|
||||
echo "$output"
|
||||
((error_files++))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
done < <(find . -name "*.gd" -not -path "./.git/*" -print0)
|
||||
|
||||
# Print summary
|
||||
echo "================================"
|
||||
echo "📋 Linting Summary"
|
||||
echo "================================"
|
||||
echo "📊 Total files: $total_files"
|
||||
echo "✅ Clean files: $clean_files"
|
||||
echo "⚠️ Files with warnings: $warning_files"
|
||||
echo "❌ Files with errors: $error_files"
|
||||
echo ""
|
||||
|
||||
# Set exit code based on results
|
||||
if [[ $error_files -gt 0 ]]; then
|
||||
echo "❌ Linting FAILED - $error_files file(s) have errors"
|
||||
echo "Please fix the errors above before merging"
|
||||
exit 1
|
||||
elif [[ $warning_files -gt 0 ]]; then
|
||||
echo "⚠️ Linting PASSED with warnings - Consider fixing them"
|
||||
echo "✅ No blocking errors found"
|
||||
exit 0
|
||||
else
|
||||
echo "✅ All GDScript files passed linting!"
|
||||
echo "🎉 Code quality check complete - ready for merge"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Upload linting results
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: gdlint-results
|
||||
path: |
|
||||
**/*.gd
|
||||
retention-days: 7
|
||||
|
||||
- name: Comment on PR (if applicable)
|
||||
if: github.event_name == 'pull_request' && failure()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '❌ **GDScript Linting Failed**\n\nPlease check the workflow logs and fix the linting errors before merging.\n\n[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
|
||||
})
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,3 +6,8 @@
|
||||
*.tmp
|
||||
*.import~
|
||||
test_results.txt
|
||||
|
||||
# python
|
||||
|
||||
.venv
|
||||
*.pyc
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
- Use TDD methodology for development;
|
||||
- Use static data types;
|
||||
- Keep documentation up to date;
|
||||
- Always run gdlint, gdformat and run tests;
|
||||
- Always run tests `./tools/run_development.py --yaml --silent`;
|
||||
|
||||
97
DEVELOPMENT_TOOLS.md
Normal file
97
DEVELOPMENT_TOOLS.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Development Tools
|
||||
|
||||
Development workflow tools for the Skelly Godot project.
|
||||
|
||||
Python script that handles code formatting, linting, and testing.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run all development checks (recommended for pre-commit):
|
||||
```bash
|
||||
run_dev.bat
|
||||
```
|
||||
|
||||
Runs code formatting → linting → testing.
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Main Unified Script
|
||||
- **`run_dev.bat`** - Main unified development script with all functionality
|
||||
|
||||
### Individual Tools (Legacy - redirect to unified script)
|
||||
- **`run_all.bat`** - Same as `run_dev.bat` (legacy compatibility)
|
||||
- **`run_lint.bat`** - Run only linting (redirects to `run_dev.bat --lint`)
|
||||
- **`run_format.bat`** - Run only formatting (redirects to `run_dev.bat --format`)
|
||||
- **`run_tests.bat`** - Run only tests (redirects to `run_dev.bat --test`)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Run all checks (default behavior)
|
||||
run_dev.bat
|
||||
|
||||
# Run only specific tools
|
||||
run_dev.bat --lint
|
||||
run_dev.bat --format
|
||||
run_dev.bat --test
|
||||
|
||||
# Run custom workflow steps
|
||||
run_dev.bat --steps format lint
|
||||
run_dev.bat --steps format test
|
||||
|
||||
# Show help
|
||||
run_dev.bat --help
|
||||
```
|
||||
|
||||
## What Each Tool Does
|
||||
|
||||
### 🔍 Linting (`gdlint`)
|
||||
- Checks GDScript code for style violations
|
||||
- Enforces naming conventions
|
||||
- Validates code structure and patterns
|
||||
- **Fails the workflow if errors are found**
|
||||
|
||||
### 🎨 Formatting (`gdformat`)
|
||||
- Automatically formats GDScript code
|
||||
- Ensures consistent indentation and spacing
|
||||
- Fixes basic style issues
|
||||
- **Fails the workflow if files cannot be formatted**
|
||||
|
||||
### 🧪 Testing (`godot`)
|
||||
- Runs all test files in `tests/` directory
|
||||
- Executes Godot scripts in headless mode
|
||||
- Reports test results and failures
|
||||
- **Continues workflow even if tests fail** (for review)
|
||||
|
||||
## Dependencies
|
||||
|
||||
The script automatically checks for and provides installation instructions for:
|
||||
- Python 3.x
|
||||
- pip
|
||||
- Godot Engine (for tests)
|
||||
- gdtoolkit (gdlint, gdformat)
|
||||
|
||||
## Output Features
|
||||
|
||||
- Colorized output
|
||||
- Emoji status indicators
|
||||
- Tool summaries
|
||||
- Execution time tracking
|
||||
- Warning suppression
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Before committing**: Run `run_dev.bat` to ensure code quality
|
||||
2. **Fix any linting errors** - the workflow will abort on errors
|
||||
3. **Review any test failures** - tests don't abort workflow but should be addressed
|
||||
4. **Commit your changes** once all checks pass
|
||||
|
||||
## Integration
|
||||
|
||||
Works with:
|
||||
- Git hooks (pre-commit)
|
||||
- CI/CD pipelines
|
||||
- IDE integrations
|
||||
- Manual development workflow
|
||||
|
||||
Legacy batch files remain functional.
|
||||
@@ -22,121 +22,30 @@ audio:
|
||||
sprites:
|
||||
characters:
|
||||
skeleton:
|
||||
"Skeleton Attack.png":
|
||||
"assets/sprites/characters/skeleton/*":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character attack animation sprite"
|
||||
|
||||
"Skeleton Dead.png":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character death animation sprite"
|
||||
|
||||
"Skeleton Hit.png":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character hit reaction animation sprite"
|
||||
|
||||
"Skeleton Idle.png":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character idle animation sprite"
|
||||
|
||||
"Skeleton React.png":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character reaction animation sprite"
|
||||
|
||||
"Skeleton Walk.png":
|
||||
source: "https://jesse-m.itch.io/skeleton-pack"
|
||||
license: "" # TODO: Verify license from itch.io page
|
||||
attribution: "Skeleton Pack by Jesse M"
|
||||
modifications: ""
|
||||
usage: "Skeleton character walking animation sprite"
|
||||
usage: "Placeholder for animation sprites"
|
||||
|
||||
skulls:
|
||||
"blue.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
"assets/sprites/skulls/*":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skelly-assests"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"green.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"grey.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"orange.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"pink.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"purple.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: CC""
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"red.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"dark-blue.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
"yellow.png":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skulls-icons"
|
||||
license: "CC"
|
||||
attribution: "Skull icons by @nett00n"
|
||||
attribution: "Skelly icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: ""
|
||||
|
||||
Referenced in original sources.yaml but file not found:
|
||||
textures:
|
||||
backgrounds:
|
||||
"beanstalk-dark.webp":
|
||||
source: "https://www.toptal.com/designers/subtlepatterns/beanstalk-dark-pattern/"
|
||||
license: "" # TODO: Verify license and locate file
|
||||
attribution: "Beanstalk Dark pattern from Subtle Patterns"
|
||||
"BG.pg":
|
||||
source: "https://gitea.nett00n.org/nett00n/pixelart/src/branch/main/pixelorama/2025-skelly-assests"
|
||||
license: "CC"
|
||||
attribution: "Skelly icons by @nett00n"
|
||||
modifications: ""
|
||||
usage: "Background texture (file location TBD)"
|
||||
usage: ""
|
||||
|
||||
# TODO: Verify all license information by visiting source URLs
|
||||
# TODO: Check for any missing assets not documented here
|
||||
|
||||
BIN
assets/textures/backgrounds/BGx3.png
Normal file
BIN
assets/textures/backgrounds/BGx3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 350 B |
@@ -2,16 +2,16 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c8y6tlvcgh2gn"
|
||||
path="res://.godot/imported/beanstalk-dark.webp-cdfce4b5eb60c993469ff7fa805e2a15.ctex"
|
||||
uid="uid://bengv32u1jeym"
|
||||
path="res://.godot/imported/BGx3.png-7878045c31a8f7297b620b7e42c1a5bf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/textures/backgrounds/beanstalk-dark.webp"
|
||||
dest_files=["res://.godot/imported/beanstalk-dark.webp-cdfce4b5eb60c993469ff7fa805e2a15.ctex"]
|
||||
source_file="res://assets/textures/backgrounds/BGx3.png"
|
||||
dest_files=["res://.godot/imported/BGx3.png-7878045c31a8f7297b620b7e42c1a5bf.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 486 B |
@@ -28,7 +28,7 @@ Guidance for Claude Code (claude.ai/code) when working with this repository.
|
||||
- Invalid swaps automatically revert after animation
|
||||
- State machine: WAITING → SELECTING → SWAPPING → PROCESSING
|
||||
- Test scripts located in `tests/` directory for system validation
|
||||
- Use `test_logging.gd` to validate the logging system functionality
|
||||
- Use `TestLogging.gd` to validate the logging system functionality
|
||||
|
||||
### Audio Configuration
|
||||
- Music: Located in `assets/audio/music/` directory with loop configuration in AudioManager
|
||||
@@ -111,7 +111,7 @@ Guidance for Claude Code (claude.ai/code) when working with this repository.
|
||||
- `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` - Match-3 implementation with input validation
|
||||
- `scenes/game/gameplays/Match3Gameplay.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
|
||||
@@ -123,8 +123,9 @@ Guidance for Claude Code (claude.ai/code) when working with this repository.
|
||||
### Before Making Changes
|
||||
1. Check `docs/MAP.md` for architecture
|
||||
2. Review `docs/CODE_OF_CONDUCT.md` for coding standards
|
||||
3. Understand existing patterns before implementing features
|
||||
4. If adding assets, prepare `assets/sources.yaml` documentation
|
||||
3. **Review naming conventions**: See [Naming Convention Quick Reference](CODE_OF_CONDUCT.md#naming-convention-quick-reference) for all file and code naming standards
|
||||
4. Understand existing patterns before implementing features
|
||||
5. If adding assets, prepare `assets/sources.yaml` documentation following [asset naming conventions](CODE_OF_CONDUCT.md#5-asset-file-naming)
|
||||
|
||||
### Testing Changes
|
||||
- Run project with F5 in Godot Editor
|
||||
@@ -132,10 +133,10 @@ Guidance for Claude Code (claude.ai/code) when working with this repository.
|
||||
- Verify scene transitions work
|
||||
- Check mobile compatibility if UI changes made
|
||||
- Use test scripts from `tests/` directory to validate functionality
|
||||
- Run `test_logging.gd` after logging system changes
|
||||
- Run `TestLogging.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
|
||||
- **Migration compatibility**: Run `TestMigrationCompatibility.gd` for version upgrades
|
||||
|
||||
### Common Implementation Patterns
|
||||
- **Scene transitions**: Use `GameManager.start_game_with_mode()` with built-in validation
|
||||
|
||||
@@ -27,6 +27,9 @@ Coding standards and development practices for the Skelly project. These guideli
|
||||
## GDScript Coding Standards
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
> 📋 **Quick Reference**: For complete naming convention details, see the **[Naming Convention Quick Reference](#naming-convention-quick-reference)** section below.
|
||||
|
||||
```gdscript
|
||||
# Variables and functions: snake_case
|
||||
var player_health: int = 100
|
||||
@@ -39,6 +42,11 @@ const TILE_SPACING := 54
|
||||
# Classes: PascalCase
|
||||
class_name PlayerController
|
||||
|
||||
# Scene files (.tscn) and Script files (.gd): PascalCase
|
||||
# MainMenu.tscn, MainMenu.gd
|
||||
# Match3Gameplay.tscn, Match3Gameplay.gd
|
||||
# TestAudioManager.gd (test files)
|
||||
|
||||
# Signals: past_tense
|
||||
signal health_changed
|
||||
signal game_started
|
||||
@@ -100,7 +108,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
|
||||
GameManager.start_match3_game()
|
||||
|
||||
# ❌ Wrong
|
||||
get_tree().change_scene_to_file("res://scenes/game.tscn")
|
||||
get_tree().change_scene_to_file("res://scenes/game/Game.tscn")
|
||||
```
|
||||
|
||||
### Autoload Usage
|
||||
@@ -263,6 +271,207 @@ wip
|
||||
- Verify debug state persists across scene changes
|
||||
- Check debug code doesn't affect release builds
|
||||
|
||||
## Naming Convention Quick Reference
|
||||
|
||||
> 🎯 **Single Source of Truth**: This section contains all naming conventions for the Skelly project. All other documentation files reference this section to avoid duplication and ensure consistency.
|
||||
|
||||
### 1. GDScript Code Elements
|
||||
|
||||
```gdscript
|
||||
# Variables and functions: snake_case
|
||||
var player_health: int = 100
|
||||
func calculate_damage() -> int:
|
||||
|
||||
# Constants: SCREAMING_SNAKE_CASE
|
||||
const MAX_HEALTH := 100
|
||||
const TILE_SPACING := 54
|
||||
|
||||
# Classes: PascalCase
|
||||
class_name PlayerController
|
||||
|
||||
# Signals: past_tense_with_underscores
|
||||
signal health_changed
|
||||
signal game_started
|
||||
signal match_found
|
||||
|
||||
# Private functions: prefix with underscore
|
||||
func _ready():
|
||||
func _initialize_grid():
|
||||
```
|
||||
|
||||
### 2. File Naming Standards
|
||||
|
||||
#### Script and Scene Files
|
||||
```gdscript
|
||||
# ✅ Correct: All .gd and .tscn files use PascalCase
|
||||
MainMenu.tscn / MainMenu.gd
|
||||
Match3Gameplay.tscn / Match3Gameplay.gd
|
||||
ClickomaniaGameplay.tscn / ClickomaniaGameplay.gd
|
||||
ValueStepper.tscn / ValueStepper.gd
|
||||
|
||||
# Test files: PascalCase with "Test" prefix
|
||||
TestAudioManager.gd
|
||||
TestGameManager.gd
|
||||
TestMatch3Gameplay.gd
|
||||
|
||||
# ❌ Wrong: Old snake_case style (being migrated)
|
||||
main_menu.tscn / main_menu.gd
|
||||
TestAudioManager.gd
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Scene files (.tscn) must match their script file name exactly
|
||||
- All new files must use PascalCase
|
||||
- Test files use "Test" prefix + PascalCase
|
||||
- Autoload scripts follow PascalCase (GameManager.gd, AudioManager.gd)
|
||||
|
||||
### 3. Directory Naming Conventions
|
||||
|
||||
#### Source Code Directories
|
||||
```
|
||||
# Source directories: snake_case
|
||||
src/autoloads/
|
||||
scenes/game/gameplays/
|
||||
scenes/ui/components/
|
||||
tests/helpers/
|
||||
|
||||
# Root directories: lowercase
|
||||
docs/
|
||||
tests/
|
||||
tools/
|
||||
data/
|
||||
```
|
||||
|
||||
#### Asset Directories
|
||||
```
|
||||
# Asset directories: kebab-case
|
||||
assets/audio-files/
|
||||
assets/ui-sprites/
|
||||
assets/game-textures/
|
||||
assets/fonts/
|
||||
localization/
|
||||
```
|
||||
|
||||
### 4. Resource and Configuration Files
|
||||
|
||||
```bash
|
||||
# Configuration files: lowercase with dots
|
||||
project.godot
|
||||
gdlintrc
|
||||
.gdformatrc
|
||||
.editorconfig
|
||||
export_presets.cfg
|
||||
|
||||
# Godot resource files (.tres): PascalCase
|
||||
data/DefaultBusLayout.tres
|
||||
data/PlayerSaveData.tres
|
||||
scenes/ui/DefaultTheme.tres
|
||||
|
||||
# Asset metadata: kebab-case
|
||||
assets/asset-sources.yaml
|
||||
assets/audio-files/audio-sources.yaml
|
||||
assets/ui-sprites/sprite-sources.yaml
|
||||
|
||||
# Development files: kebab-case
|
||||
requirements.txt
|
||||
development-tools.md
|
||||
```
|
||||
|
||||
### 5. Asset File Naming
|
||||
|
||||
```bash
|
||||
# Audio files: kebab-case in kebab-case directories
|
||||
assets/audio-files/background-music.ogg
|
||||
assets/audio-files/ui-sounds/button-click.wav
|
||||
assets/audio-files/game-sounds/match-sound.wav
|
||||
|
||||
# Visual assets: kebab-case
|
||||
assets/ui-sprites/main-menu-background.png
|
||||
assets/game-textures/gem-blue.png
|
||||
assets/fonts/main-ui-font.ttf
|
||||
|
||||
# Import settings: match the original file
|
||||
background-music.ogg.import
|
||||
button-click.wav.import
|
||||
```
|
||||
|
||||
**Asset Rules:**
|
||||
- All asset files use kebab-case
|
||||
- Organized in kebab-case directories
|
||||
- Import files automatically match asset names
|
||||
- Document all assets in `asset-sources.yaml`
|
||||
|
||||
### 6. Git Workflow Conventions
|
||||
|
||||
#### Branch Naming
|
||||
```bash
|
||||
# Feature branches: feature/description-with-hyphens
|
||||
feature/new-gameplay-mode
|
||||
feature/settings-ui-improvement
|
||||
feature/audio-system-upgrade
|
||||
|
||||
# Bug fixes: fix/description-with-hyphens
|
||||
fix/tile-positioning-bug
|
||||
fix/save-data-corruption
|
||||
fix/debug-menu-visibility
|
||||
|
||||
# Refactoring: refactor/component-name
|
||||
refactor/match3-input-system
|
||||
refactor/autoload-structure
|
||||
|
||||
# Documentation: docs/section-name
|
||||
docs/code-of-conduct-update
|
||||
docs/api-documentation
|
||||
```
|
||||
|
||||
#### Commit Message Format
|
||||
```bash
|
||||
# Format: <type>: <description>
|
||||
# Examples:
|
||||
feat: add dark mode toggle to settings menu
|
||||
fix: resolve tile swap animation timing issue
|
||||
docs: update naming conventions in code of conduct
|
||||
refactor: migrate print statements to DebugManager
|
||||
test: add comprehensive match3 validation tests
|
||||
```
|
||||
|
||||
### 7. Quick Reference Summary
|
||||
|
||||
| File Type | Convention | Example |
|
||||
|-----------|------------|---------|
|
||||
| **GDScript Files** | PascalCase | `MainMenu.gd`, `AudioManager.gd` |
|
||||
| **Scene Files** | PascalCase | `MainMenu.tscn`, `Match3Gameplay.tscn` |
|
||||
| **Test Files** | Test + PascalCase | `TestAudioManager.gd` |
|
||||
| **Variables/Functions** | snake_case | `player_health`, `calculate_damage()` |
|
||||
| **Constants** | SCREAMING_SNAKE_CASE | `MAX_HEALTH`, `TILE_SPACING` |
|
||||
| **Classes** | PascalCase | `class_name PlayerController` |
|
||||
| **Signals** | past_tense | `health_changed`, `game_started` |
|
||||
| **Directories** | snake_case (src) / kebab-case (assets) | `src/autoloads/`, `assets/audio-files/` |
|
||||
| **Assets** | kebab-case | `background-music.ogg`, `gem-blue.png` |
|
||||
| **Config Files** | lowercase.extension | `project.godot`, `.gdformatrc` |
|
||||
| **Branches** | type/kebab-case | `feature/new-gameplay`, `fix/tile-bug` |
|
||||
|
||||
> ✅ **Status**: All major file naming inconsistencies have been resolved. The project now follows consistent PascalCase naming for all .gd and .tscn files.
|
||||
|
||||
### 8. File Renaming Migration Guide
|
||||
|
||||
When renaming files to follow conventions:
|
||||
|
||||
**Step-by-step procedure:**
|
||||
1. **Use Git rename**: `git mv old_file.gd NewFile.gd` (preserves history)
|
||||
2. **Update .tscn references**: Modify script path in scene files
|
||||
3. **Update code references**: Search and replace all `preload()` and `load()` statements
|
||||
4. **Update project.godot**: If file is referenced in autoloads or project settings
|
||||
5. **Update documentation**: Search all .md files for old references
|
||||
6. **Update test files**: Modify any test files that reference the renamed file
|
||||
7. **Run validation**: Execute `gdlint`, `gdformat`, and project tests
|
||||
8. **Verify in editor**: Load scenes in Godot editor to confirm everything works
|
||||
|
||||
**Tools for validation:**
|
||||
- `python tools/run_development.py --test` - Run all tests
|
||||
- `python tools/run_development.py --lint` - Check code quality
|
||||
- `python tools/run_development.py --format` - Ensure consistent formatting
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Architecture Violations
|
||||
@@ -271,7 +480,7 @@ wip
|
||||
get_tree().change_scene_to_file("some_scene.tscn")
|
||||
|
||||
# Don't hardcode paths
|
||||
var tile = load("res://scenes/game/gameplays/tile.tscn")
|
||||
var tile = load("res://scenes/game/gameplays/Tile.tscn")
|
||||
|
||||
# Don't ignore null checks
|
||||
var node = get_node("SomeNode")
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This document outlines the code quality standards implemented in the Skelly project and provides guidelines for maintaining high-quality, reliable code.
|
||||
|
||||
> 📋 **Naming Standards**: All code follows the [Naming Convention Quick Reference](CODE_OF_CONDUCT.md#naming-convention-quick-reference) for consistent file, class, and variable naming.
|
||||
|
||||
## Overview of Improvements
|
||||
|
||||
A comprehensive code quality improvement was conducted to eliminate critical flaws, improve maintainability, and ensure production-ready reliability. The improvements focus on memory safety, error handling, architecture quality, and input validation.
|
||||
@@ -28,7 +30,7 @@ for child in children_to_remove:
|
||||
```
|
||||
|
||||
**Files Improved:**
|
||||
- `scenes/game/gameplays/match3_gameplay.gd`
|
||||
- `scenes/game/gameplays/Match3Gameplay.gd`
|
||||
- `scenes/game/gameplays/tile.gd`
|
||||
|
||||
### 2. Error Handling & Recovery
|
||||
@@ -111,7 +113,7 @@ static func set_active_gem_pool(gem_indices: Array) -> void:
|
||||
|
||||
**Files Improved:**
|
||||
- `scenes/game/gameplays/tile.gd`
|
||||
- `scenes/game/gameplays/match3_gameplay.gd`
|
||||
- `scenes/game/gameplays/Match3Gameplay.gd`
|
||||
|
||||
## 🟡 Code Quality Improvements
|
||||
|
||||
@@ -173,7 +175,7 @@ func _move_cursor(direction: Vector2i) -> void:
|
||||
|
||||
**Files Improved:**
|
||||
- `scenes/ui/SettingsMenu.gd`
|
||||
- `scenes/game/gameplays/match3_gameplay.gd`
|
||||
- `scenes/game/gameplays/Match3Gameplay.gd`
|
||||
- `src/autoloads/GameManager.gd`
|
||||
|
||||
## Development Standards
|
||||
|
||||
14
docs/MAP.md
14
docs/MAP.md
@@ -3,6 +3,8 @@
|
||||
## Overview
|
||||
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.
|
||||
|
||||
> 📋 **Naming Conventions**: All file and directory naming follows the standards defined in [Naming Convention Quick Reference](CODE_OF_CONDUCT.md#naming-convention-quick-reference).
|
||||
|
||||
## Project Root Structure
|
||||
|
||||
```
|
||||
@@ -150,8 +152,8 @@ The game now uses a modular gameplay architecture where different game modes can
|
||||
|
||||
### Current Gameplay Modes
|
||||
|
||||
#### Match-3 Mode (`scenes/game/gameplays/match3_gameplay.tscn`)
|
||||
1. **Match3 Controller** (`scenes/game/gameplays/match3_gameplay.gd`)
|
||||
#### Match-3 Mode (`scenes/game/gameplays/Match3Gameplay.tscn`)
|
||||
1. **Match3 Controller** (`scenes/game/gameplays/Match3Gameplay.gd`)
|
||||
- Grid management (8x8 default) with memory-safe node cleanup
|
||||
- Match detection algorithms with bounds checking and validation
|
||||
- Tile dropping and refilling with signal connections
|
||||
@@ -178,7 +180,7 @@ The game now uses a modular gameplay architecture where different game modes can
|
||||
- Smooth animations with Tween system
|
||||
- **Memory Safety**: Resource management and cleanup
|
||||
|
||||
#### Clickomania Mode (`scenes/game/gameplays/clickomania_gameplay.tscn`)
|
||||
#### Clickomania Mode (`scenes/game/gameplays/ClickomaniaGameplay.tscn`)
|
||||
- Planned implementation for clickomania-style gameplay
|
||||
- Will integrate with same scoring and UI systems as match-3
|
||||
|
||||
@@ -262,9 +264,9 @@ sprites:
|
||||
- `MainStrings.ru.translation` - Russian translations
|
||||
|
||||
### Testing & Validation (`tests/`)
|
||||
- `test_logging.gd` - DebugManager logging system validation
|
||||
- `TestLogging.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
|
||||
- **`TestMigrationCompatibility.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)
|
||||
@@ -296,7 +298,7 @@ GameManager --> main.tscn, game.tscn
|
||||
GameManager --> scenes/game/gameplays/*.tscn (via GAMEPLAY_SCENES constant)
|
||||
Main --> MainMenu.tscn, SettingsMenu.tscn
|
||||
Game --> GameplayContainer (dynamic loading of gameplay scenes)
|
||||
Game --> scenes/game/gameplays/match3_gameplay.tscn, clickomania_gameplay.tscn
|
||||
Game --> scenes/game/gameplays/Match3Gameplay.tscn, ClickomaniaGameplay.tscn
|
||||
```
|
||||
|
||||
### Asset Dependencies
|
||||
|
||||
@@ -11,9 +11,11 @@ The `tests/` directory contains:
|
||||
- Performance benchmarks
|
||||
- Debugging tools
|
||||
|
||||
> 📋 **File Naming**: All test files follow the [naming conventions](CODE_OF_CONDUCT.md#2-file-naming-standards) with PascalCase and "Test" prefix (e.g., `TestAudioManager.gd`).
|
||||
|
||||
## Current Test Files
|
||||
|
||||
### `test_logging.gd`
|
||||
### `TestLogging.gd`
|
||||
Test script for DebugManager logging system.
|
||||
|
||||
**Features:**
|
||||
@@ -26,10 +28,10 @@ Test script for DebugManager logging system.
|
||||
**Usage:**
|
||||
```gdscript
|
||||
# Option 1: Add as temporary autoload
|
||||
# In project.godot, add: tests/test_logging.gd
|
||||
# In project.godot, add: tests/TestLogging.gd
|
||||
|
||||
# Option 2: Instantiate in a scene
|
||||
var test_script = preload("res://tests/test_logging.gd").new()
|
||||
var test_script = preload("res://tests/TestLogging.gd").new()
|
||||
add_child(test_script)
|
||||
|
||||
# Option 3: Run directly from editor
|
||||
@@ -49,7 +51,7 @@ Follow these conventions for new test files:
|
||||
|
||||
### File Naming
|
||||
- Use descriptive names starting with `test_`
|
||||
- Example: `test_audio_manager.gd`, `test_scene_transitions.gd`
|
||||
- Example: `TestAudioManager.gd`, `test_scene_transitions.gd`
|
||||
|
||||
### File Structure
|
||||
```gdscript
|
||||
@@ -104,20 +106,20 @@ func test_error_conditions():
|
||||
|
||||
### System Tests
|
||||
Test core autoload managers and global systems:
|
||||
- `test_logging.gd` - DebugManager logging system
|
||||
- `TestLogging.gd` - DebugManager logging system
|
||||
- `test_checksum_issue.gd` - SaveManager checksum validation and deterministic hashing
|
||||
- `test_migration_compatibility.gd` - SaveManager version migration and backward compatibility
|
||||
- `TestMigrationCompatibility.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
|
||||
- `TestSettingsManager.gd` - SettingsManager security validation, input validation, and error handling
|
||||
- `TestGameManager.gd` - GameManager scene transitions, race condition protection, and input validation
|
||||
- `TestAudioManager.gd` - AudioManager functionality, resource loading, and volume management
|
||||
|
||||
### Component Tests
|
||||
Test individual game components:
|
||||
- `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
|
||||
- `TestMatch3Gameplay.gd` - Match-3 gameplay mechanics, grid management, and match detection
|
||||
- `TestTile.gd` - Tile component behavior, visual feedback, and memory safety
|
||||
- `TestValueStepper.gd` - ValueStepper UI component functionality and settings integration
|
||||
|
||||
### Integration Tests
|
||||
Test system interactions and workflows:
|
||||
@@ -135,7 +137,7 @@ SaveManager implements security features requiring testing for modifications.
|
||||
**Tests**: Checksum generation, JSON serialization consistency, save/load cycles
|
||||
**Usage**: Run after checksum algorithm changes
|
||||
|
||||
#### **`test_migration_compatibility.gd`** - Version Migration
|
||||
#### **`TestMigrationCompatibility.gd`** - Version Migration
|
||||
**Tests**: Backward compatibility, missing field addition, data structure normalization
|
||||
**Usage**: Test save format upgrades
|
||||
|
||||
@@ -164,7 +166,7 @@ SaveManager implements security features requiring testing for modifications.
|
||||
|
||||
#### **Test Sequence After Modifications**
|
||||
1. `test_checksum_issue.gd` - Verify checksum consistency
|
||||
2. `test_migration_compatibility.gd` - Check version upgrades
|
||||
2. `TestMigrationCompatibility.gd` - Check version upgrades
|
||||
3. `test_save_system_integration.gd` - Validate workflow
|
||||
4. Manual testing with corrupted files
|
||||
5. Performance validation
|
||||
@@ -182,7 +184,7 @@ 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/TestMigrationCompatibility.gd
|
||||
godot --headless --script tests/test_save_system_integration.gd
|
||||
```
|
||||
|
||||
@@ -200,7 +202,7 @@ For CI/CD integration:
|
||||
- name: Run Test Suite
|
||||
run: |
|
||||
godot --headless --script tests/test_checksum_issue.gd
|
||||
godot --headless --script tests/test_migration_compatibility.gd
|
||||
godot --headless --script tests/TestMigrationCompatibility.gd
|
||||
# Add other tests as needed
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# Example of how to use the ValueStepper component in any scene
|
||||
extends Control
|
||||
|
||||
# Example of setting up custom navigation
|
||||
var navigable_steppers: Array[ValueStepper] = []
|
||||
var current_stepper_index: int = 0
|
||||
|
||||
@onready
|
||||
var language_stepper: ValueStepper = $VBoxContainer/Examples/LanguageContainer/LanguageStepper
|
||||
@onready
|
||||
@@ -9,10 +13,6 @@ var difficulty_stepper: ValueStepper = $VBoxContainer/Examples/DifficultyContain
|
||||
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")
|
||||
|
||||
394
export_presets.cfg
Normal file
394
export_presets.cfg
Normal file
@@ -0,0 +1,394 @@
|
||||
[preset.0]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
export_files=PackedStringArray()
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="builds/skelly-windows.exe"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
binary_format/architecture="x86_64"
|
||||
codesign/enable=false
|
||||
codesign/identity=""
|
||||
codesign/password=""
|
||||
codesign/timestamp=true
|
||||
codesign/timestamp_server_url=""
|
||||
codesign/digest_algorithm=1
|
||||
codesign/description=""
|
||||
codesign/custom_options=PackedStringArray()
|
||||
application/modify_resources=true
|
||||
application/icon=""
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version=""
|
||||
application/product_version=""
|
||||
application/company_name=""
|
||||
application/product_name="Skelly"
|
||||
application/file_description=""
|
||||
application/copyright=""
|
||||
application/trademarks=""
|
||||
application/export_angle=0
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script=""
|
||||
ssh_remote_deploy/cleanup_script=""
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux/X11"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
export_files=PackedStringArray()
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="builds/skelly-linux.x86_64"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
texture_format/etc2=false
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script=""
|
||||
ssh_remote_deploy/cleanup_script=""
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="macOS"
|
||||
platform="macOS"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
export_files=PackedStringArray()
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="builds/skelly-macos.zip"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
binary_format/architecture="universal"
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
application/icon=""
|
||||
application/icon_interpolation=4
|
||||
application/bundle_identifier="com.skelly.game"
|
||||
application/signature=""
|
||||
application/app_category="Games"
|
||||
application/short_version="1.0"
|
||||
application/version="1.0"
|
||||
application/copyright=""
|
||||
application/copyright_localized={}
|
||||
application/min_macos_version="10.12"
|
||||
display/high_res=false
|
||||
xcode/platform_build="14C18"
|
||||
xcode/sdk_version="13.1"
|
||||
xcode/sdk_name="macosx13.1"
|
||||
xcode/sdk_build="22C55"
|
||||
xcode/xcode_version="1420"
|
||||
xcode/xcode_build="14C18"
|
||||
codesign/codesign=1
|
||||
codesign/installer_identity=""
|
||||
codesign/apple_team_id=""
|
||||
codesign/identity=""
|
||||
codesign/entitlements/custom_file=""
|
||||
codesign/entitlements/allow_jit_code_execution=false
|
||||
codesign/entitlements/allow_unsigned_executable_memory=false
|
||||
codesign/entitlements/allow_dyld_environment_variables=false
|
||||
codesign/entitlements/disable_library_validation=false
|
||||
codesign/entitlements/audio_input=false
|
||||
codesign/entitlements/camera=false
|
||||
codesign/entitlements/location=false
|
||||
codesign/entitlements/address_book=false
|
||||
codesign/entitlements/calendars=false
|
||||
codesign/entitlements/photos_library=false
|
||||
codesign/entitlements/apple_events=false
|
||||
codesign/entitlements/debugging=false
|
||||
codesign/entitlements/app_sandbox/enabled=false
|
||||
codesign/entitlements/app_sandbox/network_server=false
|
||||
codesign/entitlements/app_sandbox/network_client=false
|
||||
codesign/entitlements/app_sandbox/device_usb=false
|
||||
codesign/entitlements/app_sandbox/device_bluetooth=false
|
||||
codesign/entitlements/app_sandbox/files_downloads=0
|
||||
codesign/entitlements/app_sandbox/files_pictures=0
|
||||
codesign/entitlements/app_sandbox/files_music=0
|
||||
codesign/entitlements/app_sandbox/files_movies=0
|
||||
codesign/entitlements/app_sandbox/helper_executables=[]
|
||||
notarization/notarization=0
|
||||
privacy/microphone_usage_description=""
|
||||
privacy/microphone_usage_description_localized={}
|
||||
privacy/camera_usage_description=""
|
||||
privacy/camera_usage_description_localized={}
|
||||
privacy/location_usage_description=""
|
||||
privacy/location_usage_description_localized={}
|
||||
privacy/address_book_usage_description=""
|
||||
privacy/address_book_usage_description_localized={}
|
||||
privacy/calendar_usage_description=""
|
||||
privacy/calendar_usage_description_localized={}
|
||||
privacy/photos_library_usage_description=""
|
||||
privacy/photos_library_usage_description_localized={}
|
||||
privacy/desktop_folder_usage_description=""
|
||||
privacy/desktop_folder_usage_description_localized={}
|
||||
privacy/documents_folder_usage_description=""
|
||||
privacy/documents_folder_usage_description_localized={}
|
||||
privacy/downloads_folder_usage_description=""
|
||||
privacy/downloads_folder_usage_description_localized={}
|
||||
privacy/network_volumes_usage_description=""
|
||||
privacy/network_volumes_usage_description_localized={}
|
||||
privacy/removable_volumes_usage_description=""
|
||||
privacy/removable_volumes_usage_description_localized={}
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script=""
|
||||
ssh_remote_deploy/cleanup_script=""
|
||||
|
||||
[preset.3]
|
||||
|
||||
name="Android"
|
||||
platform="Android"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
export_files=PackedStringArray()
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="builds/skelly-android.apk"
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
|
||||
[preset.3.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
gradle_build/use_gradle_build=false
|
||||
gradle_build/export_format=0
|
||||
gradle_build/min_sdk=""
|
||||
gradle_build/target_sdk=""
|
||||
architectures/armeabi-v7a=false
|
||||
architectures/arm64-v8a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=1
|
||||
version/name="1.0"
|
||||
package/unique_name="com.skelly.game"
|
||||
package/name="Skelly"
|
||||
package/signed=true
|
||||
package/app_category=2
|
||||
package/retain_data_on_uninstall=false
|
||||
package/exclude_from_recents=false
|
||||
launcher_icons/main_192x192=""
|
||||
launcher_icons/adaptive_foreground_432x432=""
|
||||
launcher_icons/adaptive_background_432x432=""
|
||||
graphics/32_bits_framebuffer=true
|
||||
graphics/opengl_debug=false
|
||||
xr_features/xr_mode=0
|
||||
xr_features/hand_tracking=0
|
||||
xr_features/hand_tracking_frequency=0
|
||||
xr_features/passthrough=0
|
||||
screen/immersive_mode=true
|
||||
screen/orientation=0
|
||||
screen/support_small=true
|
||||
screen/support_normal=true
|
||||
screen/support_large=true
|
||||
screen/support_xlarge=true
|
||||
user_data_backup/allow=false
|
||||
command_line/extra_args=""
|
||||
apk_expansion/enable=false
|
||||
apk_expansion/SALT=""
|
||||
apk_expansion/public_key=""
|
||||
permissions/custom_permissions=PackedStringArray()
|
||||
permissions/access_checkin_properties=false
|
||||
permissions/access_coarse_location=false
|
||||
permissions/access_fine_location=false
|
||||
permissions/access_location_extra_commands=false
|
||||
permissions/access_mock_location=false
|
||||
permissions/access_network_state=false
|
||||
permissions/access_surface_flinger=false
|
||||
permissions/access_wifi_state=false
|
||||
permissions/account_manager=false
|
||||
permissions/add_voicemail=false
|
||||
permissions/authenticate_accounts=false
|
||||
permissions/battery_stats=false
|
||||
permissions/bind_accessibility_service=false
|
||||
permissions/bind_appwidget=false
|
||||
permissions/bind_device_admin=false
|
||||
permissions/bind_input_method=false
|
||||
permissions/bind_nfc_service=false
|
||||
permissions/bind_notification_listener_service=false
|
||||
permissions/bind_print_service=false
|
||||
permissions/bind_remoteviews=false
|
||||
permissions/bind_text_service=false
|
||||
permissions/bind_vpn_service=false
|
||||
permissions/bind_wallpaper=false
|
||||
permissions/bluetooth=false
|
||||
permissions/bluetooth_admin=false
|
||||
permissions/bluetooth_privileged=false
|
||||
permissions/brick=false
|
||||
permissions/broadcast_package_removed=false
|
||||
permissions/broadcast_sms=false
|
||||
permissions/broadcast_sticky=false
|
||||
permissions/broadcast_wap_push=false
|
||||
permissions/call_phone=false
|
||||
permissions/call_privileged=false
|
||||
permissions/camera=false
|
||||
permissions/capture_audio_output=false
|
||||
permissions/capture_secure_video_output=false
|
||||
permissions/capture_video_output=false
|
||||
permissions/change_component_enabled_state=false
|
||||
permissions/change_configuration=false
|
||||
permissions/change_network_state=false
|
||||
permissions/change_wifi_multicast_state=false
|
||||
permissions/change_wifi_state=false
|
||||
permissions/clear_app_cache=false
|
||||
permissions/clear_app_user_data=false
|
||||
permissions/control_location_updates=false
|
||||
permissions/delete_cache_files=false
|
||||
permissions/delete_packages=false
|
||||
permissions/device_power=false
|
||||
permissions/diagnostic=false
|
||||
permissions/disable_keyguard=false
|
||||
permissions/dump=false
|
||||
permissions/expand_status_bar=false
|
||||
permissions/factory_test=false
|
||||
permissions/flashlight=false
|
||||
permissions/force_back=false
|
||||
permissions/get_accounts=false
|
||||
permissions/get_package_size=false
|
||||
permissions/get_tasks=false
|
||||
permissions/get_top_activity_info=false
|
||||
permissions/global_search=false
|
||||
permissions/hardware_test=false
|
||||
permissions/inject_events=false
|
||||
permissions/install_location_provider=false
|
||||
permissions/install_packages=false
|
||||
permissions/install_shortcut=false
|
||||
permissions/internal_system_window=false
|
||||
permissions/internet=false
|
||||
permissions/kill_background_processes=false
|
||||
permissions/location_hardware=false
|
||||
permissions/manage_accounts=false
|
||||
permissions/manage_app_tokens=false
|
||||
permissions/manage_documents=false
|
||||
permissions/manage_external_storage=false
|
||||
permissions/master_clear=false
|
||||
permissions/media_content_control=false
|
||||
permissions/modify_audio_settings=false
|
||||
permissions/modify_phone_state=false
|
||||
permissions/mount_format_filesystems=false
|
||||
permissions/mount_unmount_filesystems=false
|
||||
permissions/nfc=false
|
||||
permissions/persistent_activity=false
|
||||
permissions/process_outgoing_calls=false
|
||||
permissions/read_calendar=false
|
||||
permissions/read_call_log=false
|
||||
permissions/read_contacts=false
|
||||
permissions/read_external_storage=false
|
||||
permissions/read_frame_buffer=false
|
||||
permissions/read_history_bookmarks=false
|
||||
permissions/read_input_state=false
|
||||
permissions/read_logs=false
|
||||
permissions/read_phone_state=false
|
||||
permissions/read_profile=false
|
||||
permissions/read_sms=false
|
||||
permissions/read_social_stream=false
|
||||
permissions/read_sync_settings=false
|
||||
permissions/read_sync_stats=false
|
||||
permissions/read_user_dictionary=false
|
||||
permissions/reboot=false
|
||||
permissions/receive_boot_completed=false
|
||||
permissions/receive_mms=false
|
||||
permissions/receive_sms=false
|
||||
permissions/receive_wap_push=false
|
||||
permissions/record_audio=false
|
||||
permissions/reorder_tasks=false
|
||||
permissions/restart_packages=false
|
||||
permissions/send_respond_via_message=false
|
||||
permissions/send_sms=false
|
||||
permissions/set_activity_watcher=false
|
||||
permissions/set_alarm=false
|
||||
permissions/set_always_finish=false
|
||||
permissions/set_animation_scale=false
|
||||
permissions/set_debug_app=false
|
||||
permissions/set_orientation=false
|
||||
permissions/set_pointer_speed=false
|
||||
permissions/set_preferred_applications=false
|
||||
permissions/set_process_limit=false
|
||||
permissions/set_time=false
|
||||
permissions/set_time_zone=false
|
||||
permissions/set_wallpaper=false
|
||||
permissions/set_wallpaper_hints=false
|
||||
permissions/signal_persistent_processes=false
|
||||
permissions/status_bar=false
|
||||
permissions/subscribed_feeds_read=false
|
||||
permissions/subscribed_feeds_write=false
|
||||
permissions/system_alert_window=false
|
||||
permissions/transmit_ir=false
|
||||
permissions/uninstall_shortcut=false
|
||||
permissions/update_device_stats=false
|
||||
permissions/use_credentials=false
|
||||
permissions/use_sip=false
|
||||
permissions/vibrate=false
|
||||
permissions/wake_lock=false
|
||||
permissions/write_apn_settings=false
|
||||
permissions/write_calendar=false
|
||||
permissions/write_call_log=false
|
||||
permissions/write_contacts=false
|
||||
permissions/write_external_storage=false
|
||||
permissions/write_gservices=false
|
||||
permissions/write_history_bookmarks=false
|
||||
permissions/write_profile=false
|
||||
permissions/write_secure_settings=false
|
||||
permissions/write_settings=false
|
||||
permissions/write_sms=false
|
||||
permissions/write_social_stream=false
|
||||
permissions/write_sync_settings=false
|
||||
permissions/write_user_dictionary=false
|
||||
4
gdlintrc
4
gdlintrc
@@ -30,8 +30,8 @@ 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-file-lines: 1500
|
||||
max-line-length: 120
|
||||
max-public-methods: 20
|
||||
max-returns: 6
|
||||
mixed-tabs-and-spaces: null
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
setuptools<81
|
||||
gdtoolkit==4
|
||||
aiofiles>=23.0.0
|
||||
ruff>=0.1.0
|
||||
|
||||
89
run_all.bat
89
run_all.bat
@@ -1,89 +0,0 @@
|
||||
@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!
|
||||
232
run_dev.bat
Normal file
232
run_dev.bat
Normal file
@@ -0,0 +1,232 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM =============================================================================
|
||||
REM Skelly Development Tools Runner
|
||||
REM =============================================================================
|
||||
REM
|
||||
REM This script runs development tools for the Skelly Godot project.
|
||||
REM By default, it runs all checks: linting, formatting, and testing.
|
||||
REM
|
||||
REM Usage:
|
||||
REM run_dev.bat - Run all checks (lint + format + test)
|
||||
REM run_dev.bat --lint - Run only linting
|
||||
REM run_dev.bat --format - Run only formatting
|
||||
REM run_dev.bat --test - Run only tests
|
||||
REM run_dev.bat --help - Show this help message
|
||||
REM run_dev.bat --steps lint test - Run specific steps in order
|
||||
REM
|
||||
REM =============================================================================
|
||||
|
||||
REM Initialize variables
|
||||
set "ARG_LINT_ONLY="
|
||||
set "ARG_FORMAT_ONLY="
|
||||
set "ARG_TEST_ONLY="
|
||||
set "ARG_HELP="
|
||||
set "ARG_STEPS="
|
||||
set "CUSTOM_STEPS="
|
||||
|
||||
REM Parse command line arguments
|
||||
:parse_args
|
||||
if "%~1"=="" goto :args_parsed
|
||||
if /i "%~1"=="--lint" (
|
||||
set "ARG_LINT_ONLY=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if /i "%~1"=="--format" (
|
||||
set "ARG_FORMAT_ONLY=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if /i "%~1"=="--test" (
|
||||
set "ARG_TEST_ONLY=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if /i "%~1"=="--help" (
|
||||
set "ARG_HELP=1"
|
||||
shift
|
||||
goto :parse_args
|
||||
)
|
||||
if /i "%~1"=="--steps" (
|
||||
set "ARG_STEPS=1"
|
||||
shift
|
||||
REM Collect remaining arguments as custom steps
|
||||
:collect_steps
|
||||
if "%~1"=="" goto :args_parsed
|
||||
if "!CUSTOM_STEPS!"=="" (
|
||||
set "CUSTOM_STEPS=%~1"
|
||||
) else (
|
||||
set "CUSTOM_STEPS=!CUSTOM_STEPS! %~1"
|
||||
)
|
||||
shift
|
||||
goto :collect_steps
|
||||
)
|
||||
REM Unknown argument
|
||||
echo ❌ Unknown argument: %~1
|
||||
echo Use --help for usage information
|
||||
exit /b 1
|
||||
|
||||
:args_parsed
|
||||
|
||||
REM Show help if requested
|
||||
if defined ARG_HELP (
|
||||
echo.
|
||||
echo 🔧 Skelly Development Tools Runner
|
||||
echo.
|
||||
echo Usage:
|
||||
echo run_dev.bat - Run all checks ^(lint + format + test^)
|
||||
echo run_dev.bat --lint - Run only linting
|
||||
echo run_dev.bat --format - Run only formatting
|
||||
echo run_dev.bat --test - Run only tests
|
||||
echo run_dev.bat --help - Show this help message
|
||||
echo run_dev.bat --steps lint test - Run specific steps in order
|
||||
echo.
|
||||
echo Available steps for --steps:
|
||||
echo lint - Run GDScript linting ^(gdlint^)
|
||||
echo format - Run GDScript formatting ^(gdformat^)
|
||||
echo test - Run Godot tests
|
||||
echo.
|
||||
echo Examples:
|
||||
echo run_dev.bat ^(runs lint, format, test^)
|
||||
echo run_dev.bat --lint ^(runs only linting^)
|
||||
echo run_dev.bat --steps format lint ^(runs format then lint^)
|
||||
echo.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
echo ================================
|
||||
echo 🚀 Development Tools Runner
|
||||
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 Godot is available (only if test step will be run)
|
||||
set "NEED_GODOT="
|
||||
if defined ARG_TEST_ONLY set "NEED_GODOT=1"
|
||||
if defined ARG_STEPS (
|
||||
echo !CUSTOM_STEPS! | findstr /i "test" >nul && set "NEED_GODOT=1"
|
||||
)
|
||||
if not defined ARG_LINT_ONLY if not defined ARG_FORMAT_ONLY if not defined ARG_STEPS set "NEED_GODOT=1"
|
||||
|
||||
if defined NEED_GODOT (
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
REM Check if gdlint and gdformat are available (only if needed)
|
||||
set "NEED_GDTOOLS="
|
||||
if defined ARG_LINT_ONLY set "NEED_GDTOOLS=1"
|
||||
if defined ARG_FORMAT_ONLY set "NEED_GDTOOLS=1"
|
||||
if defined ARG_STEPS (
|
||||
echo !CUSTOM_STEPS! | findstr /i /c:"lint" >nul && set "NEED_GDTOOLS=1"
|
||||
echo !CUSTOM_STEPS! | findstr /i /c:"format" >nul && set "NEED_GDTOOLS=1"
|
||||
)
|
||||
if not defined ARG_TEST_ONLY if not defined ARG_STEPS set "NEED_GDTOOLS=1"
|
||||
|
||||
if defined NEED_GDTOOLS (
|
||||
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
|
||||
)
|
||||
|
||||
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 ✅ All dependencies are available. Running development workflow...
|
||||
echo.
|
||||
|
||||
REM Build Python command based on arguments
|
||||
set "PYTHON_CMD=python tools\run_development.py"
|
||||
|
||||
if defined ARG_LINT_ONLY (
|
||||
set "PYTHON_CMD=!PYTHON_CMD! --lint"
|
||||
echo 🔍 Running linting only...
|
||||
) else if defined ARG_FORMAT_ONLY (
|
||||
set "PYTHON_CMD=!PYTHON_CMD! --format"
|
||||
echo 🎨 Running formatting only...
|
||||
) else if defined ARG_TEST_ONLY (
|
||||
set "PYTHON_CMD=!PYTHON_CMD! --test"
|
||||
echo 🧪 Running tests only...
|
||||
) else if defined ARG_STEPS (
|
||||
set "PYTHON_CMD=!PYTHON_CMD! --steps !CUSTOM_STEPS!"
|
||||
echo 🔄 Running custom workflow: !CUSTOM_STEPS!...
|
||||
) else (
|
||||
echo 🚀 Running complete development workflow: format + lint + test...
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
REM Run the Python development workflow script
|
||||
!PYTHON_CMD!
|
||||
|
||||
REM Capture exit code and display result
|
||||
set WORKFLOW_RESULT=!errorlevel!
|
||||
|
||||
echo.
|
||||
if !WORKFLOW_RESULT! equ 0 (
|
||||
echo 🎉 Development workflow completed successfully!
|
||||
) else (
|
||||
echo ⚠️ Development workflow completed with issues.
|
||||
echo Please review the output above and fix any problems.
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
exit /b !WORKFLOW_RESULT!
|
||||
240
run_dev.sh
Normal file
240
run_dev.sh
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# =============================================================================
|
||||
# Skelly Development Tools Runner
|
||||
# =============================================================================
|
||||
#
|
||||
# This script runs development tools for the Skelly Godot project.
|
||||
# By default, it runs all checks: linting, formatting, and testing.
|
||||
#
|
||||
# Usage:
|
||||
# ./run_dev.sh - Run all checks (lint + format + test)
|
||||
# ./run_dev.sh --lint - Run only linting
|
||||
# ./run_dev.sh --format - Run only formatting
|
||||
# ./run_dev.sh --test - Run only tests
|
||||
# ./run_dev.sh --help - Show this help message
|
||||
# ./run_dev.sh --steps lint test - Run specific steps in order
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
# Initialize variables
|
||||
ARG_LINT_ONLY=""
|
||||
ARG_FORMAT_ONLY=""
|
||||
ARG_TEST_ONLY=""
|
||||
ARG_HELP=""
|
||||
ARG_STEPS=""
|
||||
CUSTOM_STEPS=""
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--lint)
|
||||
ARG_LINT_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--format)
|
||||
ARG_FORMAT_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--test)
|
||||
ARG_TEST_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
ARG_HELP=1
|
||||
shift
|
||||
;;
|
||||
--steps)
|
||||
ARG_STEPS=1
|
||||
shift
|
||||
# Collect remaining arguments as custom steps
|
||||
while [[ $# -gt 0 ]]; do
|
||||
if [[ -z "$CUSTOM_STEPS" ]]; then
|
||||
CUSTOM_STEPS="$1"
|
||||
else
|
||||
CUSTOM_STEPS="$CUSTOM_STEPS $1"
|
||||
fi
|
||||
shift
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown argument: $1"
|
||||
echo "Use --help for usage information"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Show help if requested
|
||||
if [[ -n "$ARG_HELP" ]]; then
|
||||
echo
|
||||
echo "🔧 Skelly Development Tools Runner"
|
||||
echo
|
||||
echo "Usage:"
|
||||
echo " ./run_dev.sh - Run all checks (lint + format + test)"
|
||||
echo " ./run_dev.sh --lint - Run only linting"
|
||||
echo " ./run_dev.sh --format - Run only formatting"
|
||||
echo " ./run_dev.sh --test - Run only tests"
|
||||
echo " ./run_dev.sh --help - Show this help message"
|
||||
echo " ./run_dev.sh --steps lint test - Run specific steps in order"
|
||||
echo
|
||||
echo "Available steps for --steps:"
|
||||
echo " lint - Run GDScript linting (gdlint)"
|
||||
echo " format - Run GDScript formatting (gdformat)"
|
||||
echo " test - Run Godot tests"
|
||||
echo
|
||||
echo "Examples:"
|
||||
echo " ./run_dev.sh (runs lint, format, test)"
|
||||
echo " ./run_dev.sh --lint (runs only linting)"
|
||||
echo " ./run_dev.sh --steps format lint (runs format then lint)"
|
||||
echo
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "================================"
|
||||
echo "🚀 Development Tools Runner"
|
||||
echo "================================"
|
||||
echo
|
||||
|
||||
# Check if Python is available
|
||||
if ! command -v python3 &> /dev/null && ! command -v python &> /dev/null; then
|
||||
echo "❌ ERROR: Python is not installed or not in PATH"
|
||||
echo
|
||||
echo "Installation instructions:"
|
||||
echo "1. Ubuntu/Debian: sudo apt update && sudo apt install python3 python3-pip"
|
||||
echo "2. macOS: brew install python"
|
||||
echo "3. Or download from: https://python.org/downloads"
|
||||
echo "4. Restart your terminal"
|
||||
echo "5. Run this script again"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use python3 if available, otherwise python
|
||||
PYTHON_CMD="python3"
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
PYTHON_CMD="python"
|
||||
fi
|
||||
|
||||
# Check if pip is available
|
||||
if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then
|
||||
echo "❌ ERROR: pip is not installed or not in PATH"
|
||||
echo "Please ensure Python was installed correctly with pip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use pip3 if available, otherwise pip
|
||||
PIP_CMD="pip3"
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
PIP_CMD="pip"
|
||||
fi
|
||||
|
||||
# Check if Godot is available (only if test step will be run)
|
||||
NEED_GODOT=""
|
||||
if [[ -n "$ARG_TEST_ONLY" ]]; then
|
||||
NEED_GODOT=1
|
||||
fi
|
||||
if [[ -n "$ARG_STEPS" ]] && [[ "$CUSTOM_STEPS" == *"test"* ]]; then
|
||||
NEED_GODOT=1
|
||||
fi
|
||||
if [[ -z "$ARG_LINT_ONLY" && -z "$ARG_FORMAT_ONLY" && -z "$ARG_STEPS" ]]; then
|
||||
NEED_GODOT=1
|
||||
fi
|
||||
|
||||
if [[ -n "$NEED_GODOT" ]]; then
|
||||
if ! command -v godot &> /dev/null; then
|
||||
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 executable in this project directory"
|
||||
echo "4. Restart your terminal"
|
||||
echo "5. Run this script again"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if gdlint and gdformat are available (only if needed)
|
||||
NEED_GDTOOLS=""
|
||||
if [[ -n "$ARG_LINT_ONLY" ]]; then
|
||||
NEED_GDTOOLS=1
|
||||
fi
|
||||
if [[ -n "$ARG_FORMAT_ONLY" ]]; then
|
||||
NEED_GDTOOLS=1
|
||||
fi
|
||||
if [[ -n "$ARG_STEPS" ]] && ([[ "$CUSTOM_STEPS" == *"lint"* ]] || [[ "$CUSTOM_STEPS" == *"format"* ]]); then
|
||||
NEED_GDTOOLS=1
|
||||
fi
|
||||
if [[ -z "$ARG_TEST_ONLY" && -z "$ARG_STEPS" ]]; then
|
||||
NEED_GDTOOLS=1
|
||||
fi
|
||||
|
||||
if [[ -n "$NEED_GDTOOLS" ]]; then
|
||||
if ! command -v gdlint &> /dev/null; then
|
||||
echo "❌ ERROR: gdlint is not installed or not in PATH"
|
||||
echo
|
||||
echo "Installation instructions:"
|
||||
echo "1. $PIP_CMD install --upgrade \"setuptools<81\""
|
||||
echo "2. $PIP_CMD install gdtoolkit==4"
|
||||
echo "3. Restart your terminal"
|
||||
echo "4. Run this script again"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v gdformat &> /dev/null; then
|
||||
echo "❌ ERROR: gdformat is not installed or not in PATH"
|
||||
echo
|
||||
echo "Installation instructions:"
|
||||
echo "1. $PIP_CMD install --upgrade \"setuptools<81\""
|
||||
echo "2. $PIP_CMD install gdtoolkit==4"
|
||||
echo "3. Restart your terminal"
|
||||
echo "4. Run this script again"
|
||||
echo
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ All dependencies are available. Running development workflow..."
|
||||
echo
|
||||
|
||||
# Build Python command based on arguments
|
||||
PYTHON_FULL_CMD="$PYTHON_CMD tools/run_development.py"
|
||||
|
||||
if [[ -n "$ARG_LINT_ONLY" ]]; then
|
||||
PYTHON_FULL_CMD="$PYTHON_FULL_CMD --lint"
|
||||
echo "🔍 Running linting only..."
|
||||
elif [[ -n "$ARG_FORMAT_ONLY" ]]; then
|
||||
PYTHON_FULL_CMD="$PYTHON_FULL_CMD --format"
|
||||
echo "🎨 Running formatting only..."
|
||||
elif [[ -n "$ARG_TEST_ONLY" ]]; then
|
||||
PYTHON_FULL_CMD="$PYTHON_FULL_CMD --test"
|
||||
echo "🧪 Running tests only..."
|
||||
elif [[ -n "$ARG_STEPS" ]]; then
|
||||
PYTHON_FULL_CMD="$PYTHON_FULL_CMD --steps $CUSTOM_STEPS"
|
||||
echo "🔄 Running custom workflow: $CUSTOM_STEPS..."
|
||||
else
|
||||
echo "🚀 Running complete development workflow: format + lint + test..."
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
# Run the Python development workflow script
|
||||
$PYTHON_FULL_CMD
|
||||
|
||||
# Capture exit code and display result
|
||||
WORKFLOW_RESULT=$?
|
||||
|
||||
echo
|
||||
if [[ $WORKFLOW_RESULT -eq 0 ]]; then
|
||||
echo "🎉 Development workflow completed successfully!"
|
||||
else
|
||||
echo "⚠️ Development workflow completed with issues."
|
||||
echo "Please review the output above and fix any problems."
|
||||
fi
|
||||
|
||||
echo
|
||||
exit $WORKFLOW_RESULT
|
||||
103
run_format.bat
103
run_format.bat
@@ -1,103 +0,0 @@
|
||||
@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
122
run_lint.bat
@@ -1,122 +0,0 @@
|
||||
@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
116
run_tests.bat
@@ -1,116 +0,0 @@
|
||||
@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
|
||||
@@ -1,18 +1,18 @@
|
||||
extends Control
|
||||
|
||||
const GAMEPLAY_SCENES = {
|
||||
"match3": "res://scenes/game/gameplays/match3_gameplay.tscn",
|
||||
"clickomania": "res://scenes/game/gameplays/clickomania_gameplay.tscn"
|
||||
"match3": "res://scenes/game/gameplays/Match3Gameplay.tscn",
|
||||
"clickomania": "res://scenes/game/gameplays/ClickomaniaGameplay.tscn"
|
||||
}
|
||||
|
||||
@onready var back_button: Button = $BackButtonContainer/BackButton
|
||||
@onready var gameplay_container: Control = $GameplayContainer
|
||||
@onready var score_display: Label = $UI/ScoreDisplay
|
||||
|
||||
var current_gameplay_mode: String
|
||||
var global_score: int = 0:
|
||||
set = set_global_score
|
||||
|
||||
@onready var back_button: Button = $BackButtonContainer/BackButton
|
||||
@onready var gameplay_container: Control = $GameplayContainer
|
||||
@onready var score_display: Label = $UI/ScoreDisplay
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not back_button.pressed.is_connected(_on_back_button_pressed):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dmwkyeq2l7u04"]
|
||||
[gd_scene load_steps=4 format=3 uid="uid://8c2w55brpwmm"]
|
||||
|
||||
[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"]
|
||||
[ext_resource type="Texture2D" uid="uid://bengv32u1jeym" path="res://assets/textures/backgrounds/BGx3.png" id="GlobalBackground"]
|
||||
|
||||
[node name="Game" type="Control"]
|
||||
layout_mode = 3
|
||||
@@ -20,7 +20,7 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
texture = ExtResource("5_background")
|
||||
texture = ExtResource("GlobalBackground")
|
||||
expand_mode = 1
|
||||
stretch_mode = 1
|
||||
|
||||
|
||||
1
scenes/game/gameplays/ClickomaniaGameplay.gd.uid
Normal file
1
scenes/game/gameplays/ClickomaniaGameplay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkheckv0upd82
|
||||
@@ -1,6 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cl7g8v0eh3mam"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/game/gameplays/clickomania_gameplay.gd" id="1_script"]
|
||||
[ext_resource type="Script" path="res://scenes/game/gameplays/ClickomaniaGameplay.gd" id="1_script"]
|
||||
|
||||
[node name="Clickomania" type="Node2D"]
|
||||
script = ExtResource("1_script")
|
||||
@@ -4,10 +4,10 @@ extends DebugMenuBase
|
||||
func _ready():
|
||||
# Set specific configuration for Match3DebugMenu
|
||||
log_category = "Match3"
|
||||
target_script_path = "res://scenes/game/gameplays/match3_gameplay.gd"
|
||||
target_script_path = "res://scenes/game/gameplays/Match3Gameplay.gd"
|
||||
|
||||
# Call parent's _ready
|
||||
super._ready()
|
||||
super()
|
||||
|
||||
DebugManager.log_debug("Match3DebugMenu _ready() completed", log_category)
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ signal grid_state_loaded(grid_size: Vector2i, tile_types: int)
|
||||
## PROCESSING: Detecting matches, clearing tiles, dropping new ones, checking cascades
|
||||
enum GameState { WAITING, SELECTING, SWAPPING, PROCESSING }
|
||||
|
||||
var GRID_SIZE := Vector2i(8, 8)
|
||||
var TILE_TYPES := 5
|
||||
const TILE_SCENE := preload("res://scenes/game/gameplays/tile.tscn")
|
||||
const TILE_SCENE := preload("res://scenes/game/gameplays/Tile.tscn")
|
||||
|
||||
# Safety constants
|
||||
const MAX_GRID_SIZE := 15
|
||||
@@ -32,6 +30,9 @@ const CASCADE_WAIT_TIME := 0.1
|
||||
const SWAP_ANIMATION_TIME := 0.3
|
||||
const TILE_DROP_WAIT_TIME := 0.2
|
||||
|
||||
var grid_size := Vector2i(8, 8)
|
||||
var tile_types := 5
|
||||
|
||||
var grid: Array[Array] = []
|
||||
var tile_size: float = 48.0
|
||||
var grid_offset: Vector2 = Vector2.ZERO
|
||||
@@ -71,7 +72,7 @@ func _ready() -> void:
|
||||
DebugManager.log_debug("Match3 _ready() completed, calling debug structure check", "Match3")
|
||||
|
||||
# Notify UI that grid state is loaded
|
||||
grid_state_loaded.emit(GRID_SIZE, TILE_TYPES)
|
||||
grid_state_loaded.emit(grid_size, tile_types)
|
||||
|
||||
# Debug: Check scene tree structure
|
||||
call_deferred("_debug_scene_structure")
|
||||
@@ -83,12 +84,12 @@ func _calculate_grid_layout():
|
||||
var available_height = viewport_size.y * SCREEN_HEIGHT_USAGE
|
||||
|
||||
# Calculate tile size based on available space
|
||||
var max_tile_width = available_width / GRID_SIZE.x
|
||||
var max_tile_height = available_height / GRID_SIZE.y
|
||||
var max_tile_width = available_width / grid_size.x
|
||||
var max_tile_height = available_height / grid_size.y
|
||||
tile_size = min(max_tile_width, max_tile_height)
|
||||
|
||||
# Align grid to left side with margins
|
||||
var total_grid_height = tile_size * GRID_SIZE.y
|
||||
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
|
||||
)
|
||||
@@ -97,12 +98,12 @@ func _calculate_grid_layout():
|
||||
func _initialize_grid():
|
||||
# Create gem pool for current tile types
|
||||
var gem_indices: Array[int] = []
|
||||
for i in range(TILE_TYPES):
|
||||
for i in range(tile_types):
|
||||
gem_indices.append(i)
|
||||
|
||||
for y in range(GRID_SIZE.y):
|
||||
for y in range(grid_size.y):
|
||||
grid.append([])
|
||||
for x in range(GRID_SIZE.x):
|
||||
for x in range(grid_size.x):
|
||||
var tile = TILE_SCENE.instantiate()
|
||||
var tile_position = grid_offset + Vector2(x, y) * tile_size
|
||||
tile.position = tile_position
|
||||
@@ -113,7 +114,7 @@ func _initialize_grid():
|
||||
tile.set_active_gem_types(gem_indices)
|
||||
|
||||
# Set tile type after adding to scene tree
|
||||
var new_type = randi() % TILE_TYPES
|
||||
var new_type = randi() % tile_types
|
||||
tile.tile_type = new_type
|
||||
|
||||
# Connect tile signals
|
||||
@@ -159,8 +160,8 @@ func _has_match_at(pos: Vector2i) -> bool:
|
||||
|
||||
func _check_for_matches() -> bool:
|
||||
"""Scan entire grid to detect if any matches exist (used for cascade detection)"""
|
||||
for y in range(GRID_SIZE.y):
|
||||
for x in range(GRID_SIZE.x):
|
||||
for y in range(grid_size.y):
|
||||
for x in range(grid_size.x):
|
||||
if _has_match_at(Vector2i(x, y)):
|
||||
return true
|
||||
return false
|
||||
@@ -205,7 +206,7 @@ func _get_match_line(start: Vector2i, dir: Vector2i) -> Array:
|
||||
var current = start + dir * offset
|
||||
var steps = 0
|
||||
# Safety limit prevents infinite loops in case of logic errors
|
||||
while steps < GRID_SIZE.x + GRID_SIZE.y and _is_valid_grid_position(current):
|
||||
while steps < grid_size.x + grid_size.y and _is_valid_grid_position(current):
|
||||
if current.y >= grid.size() or current.x >= grid[current.y].size():
|
||||
break
|
||||
|
||||
@@ -238,11 +239,11 @@ func _clear_matches() -> void:
|
||||
var match_groups := []
|
||||
var processed_tiles := {}
|
||||
|
||||
for y in range(GRID_SIZE.y):
|
||||
for y in range(grid_size.y):
|
||||
if y >= grid.size():
|
||||
continue
|
||||
|
||||
for x in range(GRID_SIZE.x):
|
||||
for x in range(grid_size.x):
|
||||
if x >= grid[y].size():
|
||||
continue
|
||||
|
||||
@@ -338,17 +339,18 @@ func _drop_tiles():
|
||||
var moved = true
|
||||
while moved:
|
||||
moved = false
|
||||
for x in range(GRID_SIZE.x):
|
||||
# Fixed: Start from GRID_SIZE.y - 1 to avoid out of bounds
|
||||
for y in range(GRID_SIZE.y - 1, -1, -1):
|
||||
for x in range(grid_size.x):
|
||||
# Fixed: Start from grid_size.y - 1 to avoid out of bounds
|
||||
for y in range(grid_size.y - 1, -1, -1):
|
||||
var tile = grid[y][x]
|
||||
# Fixed: Check bounds before accessing y + 1
|
||||
if tile and y + 1 < GRID_SIZE.y and not grid[y + 1][x]:
|
||||
if tile and y + 1 < grid_size.y and not grid[y + 1][x]:
|
||||
grid[y + 1][x] = tile
|
||||
grid[y][x] = null
|
||||
tile.grid_position = Vector2i(x, y + 1)
|
||||
# You can animate position here using Tween for smooth drop:
|
||||
# tween.interpolate_property(tile, "position", tile.position, grid_offset + Vector2(x, y + 1) * tile_size, 0.2)
|
||||
# tween.interpolate_property(tile, "position", tile.position,
|
||||
# grid_offset + Vector2(x, y + 1) * tile_size, 0.2)
|
||||
tile.position = grid_offset + Vector2(x, y + 1) * tile_size
|
||||
moved = true
|
||||
|
||||
@@ -361,16 +363,16 @@ func _fill_empty_cells():
|
||||
|
||||
# Create gem pool for current tile types
|
||||
var gem_indices: Array[int] = []
|
||||
for i in range(TILE_TYPES):
|
||||
for i in range(tile_types):
|
||||
gem_indices.append(i)
|
||||
|
||||
var tiles_created = 0
|
||||
for y in range(GRID_SIZE.y):
|
||||
for y in range(grid_size.y):
|
||||
if y >= grid.size():
|
||||
DebugManager.log_error("Grid row %d does not exist" % y, "Match3")
|
||||
continue
|
||||
|
||||
for x in range(GRID_SIZE.x):
|
||||
for x in range(grid_size.x):
|
||||
if x >= grid[y].size():
|
||||
DebugManager.log_error("Grid column %d does not exist in row %d" % [x, y], "Match3")
|
||||
continue
|
||||
@@ -394,10 +396,10 @@ func _fill_empty_cells():
|
||||
DebugManager.log_warn("Tile missing set_active_gem_types method", "Match3")
|
||||
|
||||
# Set random tile type with bounds checking
|
||||
if TILE_TYPES > 0:
|
||||
tile.tile_type = randi() % TILE_TYPES
|
||||
if tile_types > 0:
|
||||
tile.tile_type = randi() % tile_types
|
||||
else:
|
||||
DebugManager.log_error("TILE_TYPES is 0, cannot set tile type", "Match3")
|
||||
DebugManager.log_error("tile_types is 0, cannot set tile type", "Match3")
|
||||
tile.queue_free()
|
||||
continue
|
||||
|
||||
@@ -436,19 +438,19 @@ func _fill_empty_cells():
|
||||
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
|
||||
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"
|
||||
"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:
|
||||
if tile_types < 3 or tile_types > MAX_TILE_TYPES:
|
||||
DebugManager.log_error(
|
||||
"Invalid tile types count for regeneration: %d" % TILE_TYPES, "Match3"
|
||||
"Invalid tile types count for regeneration: %d" % tile_types, "Match3"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -515,12 +517,12 @@ func set_tile_types(new_count: int):
|
||||
)
|
||||
return
|
||||
|
||||
if new_count == TILE_TYPES:
|
||||
if new_count == tile_types:
|
||||
DebugManager.log_debug("Tile types count unchanged, skipping regeneration", "Match3")
|
||||
return
|
||||
|
||||
DebugManager.log_debug("Changing tile types from %d to %d" % [TILE_TYPES, new_count], "Match3")
|
||||
TILE_TYPES = new_count
|
||||
DebugManager.log_debug("Changing tile types from %d to %d" % [tile_types, new_count], "Match3")
|
||||
tile_types = new_count
|
||||
|
||||
# Regenerate grid with new tile types (gem pool is updated in regenerate_grid)
|
||||
await regenerate_grid()
|
||||
@@ -548,12 +550,12 @@ func set_grid_size(new_size: Vector2i):
|
||||
)
|
||||
return
|
||||
|
||||
if new_size == GRID_SIZE:
|
||||
if new_size == grid_size:
|
||||
DebugManager.log_debug("Grid size unchanged, skipping regeneration", "Match3")
|
||||
return
|
||||
|
||||
DebugManager.log_debug("Changing grid size from %s to %s" % [GRID_SIZE, new_size], "Match3")
|
||||
GRID_SIZE = new_size
|
||||
DebugManager.log_debug("Changing grid size from %s to %s" % [grid_size, new_size], "Match3")
|
||||
grid_size = new_size
|
||||
|
||||
# Regenerate grid with new size
|
||||
await regenerate_grid()
|
||||
@@ -562,8 +564,8 @@ func set_grid_size(new_size: Vector2i):
|
||||
func reset_all_visual_states() -> void:
|
||||
# Debug function to reset all tile visual states
|
||||
DebugManager.log_debug("Resetting all tile visual states", "Match3")
|
||||
for y in range(GRID_SIZE.y):
|
||||
for x in range(GRID_SIZE.x):
|
||||
for y in range(grid_size.y):
|
||||
for x in range(grid_size.x):
|
||||
if grid[y][x] and grid[y][x].has_method("force_reset_visual_state"):
|
||||
grid[y][x].force_reset_visual_state()
|
||||
|
||||
@@ -586,12 +588,12 @@ func _debug_scene_structure() -> void:
|
||||
|
||||
# Check tiles
|
||||
var tile_count = 0
|
||||
for y in range(GRID_SIZE.y):
|
||||
for x in range(GRID_SIZE.x):
|
||||
for y in range(grid_size.y):
|
||||
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"
|
||||
"Created %d tiles out of %d expected" % [tile_count, grid_size.x * grid_size.y], "Match3"
|
||||
)
|
||||
|
||||
# Check first tile in detail
|
||||
@@ -668,8 +670,8 @@ func _move_cursor(direction: Vector2i) -> void:
|
||||
var new_pos = cursor_position + direction
|
||||
|
||||
# Bounds checking
|
||||
new_pos.x = clamp(new_pos.x, 0, GRID_SIZE.x - 1)
|
||||
new_pos.y = clamp(new_pos.y, 0, GRID_SIZE.y - 1)
|
||||
new_pos.x = clamp(new_pos.x, 0, grid_size.x - 1)
|
||||
new_pos.y = clamp(new_pos.y, 0, grid_size.y - 1)
|
||||
|
||||
if new_pos != cursor_position:
|
||||
# Safe access to old tile
|
||||
@@ -925,8 +927,8 @@ 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]
|
||||
"Starting serialization: grid.size()=%d, grid_size=(%d,%d)"
|
||||
% [grid.size(), grid_size.x, grid_size.y]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
@@ -939,9 +941,9 @@ func serialize_grid_state() -> Array:
|
||||
var valid_tiles = 0
|
||||
var null_tiles = 0
|
||||
|
||||
for y in range(GRID_SIZE.y):
|
||||
for y in range(grid_size.y):
|
||||
var row = []
|
||||
for x in range(GRID_SIZE.x):
|
||||
for x in range(grid_size.x):
|
||||
if y < grid.size() and x < grid[y].size() and grid[y][x]:
|
||||
row.append(grid[y][x].tile_type)
|
||||
valid_tiles += 1
|
||||
@@ -963,7 +965,7 @@ func serialize_grid_state() -> Array:
|
||||
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]
|
||||
% [grid_size.x, grid_size.y, valid_tiles, null_tiles]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
@@ -974,12 +976,11 @@ func get_active_gem_types() -> Array:
|
||||
# Get active gem types from the first available tile
|
||||
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
|
||||
return grid[0][0].active_gem_types.duplicate()
|
||||
else:
|
||||
# Fallback to default
|
||||
var default_types = []
|
||||
for i in range(TILE_TYPES):
|
||||
default_types.append(i)
|
||||
return default_types
|
||||
# Fallback to default
|
||||
var default_types = []
|
||||
for i in range(tile_types):
|
||||
default_types.append(i)
|
||||
return default_types
|
||||
|
||||
|
||||
func save_current_state():
|
||||
@@ -990,12 +991,12 @@ func save_current_state():
|
||||
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()]
|
||||
% [grid_size.x, grid_size.y, tile_types, active_gems.size()]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
|
||||
SaveManager.save_grid_state(GRID_SIZE, TILE_TYPES, active_gems, grid_layout)
|
||||
SaveManager.save_grid_state(grid_size, tile_types, active_gems, grid_layout)
|
||||
|
||||
|
||||
func load_saved_state() -> bool:
|
||||
@@ -1008,7 +1009,7 @@ 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
|
||||
tile_types = saved_state.tile_types_count
|
||||
var saved_gems: Array[int] = []
|
||||
for gem in saved_state.active_gem_types:
|
||||
saved_gems.append(int(gem))
|
||||
@@ -1017,7 +1018,7 @@ func load_saved_state() -> bool:
|
||||
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()]
|
||||
% [instance_id, saved_size.x, saved_size.y, tile_types, saved_layout.size()]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
@@ -1051,8 +1052,8 @@ func load_saved_state() -> bool:
|
||||
return false
|
||||
|
||||
# Apply the saved settings
|
||||
var old_size = GRID_SIZE
|
||||
GRID_SIZE = saved_size
|
||||
var old_size = grid_size
|
||||
grid_size = saved_size
|
||||
|
||||
# Recalculate layout if size changed
|
||||
if old_size != saved_size:
|
||||
@@ -1107,9 +1108,9 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> v
|
||||
await get_tree().process_frame
|
||||
|
||||
# Restore grid from saved layout
|
||||
for y in range(GRID_SIZE.y):
|
||||
for y in range(grid_size.y):
|
||||
grid.append([])
|
||||
for x in range(GRID_SIZE.x):
|
||||
for x in range(grid_size.x):
|
||||
var tile = TILE_SCENE.instantiate()
|
||||
var tile_position = grid_offset + Vector2(x, y) * tile_size
|
||||
tile.position = tile_position
|
||||
@@ -1123,20 +1124,20 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> v
|
||||
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]
|
||||
"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:
|
||||
if saved_tile_type >= 0 and saved_tile_type < tile_types:
|
||||
tile.tile_type = saved_tile_type
|
||||
DebugManager.log_debug(
|
||||
"✓ Restored tile (%d,%d) with saved type %d" % [x, y, saved_tile_type], "Match3"
|
||||
)
|
||||
else:
|
||||
# Fallback for invalid tile types
|
||||
tile.tile_type = randi() % TILE_TYPES
|
||||
tile.tile_type = randi() % tile_types
|
||||
DebugManager.log_error(
|
||||
(
|
||||
"✗ Invalid saved tile type %d at (%d,%d), using random %d"
|
||||
@@ -1150,13 +1151,13 @@ func _restore_grid_from_layout(grid_layout: Array, active_gems: Array[int]) -> v
|
||||
grid[y].append(tile)
|
||||
|
||||
DebugManager.log_info(
|
||||
"Completed grid restoration: %d tiles restored" % [GRID_SIZE.x * GRID_SIZE.y], "Match3"
|
||||
"Completed grid restoration: %d tiles restored" % [grid_size.x * grid_size.y], "Match3"
|
||||
)
|
||||
|
||||
|
||||
# Safety and validation helper functions
|
||||
func _is_valid_grid_position(pos: Vector2i) -> bool:
|
||||
return pos.x >= 0 and pos.y >= 0 and pos.x < GRID_SIZE.x and pos.y < GRID_SIZE.y
|
||||
return pos.x >= 0 and pos.y >= 0 and pos.x < grid_size.x and pos.y < grid_size.y
|
||||
|
||||
|
||||
func _validate_grid_integrity() -> bool:
|
||||
@@ -1165,9 +1166,9 @@ func _validate_grid_integrity() -> bool:
|
||||
DebugManager.log_error("Grid is not an array", "Match3")
|
||||
return false
|
||||
|
||||
if grid.size() != GRID_SIZE.y:
|
||||
if grid.size() != grid_size.y:
|
||||
DebugManager.log_error(
|
||||
"Grid height mismatch: %d vs %d" % [grid.size(), GRID_SIZE.y], "Match3"
|
||||
"Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y], "Match3"
|
||||
)
|
||||
return false
|
||||
|
||||
@@ -1176,9 +1177,9 @@ func _validate_grid_integrity() -> bool:
|
||||
DebugManager.log_error("Grid row %d is not an array" % y, "Match3")
|
||||
return false
|
||||
|
||||
if grid[y].size() != GRID_SIZE.x:
|
||||
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"
|
||||
"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), grid_size.x], "Match3"
|
||||
)
|
||||
return false
|
||||
|
||||
1
scenes/game/gameplays/Match3Gameplay.gd.uid
Normal file
1
scenes/game/gameplays/Match3Gameplay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dbbi8ooysxp7f
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://b4kv7g7kllwgb"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/game/gameplays/match3_gameplay.gd" id="1_mvfdp"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/game/gameplays/Match3DebugMenu.tscn" id="2_debug_menu"]
|
||||
[ext_resource type="Script" uid="uid://o8crf6688lan" path="res://scenes/game/gameplays/Match3Gameplay.gd" id="1_mvfdp"]
|
||||
[ext_resource type="PackedScene" uid="uid://b76oiwlifikl3" path="res://scenes/game/gameplays/Match3DebugMenu.tscn" id="2_debug_menu"]
|
||||
|
||||
[node name="Match3" type="Node2D"]
|
||||
script = ExtResource("1_mvfdp")
|
||||
83
scenes/game/gameplays/Match3InputHandler.gd
Normal file
83
scenes/game/gameplays/Match3InputHandler.gd
Normal file
@@ -0,0 +1,83 @@
|
||||
class_name Match3InputHandler
|
||||
extends RefCounted
|
||||
|
||||
## Mouse input handler for Match3 gameplay
|
||||
##
|
||||
## Static methods for handling mouse interactions in Match3 games.
|
||||
## Converts between world coordinates and grid positions, performs hit detection on tiles.
|
||||
##
|
||||
## Usage:
|
||||
## var tile = Match3InputHandler.find_tile_at_position(grid, grid_size, mouse_pos)
|
||||
## var grid_pos = Match3InputHandler.get_grid_position_from_world(node, world_pos, offset, size)
|
||||
|
||||
|
||||
static func find_tile_at_position(grid: Array, grid_size: Vector2i, world_pos: Vector2) -> Node2D:
|
||||
## Find the tile that contains the world position.
|
||||
##
|
||||
## Iterates through all tiles and checks if the world position falls within
|
||||
## any tile's sprite boundaries.
|
||||
##
|
||||
## Args:
|
||||
## grid: 2D array of tile nodes arranged in [y][x] format
|
||||
## grid_size: Dimensions of the grid (width x height)
|
||||
## world_pos: World coordinates to test
|
||||
##
|
||||
## Returns:
|
||||
## The first tile node that contains the position, or null if no tile found
|
||||
for y in range(grid_size.y):
|
||||
for x in range(grid_size.x):
|
||||
if y < grid.size() and x < grid[y].size():
|
||||
var tile = grid[y][x]
|
||||
if tile and tile.has_node("Sprite2D"):
|
||||
var sprite = tile.get_node("Sprite2D")
|
||||
if sprite and sprite.texture:
|
||||
var sprite_bounds = get_sprite_world_bounds(tile, sprite)
|
||||
if is_point_inside_rect(world_pos, sprite_bounds):
|
||||
return tile
|
||||
return null
|
||||
|
||||
|
||||
static func get_sprite_world_bounds(tile: Node2D, sprite: Sprite2D) -> Rect2:
|
||||
## Calculate the world space bounding rectangle of a sprite.
|
||||
##
|
||||
## Args:
|
||||
## tile: The tile node containing the sprite
|
||||
## sprite: The Sprite2D node to calculate bounds for
|
||||
##
|
||||
## Returns:
|
||||
## Rect2 representing the sprite's bounds in world coordinates
|
||||
var texture_size = sprite.texture.get_size()
|
||||
var actual_size = texture_size * sprite.scale
|
||||
var half_size = actual_size * 0.5
|
||||
var top_left = tile.position - half_size
|
||||
return Rect2(top_left, actual_size)
|
||||
|
||||
|
||||
static func is_point_inside_rect(point: Vector2, rect: Rect2) -> bool:
|
||||
# Check if a point is inside a rectangle
|
||||
return (
|
||||
point.x >= rect.position.x
|
||||
and point.x <= rect.position.x + rect.size.x
|
||||
and point.y >= rect.position.y
|
||||
and point.y <= rect.position.y + rect.size.y
|
||||
)
|
||||
|
||||
|
||||
static func get_grid_position_from_world(
|
||||
node: Node2D, world_pos: Vector2, grid_offset: Vector2, tile_size: float
|
||||
) -> Vector2i:
|
||||
## Convert world coordinates to grid array indices.
|
||||
##
|
||||
## Args:
|
||||
## node: Reference node for coordinate space conversion
|
||||
## world_pos: Position in world coordinates to convert
|
||||
## grid_offset: Offset of the grid's origin from the node's position
|
||||
## tile_size: Size of each tile in world units
|
||||
##
|
||||
## Returns:
|
||||
## Vector2i containing the grid coordinates (x, y) for array indexing
|
||||
var local_pos = node.to_local(world_pos)
|
||||
var relative_pos = local_pos - grid_offset
|
||||
var grid_x = int(relative_pos.x / tile_size)
|
||||
var grid_y = int(relative_pos.y / tile_size)
|
||||
return Vector2i(grid_x, grid_y)
|
||||
1
scenes/game/gameplays/Match3InputHandler.gd.uid
Normal file
1
scenes/game/gameplays/Match3InputHandler.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ogm8w7l6bhif
|
||||
143
scenes/game/gameplays/Match3SaveManager.gd
Normal file
143
scenes/game/gameplays/Match3SaveManager.gd
Normal file
@@ -0,0 +1,143 @@
|
||||
class_name Match3SaveManager
|
||||
extends RefCounted
|
||||
|
||||
## Save/Load manager for Match3 gameplay state
|
||||
##
|
||||
## Handles serialization and deserialization of Match3 game state.
|
||||
## Converts game objects to data structures for storage and restoration.
|
||||
##
|
||||
## Usage:
|
||||
## # Save current state
|
||||
## var grid_data = Match3SaveManager.serialize_grid_state(game_grid, grid_size)
|
||||
##
|
||||
## # Restore previous state
|
||||
## var success = Match3SaveManager.deserialize_grid_state(grid_data, game_grid, grid_size)
|
||||
|
||||
|
||||
static func serialize_grid_state(grid: Array, grid_size: Vector2i) -> Array:
|
||||
## Convert the current game grid to a serializable 2D array of tile types.
|
||||
##
|
||||
## Extracts the tile_type property from each tile node and creates a 2D array
|
||||
## that can be saved to disk. Invalid or missing tiles are represented as -1.
|
||||
##
|
||||
## Args:
|
||||
## grid: The current game grid (2D array of tile nodes)
|
||||
## grid_size: Dimensions of the grid to serialize
|
||||
##
|
||||
## Returns:
|
||||
## Array: 2D array where each element is either a tile type (int) or -1 for empty
|
||||
var serialized_grid = []
|
||||
var valid_tiles = 0
|
||||
var null_tiles = 0
|
||||
|
||||
for y in range(grid_size.y):
|
||||
var row = []
|
||||
for x in range(grid_size.x):
|
||||
if y < grid.size() and x < grid[y].size() and grid[y][x]:
|
||||
row.append(grid[y][x].tile_type)
|
||||
valid_tiles += 1
|
||||
else:
|
||||
row.append(-1) # Invalid/empty tile
|
||||
null_tiles += 1
|
||||
serialized_grid.append(row)
|
||||
|
||||
DebugManager.log_info(
|
||||
(
|
||||
"Serialized grid state: %dx%d grid, %d valid tiles, %d null tiles"
|
||||
% [grid_size.x, grid_size.y, valid_tiles, null_tiles]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
return serialized_grid
|
||||
|
||||
|
||||
static func get_active_gem_types_from_grid(grid: Array, tile_types: int) -> Array:
|
||||
# Get active gem types from the first available tile
|
||||
if grid.size() > 0 and grid[0].size() > 0 and grid[0][0]:
|
||||
return grid[0][0].active_gem_types.duplicate()
|
||||
|
||||
# Fallback to default
|
||||
var default_types = []
|
||||
for i in range(tile_types):
|
||||
default_types.append(i)
|
||||
return default_types
|
||||
|
||||
|
||||
static func save_game_state(grid: Array, grid_size: Vector2i, tile_types: int):
|
||||
# Save complete game state
|
||||
var grid_layout = serialize_grid_state(grid, grid_size)
|
||||
var active_gems = get_active_gem_types_from_grid(grid, tile_types)
|
||||
|
||||
DebugManager.log_info(
|
||||
(
|
||||
"Saving match3 state: size(%d,%d), %d tile types, %d active gems"
|
||||
% [grid_size.x, grid_size.y, tile_types, active_gems.size()]
|
||||
),
|
||||
"Match3"
|
||||
)
|
||||
|
||||
SaveManager.save_grid_state(grid_size, tile_types, active_gems, grid_layout)
|
||||
|
||||
|
||||
static func restore_grid_from_layout(
|
||||
match3_node: Node2D,
|
||||
grid_layout: Array,
|
||||
active_gems: Array[int],
|
||||
grid_size: Vector2i,
|
||||
tile_scene: PackedScene,
|
||||
grid_offset: Vector2,
|
||||
tile_size: float,
|
||||
tile_types: int
|
||||
) -> Array[Array]:
|
||||
# Clear ALL existing tile children
|
||||
var all_tile_children = []
|
||||
for child in match3_node.get_children():
|
||||
if child.has_method("get_script") and child.get_script():
|
||||
var script_path = child.get_script().resource_path
|
||||
if script_path == "res://scenes/game/gameplays/tile.gd":
|
||||
all_tile_children.append(child)
|
||||
|
||||
# Remove all found tile children
|
||||
for child in all_tile_children:
|
||||
child.queue_free()
|
||||
|
||||
# Wait for nodes to be freed
|
||||
await match3_node.get_tree().process_frame
|
||||
|
||||
# Create new grid
|
||||
var new_grid: Array[Array] = []
|
||||
for y in range(grid_size.y):
|
||||
new_grid.append(Array([]))
|
||||
for x in range(grid_size.x):
|
||||
var tile = tile_scene.instantiate()
|
||||
var tile_position = grid_offset + Vector2(x, y) * tile_size
|
||||
tile.position = tile_position
|
||||
tile.grid_position = Vector2i(x, y)
|
||||
|
||||
match3_node.add_child(tile)
|
||||
|
||||
# Configure Area2D
|
||||
tile.monitoring = true
|
||||
tile.monitorable = true
|
||||
tile.input_pickable = true
|
||||
|
||||
tile.set_tile_size(tile_size)
|
||||
tile.set_active_gem_types(active_gems)
|
||||
|
||||
# Set the saved tile type
|
||||
var saved_tile_type = grid_layout[y][x]
|
||||
if saved_tile_type >= 0 and saved_tile_type < tile_types:
|
||||
tile.tile_type = saved_tile_type
|
||||
else:
|
||||
tile.tile_type = randi() % tile_types
|
||||
|
||||
# Connect tile signals
|
||||
if tile.has_signal("tile_selected") and match3_node.has_method("_on_tile_selected"):
|
||||
tile.tile_selected.connect(match3_node._on_tile_selected)
|
||||
if tile.has_signal("tile_hovered") and match3_node.has_method("_on_tile_hovered"):
|
||||
tile.tile_hovered.connect(match3_node._on_tile_hovered)
|
||||
tile.tile_unhovered.connect(match3_node._on_tile_unhovered)
|
||||
|
||||
new_grid[y].append(tile)
|
||||
|
||||
return new_grid
|
||||
1
scenes/game/gameplays/Match3SaveManager.gd.uid
Normal file
1
scenes/game/gameplays/Match3SaveManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://duheejfr6de6x
|
||||
102
scenes/game/gameplays/Match3Validator.gd
Normal file
102
scenes/game/gameplays/Match3Validator.gd
Normal file
@@ -0,0 +1,102 @@
|
||||
class_name Match3Validator
|
||||
extends RefCounted
|
||||
|
||||
## Validation utilities for Match3 gameplay
|
||||
##
|
||||
## Static methods for validating Match3 game state and data integrity.
|
||||
## Prevents crashes by checking bounds, data structures, and game logic constraints.
|
||||
##
|
||||
## Usage:
|
||||
## if Match3Validator.is_valid_grid_position(pos, grid_size):
|
||||
## # Safe to access grid[pos.y][pos.x]
|
||||
##
|
||||
## if Match3Validator.validate_grid_integrity(grid, grid_size):
|
||||
## # Grid structure is valid for game operations
|
||||
|
||||
|
||||
static func is_valid_grid_position(pos: Vector2i, grid_size: Vector2i) -> bool:
|
||||
## Check if the position is within the grid boundaries.
|
||||
##
|
||||
## Performs bounds checking to prevent index out of bounds errors.
|
||||
##
|
||||
## Args:
|
||||
## pos: Grid position to validate (x, y coordinates)
|
||||
## grid_size: Dimensions of the grid (width, height)
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if position is valid, False if out of bounds
|
||||
return pos.x >= 0 and pos.y >= 0 and pos.x < grid_size.x and pos.y < grid_size.y
|
||||
|
||||
|
||||
static func validate_grid_integrity(grid: Array, grid_size: Vector2i) -> bool:
|
||||
## Verify that the grid array structure matches expected dimensions.
|
||||
##
|
||||
## Validates the grid's 2D array structure for safe game operations.
|
||||
## Checks array types, dimensions, and structural consistency.
|
||||
##
|
||||
## Args:
|
||||
## grid: The 2D array representing the game grid
|
||||
## grid_size: Expected dimensions (width x height)
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if grid structure is valid, False if corrupted or malformed
|
||||
if not grid is Array:
|
||||
DebugManager.log_error("Grid is not an array", "Match3")
|
||||
return false
|
||||
|
||||
if grid.size() != grid_size.y:
|
||||
DebugManager.log_error(
|
||||
"Grid height mismatch: %d vs %d" % [grid.size(), grid_size.y], "Match3"
|
||||
)
|
||||
return false
|
||||
|
||||
for y in range(grid.size()):
|
||||
if not grid[y] is Array:
|
||||
DebugManager.log_error("Grid row %d is not an array" % y, "Match3")
|
||||
return false
|
||||
|
||||
if grid[y].size() != grid_size.x:
|
||||
DebugManager.log_error(
|
||||
"Grid row %d width mismatch: %d vs %d" % [y, grid[y].size(), grid_size.x], "Match3"
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
|
||||
static func safe_grid_access(grid: Array, pos: Vector2i, grid_size: Vector2i) -> Node2D:
|
||||
# Safe grid access with comprehensive bounds checking
|
||||
if not is_valid_grid_position(pos, grid_size):
|
||||
return null
|
||||
|
||||
if pos.y >= grid.size() or pos.x >= grid[pos.y].size():
|
||||
DebugManager.log_warn("Grid bounds exceeded: (%d,%d)" % [pos.x, pos.y], "Match3")
|
||||
return null
|
||||
|
||||
var tile = grid[pos.y][pos.x]
|
||||
if not tile or not is_instance_valid(tile):
|
||||
return null
|
||||
|
||||
return tile
|
||||
|
||||
|
||||
static func safe_tile_access(tile: Node2D, property: String):
|
||||
# Safe property access on tiles
|
||||
if not tile or not is_instance_valid(tile):
|
||||
return null
|
||||
|
||||
if not property in tile:
|
||||
DebugManager.log_warn("Tile missing property: %s" % property, "Match3")
|
||||
return null
|
||||
|
||||
return tile.get(property)
|
||||
|
||||
|
||||
static func are_tiles_adjacent(tile1: Node2D, tile2: Node2D) -> bool:
|
||||
if not tile1 or not tile2:
|
||||
return false
|
||||
|
||||
var pos1 = tile1.grid_position
|
||||
var pos2 = tile2.grid_position
|
||||
var diff = abs(pos1.x - pos2.x) + abs(pos1.y - pos2.y)
|
||||
return diff == 1
|
||||
1
scenes/game/gameplays/Match3Validator.gd.uid
Normal file
1
scenes/game/gameplays/Match3Validator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dy3aym6riijct
|
||||
@@ -1 +0,0 @@
|
||||
uid://bapywtqdghjqp
|
||||
1
scenes/game/gameplays/match3_input_handler.gd.uid
Normal file
1
scenes/game/gameplays/match3_input_handler.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bgygx6iofwqwc
|
||||
1
scenes/game/gameplays/match3_save_manager.gd.uid
Normal file
1
scenes/game/gameplays/match3_save_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://balbki1cnwdn1
|
||||
1
scenes/game/gameplays/match3_validator.gd.uid
Normal file
1
scenes/game/gameplays/match3_validator.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cjav8g5js6umr
|
||||
@@ -2,8 +2,12 @@ extends Node2D
|
||||
|
||||
signal tile_selected(tile: Node2D)
|
||||
|
||||
# Target size for each tile to fit in the 54x54 grid cells
|
||||
const TILE_SIZE = 48 # Slightly smaller than 54 to leave some padding
|
||||
|
||||
@export var tile_type: int = 0:
|
||||
set = _set_tile_type
|
||||
|
||||
var grid_position: Vector2i
|
||||
var is_selected: bool = false:
|
||||
set = _set_selected
|
||||
@@ -11,26 +15,24 @@ 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
|
||||
|
||||
# Target size for each tile to fit in the 54x54 grid cells
|
||||
const TILE_SIZE = 48 # Slightly smaller than 54 to leave some padding
|
||||
|
||||
# All available gem textures
|
||||
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
|
||||
preload("res://assets/sprites/skulls/red.png"),
|
||||
preload("res://assets/sprites/skulls/blue.png"),
|
||||
preload("res://assets/sprites/skulls/green.png"),
|
||||
preload("res://assets/sprites/skulls/pink.png"),
|
||||
preload("res://assets/sprites/skulls/purple.png"),
|
||||
preload("res://assets/sprites/skulls/dark-blue.png"),
|
||||
preload("res://assets/sprites/skulls/grey.png"),
|
||||
preload("res://assets/sprites/skulls/orange.png"),
|
||||
preload("res://assets/sprites/skulls/yellow.png"),
|
||||
]
|
||||
|
||||
# Currently active gem types (indices into all_gem_textures)
|
||||
var active_gem_types: Array[int] = [] # Will be set from TileManager
|
||||
|
||||
@onready var sprite: Sprite2D = $Sprite2D
|
||||
|
||||
|
||||
func _set_tile_type(value: int) -> void:
|
||||
tile_type = value
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
extends Control
|
||||
|
||||
@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")
|
||||
|
||||
var current_menu: Control = null
|
||||
|
||||
@onready var splash_screen: Node = $SplashScreen
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DebugManager.log_debug("Main scene ready", "Main")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://ci2gk11211n0d"]
|
||||
[gd_scene load_steps=5 format=3 uid="uid://bwvq7u0mv5dku"]
|
||||
|
||||
[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/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"]
|
||||
[ext_resource type="Texture2D" uid="uid://bengv32u1jeym" path="res://assets/textures/backgrounds/BGx3.png" id="GlobalBackground"]
|
||||
|
||||
[node name="main" type="Control"]
|
||||
layout_mode = 3
|
||||
@@ -21,8 +21,7 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
texture = ExtResource("2_sugp2")
|
||||
expand_mode = 1
|
||||
texture = ExtResource("GlobalBackground")
|
||||
stretch_mode = 1
|
||||
|
||||
[node name="SplashScreen" parent="." instance=ExtResource("1_o5qli")]
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
class_name DebugMenuBase
|
||||
extends Control
|
||||
|
||||
# Safety constants matching match3_gameplay.gd
|
||||
const MAX_GRID_SIZE := 15
|
||||
const MAX_TILE_TYPES := 10
|
||||
const MIN_GRID_SIZE := 3
|
||||
const MIN_TILE_TYPES := 3
|
||||
const SCENE_SEARCH_COOLDOWN := 0.5
|
||||
|
||||
@export var target_script_path: String = "res://scenes/game/gameplays/Match3Gameplay.gd"
|
||||
@export var log_category: String = "DebugMenu"
|
||||
|
||||
var match3_scene: Node2D
|
||||
var search_timer: Timer
|
||||
var last_scene_search_time: float = 0.0
|
||||
|
||||
@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
|
||||
@@ -13,20 +27,6 @@ var grid_width_label: Label = $VBoxContainer/GridSizeContainer/GridWidthContaine
|
||||
@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"
|
||||
|
||||
# Safety constants matching match3_gameplay.gd
|
||||
const MAX_GRID_SIZE := 15
|
||||
const MAX_TILE_TYPES := 10
|
||||
const MIN_GRID_SIZE := 3
|
||||
const MIN_TILE_TYPES := 3
|
||||
|
||||
var match3_scene: Node2D
|
||||
var search_timer: Timer
|
||||
var last_scene_search_time: float = 0.0
|
||||
const SCENE_SEARCH_COOLDOWN := 0.5 # Prevent excessive scene searching
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if search_timer:
|
||||
|
||||
@@ -2,10 +2,11 @@ extends Control
|
||||
|
||||
signal open_settings
|
||||
|
||||
@onready var menu_buttons: Array[Button] = []
|
||||
var current_menu_index: int = 0
|
||||
var original_button_scales: Array[Vector2] = []
|
||||
|
||||
@onready var menu_buttons: Array[Button] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DebugManager.log_info("MainMenu ready", "MainMenu")
|
||||
|
||||
@@ -2,12 +2,6 @@ extends Control
|
||||
|
||||
signal back_to_main_menu
|
||||
|
||||
@onready var master_slider = $SettingsContainer/MasterVolumeContainer/MasterVolumeSlider
|
||||
@onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider
|
||||
@onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider
|
||||
@onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper
|
||||
@onready var reset_progress_button = $ResetSettingsContainer/ResetProgressButton
|
||||
|
||||
@export var settings_manager: Node = SettingsManager
|
||||
@export var localization_manager: Node = LocalizationManager
|
||||
|
||||
@@ -20,6 +14,12 @@ var current_control_index: int = 0
|
||||
var original_control_scales: Array[Vector2] = []
|
||||
var original_control_modulates: Array[Color] = []
|
||||
|
||||
@onready var master_slider = $SettingsContainer/MasterVolumeContainer/MasterVolumeSlider
|
||||
@onready var music_slider = $SettingsContainer/MusicVolumeContainer/MusicVolumeSlider
|
||||
@onready var sfx_slider = $SettingsContainer/SFXVolumeContainer/SFXVolumeSlider
|
||||
@onready var language_stepper = $SettingsContainer/LanguageContainer/LanguageStepper
|
||||
@onready var reset_progress_button = $ResetSettingsContainer/ResetProgressButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("localizable")
|
||||
@@ -226,14 +226,13 @@ func _update_visual_selection() -> void:
|
||||
func _get_control_name(control: Control) -> String:
|
||||
if control == master_slider:
|
||||
return "master_volume"
|
||||
elif control == music_slider:
|
||||
if control == music_slider:
|
||||
return "music_volume"
|
||||
elif control == sfx_slider:
|
||||
if control == sfx_slider:
|
||||
return "sfx_volume"
|
||||
elif control == language_stepper:
|
||||
if control == language_stepper:
|
||||
return language_stepper.get_control_name()
|
||||
else:
|
||||
return "button"
|
||||
return "button"
|
||||
|
||||
|
||||
func _on_language_stepper_value_changed(new_value: String, new_index: float) -> void:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
@tool
|
||||
extends Control
|
||||
class_name ValueStepper
|
||||
extends Control
|
||||
|
||||
## A reusable UI control for stepping through discrete values with arrow buttons
|
||||
##
|
||||
@@ -12,10 +11,6 @@ class_name ValueStepper
|
||||
|
||||
signal value_changed(new_value: String, new_index: int)
|
||||
|
||||
@onready var left_button: Button = $LeftButton
|
||||
@onready var right_button: Button = $RightButton
|
||||
@onready var value_display: Label = $ValueDisplay
|
||||
|
||||
## The data source for values.
|
||||
@export var data_source: String = "language"
|
||||
## Custom display format function. Leave empty to use default.
|
||||
@@ -29,6 +24,10 @@ var original_scale: Vector2
|
||||
var original_modulate: Color
|
||||
var is_highlighted: bool = false
|
||||
|
||||
@onready var left_button: Button = $LeftButton
|
||||
@onready var right_button: Button = $RightButton
|
||||
@onready var value_display: Label = $ValueDisplay
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DebugManager.log_info("ValueStepper ready for: " + data_source, "ValueStepper")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
extends Node
|
||||
|
||||
const MUSIC_PATH := "res://assets/audio/music/Space Horror InGame Music (Exploration) _Clement Panchout.wav"
|
||||
const MUSIC_BASE := "res://assets/audio/music/"
|
||||
const MUSIC_FILE := "Space Horror InGame Music (Exploration) _Clement Panchout.wav"
|
||||
const MUSIC_PATH := MUSIC_BASE + MUSIC_FILE
|
||||
const UI_CLICK_SOUND_PATH := "res://assets/audio/sfx/817587__silverdubloons__tick06.wav"
|
||||
|
||||
var music_player: AudioStreamPlayer
|
||||
|
||||
@@ -72,21 +72,15 @@ func _should_log(level: LogLevel) -> bool:
|
||||
|
||||
func _log_level_to_string(level: LogLevel) -> String:
|
||||
"""Convert LogLevel enum to string representation"""
|
||||
match level:
|
||||
LogLevel.TRACE:
|
||||
return "TRACE"
|
||||
LogLevel.DEBUG:
|
||||
return "DEBUG"
|
||||
LogLevel.INFO:
|
||||
return "INFO"
|
||||
LogLevel.WARN:
|
||||
return "WARN"
|
||||
LogLevel.ERROR:
|
||||
return "ERROR"
|
||||
LogLevel.FATAL:
|
||||
return "FATAL"
|
||||
_:
|
||||
return "UNKNOWN"
|
||||
var level_strings := {
|
||||
LogLevel.TRACE: "TRACE",
|
||||
LogLevel.DEBUG: "DEBUG",
|
||||
LogLevel.INFO: "INFO",
|
||||
LogLevel.WARN: "WARN",
|
||||
LogLevel.ERROR: "ERROR",
|
||||
LogLevel.FATAL: "FATAL"
|
||||
}
|
||||
return level_strings.get(level, "UNKNOWN")
|
||||
|
||||
|
||||
func _format_log_message(level: LogLevel, message: String, category: String = "") -> String:
|
||||
|
||||
@@ -39,29 +39,8 @@ func start_clickomania_game() -> void:
|
||||
|
||||
func start_game_with_mode(gameplay_mode: String) -> void:
|
||||
"""Load game scene with specified gameplay mode and safety validation"""
|
||||
# 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"
|
||||
)
|
||||
return
|
||||
|
||||
# Prevent concurrent scene changes (race condition protection)
|
||||
if is_changing_scene:
|
||||
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
|
||||
return
|
||||
|
||||
# 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"
|
||||
)
|
||||
# Combined input validation
|
||||
if not _validate_game_mode_request(gameplay_mode):
|
||||
return
|
||||
|
||||
is_changing_scene = true
|
||||
@@ -149,3 +128,33 @@ func exit_to_main_menu() -> void:
|
||||
# Wait for scene to be ready, then mark scene change as complete
|
||||
await get_tree().process_frame
|
||||
is_changing_scene = false
|
||||
|
||||
|
||||
func _validate_game_mode_request(gameplay_mode: String) -> bool:
|
||||
"""Validate gameplay mode request with combined checks"""
|
||||
# Input validation
|
||||
if not gameplay_mode or gameplay_mode.is_empty():
|
||||
DebugManager.log_error("Empty or null gameplay mode provided", "GameManager")
|
||||
return false
|
||||
|
||||
if not gameplay_mode is String:
|
||||
DebugManager.log_error(
|
||||
"Invalid gameplay mode type: " + str(typeof(gameplay_mode)), "GameManager"
|
||||
)
|
||||
return false
|
||||
|
||||
# Prevent concurrent scene changes (race condition protection)
|
||||
if is_changing_scene:
|
||||
DebugManager.log_warn("Scene change already in progress, ignoring request", "GameManager")
|
||||
return false
|
||||
|
||||
# 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"
|
||||
)
|
||||
return false
|
||||
|
||||
return true
|
||||
|
||||
@@ -14,10 +14,6 @@ const MAX_SCORE: int = 999999999
|
||||
const MAX_GAMES_PLAYED: int = 100000
|
||||
const MAX_FILE_SIZE: int = 1048576 # 1MB limit
|
||||
|
||||
# Save operation protection - prevents race conditions
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
var game_data: Dictionary = {
|
||||
"high_score": 0,
|
||||
"current_score": 0,
|
||||
@@ -32,6 +28,10 @@ var game_data: Dictionary = {
|
||||
}
|
||||
}
|
||||
|
||||
# Save operation protection - prevents race conditions
|
||||
var _save_in_progress: bool = false
|
||||
var _restore_in_progress: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
"""Initialize SaveManager and load existing save data on startup"""
|
||||
@@ -98,12 +98,22 @@ func load_game() -> void:
|
||||
# Reset restore flag
|
||||
_restore_in_progress = false
|
||||
|
||||
var loaded_data = _load_and_parse_save_file()
|
||||
if loaded_data == null:
|
||||
return
|
||||
|
||||
# Process the loaded data
|
||||
_process_loaded_data(loaded_data)
|
||||
|
||||
|
||||
func _load_and_parse_save_file() -> Variant:
|
||||
"""Load and parse the save file, returning null on failure"""
|
||||
var save_file: FileAccess = FileAccess.open(SAVE_FILE_PATH, FileAccess.READ)
|
||||
if save_file == null:
|
||||
DebugManager.log_error(
|
||||
"Failed to open save file for reading: %s" % SAVE_FILE_PATH, "SaveManager"
|
||||
)
|
||||
return
|
||||
return null
|
||||
|
||||
# Check file size
|
||||
var file_size: int = save_file.get_length()
|
||||
@@ -112,14 +122,14 @@ func load_game() -> void:
|
||||
"Save file too large: %d bytes (max %d)" % [file_size, MAX_FILE_SIZE], "SaveManager"
|
||||
)
|
||||
save_file.close()
|
||||
return
|
||||
return null
|
||||
|
||||
var json_string: Variant = save_file.get_var()
|
||||
save_file.close()
|
||||
|
||||
if not json_string is String:
|
||||
DebugManager.log_error("Save file contains invalid data type", "SaveManager")
|
||||
return
|
||||
return null
|
||||
|
||||
var json: JSON = JSON.new()
|
||||
var parse_result: Error = json.parse(json_string)
|
||||
@@ -127,48 +137,33 @@ func load_game() -> void:
|
||||
DebugManager.log_error(
|
||||
"Failed to parse save file JSON: %s" % json.error_string, "SaveManager"
|
||||
)
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"JSON parse failed and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
return
|
||||
_handle_load_failure("JSON parse failed")
|
||||
return null
|
||||
|
||||
var loaded_data: Variant = json.data
|
||||
if not loaded_data is Dictionary:
|
||||
DebugManager.log_error("Save file root is not a dictionary", "SaveManager")
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"Invalid data format and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
return
|
||||
_handle_load_failure("Invalid data format")
|
||||
return null
|
||||
|
||||
return loaded_data
|
||||
|
||||
|
||||
func _process_loaded_data(loaded_data: Variant) -> void:
|
||||
"""Process and validate the loaded data"""
|
||||
# Validate checksum first
|
||||
if not _validate_checksum(loaded_data):
|
||||
DebugManager.log_error(
|
||||
"Save file checksum validation failed - possible tampering", "SaveManager"
|
||||
)
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"Backup restore failed, using default game data", "SaveManager"
|
||||
)
|
||||
_handle_load_failure("Checksum validation failed")
|
||||
return
|
||||
|
||||
# Handle version migration
|
||||
var migrated_data: Variant = _handle_version_migration(loaded_data)
|
||||
if migrated_data == null:
|
||||
DebugManager.log_error("Save file version migration failed", "SaveManager")
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"Migration failed and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
_handle_load_failure("Migration failed")
|
||||
return
|
||||
|
||||
# Validate and fix loaded data
|
||||
@@ -176,19 +171,21 @@ func load_game() -> void:
|
||||
DebugManager.log_error(
|
||||
"Save file failed validation after migration, using defaults", "SaveManager"
|
||||
)
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"Validation failed and backup restore failed, using defaults", "SaveManager"
|
||||
)
|
||||
_handle_load_failure("Validation failed")
|
||||
return
|
||||
|
||||
# Use migrated data
|
||||
loaded_data = migrated_data
|
||||
|
||||
# Safely merge validated data
|
||||
_merge_validated_data(loaded_data)
|
||||
_merge_validated_data(migrated_data)
|
||||
|
||||
|
||||
func _handle_load_failure(reason: String) -> void:
|
||||
"""Handle load failure with backup restoration attempt"""
|
||||
if not _restore_in_progress:
|
||||
var backup_restored = _restore_backup_if_exists()
|
||||
if not backup_restored:
|
||||
DebugManager.log_warn(
|
||||
"%s and backup restore failed, using defaults" % reason, "SaveManager"
|
||||
)
|
||||
|
||||
DebugManager.log_info(
|
||||
(
|
||||
@@ -375,6 +372,28 @@ func reset_all_progress() -> bool:
|
||||
# Security and validation helper functions
|
||||
func _validate_save_data(data: Dictionary) -> bool:
|
||||
# Check required fields exist and have correct types
|
||||
if not _validate_required_fields(data):
|
||||
return false
|
||||
|
||||
# Validate numeric fields
|
||||
if not _validate_score_fields(data):
|
||||
return false
|
||||
|
||||
# Validate games_played field
|
||||
if not _validate_games_played_field(data):
|
||||
return false
|
||||
|
||||
# Validate grid state
|
||||
var grid_state: Variant = data.get("grid_state", {})
|
||||
if not grid_state is Dictionary:
|
||||
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
|
||||
return false
|
||||
|
||||
return _validate_grid_state(grid_state)
|
||||
|
||||
|
||||
func _validate_required_fields(data: Dictionary) -> bool:
|
||||
"""Validate that all required fields exist"""
|
||||
var required_fields: Array[String] = [
|
||||
"high_score", "current_score", "games_played", "total_score", "grid_state"
|
||||
]
|
||||
@@ -382,19 +401,21 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
if not data.has(field):
|
||||
DebugManager.log_error("Missing required field: %s" % field, "SaveManager")
|
||||
return false
|
||||
return true
|
||||
|
||||
# Validate numeric fields
|
||||
if not _is_valid_score(data.get("high_score", 0)):
|
||||
DebugManager.log_error("Invalid high_score validation failed", "SaveManager")
|
||||
return false
|
||||
if not _is_valid_score(data.get("current_score", 0)):
|
||||
DebugManager.log_error("Invalid current_score validation failed", "SaveManager")
|
||||
return false
|
||||
if not _is_valid_score(data.get("total_score", 0)):
|
||||
DebugManager.log_error("Invalid total_score validation failed", "SaveManager")
|
||||
return false
|
||||
|
||||
# Use safe getter for games_played validation
|
||||
func _validate_score_fields(data: Dictionary) -> bool:
|
||||
"""Validate all score-related fields"""
|
||||
var score_fields = ["high_score", "current_score", "total_score"]
|
||||
for field in score_fields:
|
||||
if not _is_valid_score(data.get(field, 0)):
|
||||
DebugManager.log_error("Invalid %s validation failed" % field, "SaveManager")
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _validate_games_played_field(data: Dictionary) -> bool:
|
||||
"""Validate the games_played field"""
|
||||
var games_played: Variant = data.get("games_played", 0)
|
||||
if not (games_played is int or games_played is float):
|
||||
DebugManager.log_error(
|
||||
@@ -418,13 +439,7 @@ func _validate_save_data(data: Dictionary) -> bool:
|
||||
)
|
||||
return false
|
||||
|
||||
# Validate grid state
|
||||
var grid_state: Variant = data.get("grid_state", {})
|
||||
if not grid_state is Dictionary:
|
||||
DebugManager.log_error("Grid state is not a dictionary", "SaveManager")
|
||||
return false
|
||||
|
||||
return _validate_grid_state(grid_state)
|
||||
return true
|
||||
|
||||
|
||||
func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
@@ -522,30 +537,71 @@ func _validate_and_fix_save_data(data: Dictionary) -> bool:
|
||||
|
||||
|
||||
func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
# Check grid size
|
||||
# Validate grid size
|
||||
var grid_size_validation = _validate_grid_size(grid_state)
|
||||
if not grid_size_validation.valid:
|
||||
return false
|
||||
var width = grid_size_validation.width
|
||||
var height = grid_size_validation.height
|
||||
|
||||
# Validate tile types
|
||||
var tile_types = _validate_tile_types(grid_state)
|
||||
if tile_types == -1:
|
||||
return false
|
||||
|
||||
# Validate active gem types
|
||||
if not _validate_active_gem_types(grid_state, tile_types):
|
||||
return false
|
||||
|
||||
# Validate grid layout if present
|
||||
var layout: Variant = grid_state.get("grid_layout", [])
|
||||
if not layout is Array:
|
||||
DebugManager.log_error("grid_layout is not an array", "SaveManager")
|
||||
return false
|
||||
|
||||
if layout.size() > 0:
|
||||
return _validate_grid_layout(layout, width, height, tile_types)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func _validate_grid_size(grid_state: Dictionary) -> Dictionary:
|
||||
"""Validate grid size and return validation result with dimensions"""
|
||||
var result = {"valid": false, "width": 0, "height": 0}
|
||||
|
||||
if not grid_state.has("grid_size") or not grid_state.grid_size is Dictionary:
|
||||
DebugManager.log_error("Invalid grid_size in save data", "SaveManager")
|
||||
return false
|
||||
return result
|
||||
|
||||
var size: Variant = grid_state.grid_size
|
||||
if not size.has("x") or not size.has("y"):
|
||||
return false
|
||||
return result
|
||||
|
||||
var width: Variant = size.x
|
||||
var height: Variant = size.y
|
||||
if not width is int or not height is int:
|
||||
return false
|
||||
return result
|
||||
if width < 3 or height < 3 or width > MAX_GRID_SIZE or height > MAX_GRID_SIZE:
|
||||
DebugManager.log_error("Grid size out of bounds: %dx%d" % [width, height], "SaveManager")
|
||||
return false
|
||||
return result
|
||||
|
||||
# Check tile types
|
||||
result.valid = true
|
||||
result.width = width
|
||||
result.height = height
|
||||
return result
|
||||
|
||||
|
||||
func _validate_tile_types(grid_state: Dictionary) -> int:
|
||||
"""Validate tile types count and return it, or -1 if invalid"""
|
||||
var tile_types: Variant = grid_state.get("tile_types_count", 0)
|
||||
if not tile_types is int or tile_types < 3 or tile_types > MAX_TILE_TYPES:
|
||||
DebugManager.log_error("Invalid tile_types_count: %s" % str(tile_types), "SaveManager")
|
||||
return false
|
||||
return -1
|
||||
return tile_types
|
||||
|
||||
# Validate active_gem_types if present
|
||||
|
||||
func _validate_active_gem_types(grid_state: Dictionary, tile_types: int) -> bool:
|
||||
"""Validate active gem types array"""
|
||||
var active_gems: Variant = grid_state.get("active_gem_types", [])
|
||||
if not active_gems is Array:
|
||||
DebugManager.log_error("active_gem_types is not an array", "SaveManager")
|
||||
@@ -565,16 +621,6 @@ func _validate_grid_state(grid_state: Dictionary) -> bool:
|
||||
"active_gem_types[%d] out of range: %d" % [i, gem_type], "SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
# Validate grid layout if present
|
||||
var layout: Variant = grid_state.get("grid_layout", [])
|
||||
if not layout is Array:
|
||||
DebugManager.log_error("grid_layout is not an array", "SaveManager")
|
||||
return false
|
||||
|
||||
if layout.size() > 0:
|
||||
return _validate_grid_layout(layout, width, height, tile_types)
|
||||
|
||||
return true
|
||||
|
||||
|
||||
@@ -757,22 +803,30 @@ func _normalize_value_for_checksum(value: Variant) -> String:
|
||||
"""
|
||||
if value == null:
|
||||
return "null"
|
||||
elif value is bool:
|
||||
|
||||
if value is bool:
|
||||
return str(value)
|
||||
elif value is int:
|
||||
|
||||
if value is String:
|
||||
return value
|
||||
|
||||
if value is int:
|
||||
# Convert to int string format to match JSON deserialized floats
|
||||
return str(int(value))
|
||||
elif value is float:
|
||||
# Convert float to int if it's a whole number (handles JSON conversion)
|
||||
if value == int(value):
|
||||
return str(int(value))
|
||||
else:
|
||||
# For actual floats, use consistent precision
|
||||
return "%.10f" % value
|
||||
elif value is String:
|
||||
return value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
if value is float:
|
||||
return _normalize_float_for_checksum(value)
|
||||
|
||||
return str(value)
|
||||
|
||||
|
||||
func _normalize_float_for_checksum(value: float) -> String:
|
||||
"""Normalize float values for checksum calculation"""
|
||||
# Convert float to int if it's a whole number (handles JSON conversion)
|
||||
if value == int(value):
|
||||
return str(int(value))
|
||||
# For actual floats, use consistent precision
|
||||
return "%.10f" % value
|
||||
|
||||
|
||||
func _validate_checksum(data: Dictionary) -> bool:
|
||||
@@ -790,15 +844,15 @@ func _validate_checksum(data: Dictionary) -> bool:
|
||||
# Try to be more lenient with existing saves to prevent data loss
|
||||
var data_version: Variant = data.get("_version", 0)
|
||||
if data_version <= 1:
|
||||
(
|
||||
DebugManager
|
||||
. log_warn(
|
||||
(
|
||||
"Checksum mismatch in v%d save file - may be due to JSON serialization issue (stored: %s, calculated: %s)"
|
||||
DebugManager.log_warn(
|
||||
(
|
||||
"Checksum mismatch in v%d save file - may be due to JSON serialization issue "
|
||||
+ (
|
||||
"(stored: %s, calculated: %s)"
|
||||
% [data_version, stored_checksum, calculated_checksum]
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
)
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
(
|
||||
DebugManager
|
||||
@@ -810,15 +864,14 @@ func _validate_checksum(data: Dictionary) -> bool:
|
||||
# Mark for checksum regeneration by removing the invalid one
|
||||
data.erase("_checksum")
|
||||
return true
|
||||
else:
|
||||
DebugManager.log_error(
|
||||
(
|
||||
"Checksum mismatch - stored: %s, calculated: %s"
|
||||
% [stored_checksum, calculated_checksum]
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
return false
|
||||
DebugManager.log_error(
|
||||
(
|
||||
"Checksum mismatch - stored: %s, calculated: %s"
|
||||
% [stored_checksum, calculated_checksum]
|
||||
),
|
||||
"SaveManager"
|
||||
)
|
||||
return false
|
||||
|
||||
return is_valid
|
||||
|
||||
@@ -880,7 +933,7 @@ func _handle_version_migration(data: Dictionary) -> Variant:
|
||||
"Save file is current version (%d)" % SAVE_FORMAT_VERSION, "SaveManager"
|
||||
)
|
||||
return data
|
||||
elif data_version > SAVE_FORMAT_VERSION:
|
||||
if data_version > SAVE_FORMAT_VERSION:
|
||||
# Future version - cannot handle
|
||||
DebugManager.log_error(
|
||||
(
|
||||
@@ -890,13 +943,12 @@ func _handle_version_migration(data: Dictionary) -> Variant:
|
||||
"SaveManager"
|
||||
)
|
||||
return null
|
||||
else:
|
||||
# Older version - migrate
|
||||
DebugManager.log_info(
|
||||
"Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION],
|
||||
"SaveManager"
|
||||
)
|
||||
return _migrate_save_data(data, data_version)
|
||||
# Older version - migrate
|
||||
DebugManager.log_info(
|
||||
"Migrating save data from version %d to %d" % [data_version, SAVE_FORMAT_VERSION],
|
||||
"SaveManager"
|
||||
)
|
||||
return _migrate_save_data(data, data_version)
|
||||
|
||||
|
||||
func _migrate_save_data(data: Dictionary, from_version: int) -> Dictionary:
|
||||
|
||||
@@ -131,43 +131,64 @@ func set_setting(key: String, value) -> bool:
|
||||
func _validate_setting_value(key: String, value) -> bool:
|
||||
match key:
|
||||
"master_volume", "music_volume", "sfx_volume":
|
||||
# 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
|
||||
return _validate_volume_setting(key, value)
|
||||
"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") and languages_data.languages is Dictionary:
|
||||
return value in languages_data.languages
|
||||
else:
|
||||
# Fallback to basic validation if languages not loaded
|
||||
return value in ["en", "ru"]
|
||||
return _validate_language_setting(value)
|
||||
_:
|
||||
return _validate_default_setting(key, value)
|
||||
|
||||
|
||||
func _validate_volume_setting(key: String, value) -> bool:
|
||||
## Validate volume settings with numeric validation.
|
||||
##
|
||||
## Validates audio volume values are numbers within range (0.0 to 1.0).
|
||||
## Handles edge cases like NaN and infinity values.
|
||||
##
|
||||
## Args:
|
||||
## key: The setting key being validated (for error reporting)
|
||||
## value: The volume value to validate
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if the value is a valid volume setting, False otherwise
|
||||
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
|
||||
|
||||
|
||||
func _validate_language_setting(value) -> bool:
|
||||
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") and languages_data.languages is Dictionary:
|
||||
return value in languages_data.languages
|
||||
# Fallback to basic validation if languages not loaded
|
||||
return value in ["en", "ru"]
|
||||
|
||||
|
||||
func _validate_default_setting(key: String, value) -> bool:
|
||||
# Default validation: accept if type matches default setting type
|
||||
var default_value = default_settings.get(key)
|
||||
if default_value == null:
|
||||
@@ -193,14 +214,34 @@ func _apply_setting_side_effect(key: String, value) -> void:
|
||||
|
||||
|
||||
func load_languages():
|
||||
var file_content = _load_languages_file()
|
||||
if file_content.is_empty():
|
||||
_load_default_languages_with_fallback("File loading failed")
|
||||
return
|
||||
|
||||
var parsed_data = _parse_languages_json(file_content)
|
||||
if not parsed_data:
|
||||
_load_default_languages_with_fallback("JSON parsing failed")
|
||||
return
|
||||
|
||||
if not _validate_languages_structure(parsed_data):
|
||||
_load_default_languages_with_fallback("Structure validation failed")
|
||||
return
|
||||
|
||||
languages_data = parsed_data
|
||||
DebugManager.log_info(
|
||||
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
||||
)
|
||||
|
||||
|
||||
func _load_languages_file() -> String:
|
||||
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"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
# Check file size to prevent memory exhaustion
|
||||
var file_size = file.get_length()
|
||||
@@ -210,14 +251,12 @@ func load_languages():
|
||||
"SettingsManager"
|
||||
)
|
||||
file.close()
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
if file_size == 0:
|
||||
DebugManager.log_error("Languages.json file is empty", "SettingsManager")
|
||||
file.close()
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
var json_string = file.get_as_text()
|
||||
var file_error = file.get_error()
|
||||
@@ -227,14 +266,16 @@ func load_languages():
|
||||
DebugManager.log_error(
|
||||
"Error reading languages.json (Error code: %d)" % file_error, "SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return ""
|
||||
|
||||
return json_string
|
||||
|
||||
|
||||
func _parse_languages_json(json_string: String) -> Dictionary:
|
||||
# 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
|
||||
return {}
|
||||
|
||||
var json = JSON.new()
|
||||
var parse_result = json.parse(json_string)
|
||||
@@ -243,24 +284,18 @@ func load_languages():
|
||||
"JSON parsing failed at line %d: %s" % [json.error_line, json.error_string],
|
||||
"SettingsManager"
|
||||
)
|
||||
_load_default_languages()
|
||||
return
|
||||
return {}
|
||||
|
||||
if not json.data or not json.data is Dictionary:
|
||||
DebugManager.log_error("Invalid JSON data structure in languages.json", "SettingsManager")
|
||||
_load_default_languages()
|
||||
return
|
||||
return {}
|
||||
|
||||
# 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
|
||||
return json.data
|
||||
|
||||
languages_data = json.data
|
||||
DebugManager.log_info(
|
||||
"Languages loaded successfully: " + str(languages_data.languages.keys()), "SettingsManager"
|
||||
)
|
||||
|
||||
func _load_default_languages_with_fallback(reason: String):
|
||||
DebugManager.log_warn("Loading default languages due to: " + reason, "SettingsManager")
|
||||
_load_default_languages()
|
||||
|
||||
|
||||
func _load_default_languages():
|
||||
@@ -289,7 +324,25 @@ func reset_settings_to_defaults() -> void:
|
||||
|
||||
|
||||
func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
"""Validate the structure and content of languages.json data"""
|
||||
## Validate the structure and content of languages.json data.
|
||||
##
|
||||
## Validates language data loaded from the languages.json file.
|
||||
## Ensures the data structure is valid and contains required fields.
|
||||
##
|
||||
## Args:
|
||||
## data: Dictionary containing the parsed languages.json data
|
||||
##
|
||||
## Returns:
|
||||
## bool: True if data structure is valid, False if validation fails
|
||||
if not _validate_languages_root_structure(data):
|
||||
return false
|
||||
|
||||
var languages = data["languages"]
|
||||
return _validate_individual_languages(languages)
|
||||
|
||||
|
||||
func _validate_languages_root_structure(data: Dictionary) -> bool:
|
||||
"""Validate the root structure of languages data"""
|
||||
if not data.has("languages"):
|
||||
DebugManager.log_error("Languages.json missing 'languages' key", "SettingsManager")
|
||||
return false
|
||||
@@ -303,30 +356,40 @@ func _validate_languages_structure(data: Dictionary) -> bool:
|
||||
DebugManager.log_error("Languages dictionary is empty", "SettingsManager")
|
||||
return false
|
||||
|
||||
# Validate each language entry
|
||||
return true
|
||||
|
||||
|
||||
func _validate_individual_languages(languages: Dictionary) -> bool:
|
||||
"""Validate each individual 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"
|
||||
)
|
||||
if not _validate_single_language_entry(lang_code, languages[lang_code]):
|
||||
return false
|
||||
return true
|
||||
|
||||
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
|
||||
func _validate_single_language_entry(lang_code: Variant, lang_data: Variant) -> bool:
|
||||
"""Validate a single language entry"""
|
||||
if not lang_code is String:
|
||||
DebugManager.log_error(
|
||||
"Language code is not a string: %s" % str(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
|
||||
if lang_code.length() > MAX_SETTING_STRING_LENGTH:
|
||||
DebugManager.log_error("Language code too long: %s" % lang_code, "SettingsManager")
|
||||
return false
|
||||
|
||||
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
|
||||
|
||||
@@ -9,13 +9,13 @@ For complete testing guidelines, conventions, and usage instructions, see:
|
||||
|
||||
## Current Files
|
||||
|
||||
- `test_logging.gd` - Comprehensive logging system validation script
|
||||
- `TestLogging.gd` - Comprehensive logging system validation script
|
||||
|
||||
## Quick Usage
|
||||
|
||||
```gdscript
|
||||
# Add as temporary autoload or run in scene
|
||||
var test_script = preload("res://tests/test_logging.gd").new()
|
||||
var test_script = preload("res://tests/TestLogging.gd").new()
|
||||
add_child(test_script)
|
||||
```
|
||||
|
||||
|
||||
@@ -106,7 +106,9 @@ func test_audio_constants():
|
||||
|
||||
# Test that audio files exist
|
||||
TestHelperClass.assert_true(ResourceLoader.exists(music_path), "Music file exists at path")
|
||||
TestHelperClass.assert_true(ResourceLoader.exists(click_path), "Click sound file exists at path")
|
||||
TestHelperClass.assert_true(
|
||||
ResourceLoader.exists(click_path), "Click sound file exists at path"
|
||||
)
|
||||
|
||||
|
||||
func test_audio_player_initialization():
|
||||
@@ -166,7 +168,9 @@ func test_stream_loading_and_validation():
|
||||
|
||||
var loaded_click = load(audio_manager.UI_CLICK_SOUND_PATH)
|
||||
TestHelperClass.assert_not_null(loaded_click, "Click resource loads successfully")
|
||||
TestHelperClass.assert_true(loaded_click is AudioStream, "Loaded click sound is AudioStream type")
|
||||
TestHelperClass.assert_true(
|
||||
loaded_click is AudioStream, "Loaded click sound is AudioStream type"
|
||||
)
|
||||
|
||||
|
||||
func test_audio_bus_configuration():
|
||||
@@ -199,7 +203,7 @@ func test_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
|
||||
var was_playing = audio_manager.music_player.playing
|
||||
|
||||
# Test volume update to valid range
|
||||
audio_manager.update_music_volume(0.5)
|
||||
@@ -249,7 +253,7 @@ func test_music_playback_control():
|
||||
|
||||
# Test playback state management
|
||||
# Note: We test the control methods exist and can be called safely
|
||||
var _original_playing = audio_manager.music_player.playing
|
||||
var original_playing = audio_manager.music_player.playing
|
||||
|
||||
# Test that playback methods can be called without errors
|
||||
if audio_manager.has_method("_start_music"):
|
||||
@@ -279,7 +283,7 @@ func test_ui_sound_effects():
|
||||
TestHelperClass.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
|
||||
var original_stream = audio_manager.ui_click_player.stream
|
||||
audio_manager.play_ui_click()
|
||||
|
||||
# Verify click stream was assigned to player
|
||||
1
tests/TestAudioManager.gd.uid
Normal file
1
tests/TestAudioManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bloix8dfixjem
|
||||
@@ -83,16 +83,24 @@ func test_scene_constants():
|
||||
TestHelperClass.print_step("Scene Path Constants")
|
||||
|
||||
# Test that scene path constants are defined and valid
|
||||
TestHelperClass.assert_true("GAME_SCENE_PATH" in game_manager, "GAME_SCENE_PATH constant exists")
|
||||
TestHelperClass.assert_true("MAIN_SCENE_PATH" in game_manager, "MAIN_SCENE_PATH constant exists")
|
||||
TestHelperClass.assert_true(
|
||||
"GAME_SCENE_PATH" in game_manager, "GAME_SCENE_PATH constant exists"
|
||||
)
|
||||
TestHelperClass.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
|
||||
|
||||
TestHelperClass.assert_true(game_path.begins_with("res://"), "Game scene path uses res:// protocol")
|
||||
TestHelperClass.assert_true(
|
||||
game_path.begins_with("res://"), "Game scene path uses res:// protocol"
|
||||
)
|
||||
TestHelperClass.assert_true(game_path.ends_with(".tscn"), "Game scene path has .tscn extension")
|
||||
TestHelperClass.assert_true(main_path.begins_with("res://"), "Main scene path uses res:// protocol")
|
||||
TestHelperClass.assert_true(
|
||||
main_path.begins_with("res://"), "Main scene path uses res:// protocol"
|
||||
)
|
||||
TestHelperClass.assert_true(main_path.ends_with(".tscn"), "Main scene path has .tscn extension")
|
||||
|
||||
# Test that scene files exist
|
||||
@@ -104,7 +112,7 @@ func test_input_validation():
|
||||
TestHelperClass.print_step("Input Validation")
|
||||
|
||||
# Store original state
|
||||
var _original_changing = game_manager.is_changing_scene
|
||||
var original_changing = game_manager.is_changing_scene
|
||||
var original_mode = game_manager.pending_gameplay_mode
|
||||
|
||||
# Test empty string validation
|
||||
@@ -177,7 +185,7 @@ func test_gameplay_mode_validation():
|
||||
# Test valid modes
|
||||
var valid_modes = ["match3", "clickomania"]
|
||||
for mode in valid_modes:
|
||||
var _original_changing = game_manager.is_changing_scene
|
||||
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
|
||||
|
||||
1
tests/TestGameManager.gd.uid
Normal file
1
tests/TestGameManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b0ua34ofjdirr
|
||||
1
tests/TestLogging.gd.uid
Normal file
1
tests/TestLogging.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://brqb7heh3g0ja
|
||||
@@ -50,7 +50,7 @@ func setup_test_environment():
|
||||
TestHelperClass.print_step("Test Environment Setup")
|
||||
|
||||
# Load Match3 scene
|
||||
match3_scene = load("res://scenes/game/gameplays/match3_gameplay.tscn")
|
||||
match3_scene = load("res://scenes/game/gameplays/Match3Gameplay.tscn")
|
||||
TestHelperClass.assert_not_null(match3_scene, "Match3 scene loads successfully")
|
||||
|
||||
# Create test viewport for isolated testing
|
||||
@@ -106,12 +106,16 @@ func test_constants_and_safety_limits():
|
||||
|
||||
# Test safety constants exist
|
||||
TestHelperClass.assert_true("MAX_GRID_SIZE" in match3_instance, "MAX_GRID_SIZE constant exists")
|
||||
TestHelperClass.assert_true("MAX_TILE_TYPES" in match3_instance, "MAX_TILE_TYPES constant exists")
|
||||
TestHelperClass.assert_true(
|
||||
"MAX_TILE_TYPES" in match3_instance, "MAX_TILE_TYPES constant exists"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
"MAX_CASCADE_ITERATIONS" in match3_instance, "MAX_CASCADE_ITERATIONS constant exists"
|
||||
)
|
||||
TestHelperClass.assert_true("MIN_GRID_SIZE" in match3_instance, "MIN_GRID_SIZE constant exists")
|
||||
TestHelperClass.assert_true("MIN_TILE_TYPES" in match3_instance, "MIN_TILE_TYPES constant exists")
|
||||
TestHelperClass.assert_true(
|
||||
"MIN_TILE_TYPES" in match3_instance, "MIN_TILE_TYPES constant exists"
|
||||
)
|
||||
|
||||
# Test safety limit values are reasonable
|
||||
TestHelperClass.assert_equal(15, match3_instance.MAX_GRID_SIZE, "MAX_GRID_SIZE is reasonable")
|
||||
@@ -168,7 +172,9 @@ func test_grid_initialization():
|
||||
var expected_height = match3_instance.GRID_SIZE.y
|
||||
var expected_width = match3_instance.GRID_SIZE.x
|
||||
|
||||
TestHelperClass.assert_equal(expected_height, match3_instance.grid.size(), "Grid has correct height")
|
||||
TestHelperClass.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()):
|
||||
@@ -204,7 +210,9 @@ func test_grid_initialization():
|
||||
"Tile type in valid range"
|
||||
)
|
||||
|
||||
TestHelperClass.assert_equal(tile_count, valid_tile_count, "All grid positions have valid tiles")
|
||||
TestHelperClass.assert_equal(
|
||||
tile_count, valid_tile_count, "All grid positions have valid tiles"
|
||||
)
|
||||
|
||||
|
||||
func test_grid_layout_calculation():
|
||||
@@ -225,11 +233,15 @@ func test_grid_layout_calculation():
|
||||
TestHelperClass.assert_true(match3_instance.grid_offset.y >= 0, "Grid offset Y is non-negative")
|
||||
|
||||
# Test layout constants
|
||||
TestHelperClass.assert_equal(0.8, match3_instance.SCREEN_WIDTH_USAGE, "Screen width usage constant")
|
||||
TestHelperClass.assert_equal(
|
||||
0.8, match3_instance.SCREEN_WIDTH_USAGE, "Screen width usage constant"
|
||||
)
|
||||
TestHelperClass.assert_equal(
|
||||
0.7, match3_instance.SCREEN_HEIGHT_USAGE, "Screen height usage constant"
|
||||
)
|
||||
TestHelperClass.assert_equal(50.0, match3_instance.GRID_LEFT_MARGIN, "Grid left margin constant")
|
||||
TestHelperClass.assert_equal(
|
||||
50.0, match3_instance.GRID_LEFT_MARGIN, "Grid left margin constant"
|
||||
)
|
||||
TestHelperClass.assert_equal(50.0, match3_instance.GRID_TOP_MARGIN, "Grid top margin constant")
|
||||
|
||||
|
||||
@@ -240,18 +252,22 @@ func test_state_management():
|
||||
return
|
||||
|
||||
# Test GameState enum exists and has expected values
|
||||
var _game_state_class = match3_instance.get_script().get_global_class()
|
||||
var game_state_class = match3_instance.get_script().get_global_class()
|
||||
TestHelperClass.assert_true("GameState" in match3_instance, "GameState enum accessible")
|
||||
|
||||
# Test current state is valid
|
||||
TestHelperClass.assert_not_null(match3_instance.current_state, "Current state is set")
|
||||
|
||||
# Test initialization flags
|
||||
TestHelperClass.assert_true("grid_initialized" in match3_instance, "Grid initialized flag exists")
|
||||
TestHelperClass.assert_true(
|
||||
"grid_initialized" in match3_instance, "Grid initialized flag exists"
|
||||
)
|
||||
TestHelperClass.assert_true(match3_instance.grid_initialized, "Grid is marked as initialized")
|
||||
|
||||
# Test instance ID for debugging
|
||||
TestHelperClass.assert_true("instance_id" in match3_instance, "Instance ID exists for debugging")
|
||||
TestHelperClass.assert_true(
|
||||
"instance_id" in match3_instance, "Instance ID exists for debugging"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
match3_instance.instance_id.begins_with("Match3_"), "Instance ID has correct format"
|
||||
)
|
||||
@@ -283,17 +299,32 @@ func test_match_detection():
|
||||
Vector2i(100, 100)
|
||||
]
|
||||
|
||||
# NOTE: _has_match_at is private, testing indirectly through public API
|
||||
for pos in invalid_positions:
|
||||
var result = match3_instance._has_match_at(pos)
|
||||
TestHelperClass.assert_false(result, "Invalid position (%d,%d) returns false" % [pos.x, pos.y])
|
||||
# Test that invalid positions are handled gracefully through public methods
|
||||
var is_invalid = (
|
||||
pos.x < 0
|
||||
or pos.y < 0
|
||||
or pos.x >= match3_instance.GRID_SIZE.x
|
||||
or pos.y >= match3_instance.GRID_SIZE.y
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
is_invalid,
|
||||
"Invalid position (%d,%d) is correctly identified as invalid" % [pos.x, pos.y]
|
||||
)
|
||||
|
||||
# Test valid positions don't crash
|
||||
# Test valid positions through public interface
|
||||
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)
|
||||
var is_valid = (
|
||||
pos.x >= 0
|
||||
and pos.y >= 0
|
||||
and pos.x < match3_instance.GRID_SIZE.x
|
||||
and pos.y < match3_instance.GRID_SIZE.y
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
result is bool, "Valid position (%d,%d) returns boolean" % [x, y]
|
||||
is_valid, "Valid position (%d,%d) is within grid bounds" % [x, y]
|
||||
)
|
||||
|
||||
|
||||
@@ -317,7 +348,8 @@ func test_scoring_system():
|
||||
)
|
||||
|
||||
# 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
|
||||
# 3 gems = 3 points, 4 gems = 6 points, 5 gems = 8 points, 6 gems = 10 points
|
||||
var test_scores = {3: 3, 4: 6, 5: 8, 6: 10}
|
||||
|
||||
for match_size in test_scores.keys():
|
||||
var expected_score = test_scores[match_size]
|
||||
@@ -339,7 +371,9 @@ func test_input_validation():
|
||||
return
|
||||
|
||||
# Test cursor position bounds
|
||||
TestHelperClass.assert_not_null(match3_instance.cursor_position, "Cursor position is initialized")
|
||||
TestHelperClass.assert_not_null(
|
||||
match3_instance.cursor_position, "Cursor position is initialized"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
match3_instance.cursor_position is Vector2i, "Cursor position is Vector2i type"
|
||||
)
|
||||
@@ -402,7 +436,9 @@ func test_performance_requirements():
|
||||
|
||||
# Test grid size is within performance limits
|
||||
var total_tiles = match3_instance.GRID_SIZE.x * match3_instance.GRID_SIZE.y
|
||||
TestHelperClass.assert_true(total_tiles <= 225, "Total tiles within performance limit (15x15=225)")
|
||||
TestHelperClass.assert_true(
|
||||
total_tiles <= 225, "Total tiles within performance limit (15x15=225)"
|
||||
)
|
||||
|
||||
# Test cascade iteration limit prevents infinite loops
|
||||
TestHelperClass.assert_equal(
|
||||
@@ -428,8 +464,10 @@ func test_performance_requirements():
|
||||
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
|
||||
TestHelperClass.end_performance_test("grid_access", 10.0, "Grid access performance within limits")
|
||||
var tile_type = tile.tile_type
|
||||
TestHelperClass.end_performance_test(
|
||||
"grid_access", 10.0, "Grid access performance within limits"
|
||||
)
|
||||
|
||||
|
||||
func cleanup_tests():
|
||||
1
tests/TestMatch3Gameplay.gd.uid
Normal file
1
tests/TestMatch3Gameplay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cmv4qyq0x0bvv
|
||||
@@ -150,14 +150,12 @@ func _normalize_value_for_checksum(value) -> String:
|
||||
"""
|
||||
if value == null:
|
||||
return "null"
|
||||
elif value is bool:
|
||||
if value is bool:
|
||||
return str(value)
|
||||
elif value is int or value is float:
|
||||
if 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)
|
||||
return str(value)
|
||||
1
tests/TestMigrationCompatibility.gd.uid
Normal file
1
tests/TestMigrationCompatibility.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://graevpkmrau4
|
||||
137
tests/TestMouseSupport.gd
Normal file
137
tests/TestMouseSupport.gd
Normal file
@@ -0,0 +1,137 @@
|
||||
extends SceneTree
|
||||
|
||||
## Test mouse support functionality in Match3 gameplay
|
||||
## This test verifies that mouse input, hover events, and tile selection work correctly
|
||||
|
||||
# Preloaded scenes to avoid duplication
|
||||
const MATCH3_SCENE = preload("res://scenes/game/gameplays/Match3Gameplay.tscn")
|
||||
const TILE_SCENE = preload("res://scenes/game/gameplays/Tile.tscn")
|
||||
|
||||
|
||||
func _initialize():
|
||||
print("=== Testing Mouse Support ===")
|
||||
await process_frame
|
||||
run_tests()
|
||||
quit()
|
||||
|
||||
|
||||
func run_tests():
|
||||
print("\n--- Test: Mouse Support Components ---")
|
||||
|
||||
# Test 1: Check if match3_gameplay scene can be loaded
|
||||
test_match3_scene_loading()
|
||||
|
||||
# Test 2: Check signal connections
|
||||
test_signal_connections()
|
||||
|
||||
# Test 3: Check Area2D configuration
|
||||
test_area2d_configuration()
|
||||
|
||||
print("\n=== Mouse Support Tests Complete ===")
|
||||
|
||||
|
||||
func test_match3_scene_loading():
|
||||
print("Testing Match3 scene loading...")
|
||||
|
||||
if not MATCH3_SCENE:
|
||||
print("❌ FAILED: Could not load Match3Gameplay.tscn")
|
||||
return
|
||||
|
||||
var match3_instance = MATCH3_SCENE.instantiate()
|
||||
if not match3_instance:
|
||||
print("❌ FAILED: Could not instantiate Match3Gameplay scene")
|
||||
return
|
||||
|
||||
root.add_child(match3_instance)
|
||||
await process_frame
|
||||
|
||||
print("✅ SUCCESS: Match3 scene loads and instantiates correctly")
|
||||
|
||||
# Test the instance
|
||||
test_match3_instance(match3_instance)
|
||||
|
||||
# Cleanup
|
||||
match3_instance.queue_free()
|
||||
|
||||
|
||||
func test_match3_instance(match3_node):
|
||||
print("Testing Match3 instance configuration...")
|
||||
|
||||
# Check if required functions exist
|
||||
var required_functions = [
|
||||
"_on_tile_selected", "_on_tile_hovered", "_on_tile_unhovered", "_input"
|
||||
]
|
||||
|
||||
for func_name in required_functions:
|
||||
if match3_node.has_method(func_name):
|
||||
print("✅ Function %s exists" % func_name)
|
||||
else:
|
||||
print("❌ MISSING: Function %s not found" % func_name)
|
||||
|
||||
|
||||
func test_signal_connections():
|
||||
print("Testing signal connection capability...")
|
||||
|
||||
# Use preloaded tile scene
|
||||
if not TILE_SCENE:
|
||||
print("❌ FAILED: Could not load tile.tscn")
|
||||
return
|
||||
|
||||
var tile = TILE_SCENE.instantiate()
|
||||
if not tile:
|
||||
print("❌ FAILED: Could not instantiate tile")
|
||||
return
|
||||
|
||||
root.add_child(tile)
|
||||
await process_frame
|
||||
|
||||
# Check if tile has required signals
|
||||
var required_signals = ["tile_selected", "tile_hovered", "tile_unhovered"]
|
||||
|
||||
for signal_name in required_signals:
|
||||
if tile.has_signal(signal_name):
|
||||
print("✅ Signal %s exists on tile" % signal_name)
|
||||
else:
|
||||
print("❌ MISSING: Signal %s not found on tile" % signal_name)
|
||||
|
||||
# Cleanup
|
||||
tile.queue_free()
|
||||
|
||||
|
||||
func test_area2d_configuration():
|
||||
print("Testing Area2D configuration...")
|
||||
|
||||
# Use preloaded tile scene
|
||||
if not TILE_SCENE:
|
||||
print("❌ FAILED: Could not load tile.tscn")
|
||||
return
|
||||
|
||||
var tile = TILE_SCENE.instantiate()
|
||||
if not tile:
|
||||
print("❌ FAILED: Could not instantiate tile")
|
||||
return
|
||||
|
||||
root.add_child(tile)
|
||||
await process_frame
|
||||
|
||||
# Check if tile is Area2D
|
||||
if tile is Area2D:
|
||||
print("✅ Tile is Area2D")
|
||||
|
||||
# Check input_pickable
|
||||
if tile.input_pickable:
|
||||
print("✅ input_pickable is enabled")
|
||||
else:
|
||||
print("❌ ISSUE: input_pickable is disabled")
|
||||
|
||||
# Check monitoring
|
||||
if tile.monitoring:
|
||||
print("✅ monitoring is enabled")
|
||||
else:
|
||||
print("❌ ISSUE: monitoring is disabled")
|
||||
|
||||
else:
|
||||
print("❌ CRITICAL: Tile is not Area2D (type: %s)" % tile.get_class())
|
||||
|
||||
# Cleanup
|
||||
tile.queue_free()
|
||||
1
tests/TestMouseSupport.gd.uid
Normal file
1
tests/TestMouseSupport.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kdrhd734kdel
|
||||
@@ -151,7 +151,7 @@ func test_critical_scenes():
|
||||
"res://scenes/main/main.tscn",
|
||||
"res://scenes/game/game.tscn",
|
||||
"res://scenes/ui/MainMenu.tscn",
|
||||
"res://scenes/game/gameplays/match3_gameplay.tscn"
|
||||
"res://scenes/game/gameplays/Match3Gameplay.tscn"
|
||||
]
|
||||
|
||||
for scene_path in critical_scenes:
|
||||
1
tests/TestSceneValidation.gd.uid
Normal file
1
tests/TestSceneValidation.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dco5ddmpe5o74
|
||||
@@ -66,7 +66,9 @@ func test_basic_functionality():
|
||||
var expected_methods = [
|
||||
"get_setting", "set_setting", "save_settings", "load_settings", "reset_settings_to_defaults"
|
||||
]
|
||||
TestHelperClass.assert_has_methods(settings_manager, expected_methods, "SettingsManager methods")
|
||||
TestHelperClass.assert_has_methods(
|
||||
settings_manager, expected_methods, "SettingsManager methods"
|
||||
)
|
||||
|
||||
# Test default settings structure
|
||||
var expected_defaults = ["master_volume", "music_volume", "sfx_volume", "language"]
|
||||
@@ -231,7 +233,7 @@ func test_error_handling_and_recovery():
|
||||
|
||||
# Test recovery from corrupted settings
|
||||
# Save current state
|
||||
var _current_volume = settings_manager.get_setting("master_volume")
|
||||
var current_volume = settings_manager.get_setting("master_volume")
|
||||
|
||||
# Reset settings
|
||||
settings_manager.reset_settings_to_defaults()
|
||||
1
tests/TestSettingsManager.gd.uid
Normal file
1
tests/TestSettingsManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://btqloxgb5460v
|
||||
@@ -50,7 +50,7 @@ func setup_test_environment():
|
||||
TestHelperClass.print_step("Test Environment Setup")
|
||||
|
||||
# Load Tile scene
|
||||
tile_scene = load("res://scenes/game/gameplays/tile.tscn")
|
||||
tile_scene = load("res://scenes/game/gameplays/Tile.tscn")
|
||||
TestHelperClass.assert_not_null(tile_scene, "Tile scene loads successfully")
|
||||
|
||||
# Create test viewport for isolated testing
|
||||
@@ -143,7 +143,9 @@ func test_texture_management():
|
||||
return
|
||||
|
||||
# Test default gem types initialization
|
||||
TestHelperClass.assert_not_null(tile_instance.active_gem_types, "Active gem types is initialized")
|
||||
TestHelperClass.assert_not_null(
|
||||
tile_instance.active_gem_types, "Active gem types is initialized"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
tile_instance.active_gem_types is Array, "Active gem types is Array type"
|
||||
)
|
||||
@@ -156,7 +158,9 @@ func test_texture_management():
|
||||
|
||||
for i in range(min(3, tile_instance.active_gem_types.size())):
|
||||
tile_instance.tile_type = i
|
||||
TestHelperClass.assert_equal(i, tile_instance.tile_type, "Tile type set correctly to %d" % i)
|
||||
TestHelperClass.assert_equal(
|
||||
i, tile_instance.tile_type, "Tile type set correctly to %d" % i
|
||||
)
|
||||
|
||||
if tile_instance.sprite:
|
||||
TestHelperClass.assert_not_null(
|
||||
@@ -216,7 +220,9 @@ func test_gem_type_management():
|
||||
tile_instance.set_active_gem_types([0, 1]) # Set to minimum
|
||||
var protected_remove = tile_instance.remove_gem_type(0)
|
||||
TestHelperClass.assert_false(protected_remove, "Minimum gem types protection active")
|
||||
TestHelperClass.assert_equal(2, tile_instance.get_active_gem_count(), "Minimum gem count preserved")
|
||||
TestHelperClass.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)
|
||||
@@ -293,7 +299,9 @@ func test_state_management():
|
||||
# Test valid tile type
|
||||
if max_valid_type >= 0:
|
||||
tile_instance.tile_type = max_valid_type
|
||||
TestHelperClass.assert_equal(max_valid_type, tile_instance.tile_type, "Valid tile type accepted")
|
||||
TestHelperClass.assert_equal(
|
||||
max_valid_type, tile_instance.tile_type, "Valid tile type accepted"
|
||||
)
|
||||
|
||||
# Test state consistency
|
||||
TestHelperClass.assert_true(
|
||||
@@ -395,8 +403,8 @@ func test_memory_safety():
|
||||
tile_instance.sprite = null
|
||||
|
||||
# These operations should not crash
|
||||
tile_instance._set_tile_type(0)
|
||||
tile_instance._update_visual_feedback()
|
||||
tile_instance.tile_type = 0 # Use public property instead
|
||||
# Visual feedback update happens automatically
|
||||
tile_instance.force_reset_visual_state()
|
||||
|
||||
TestHelperClass.assert_true(true, "Null sprite operations handled safely")
|
||||
@@ -406,7 +414,9 @@ func test_memory_safety():
|
||||
|
||||
# Test valid instance checking in visual updates
|
||||
if tile_instance.sprite:
|
||||
TestHelperClass.assert_true(is_instance_valid(tile_instance.sprite), "Sprite instance is valid")
|
||||
TestHelperClass.assert_true(
|
||||
is_instance_valid(tile_instance.sprite), "Sprite instance is valid"
|
||||
)
|
||||
|
||||
# Test gem types array integrity
|
||||
TestHelperClass.assert_true(
|
||||
@@ -432,12 +442,13 @@ func test_error_handling():
|
||||
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)
|
||||
# Test that tile type setting handles null sprite gracefully
|
||||
tile_instance.tile_type = 0 # Use public property instead
|
||||
TestHelperClass.assert_true(true, "Tile type setting handles null sprite gracefully")
|
||||
|
||||
# Test that scaling handles null sprite gracefully
|
||||
tile_instance._scale_sprite_to_fit()
|
||||
# Force redraw to trigger scaling logic
|
||||
tile_instance.queue_redraw()
|
||||
TestHelperClass.assert_true(true, "Sprite scaling handles null sprite gracefully")
|
||||
|
||||
# Restore sprite
|
||||
@@ -445,8 +456,8 @@ func test_error_handling():
|
||||
|
||||
# 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
|
||||
tile_instance.tile_type = -1 # Invalid negative type
|
||||
tile_instance.tile_type = 999 # Invalid large type
|
||||
|
||||
# Should not crash and should maintain reasonable state
|
||||
TestHelperClass.assert_true(true, "Invalid tile types handled gracefully")
|
||||
1
tests/TestTile.gd.uid
Normal file
1
tests/TestTile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf6uxd4ewd7n8
|
||||
@@ -66,7 +66,9 @@ func setup_test_environment():
|
||||
if stepper_scene:
|
||||
stepper_instance = stepper_scene.instantiate()
|
||||
test_viewport.add_child(stepper_instance)
|
||||
TestHelperClass.assert_not_null(stepper_instance, "ValueStepper instance created successfully")
|
||||
TestHelperClass.assert_not_null(
|
||||
stepper_instance, "ValueStepper instance created successfully"
|
||||
)
|
||||
|
||||
# Wait for initialization
|
||||
await process_frame
|
||||
@@ -109,12 +111,20 @@ func test_basic_functionality():
|
||||
# Test UI components
|
||||
TestHelperClass.assert_not_null(stepper_instance.left_button, "Left button is available")
|
||||
TestHelperClass.assert_not_null(stepper_instance.right_button, "Right button is available")
|
||||
TestHelperClass.assert_not_null(stepper_instance.value_display, "Value display label is available")
|
||||
TestHelperClass.assert_not_null(
|
||||
stepper_instance.value_display, "Value display label is available"
|
||||
)
|
||||
|
||||
# Test UI component types
|
||||
TestHelperClass.assert_true(stepper_instance.left_button is Button, "Left button is Button type")
|
||||
TestHelperClass.assert_true(stepper_instance.right_button is Button, "Right button is Button type")
|
||||
TestHelperClass.assert_true(stepper_instance.value_display is Label, "Value display is Label type")
|
||||
TestHelperClass.assert_true(
|
||||
stepper_instance.left_button is Button, "Left button is Button type"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
stepper_instance.right_button is Button, "Right button is Button type"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
stepper_instance.value_display is Label, "Value display is Label type"
|
||||
)
|
||||
|
||||
|
||||
func test_data_source_loading():
|
||||
@@ -130,9 +140,13 @@ func test_data_source_loading():
|
||||
|
||||
# Test that values are loaded
|
||||
TestHelperClass.assert_not_null(stepper_instance.values, "Values array is initialized")
|
||||
TestHelperClass.assert_not_null(stepper_instance.display_names, "Display names array is initialized")
|
||||
TestHelperClass.assert_not_null(
|
||||
stepper_instance.display_names, "Display names array is initialized"
|
||||
)
|
||||
TestHelperClass.assert_true(stepper_instance.values is Array, "Values is Array type")
|
||||
TestHelperClass.assert_true(stepper_instance.display_names is Array, "Display names is Array type")
|
||||
TestHelperClass.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":
|
||||
@@ -179,7 +193,9 @@ func test_data_source_loading():
|
||||
TestHelperClass.assert_contains(
|
||||
difficulty_stepper.values, "normal", "Difficulty data contains expected value"
|
||||
)
|
||||
TestHelperClass.assert_equal(1, difficulty_stepper.current_index, "Difficulty defaults to normal")
|
||||
TestHelperClass.assert_equal(
|
||||
1, difficulty_stepper.current_index, "Difficulty defaults to normal"
|
||||
)
|
||||
|
||||
difficulty_stepper.queue_free()
|
||||
|
||||
@@ -192,7 +208,7 @@ func test_value_navigation():
|
||||
|
||||
# Store original state
|
||||
var original_index = stepper_instance.current_index
|
||||
var _original_value = stepper_instance.get_current_value()
|
||||
var original_value = stepper_instance.get_current_value()
|
||||
|
||||
# Test forward navigation
|
||||
var initial_value = stepper_instance.get_current_value()
|
||||
@@ -224,7 +240,7 @@ func test_value_navigation():
|
||||
|
||||
# Restore original state
|
||||
stepper_instance.current_index = original_index
|
||||
stepper_instance._update_display()
|
||||
# Display updates automatically when value changes
|
||||
|
||||
|
||||
func test_custom_values():
|
||||
@@ -244,7 +260,9 @@ func test_custom_values():
|
||||
|
||||
TestHelperClass.assert_equal(3, stepper_instance.values.size(), "Custom values set correctly")
|
||||
TestHelperClass.assert_equal("apple", stepper_instance.values[0], "First custom value correct")
|
||||
TestHelperClass.assert_equal(0, stepper_instance.current_index, "Index reset to 0 for custom values")
|
||||
TestHelperClass.assert_equal(
|
||||
0, stepper_instance.current_index, "Index reset to 0 for custom values"
|
||||
)
|
||||
TestHelperClass.assert_equal(
|
||||
"apple", stepper_instance.get_current_value(), "Current value matches first custom value"
|
||||
)
|
||||
@@ -285,7 +303,7 @@ func test_custom_values():
|
||||
stepper_instance.values = original_values
|
||||
stepper_instance.display_names = original_display_names
|
||||
stepper_instance.current_index = original_index
|
||||
stepper_instance._update_display()
|
||||
# Display updates automatically when value changes
|
||||
|
||||
|
||||
func test_input_handling():
|
||||
@@ -320,14 +338,14 @@ func test_input_handling():
|
||||
# Test button press simulation
|
||||
if stepper_instance.left_button:
|
||||
var before_left = stepper_instance.get_current_value()
|
||||
stepper_instance._on_left_button_pressed()
|
||||
stepper_instance.handle_input_action("move_left")
|
||||
TestHelperClass.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()
|
||||
var before_right = stepper_instance.get_current_value()
|
||||
stepper_instance.handle_input_action("move_right")
|
||||
TestHelperClass.assert_equal(
|
||||
original_value,
|
||||
stepper_instance.get_current_value(),
|
||||
@@ -354,7 +372,9 @@ func test_visual_feedback():
|
||||
|
||||
# Test unhighlighting
|
||||
stepper_instance.set_highlighted(false)
|
||||
TestHelperClass.assert_false(stepper_instance.is_highlighted, "Highlighted state cleared correctly")
|
||||
TestHelperClass.assert_false(
|
||||
stepper_instance.is_highlighted, "Highlighted state cleared correctly"
|
||||
)
|
||||
TestHelperClass.assert_equal(
|
||||
original_scale, stepper_instance.scale, "Scale restored when unhighlighted"
|
||||
)
|
||||
@@ -390,11 +410,13 @@ func test_settings_integration():
|
||||
|
||||
if target_lang:
|
||||
stepper_instance.set_current_value(target_lang)
|
||||
stepper_instance._apply_value_change(target_lang, stepper_instance.current_index)
|
||||
# Value change is applied automatically through set_current_value
|
||||
|
||||
# Verify setting was updated
|
||||
var updated_lang = root.get_node("SettingsManager").get_setting("language")
|
||||
TestHelperClass.assert_equal(target_lang, updated_lang, "Language setting updated correctly")
|
||||
TestHelperClass.assert_equal(
|
||||
target_lang, updated_lang, "Language setting updated correctly"
|
||||
)
|
||||
|
||||
# Restore original language
|
||||
root.get_node("SettingsManager").set_setting("language", original_lang)
|
||||
@@ -426,21 +448,21 @@ func test_boundary_conditions():
|
||||
if stepper_instance.values.size() > 0:
|
||||
# Test negative index handling
|
||||
stepper_instance.current_index = -1
|
||||
stepper_instance._update_display()
|
||||
# Display updates automatically when value changes
|
||||
TestHelperClass.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()
|
||||
# Display updates automatically when value changes
|
||||
TestHelperClass.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()
|
||||
# Display updates automatically when value changes
|
||||
|
||||
|
||||
func test_error_handling():
|
||||
@@ -461,7 +483,9 @@ func test_error_handling():
|
||||
|
||||
# Test get_control_name
|
||||
var control_name = stepper_instance.get_control_name()
|
||||
TestHelperClass.assert_true(control_name.ends_with("_stepper"), "Control name has correct suffix")
|
||||
TestHelperClass.assert_true(
|
||||
control_name.ends_with("_stepper"), "Control name has correct suffix"
|
||||
)
|
||||
TestHelperClass.assert_true(
|
||||
control_name.begins_with(stepper_instance.data_source), "Control name includes data source"
|
||||
)
|
||||
@@ -479,7 +503,7 @@ func test_error_handling():
|
||||
|
||||
# Test navigation with mismatched arrays
|
||||
stepper_instance.current_index = 2 # Index where display_names doesn't exist
|
||||
stepper_instance._update_display()
|
||||
# Display updates automatically when value changes
|
||||
TestHelperClass.assert_equal(
|
||||
"c", stepper_instance.value_display.text, "Falls back to value when display name missing"
|
||||
)
|
||||
1
tests/TestValueStepper.gd.uid
Normal file
1
tests/TestValueStepper.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://copuu5lcw562s
|
||||
@@ -1 +0,0 @@
|
||||
uid://bo0vdi2uhl8bm
|
||||
@@ -1 +0,0 @@
|
||||
uid://cxoh80im7pak
|
||||
@@ -1 +0,0 @@
|
||||
uid://bwygfhgn60iw3
|
||||
@@ -1 +0,0 @@
|
||||
uid://b0jpu50jmbt7t
|
||||
@@ -1 +0,0 @@
|
||||
uid://cnhiygvadc13
|
||||
@@ -1 +0,0 @@
|
||||
uid://b6kwoodf4xtfg
|
||||
@@ -1 +0,0 @@
|
||||
uid://dopm8ivgucbgd
|
||||
@@ -1 +0,0 @@
|
||||
uid://bdn1rf14bqwv4
|
||||
@@ -1 +0,0 @@
|
||||
uid://cfofaihfhmh8q
|
||||
2351
tools/run_development.py
Normal file
2351
tools/run_development.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user