diff --git a/.gitignore b/.gitignore index 23149bf11..29d9f51b5 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ artifacts/main_menu.png v8_coverage_*.json tests/refactor/__pycache__/ report.html +scan_project.py +project_structure.txt diff --git a/Dockerfile b/Dockerfile index 2688cadf1..565344042 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN pip install yamllint RUN pip install pytest-html pytest-timeout # Install markdownlint-cli2 via npm (Node.js tool) -RUN npm install -g markdownlint-cli2 +RUN npm install -g markdownlint-cli2@0.12.1 # Download Godot v4.5 binary and export templates RUN wget https://github.com/godotengine/godot/releases/download/4.5-stable/Godot_v4.5-stable_linux.x86_64.zip \ @@ -62,6 +62,14 @@ RUN mkdir -p /project/addons \ && rm -rf /project/addons/gdUnit4-6.0.0 v6.0.0.zip \ && chown -R godotuser:godotuser /project # Make project dir accessible +# Install GUT v9.5.0 +RUN mkdir -p /project/addons \ + && wget https://github.com/bitwes/Gut/archive/refs/tags/v9.5.0.zip \ + && unzip v9.5.0.zip -d /project/addons \ + && mv /project/addons/Gut-9.5.0/addons/gut /project/addons/gut \ + && rm -rf /project/addons/Gut-9.5.0 v9.5.0.zip \ + && chown -R godotuser:godotuser /project + # Install Playwright Python packages and system deps (as root) RUN pip install playwright pytest-playwright pytest-asyncio \ && playwright install-deps \ diff --git a/README.md b/README.md index dc3f0ad90..d0af7d3dd 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ source code to users. For closed-source commercial alternatives without these GPL requirements, a separate license is available upon request. ### Key Terms + - **Open Source**: You can view, modify, and distribute the code freely, as long as derivatives remain under GPLv3. - **Commercial Use**: Allowed under GPLv3 (with source code obligations @@ -253,18 +254,21 @@ Milestone 12 focused on making the game more navigable and responsive to user input devices: ### Input Remapping + - Conflict detection dialog when assigning existing bindings - Per-device last input selection persists between sessions - Critical control warnings if actions are unbound - Remap menu accessible from all relevant UI paths ### Menu Navigation + - Keyboard + gamepad (D-Pad) support for all menu flows - Guaranteed core navigation actions remain bound - Focus restoration when leaving submenus (Audio → Options → Main) - Modifier key respect (Ctrl/Shift/Alt/Meta) in remapping UI ### Audio Settings Controls + - Use keyboard/gamepad accept action for sliders and toggles - Focus highlighting for better visual feedback - Unified UI interactions without relying on the mouse @@ -273,18 +277,18 @@ to user input devices: synchronization. ### Godot Resource Migration + - Replaced hard-coded globals with a `GameSettingsResource` - Easier inspector-based editing and persistence - Safer loading with fallback on corrupted configs ### Known Limitations -* Some complex menu flows may still rely on the mouse until additional +- Some complex menu flows may still rely on the mouse until additional focus neighbors are defined. -* Modifier-aware remapping requires explicit key+modifier press for +- Modifier-aware remapping requires explicit key+modifier press for unique bindings. - Track progress via [Milestones](https://github.com/ikostan/SkyLockAssault/milestones). --- diff --git a/files/docs/Development_Guide.md b/files/docs/Development_Guide.md index 0b0946ea5..82821a3ab 100644 --- a/files/docs/Development_Guide.md +++ b/files/docs/Development_Guide.md @@ -98,7 +98,7 @@ When adding a new setting: 4. No Manual Saves: Do not call save functions directly from the UI; changing the resource value is sufficient to trigger a save. -#### The technical documentation for the "Working with Game Settings" section. +#### The technical documentation for the "Working with Game Settings" section This documentation explicitly defines the signal signature and the specific files responsible for the **Observer Pattern** architecture. @@ -112,10 +112,10 @@ and a centralized observer handles persistence and logging. When connecting a UI element or a new system to the settings resource, use the following signature: -* **Signal Name**: `setting_changed` -* **Parameters**: - * `setting_name`: **String** (The name of the property that changed, e.g., "difficulty") - * `new_value`: **Variant** (The newly assigned, clamped value) +- **Signal Name**: `setting_changed` +- **Parameters**: + - `setting_name`: **String** (The name of the property that changed, e.g., "difficulty") + - `new_value`: **Variant** (The newly assigned, clamped value) #### 2. Core Files Reference diff --git a/files/docs/Platforms_for_Web_Deployment_Guide.md b/files/docs/Platforms_for_Web_Deployment_Guide.md index 349dd8769..0df701256 100644 --- a/files/docs/Platforms_for_Web_Deployment_Guide.md +++ b/files/docs/Platforms_for_Web_Deployment_Guide.md @@ -4,26 +4,27 @@ ## Project Context -We're building **SkyLockAssault** — a totally free-to-play browser +- We're building **SkyLockAssault** — a totally free-to-play browser game in **Godot v4.5** on **Windows 10 64-bit**. This is our learning journey into game dev, so we're keeping everything practical, low-friction, and focused on **automatic deploys** via GitHub Actions -+ CI/CD where possible. -This `.md` file is your living playbook. Update it as you go (e.g. mark +- CI/CD where possible. + +- This `.md` file is your living playbook. Update it as you go (e.g. mark new platforms as "Deployed: ✅ Yes"). -**Target**: Automatic GitHub Actions deploys where possible. +- **Target**: Automatic GitHub Actions deploys where possible. --- ## Why Web Platforms for a Free F2P Godot Game? -- **Zero cost to publish** (no Steam $100 fee) -- **Instant browser play** (Godot WebGL export = one ZIP) -- **High traffic** for casual games like SkyLockAssault -- **Ad revenue or donations** without forcing monetization -- **GitHub Actions** = push → auto-deploy (your dream workflow) ++ **Zero cost to publish** (no Steam $100 fee) ++ **Instant browser play** (Godot WebGL export = one ZIP) ++ **High traffic** for casual games like SkyLockAssault ++ **Ad revenue or donations** without forcing monetization ++ **GitHub Actions** = push → auto-deploy (your dream workflow) --- @@ -75,9 +76,9 @@ but it's maintenance work. ## Automation Strategy -- 100% Auto: itch.io, Poki, Viverse, Game Jolt -- Playwright Auto: iDev.games, GameMonetize (easiest) -- Manual + Occasional: CrazyGames, Y8, GameDistribution, GamePix, Newgrounds, ++ 100% Auto: itch.io, Poki, Viverse, Game Jolt ++ Playwright Auto: iDev.games, GameMonetize (easiest) ++ Manual + Occasional: CrazyGames, Y8, GameDistribution, GamePix, Newgrounds, SoftGames --- @@ -92,12 +93,12 @@ but it's maintenance work. ### Phase 2: Semi-Auto Bonus (Playwright) -- **iDev.games** + **GameMonetize** (easiest forms) -- Update every 2–4 weeks via one shared script ++ **iDev.games** + **GameMonetize** (easiest forms) ++ Update every 2–4 weeks via one shared script ### Phase 3: Manual Once + Occasional Updates -- CrazyGames, Y8, GameDistribution, GamePix, Newgrounds, SoftGames, Game Jolt ++ CrazyGames, Y8, GameDistribution, GamePix, Newgrounds, SoftGames **Goal:** Push to `main` → 10+ platforms updated automatically. diff --git a/project.godot b/project.godot index 7e2836aac..b701295a4 100644 --- a/project.godot +++ b/project.godot @@ -24,10 +24,10 @@ general/default_playback_type.web=0 [autoload] -Globals="*res://scripts/globals.gd" -Settings="*res://scripts/settings.gd" +Globals="*res://scripts/core/globals.gd" +Settings="*res://scripts/core/settings.gd" AudioConstants="*res://scripts/audio_constants.gd" -AudioManager="*res://scripts/audio_manager.gd" +AudioManager="*res://scripts/managers/audio_manager.gd" AudioWebBridge="*res://scripts/audio_web_bridge.gd" [debug] diff --git a/run_unit_tests.sh b/run_gdunit4_unit_tests.sh similarity index 100% rename from run_unit_tests.sh rename to run_gdunit4_unit_tests.sh diff --git a/run_pipeline.sh b/run_pipeline.sh index 0f214237b..72f264c0d 100644 --- a/run_pipeline.sh +++ b/run_pipeline.sh @@ -6,7 +6,7 @@ PROJECT_DIR="/project" EXPORT_DIR="$PROJECT_DIR/export/web_thread_off" SERVER_PORT=8080 -PW_TIMEOUT=10000 # Default timeout in ms; adjustable +PW_TIMEOUT=10 # Value is in SECONDS for pytest-timeout compatibility # Function to check if a step failed check_exit() { @@ -27,7 +27,8 @@ check_exit "GDScript Lint" # 2. Markdown Lint echo "Running Markdown Lint..." -markdownlint-cli2 "**/*.md" --config .markdownlint-cli2.yaml --fix +# Now using the version pre-installed in the Docker image +markdownlint-cli2 "**/*.md" "!venv/**" --config .markdownlint-cli2.yaml --fix check_exit "Markdown Lint" # 3. YAML Lint @@ -36,59 +37,77 @@ yamllint -c .yamllint.yaml .github/workflows/*.yml check_exit "YAML Lint" # 4. Godot Unit Tests (GDUnit4 v6) -echo "Downloading GDUnit4 if needed (already in image, but ensure project addons)..." -cp -r /project/addons/gdUnit4 $PROJECT_DIR/addons/ || true # Copy if not present +echo "Ensuring GDUnit4 addons are present..." +if [ ! -d "$PROJECT_DIR/addons/gdUnit4" ]; then + echo "GDUnit4 addon missing at $PROJECT_DIR/addons/gdUnit4" + exit 1 +fi echo "Importing Resources..." godot --headless --path $PROJECT_DIR --import --quit check_exit "Resource Import" echo "Running GDUnit4 Tests..." -godot --headless --path $PROJECT_DIR -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --verbose --ignoreHeadlessMode --add res://test +godot --headless --path $PROJECT_DIR -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --verbose --ignoreHeadlessMode --add res://test/gdunit4 check_exit "GDUnit4 Tests" -# Upload reports (simulate artifact upload by copying to a reports dir) +# 5. GUT Unit Tests +# FIXED: Replaced download/unpack logic with existence check +echo "Checking for GUT installation..." +if [ ! -d "$PROJECT_DIR/addons/gut" ]; then + echo "GUT not found at '$PROJECT_DIR/addons/gut'." + echo "CRITICAL: GUT must be pre-installed in the Docker image or cached volume." + exit 1 +fi + +echo "Running GUT Unit Tests..." +# Let .gutconfig.json govern discovery +godot --headless --verbose --path $PROJECT_DIR \ + -s res://addons/gut/gut_cmdln.gd \ + -gconfig=res://.gutconfig.json \ + -gexit +check_exit "GUT Unit Tests" + mkdir -p $PROJECT_DIR/reports cp -r reports/** $PROJECT_DIR/reports || true -# 5. Browser Functional Tests +# 6. Browser Functional Tests echo "Exporting Godot Project to Web..." mkdir -p $EXPORT_DIR - -# Simulate firebelley/godot-export action: Run Godot export to HTML5 godot --headless --path $PROJECT_DIR --export-release "Web_thread_off" $EXPORT_DIR/index.html check_exit "Godot Web Export" -# Start web server in background python3 -m http.server $SERVER_PORT --directory $EXPORT_DIR & SERVER_PID=$! -# Wait for server to be ready +server_ready=false for i in {1..20}; do if curl -f http://localhost:$SERVER_PORT/index.html >/dev/null 2>&1; then echo "Web server ready" + server_ready=true break fi sleep 1 done -if [ $i -eq 20 ]; then + +if [ "$server_ready" != true ]; then echo "Web server failed to start" kill $SERVER_PID exit 1 fi -# Run Playwright tests echo "Running Playwright Browser Tests..." -pytest tests/ --ignore=tests/refactor -v --junitxml=$PROJECT_DIR/report.xml +pytest tests/ --ignore=tests/refactor -v --timeout=$PW_TIMEOUT --junitxml=$PROJECT_DIR/report.xml check_exit "Playwright Tests" -# Generate test report summary +# 7. Report Summary & Failure Check if [ -f $PROJECT_DIR/report.xml ]; then total=$(xmllint --xpath 'count(//testcase)' $PROJECT_DIR/report.xml) failures=$(xmllint --xpath 'count(//testcase/failure)' $PROJECT_DIR/report.xml) errors=$(xmllint --xpath 'count(//testcase/error)' $PROJECT_DIR/report.xml) skipped=$(xmllint --xpath 'count(//testcase/skipped)' $PROJECT_DIR/report.xml) passed=$((total - failures - errors - skipped)) + echo "Test Report Summary:" echo "- Total tests: $total" echo "- Passed: $passed" @@ -96,16 +115,16 @@ if [ -f $PROJECT_DIR/report.xml ]; then echo "- Errors: $errors" echo "- Skipped: $skipped" else - echo "No report.xml found—tests may not have run." + echo "CRITICAL ERROR: report.xml not found! Playwright tests failed to generate results." + kill $SERVER_PID + exit 1 fi -# Cleanup: Stop server kill $SERVER_PID -# Simulate artifact uploads (copy to host via mounted volume) mkdir -p $PROJECT_DIR/artifacts cp $PROJECT_DIR/report.xml $PROJECT_DIR/artifacts/ || true -cp main_menu.png $PROJECT_DIR/artifacts/ || true # If screenshot exists +cp main_menu.png $PROJECT_DIR/artifacts/ || true cp -r $PROJECT_DIR/reports $PROJECT_DIR/artifacts/gdunit-reports || true -echo "Pipeline completed successfully!" +echo "Pipeline completed successfully!" \ No newline at end of file diff --git a/scenes/main_scene.tscn b/scenes/main_scene.tscn index 888ca363d..d3831c25f 100644 --- a/scenes/main_scene.tscn +++ b/scenes/main_scene.tscn @@ -1,11 +1,11 @@ [gd_scene load_steps=83 format=3 uid="uid://nnnc0qhx07i8"] -[ext_resource type="Script" uid="uid://ctm7qg12s2swt" path="res://scripts/main_scene.gd" id="1_7ykc4"] +[ext_resource type="Script" uid="uid://ctm7qg12s2swt" path="res://scripts/core/main_scene.gd" id="1_7ykc4"] [ext_resource type="PackedScene" uid="uid://cb4n4cqkuddqg" path="res://scenes/pause_menu.tscn" id="1_w2twt"] [ext_resource type="Texture2D" uid="uid://ce63ga8f5mua7" path="res://files/trees/tree_01.png" id="2_fm3ay"] [ext_resource type="FontFile" uid="uid://borwvgqdgawbj" path="res://files/fonts/EMPIREST.TTF" id="2_pu3yx"] [ext_resource type="PackedScene" uid="uid://37rarq1yywmc" path="res://scenes/Player.tscn" id="2_pw63i"] -[ext_resource type="Script" uid="uid://csdce7ynrpivs" path="res://scripts/resource_preloader.gd" id="3_c1pb6"] +[ext_resource type="Script" uid="uid://csdce7ynrpivs" path="res://scripts/managers/resource_preloader.gd" id="3_c1pb6"] [ext_resource type="Texture2D" uid="uid://bsmbcgovcph7p" path="res://files/trees/tree_02.png" id="3_sgkfd"] [ext_resource type="Texture2D" uid="uid://syw5eae1ebna" path="res://files/trees/tree_11.png" id="4_qj6t7"] [ext_resource type="Texture2D" uid="uid://grifp8c6vrgt" path="res://files/trees/tree_12.png" id="5_k78mq"] diff --git a/scripts/globals.gd b/scripts/core/globals.gd similarity index 100% rename from scripts/globals.gd rename to scripts/core/globals.gd diff --git a/scripts/globals.gd.uid b/scripts/core/globals.gd.uid similarity index 100% rename from scripts/globals.gd.uid rename to scripts/core/globals.gd.uid diff --git a/scripts/main_scene.gd b/scripts/core/main_scene.gd similarity index 100% rename from scripts/main_scene.gd rename to scripts/core/main_scene.gd diff --git a/scripts/main_scene.gd.uid b/scripts/core/main_scene.gd.uid similarity index 100% rename from scripts/main_scene.gd.uid rename to scripts/core/main_scene.gd.uid diff --git a/scripts/settings.gd b/scripts/core/settings.gd similarity index 100% rename from scripts/settings.gd rename to scripts/core/settings.gd diff --git a/scripts/settings.gd.uid b/scripts/core/settings.gd.uid similarity index 100% rename from scripts/settings.gd.uid rename to scripts/core/settings.gd.uid diff --git a/scripts/audio_manager.gd b/scripts/managers/audio_manager.gd similarity index 100% rename from scripts/audio_manager.gd rename to scripts/managers/audio_manager.gd diff --git a/scripts/audio_manager.gd.uid b/scripts/managers/audio_manager.gd.uid similarity index 100% rename from scripts/audio_manager.gd.uid rename to scripts/managers/audio_manager.gd.uid diff --git a/scripts/resource_preloader.gd b/scripts/managers/resource_preloader.gd similarity index 100% rename from scripts/resource_preloader.gd rename to scripts/managers/resource_preloader.gd diff --git a/scripts/resource_preloader.gd.uid b/scripts/managers/resource_preloader.gd.uid similarity index 100% rename from scripts/resource_preloader.gd.uid rename to scripts/managers/resource_preloader.gd.uid diff --git a/test/gdunit4/test_audio_manager.gd b/test/gdunit4/test_audio_manager.gd index 3d9f1bd22..e441349e1 100644 --- a/test/gdunit4/test_audio_manager.gd +++ b/test/gdunit4/test_audio_manager.gd @@ -16,7 +16,7 @@ var test_path: String = "user://test_audio.cfg" # Temp for isolation ## Per-test setup: Instantiate manager and init defaults. ## :rtype: void func before_test() -> void: - manager = auto_free(load("res://scripts/audio_manager.gd").new()) + manager = auto_free(load("res://scripts/managers/audio_manager.gd").new()) manager._init_to_defaults() # Manually set defaults (since _ready() not called in isolation) diff --git a/test/gdunit4/test_globals.gd b/test/gdunit4/test_globals.gd index f6f3b2563..47d126f13 100644 --- a/test/gdunit4/test_globals.gd +++ b/test/gdunit4/test_globals.gd @@ -13,7 +13,7 @@ var test_path: String = "user://test_globals.cfg" # Temp for isolation func before_test() -> void: # Instantiate the script - globals = auto_free(load("res://scripts/globals.gd").new()) + globals = auto_free(load("res://scripts/core/globals.gd").new()) # FIX: Manually initialize the settings resource # because _ready() hasn't run yet.