diff --git a/.agents/workflows/do-task.md b/.agents/workflows/do-task.md index 1c9d703..53db83c 100644 --- a/.agents/workflows/do-task.md +++ b/.agents/workflows/do-task.md @@ -2,7 +2,7 @@ description: This document tells AI agents how to work on Tekton Dash tasks end-to-end. --- -# SKILLS.md — AI Agent Workflow Guide for Tekton Dash +# AI Agent Workflow Guide for Tekton Dash This document tells AI agents how to work on Tekton Dash tasks end-to-end. @@ -12,7 +12,7 @@ This document tells AI agents how to work on Tekton Dash tasks end-to-end. All tasks live on the **"TektonDash - Armageddon PR Tasks"** Notion board. -https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36633be43b29803891cd000c6f6e5c5f +https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36433be43b2980de8635000c0a910a0d ### Finding Tasks diff --git a/.agents/workflows/pending-tasks.md b/.agents/workflows/pending-tasks.md new file mode 100644 index 0000000..9302514 --- /dev/null +++ b/.agents/workflows/pending-tasks.md @@ -0,0 +1,53 @@ +# Pending Notion Tasks for core_coder + +## Instructions +Run these commands as `core_coder` profile to complete the 5 "In Progress" tasks: + +```bash +# Switch to core_coder profile +hermes profile use core_coder + +# Navigate to project +cd /home/beng/Godot/Projects/tekton-enet + +# Start interactive session with task context +hermes chat -z "Complete the 5 'In Progress' tasks from Notion. Start with P0 task [Gauntlet] #4 Cannon Timer & Basic Volley (ID: 36833be4-3b29-812b-b25e-d087446c57f6). Follow TASKS.md procedure for each task." +``` + +## Task List (Priority Order) + +### 1. [P0] [Gauntlet] #4 Cannon Timer & Basic Volley +- **Notion ID:** 36833be4-3b29-812b-b25e-d087446c57f6 +- **Type:** CORE +- **Description:** Create candy_cannon_controller.gd. Implement 5-second interval cannon timer in GauntletManager._process(). Fire 5 shots per volley at random valid cells (1×1 only initially). Create candy_cannon.tscn (3×3 static NPC scene, is_static_turret=true pattern). Reuse tekton.gd spawn_projectile_rpc() arc-tween pattern for projectile visuals. Sync via rpc(). + +### 2. [P1] [036] Mode Config Completeness +- **Notion ID:** 36433be4-3b29-8129-bbc9-c3f8437559a8 +- **Type:** CORE + +### 3. [P1] [024] Set up GitHub Actions CI/CD Workflow +- **Notion ID:** 36633be4-3b29-8179-87bf-ec462fb6629f +- **Type:** DEVOPS + +### 4. [P1] [032] Set up Artifact Storage & Versioning +- **Notion ID:** 36633be4-3b29-812b-9537-f44fbf89c3f9 +- **Type:** DEVOPS + +### 5. [P1] [031] Configure Platform-Specific Export Presets +- **Notion ID:** 36633be4-3b29-8147-8180-eebc89a08dc4 +- **Type:** DEVOPS + +## Workflow Per Task + +1. Read task details: `mcp_notion_API_retrieve_a_page(page_id="...")` +2. Implement changes following TASKS.md +3. Write unit test in `tests/test_.gd` +4. Run tests: `./run_tests.cmd` (Windows) or equivalent +5. Update CHANGELOG_DRAFT.md (append to existing version block if changes exist) +6. Mark Done: `mcp_notion_API_patch_page(page_id="...", properties={"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}})` +7. Commit changes + +## Notes +- Project path: /home/beng/Godot/Projects/tekton-enet +- Follow GUT_SETUP_SKILLS.md for testing +- Version 2.3.7 already has uncommitted changes — append changelog entries, don't bump version diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e8256b6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,138 @@ +name: Build and Export + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to build (e.g., 2.3.8)' + required: true + type: string + +jobs: + build-windows: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Setup Export Templates + run: | + mkdir -p ~/.local/share/godot/export_templates/4.3.0.stable + wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz + unzip Godot_v4.3-stable_export_templates.tpz -d ~/.local/share/godot/export_templates/4.3.0.stable + + - name: Export Windows Build + run: | + mkdir -p build + godot --headless --export-release "Windows Desktop" build/tekton_armageddon_windows.exe + + - name: Upload Windows Artifact + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: build/tekton_armageddon_windows.exe + retention-days: 30 + + build-android: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Setup Export Templates + run: | + mkdir -p ~/.local/share/godot/export_templates/4.3.0.stable + wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz + unzip Godot_v4.3-stable_export_templates.tpz -d ~/.local/share/godot/export_templates/4.3.0.stable + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 + + - name: Export Android Build + run: | + mkdir -p build + godot --headless --export-release "Android" build/tekton_armageddon_android.apk + + - name: Upload Android Artifact + uses: actions/upload-artifact@v4 + with: + name: android-build + path: build/tekton_armageddon_android.apk + retention-days: 30 + + build-macos: + runs-on: macos-latest + permissions: + contents: write + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Setup Export Templates + run: | + mkdir -p ~/Library/Application\ Support/Godot/export_templates/4.3.0.stable + wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz + unzip Godot_v4.3-stable_export_templates.tpz -d ~/Library/Application\ Support/Godot/export_templates/4.3.0.stable + + - name: Export macOS Build + run: | + mkdir -p build + godot --headless --export-release "macOS" build/tekton_armageddon_macos.zip + + - name: Upload macOS Artifact + uses: actions/upload-artifact@v4 + with: + name: macos-build + path: build/tekton_armageddon_macos.zip + retention-days: 30 + + create-release: + needs: [build-windows, build-android, build-macos] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + + steps: + - name: Download All Artifacts + uses: actions/download-artifact@v4 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + files: | + windows-build/tekton_armageddon_windows.exe + android-build/tekton_armageddon_android.apk + macos-build/tekton_armageddon_macos.zip + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build_artifacts.yml b/.github/workflows/build_artifacts.yml new file mode 100644 index 0000000..b77db7b --- /dev/null +++ b/.github/workflows/build_artifacts.yml @@ -0,0 +1,163 @@ +name: Build Platform Artifacts + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to build (e.g., 2.3.8)' + required: true + type: string + +jobs: + build-artifacts: + runs-on: ubuntu-latest + permissions: + contents: write + + strategy: + matrix: + platform: + - name: Windows + preset: "Windows Desktop" + extension: exe + - name: Linux + preset: "Linux/X11" + extension: x86_64 + - name: Android + preset: "Android" + extension: apk + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Setup Android SDK (Android only) + if: matrix.platform.name == 'Android' + uses: android-actions/setup-android@v3 + + - name: Extract Version + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + else + VERSION="${GITHUB_REF#refs/tags/v}" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building version: $VERSION" + + - name: Create Build Directory + run: mkdir -p build + + - name: Export Game + run: | + godot --headless --export-release "${{ matrix.platform.preset }}" \ + "build/tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.${{ matrix.platform.extension }}" + + - name: Generate Checksums + run: | + cd build + sha256sum tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.${{ matrix.platform.extension }} \ + > tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.sha256 + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: tekton-${{ matrix.platform.name }}-v${{ steps.version.outputs.version }} + path: | + build/tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.${{ matrix.platform.extension }} + build/tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.sha256 + retention-days: 90 + compression-level: 0 + + - name: Create Release Asset + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: | + build/tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.${{ matrix.platform.extension }} + build/tekton_armageddon_${{ matrix.platform.name }}_v${{ steps.version.outputs.version }}.sha256 + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-patch: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Extract Version + id: version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + else + VERSION="${GITHUB_REF#refs/tags/v}" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build Patch PCK + run: godot --headless -s tools/build_patch.gd + + - name: Generate Patch Checksum + run: | + sha256sum patch.pck > patch.pck.sha256 + + - name: Upload Patch Artifact + uses: actions/upload-artifact@v4 + with: + name: tekton-patch-v${{ steps.version.outputs.version }} + path: | + patch.pck + patch.pck.sha256 + retention-days: 90 + + - name: Push to Updates Repository + if: startsWith(github.ref, 'refs/tags/') + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }} + with: + source_file: 'patch.pck' + destination_repo: '${{ github.actor }}/tekton-updates' + destination_folder: 'v${{ steps.version.outputs.version }}' + user_email: 'action@github.com' + user_name: 'PatchBot' + commit_message: '[AUTO] Release v${{ steps.version.outputs.version }} patch' + + - name: Push Checksum to Updates Repository + if: startsWith(github.ref, 'refs/tags/') + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }} + with: + source_file: 'patch.pck.sha256' + destination_repo: '${{ github.actor }}/tekton-updates' + destination_folder: 'v${{ steps.version.outputs.version }}' + user_email: 'action@github.com' + user_name: 'PatchBot' + commit_message: '[AUTO] Release v${{ steps.version.outputs.version }} checksum' diff --git a/.github/workflows/deploy_patch.yml b/.github/workflows/deploy_patch.yml index 0083ba7..5012bff 100644 --- a/.github/workflows/deploy_patch.yml +++ b/.github/workflows/deploy_patch.yml @@ -54,7 +54,32 @@ jobs: - name: Run Build Patch Script run: godot --headless -s tools/build_patch.gd - # ── 5. Push patch.pck to public repo ───────────────────────────────── + # ── 5. Generate checksums ───────────────────────────────────────────────── + - name: Generate Checksums + run: | + sha256sum patch.pck > patch.pck.sha256 + sha256sum assets/data/version.json > version.json.sha256 + + # ── 6. Upload artifacts to GitHub ───────────────────────────────────────── + - name: Upload Patch Artifacts + uses: actions/upload-artifact@v4 + with: + name: patch-pck-${{ github.sha }} + path: | + patch.pck + patch.pck.sha256 + retention-days: 90 + + - name: Upload Version Manifest + uses: actions/upload-artifact@v4 + with: + name: version-manifest-${{ github.sha }} + path: | + assets/data/version.json + version.json.sha256 + retention-days: 90 + + # ── 7. Push patch.pck to public repo ───────────────────────────────────── - name: Push patch.pck to Public Repository uses: dmnemec/copy_file_to_another_repo_action@main env: @@ -67,7 +92,19 @@ jobs: user_name: 'PatchBot' commit_message: '[AUTO] Pushed new patch.pck' - # ── 6. Push version.json to public repo ────────────────────────────── + - name: Push patch checksum to Public Repository + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }} + with: + source_file: 'patch.pck.sha256' + destination_repo: '${{ github.actor }}/tekton-updates' + destination_folder: 'latest' + user_email: 'action@github.com' + user_name: 'PatchBot' + commit_message: '[AUTO] Pushed patch checksum' + + # ── 8. Push version.json to public repo ────────────────────────────────── - name: Push version.json to Public Repository uses: dmnemec/copy_file_to_another_repo_action@main env: @@ -79,3 +116,15 @@ jobs: user_email: 'action@github.com' user_name: 'PatchBot' commit_message: '[AUTO] Pushed new version.json' + + - name: Push version checksum to Public Repository + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }} + with: + source_file: 'version.json.sha256' + destination_repo: '${{ github.actor }}/tekton-updates' + destination_folder: 'latest' + user_email: 'action@github.com' + user_name: 'PatchBot' + commit_message: '[AUTO] Pushed version checksum' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..026ae44 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,59 @@ +name: Automated Testing + +on: + push: + branches: + - main + - develop + - 'feature/**' + - 'patch-release' + pull_request: + branches: + - main + - develop + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: '4.3.0' + use-dotnet: false + + - name: Verify GUT Installation + run: | + if [ ! -d "addons/gut" ]; then + echo "ERROR: GUT addon not found at addons/gut" + exit 1 + fi + echo "GUT addon found" + + - name: Run Unit Tests + run: | + godot --headless --path . -s res://addons/gut/gut_cmdln.gd \ + -gdir=res://tests/ \ + -gexit \ + -glog=2 + + - name: Check Test Results + if: failure() + run: | + echo "Tests failed. Check logs above for details." + exit 1 + + - name: Upload Test Reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-reports + path: test_reports/ + retention-days: 30 diff --git a/.vscode/settings.json b/.vscode/settings.json index c803b21..924f7cc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "godotTools.editorPath.godot4": "c:\\Users\\beng\\Godot\\Editors\\4.6.2-stable\\Godot_v4.6.2-stable_win64.exe", + "godotTools.editorPath.godot4": "/home/beng/Godot/Editors/4.6.3-stable/Godot_v4.6.3-stable_linux.x86_64", "editor.tabSize": 4, "editor.insertSpaces": false, "files.eol": "\n", diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index 8b71938..5967f47 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -1,8 +1,11 @@ -## [2.3.6] — 2026-05-22 +## [2.3.7] — 2026-05-25 +- Added Candy Cannon timer, firing mechanics, and 5-shot volleys to Gauntlet. - Added new game mode: **Candy Cannon Survival** — dodge candy volleys from a giant cannon in the center of the arena! - New 20×20 arena with a central Candy Cannon obstacle and three escalating phases: Open Arena, Route Pressure, and Survival. - Candy Cannon Survival is now selectable from the lobby game mode list with its own dedicated arena. - The arena now spawns collectible tiles (Hearts, Diamonds, Stars, Coins) with pattern-matching missions — complete goals while dodging candy! +- Integrated automated testing into CI pipeline — all commits now run unit and integration tests via GitHub Actions. +- Set up complete GitHub Actions CI/CD pipeline with build, test, and deployment workflows for Windows, Android, and macOS platforms. ## [2.3.5] — 2026-05-22 - Refactored `lobby.gd` into modular helper classes (`LobbyChat`, `LobbyMainMenu`, `LobbyRoomList`, `LobbyRoom`) to reduce file size and improve maintainability. diff --git a/SKILLS.md b/TASKS.md similarity index 69% rename from SKILLS.md rename to TASKS.md index 16a6212..1601012 100644 --- a/SKILLS.md +++ b/TASKS.md @@ -1,4 +1,4 @@ -# SKILLS.md — AI Agent Workflow Guide for Tekton Dash +# AI Agent Workflow Guide for Tekton Dash This document tells AI agents how to work on Tekton Dash tasks end-to-end. @@ -8,21 +8,29 @@ This document tells AI agents how to work on Tekton Dash tasks end-to-end. All tasks live on the **"TektonDash - Armageddon PR Tasks"** Notion board. -https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36633be43b29803891cd000c6f6e5c5f +https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36433be43b2980de8635000c0a910a0d ### Finding Tasks -Should always start with this to find tasks, find the highest priority task that is not done, second is In Progress, then To Do. +**CRITICAL:** Always start by finding tasks from Notion. Priority order: P0 > P1 > P2 > P3. Status order: In Progress > To Do. -example, query for "Gauntlet" +Example search for "Gauntlet" tasks: ``` -Use: mcp_notion-mcp-server_API-post-search +Use: mcp_notion_API_post_search query: "[Gauntlet]" or task name filter: {"property": "object", "value": "page"} ``` ### Reading a Task +**CRITICAL:** The **Description** field contains the actual implementation requirements. TASKS.md defines workflow procedure only — each task's unique problem and solution are in Notion's Description field. + +Always read full task details: +``` +Use: mcp_notion_API_retrieve_a_page + page_id: "" +``` + Each task page has these properties: | Property | Type | Purpose | @@ -44,15 +52,18 @@ Each task page has these properties: To Do → In Progress → Done ``` -1. **Pick up task**: Set `Status` → `In Progress` -2. **Do the work**: Read `Description`, implement the changes -3. **Write unit tests**: Follow pattern in `tests/` directory -4. **Mark complete**: Set `Status` → `Done`, check `Acceptance` ✅ -5. **Update changelog**: Add entry to `CHANGELOG_DRAFT.md` (consumer language) -6. **Bump version**: Update `project.godot` + `export_presets.cfg` +**CRITICAL WORKFLOW:** + +1. **Read task from Notion**: Use `mcp_notion_API_retrieve_a_page(page_id="...")` to get full task details +2. **Read Description field carefully**: This contains the actual implementation requirements — file names, function signatures, integration points, RPC patterns, etc. +3. **Implement exactly what Description specifies**: Don't invent your own approach — follow the Description's technical requirements +4. **Write unit tests**: Follow pattern in `tests/` directory (see GUT_SETUP_SKILLS.md) +5. **Update changelog**: Add entry to `CHANGELOG_DRAFT.md` (consumer-facing language, not technical jargon) +6. **Version management**: Check git diff for existing version changes (see Version Bumping section below) +7. **Mark complete in Notion**: Set `Status` → `Done`, check `Acceptance` ✅ ``` -Use: mcp_notion-mcp-server_API-patch-page +Use: mcp_notion_API_patch_page page_id: "" properties: {"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}} ``` @@ -154,7 +165,10 @@ Entries are **consumer-facing** (readable by players). No internal jargon. ## 5. Key Conventions - **Caveman Mode**: Be terse. No filler. Execute first, talk second. -- **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files. -- **Notion status flow**: `To Do` → `In Progress` → `Done` (never skip steps). -- **Test everything**: Every completed task gets a `test_.gd` in `tests/`. -- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`. +- **Read task from Notion FIRST**: Always use `mcp_notion_API_retrieve_a_page` to get the Description field before implementing +- **Description field is the spec**: TASKS.md is workflow procedure only. Each task's unique requirements (file names, function signatures, RPC patterns, integration points) are in Notion's Description field +- **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files +- **Notion status flow**: `To Do` → `In Progress` → `Done` (never skip steps) +- **Test everything**: Every completed task gets a `test_.gd` in `tests/` +- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/` +- **Version discipline**: Check `git diff -- project.godot CHANGELOG_DRAFT.md` before bumping version (see Version Bumping section) diff --git a/addons/enhanced_gridmap/meshlibrary/default.tres b/addons/enhanced_gridmap/meshlibrary/default.tres index 1f089fa..b6bccc8 100644 --- a/addons/enhanced_gridmap/meshlibrary/default.tres +++ b/addons/enhanced_gridmap/meshlibrary/default.tres @@ -16,6 +16,7 @@ [ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"] [ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"] [ext_resource type="Texture2D" uid="uid://cdnxwlysxnujd" path="res://assets/textures/tile_heart.png" id="12_heart_tex"] +[ext_resource type="BoxMesh" uid="uid://tile_sticky_uid" path="res://addons/enhanced_gridmap/meshlibrary/tile_sticky.tres" id="13_sticky"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uxput"] resource_name = "boost" @@ -198,3 +199,10 @@ item/16/mesh_cast_shadow = 1 item/16/shapes = [] item/16/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/16/navigation_layers = 1 +item/17/name = "tile_sticky" +item/17/mesh = ExtResource("13_sticky") +item/17/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +item/17/mesh_cast_shadow = 1 +item/17/shapes = [] +item/17/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +item/17/navigation_layers = 1 diff --git a/addons/enhanced_gridmap/meshlibrary/tile_sticky.tres b/addons/enhanced_gridmap/meshlibrary/tile_sticky.tres new file mode 100644 index 0000000..ff1e086 --- /dev/null +++ b/addons/enhanced_gridmap/meshlibrary/tile_sticky.tres @@ -0,0 +1,9 @@ +[gd_resource type="BoxMesh" format=3 uid="uid://tile_sticky_uid"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_sticky"] +transparency = 1 +albedo_color = Color(1, 0.4, 0.7, 0.65) + +[resource] +material = SubResource("StandardMaterial3D_sticky") +size = Vector3(1, 0.1, 1) diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md new file mode 100644 index 0000000..f8dab1c --- /dev/null +++ b/docs/VERSIONING.md @@ -0,0 +1,157 @@ +# Versioning & Artifact Storage Strategy + +## Version Format + +Tekton Armageddon uses semantic versioning: `MAJOR.MINOR.PATCH` + +- **MAJOR**: Breaking changes, incompatible API changes +- **MINOR**: New features, backward-compatible +- **PATCH**: Bug fixes, content updates, backward-compatible + +Current version source: `project.godot` → `config/version` + +## Artifact Types + +### 1. Patch PCK Files +- **Purpose**: Hot-patch content updates without full client reinstall +- **Storage**: GitHub Actions artifacts (90 days) + tekton-updates repo +- **Naming**: `patch.pck` (latest), versioned in tekton-updates repo +- **Integrity**: SHA256 checksums generated and stored alongside + +### 2. Platform Builds +- **Platforms**: Windows, Linux, Android +- **Storage**: GitHub Actions artifacts (90 days) + GitHub Releases (permanent) +- **Naming**: `tekton_armageddon_{Platform}_v{VERSION}.{ext}` +- **Trigger**: Git tags (`v*.*.*`) or manual workflow dispatch +- **Integrity**: SHA256 checksums for each platform build + +### 3. Version Manifest +- **File**: `assets/data/version.json` +- **Purpose**: Client version checking, changelog delivery, patch URLs +- **Storage**: Embedded in builds + tekton-updates repo +- **Integrity**: SHA256 checksum + +## Workflows + +### Patch Deployment (`deploy_patch.yml`) +**Trigger**: Push to `patch-release` branch or manual dispatch + +**Process**: +1. Auto-bump patch version from `project.godot` +2. Extract changelog from `CHANGELOG_DRAFT.md` [NEXT] section +3. Generate `version.json` with new release entry +4. Commit version bump back to repo +5. Build `patch.pck` from changed files +6. Generate SHA256 checksums +7. Upload artifacts to GitHub Actions (90-day retention) +8. Push to `tekton-updates` public repo (`latest/` folder) + +**Artifacts**: +- `patch-pck-{SHA}`: patch.pck + checksum +- `version-manifest-{SHA}`: version.json + checksum + +### Platform Builds (`build_artifacts.yml`) +**Trigger**: Git tag push (`v*.*.*`) or manual dispatch with version input + +**Process**: +1. Matrix build for Windows, Linux, Android +2. Export using Godot export presets +3. Generate SHA256 checksums per platform +4. Upload artifacts to GitHub Actions (90-day retention) +5. Create GitHub Release with all platform builds + checksums + +**Artifacts**: +- `tekton-{Platform}-v{VERSION}`: platform binary + checksum + +## Artifact Retention + +| Artifact Type | Storage Location | Retention | Purpose | +|--------------|------------------|-----------|---------| +| Patch PCK | GitHub Actions | 90 days | CI/CD history, rollback | +| Patch PCK | tekton-updates repo | Permanent | Client downloads | +| Platform Builds | GitHub Actions | 90 days | CI/CD history | +| Platform Builds | GitHub Releases | Permanent | Distribution | +| Version Manifest | tekton-updates repo | Permanent | Client version checks | + +## Checksum Verification + +All artifacts include SHA256 checksums for integrity verification: + +```bash +# Verify patch.pck +sha256sum -c patch.pck.sha256 + +# Verify platform build +sha256sum -c tekton_armageddon_Windows_v2.3.8.sha256 +``` + +## Version Compatibility + +`version.json` includes `minimum_app_version` field: +- Clients below this version must reinstall full build +- Clients at or above can use patch system + +## Changelog Management + +**Source**: `CHANGELOG_DRAFT.md` + +**Format**: +```markdown +## [NEXT] +- Feature or fix description +- Another change + +## [2.3.7] — 2026-05-15 +- Archived release notes +``` + +**Process**: +1. Developers add entries under `[NEXT]` +2. CI extracts `[NEXT]` entries on patch deployment +3. CI archives to versioned section +4. CI clears `[NEXT]` for next cycle + +## Manual Version Bump + +For local testing without CI: + +```bash +# Update versions but keep changelog +python3 tools/generate_version_json.py --local + +# Skip changelog update entirely +python3 tools/generate_version_json.py --skip-changelog +``` + +## Release Process + +### Patch Release +1. Merge changes to `patch-release` branch +2. CI auto-bumps version, builds, deploys +3. Clients auto-download on next launch + +### Full Release +1. Tag commit: `git tag v2.4.0` +2. Push tag: `git push origin v2.4.0` +3. CI builds all platforms, creates GitHub Release +4. Distribute via Steam, Google Play, etc. + +## Rollback Strategy + +**Patch Rollback**: +1. Locate previous patch in GitHub Actions artifacts or tekton-updates repo +2. Manually push to `tekton-updates/latest/` +3. Update `version.json` to point to previous version + +**Full Build Rollback**: +1. Download previous release from GitHub Releases +2. Re-tag or create hotfix branch +3. Redeploy via standard release process + +## Future Enhancements + +- [ ] Automated rollback on failed health checks +- [ ] Delta patching for bandwidth optimization +- [ ] Multi-region CDN distribution +- [ ] Staged rollout (canary deployments) +- [ ] Automated compatibility testing matrix diff --git a/export_presets.cfg b/export_presets.cfg index 5cfc6cd..7d16315 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -8,7 +8,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.6.exe" +export_path="build/tekton_armageddon_v2.3.7.exe" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -42,8 +42,8 @@ application/modify_resources=false application/icon="" application/console_wrapper_icon="" application/icon_interpolation=4 -application/file_version="2.3.6" -application/product_version="2.3.6" +application/file_version="2.3.7" +application/product_version="2.3.7" application/company_name="DanchieGo" application/product_name="Tekton Armageddon" application/file_description="" @@ -80,7 +80,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton-dash-armageddon-v.2.3.6.apk" +export_path="build/tekton-dash-armageddon-v.2.3.7.apk" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -111,7 +111,7 @@ architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false version/code=3 -version/name="2.3.6" +version/name="2.3.7" package/unique_name="com.danchiego.$genname" package/name="Tekton Dash Armageddon" package/signed=true @@ -306,7 +306,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.6.zip" +export_path="build/tekton_armageddon_v2.3.7.zip" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -565,8 +565,8 @@ codesign/digest_algorithm=1 codesign/identity_type=0 application/modify_resources=false application/console_wrapper_icon="" -application/file_version="2.3.6" -application/product_version="2.3.6" +application/file_version="2.3.7" +application/product_version="2.3.7" application/company_name="DanchieGo" application/product_name="Tekton Armageddon" application/file_description="" @@ -582,7 +582,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.6.x86_64" +export_path="build/tekton_armageddon_v2.3.7.x86_64" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 diff --git a/project.godot b/project.godot index 2b63bf9..8513d06 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Tekton Dash Armageddon" -config/version="2.3.6" +config/version="2.3.7" run/main_scene="res://scenes/ui/boot_screen.tscn" config/features=PackedStringArray("4.6", "Forward Plus") boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) diff --git a/scenes/candy_cannon.tscn b/scenes/candy_cannon.tscn new file mode 100644 index 0000000..7d82bd3 --- /dev/null +++ b/scenes/candy_cannon.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=3 format=3 uid="uid://ddy2r7xto80gq"] + +[ext_resource type="Script" path="res://scripts/controllers/candy_cannon_controller.gd" id="1_canon"] + +[sub_resource type="BoxMesh" id="BoxMesh_canon"] +size = Vector3(1.5, 3, 1.5) + +[node name="CandyCannon" type="Node3D"] +script = ExtResource("1_canon") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0) +mesh = SubResource("BoxMesh_canon") diff --git a/scripts/controllers/candy_cannon_controller.gd b/scripts/controllers/candy_cannon_controller.gd new file mode 100644 index 0000000..9728d43 --- /dev/null +++ b/scripts/controllers/candy_cannon_controller.gd @@ -0,0 +1,47 @@ +extends Node3D +class_name CandyCannonController + +@export var is_static_turret: bool = true + +func _ready() -> void: + pass + +@rpc("authority", "call_local", "reliable") +func play_animation_rpc(anim_name: String) -> void: + # Stub for future model animations + pass + +@rpc("authority", "call_local", "reliable") +func spawn_projectile_rpc(target_world_pos: Vector3, duration: float) -> void: + var projectile = MeshInstance3D.new() + var sphere = BoxMesh.new() + sphere.size = Vector3(0.4, 0.4, 0.4) + projectile.mesh = sphere + var mat = StandardMaterial3D.new() + mat.albedo_color = Color(1.0, 0.4, 0.8) # Candy pink for Gauntlet + projectile.material_override = mat + + get_tree().get_root().add_child(projectile) + + # Start projectile slightly above the cannon center + projectile.global_position = global_position + Vector3(0, 2.0, 0) + + var tween = create_tween() + if not tween: + projectile.queue_free() + return + + tween.set_parallel(true) + tween.tween_property(projectile, "global_position:x", target_world_pos.x, duration).set_trans(Tween.TRANS_LINEAR) + tween.tween_property(projectile, "global_position:z", target_world_pos.z, duration).set_trans(Tween.TRANS_LINEAR) + + var mid_y = max(global_position.y, target_world_pos.y) + 4.0 + var tween_y = create_tween() + tween_y.tween_property(projectile, "global_position:y", mid_y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + tween_y.tween_property(projectile, "global_position:y", target_world_pos.y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(duration / 2.0) + + tween.chain().tween_callback(projectile.queue_free) + +func can_rpc() -> bool: + if not multiplayer.has_multiplayer_peer(): return false + return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED diff --git a/scripts/managers/gauntlet_manager.gd b/scripts/managers/gauntlet_manager.gd index 9b77fa3..08e0607 100644 --- a/scripts/managers/gauntlet_manager.gd +++ b/scripts/managers/gauntlet_manager.gd @@ -86,6 +86,8 @@ var trapped_players: Dictionary = {} # player_id → true var main_scene: Node = null var gridmap: Node = null +var candy_cannon_scene: PackedScene = preload("res://scenes/candy_cannon.tscn") +var cannon_instance: Node3D = null # HUD var hud_layer: CanvasLayer @@ -250,6 +252,14 @@ func _apply_arena_setup() -> void: gridmap.update_grid_data() gridmap.initialize_astar() + if not cannon_instance and main_scene: + cannon_instance = candy_cannon_scene.instantiate() + cannon_instance.name = "CandyCannon" + var cx = NPC_CENTER.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0 + var cz = NPC_CENTER.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0 + cannon_instance.position = Vector3(cx, 0, cz) + main_scene.add_child(cannon_instance) + print("[Gauntlet] Arena setup complete. Center NPC at (%d,%d), size %dx%d" % [ NPC_CENTER.x, NPC_CENTER.y, NPC_SIZE, NPC_SIZE ]) @@ -336,6 +346,13 @@ func _fire_volley() -> void: # Telegraph phase — show warning if _can_rpc(): rpc("sync_telegraph", targets) + + # Shoot projectiles visually + if cannon_instance and cannon_instance.has_method("spawn_projectile_rpc") and cannon_instance.can_rpc(): + var cs = gridmap.cell_size + for target in targets: + var target_pos = Vector3(target.x * cs.x + cs.x / 2.0, 0, target.y * cs.z + cs.z / 2.0) + cannon_instance.rpc("spawn_projectile_rpc", target_pos, telegraph_time) # Wait telegraph duration, then apply impact await get_tree().create_timer(telegraph_time).timeout @@ -443,24 +460,34 @@ func _check_all_players_trapped() -> void: var all_players = get_tree().get_nodes_in_group("Players") for player in all_players: var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1) - if is_sticky_cell(pos) and not trapped_players.has(player.get("peer_id", -1)): + if is_sticky_cell(pos) and not trapped_players.has(player.get("peer_id") if "peer_id" in player else -1): _trap_player(player) func _trap_player(player: Node) -> void: - var pid = player.get("peer_id", -1) + var pid = player.get("peer_id") if "peer_id" in player else -1 if pid == -1: return trapped_players[pid] = true print("[Gauntlet] Player %d TRAPPED at %s" % [pid, str(player.current_position)]) emit_signal("player_trapped", pid) - # TODO: Apply movement lockout, score penalty, visual feedback - # For now, just mark as trapped — will be expanded in Task #4 + # Apply visual feedback and notify + if player.has_method("apply_stagger"): + if _can_rpc(): + player.rpc("apply_stagger", 999.0) # Basically infinite until cleansed + else: + player.apply_stagger(999.0) + + NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING) func clear_sticky_cell(pos: Vector2i) -> void: """Used by Cleanser power-up to remove a sticky cell.""" sticky_cells.erase(pos) if gridmap: gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1) + + # Sync removal to clients + if main_scene and _can_rpc(): + main_scene.rpc("sync_grid_item", pos.x, 2, pos.y, -1) # ============================================================================= # HUD diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index e97f03c..287c4b9 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -131,6 +131,18 @@ func simple_move_to(grid_position: Vector2i) -> bool: if not try_push(grid_position, push_dir): return false + var gm = null + var main = player.get_tree().root.get_node_or_null("Main") + if main and main.get("gauntlet_manager"): + gm = main.gauntlet_manager + + # Check if currently trapped + if gm and gm.is_active: + var pid = player.get("peer_id") if "peer_id" in player else -1 + if pid != -1 and gm.trapped_players.has(pid): + print("[Move] Failed: Player is trapped in a sticky cell") + return false + # Check for Tekton interaction (Knock Mode) # If moving into a Tekton's space while in Knock Mode, trigger knock if player.get("is_knock_mode"): @@ -142,6 +154,12 @@ func simple_move_to(grid_position: Vector2i) -> bool: player.knock_tekton() return false # Don't move into the tile, just knock + # If moving into a sticky cell, trigger trap + if gm and gm.is_active and gm.is_sticky_cell(grid_position): + print("[Move] Player stepping into sticky cell at %s" % grid_position) + movement_queue.clear() + if player.is_multiplayer_authority() or multiplayer.is_server(): + gm._trap_player(player) rotate_towards_target(grid_position) @@ -249,6 +267,15 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool: other_player.target_position = pushed_to_pos # Logical update + # Check if landing spot is sticky + var main = player.get_tree().root.get_node_or_null("Main") + if main and main.get("gauntlet_manager"): + var gm = main.gauntlet_manager + if gm.is_active and gm.is_sticky_cell(pushed_to_pos): + print("[Move] Player pushed into sticky cell at %s" % pushed_to_pos) + if multiplayer.is_server() or other_player.is_multiplayer_authority(): + gm._trap_player(other_player) + # 2. Apply freeze/stun effect (blue tint) if _can_rpc(): other_player.rpc("apply_stagger", 1.5) diff --git a/scripts/mode_config.gd b/scripts/mode_config.gd new file mode 100644 index 0000000..46c5eb7 --- /dev/null +++ b/scripts/mode_config.gd @@ -0,0 +1,108 @@ +extends RefCounted +class_name ModeConfig + +# ModeConfig - Schema-driven validation for game mode settings +# Task [036]: Consolidates duplicated/inconsistent option toggles + +# Schema definition for all mode-specific settings +const SCHEMA = { + "Freemode": { + "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, + "randomize_spawn": {"type": TYPE_BOOL, "default": false}, + "enable_cycle_timer": {"type": TYPE_BOOL, "default": false}, + "scarcity_mode": {"type": TYPE_STRING, "default": "Normal", "allowed": ["Normal", "Aggressive", "Chaos"]} + }, + "Stop n Go": { + "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, + "sng_go_duration": {"type": TYPE_INT, "default": 20, "min": 10, "max": 60}, + "sng_stop_duration": {"type": TYPE_INT, "default": 4, "min": 2, "max": 10}, + "sng_required_goals": {"type": TYPE_INT, "default": 8, "min": 3, "max": 20} + }, + "Tekton Doors": { + "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, + "doors_swap_time": {"type": TYPE_INT, "default": 15, "min": 10, "max": 30}, + "doors_refresh_time": {"type": TYPE_INT, "default": 25, "min": 15, "max": 40}, + "doors_required_goals": {"type": TYPE_INT, "default": 8, "min": 5, "max": 12} + }, + "Candy Cannon Survival": { + "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, + "gauntlet_cannon_interval": {"type": TYPE_FLOAT, "default": 5.0, "min": 2.0, "max": 10.0}, + "gauntlet_volley_size": {"type": TYPE_INT, "default": 5, "min": 3, "max": 15} + } +} + +# Get default config for a mode +static func get_defaults(mode: String) -> Dictionary: + if not SCHEMA.has(mode): + push_error("ModeConfig: Unknown mode '%s'" % mode) + return {} + + var defaults = {} + for key in SCHEMA[mode]: + defaults[key] = SCHEMA[mode][key]["default"] + return defaults + +# Validate a single setting +static func validate_setting(mode: String, key: String, value: Variant) -> Dictionary: + if not SCHEMA.has(mode): + return {"valid": false, "error": "Unknown mode: %s" % mode} + + if not SCHEMA[mode].has(key): + return {"valid": false, "error": "Unknown setting '%s' for mode '%s'" % [key, mode]} + + var schema = SCHEMA[mode][key] + + # Type check + if typeof(value) != schema["type"]: + return {"valid": false, "error": "Setting '%s' expects type %s, got %s" % [key, schema["type"], typeof(value)]} + + # Range check for numbers + if schema["type"] == TYPE_INT or schema["type"] == TYPE_FLOAT: + if schema.has("min") and value < schema["min"]: + return {"valid": false, "error": "Setting '%s' must be >= %s" % [key, schema["min"]]} + if schema.has("max") and value > schema["max"]: + return {"valid": false, "error": "Setting '%s' must be <= %s" % [key, schema["max"]]} + + # Allowed values check for strings + if schema["type"] == TYPE_STRING and schema.has("allowed"): + if value not in schema["allowed"]: + return {"valid": false, "error": "Setting '%s' must be one of %s" % [key, schema["allowed"]]} + + return {"valid": true} + +# Validate entire config for a mode +static func validate_config(mode: String, config: Dictionary) -> Dictionary: + if not SCHEMA.has(mode): + return {"valid": false, "error": "Unknown mode: %s" % mode} + + var errors = [] + + for key in config: + var result = validate_setting(mode, key, config[key]) + if not result["valid"]: + errors.append(result["error"]) + + if errors.is_empty(): + return {"valid": true} + else: + return {"valid": false, "errors": errors} + +# Get all settings for a mode +static func get_mode_settings(mode: String) -> Array: + if not SCHEMA.has(mode): + return [] + return SCHEMA[mode].keys() + +# Get schema for a specific setting +static func get_setting_schema(mode: String, key: String) -> Dictionary: + if not SCHEMA.has(mode) or not SCHEMA[mode].has(key): + return {} + return SCHEMA[mode][key] + +# Check if a mode has a specific setting +static func has_setting(mode: String, key: String) -> bool: + return SCHEMA.has(mode) and SCHEMA[mode].has(key) + +# Get all supported modes +static func get_supported_modes() -> Array: + return SCHEMA.keys() diff --git a/tests/test_gauntlet_cannon_timer.gd b/tests/test_gauntlet_cannon_timer.gd new file mode 100644 index 0000000..4862841 --- /dev/null +++ b/tests/test_gauntlet_cannon_timer.gd @@ -0,0 +1,41 @@ +extends GutTest + +const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd") +var gauntlet_manager: Node +var main_mock: Node +var gridmap_mock: Node + +func before_all(): + gut.p("=== Feature Tests [Gauntlet #4 Cannon Timer] ===") + +func before_each(): + main_mock = Node.new() + add_child(main_mock) + gridmap_mock = Node.new() + gridmap_mock.name = "EnhancedGridMap" + main_mock.add_child(gridmap_mock) + + gauntlet_manager = GauntletManager.new() + main_mock.add_child(gauntlet_manager) + gauntlet_manager.initialize(main_mock, gridmap_mock) + +func test_cannon_timer_initialization(): + assert_eq(gauntlet_manager.cannon_timer, 0.0, "Timer should start at 0.0 before phase starts") + + # Manually start phase to setup interval + gauntlet_manager.current_phase = 0 # GauntletManager.Phase.OPEN_ARENA + var config = gauntlet_manager.phase_configs[0] + gauntlet_manager.cannon_interval = config["interval"] + gauntlet_manager.cannon_timer = gauntlet_manager.cannon_interval + + assert_eq(gauntlet_manager.cannon_timer, 5.0, "Timer should initialize to Phase 1 interval (5.0)") + +func test_volley_size_configuration(): + assert_eq(gauntlet_manager.phase_configs[0]["volley"], 5, "Phase 1 volley size should be 5") + +func after_each(): + if main_mock: + main_mock.queue_free() + +func after_all(): + gut.p("=== Feature Tests Complete ===")