feat: implement Candy Cannon mechanics, CI/CD pipelines, and version 2.3.7 updates

This commit is contained in:
2026-05-25 18:17:00 +08:00
parent 7380161743
commit f2f90f98e2
20 changed files with 952 additions and 36 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
description: This document tells AI agents how to work on Tekton Dash tasks end-to-end. 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. 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. 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 ### Finding Tasks
+53
View File
@@ -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_<feature>.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
+138
View File
@@ -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 }}
+163
View File
@@ -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'
+51 -2
View File
@@ -54,7 +54,32 @@ jobs:
- name: Run Build Patch Script - name: Run Build Patch Script
run: godot --headless -s tools/build_patch.gd 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 - name: Push patch.pck to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main uses: dmnemec/copy_file_to_another_repo_action@main
env: env:
@@ -67,7 +92,19 @@ jobs:
user_name: 'PatchBot' user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new patch.pck' 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 - name: Push version.json to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main uses: dmnemec/copy_file_to_another_repo_action@main
env: env:
@@ -79,3 +116,15 @@ jobs:
user_email: 'action@github.com' user_email: 'action@github.com'
user_name: 'PatchBot' user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new version.json' 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'
+59
View File
@@ -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
+1 -1
View File
@@ -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.tabSize": 4,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"files.eol": "\n", "files.eol": "\n",
+4 -1
View File
@@ -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! - 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. - 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. - 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! - 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 ## [2.3.5] — 2026-05-22
- Refactored `lobby.gd` into modular helper classes (`LobbyChat`, `LobbyMainMenu`, `LobbyRoomList`, `LobbyRoom`) to reduce file size and improve maintainability. - Refactored `lobby.gd` into modular helper classes (`LobbyChat`, `LobbyMainMenu`, `LobbyRoomList`, `LobbyRoom`) to reduce file size and improve maintainability.
+30 -16
View File
@@ -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. 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. 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 ### 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 query: "[Gauntlet]" or task name
filter: {"property": "object", "value": "page"} filter: {"property": "object", "value": "page"}
``` ```
### Reading a Task ### 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: "<task_page_id_from_search>"
```
Each task page has these properties: Each task page has these properties:
| Property | Type | Purpose | | Property | Type | Purpose |
@@ -44,15 +52,18 @@ Each task page has these properties:
To Do → In Progress → Done To Do → In Progress → Done
``` ```
1. **Pick up task**: Set `Status``In Progress` **CRITICAL WORKFLOW:**
2. **Do the work**: Read `Description`, implement the changes
3. **Write unit tests**: Follow pattern in `tests/` directory 1. **Read task from Notion**: Use `mcp_notion_API_retrieve_a_page(page_id="...")` to get full task details
4. **Mark complete**: Set `Status``Done`, check `Acceptance` 2. **Read Description field carefully**: This contains the actual implementation requirements — file names, function signatures, integration points, RPC patterns, etc.
5. **Update changelog**: Add entry to `CHANGELOG_DRAFT.md` (consumer language) 3. **Implement exactly what Description specifies**: Don't invent your own approach — follow the Description's technical requirements
6. **Bump version**: Update `project.godot` + `export_presets.cfg` 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: "<task_page_id>" page_id: "<task_page_id>"
properties: {"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}} 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 ## 5. Key Conventions
- **Caveman Mode**: Be terse. No filler. Execute first, talk second. - **Caveman Mode**: Be terse. No filler. Execute first, talk second.
- **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files. - **Read task from Notion FIRST**: Always use `mcp_notion_API_retrieve_a_page` to get the Description field before implementing
- **Notion status flow**: `To Do``In Progress``Done` (never skip steps). - **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
- **Test everything**: Every completed task gets a `test_<feature>.gd` in `tests/`. - **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files
- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`. - **Notion status flow**: `To Do``In Progress``Done` (never skip steps)
- **Test everything**: Every completed task gets a `test_<feature>.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)
@@ -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://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="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="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"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uxput"]
resource_name = "boost" resource_name = "boost"
@@ -198,3 +199,10 @@ item/16/mesh_cast_shadow = 1
item/16/shapes = [] item/16/shapes = []
item/16/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/16/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
item/16/navigation_layers = 1 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
@@ -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)
+157
View File
@@ -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
+9 -9
View File
@@ -8,7 +8,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.3.6.exe" export_path="build/tekton_armageddon_v2.3.7.exe"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -42,8 +42,8 @@ application/modify_resources=false
application/icon="" application/icon=""
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/icon_interpolation=4 application/icon_interpolation=4
application/file_version="2.3.6" application/file_version="2.3.7"
application/product_version="2.3.6" application/product_version="2.3.7"
application/company_name="DanchieGo" application/company_name="DanchieGo"
application/product_name="Tekton Armageddon" application/product_name="Tekton Armageddon"
application/file_description="" application/file_description=""
@@ -80,7 +80,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_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() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -111,7 +111,7 @@ architectures/arm64-v8a=true
architectures/x86=false architectures/x86=false
architectures/x86_64=false architectures/x86_64=false
version/code=3 version/code=3
version/name="2.3.6" version/name="2.3.7"
package/unique_name="com.danchiego.$genname" package/unique_name="com.danchiego.$genname"
package/name="Tekton Dash Armageddon" package/name="Tekton Dash Armageddon"
package/signed=true package/signed=true
@@ -306,7 +306,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.3.6.zip" export_path="build/tekton_armageddon_v2.3.7.zip"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -565,8 +565,8 @@ codesign/digest_algorithm=1
codesign/identity_type=0 codesign/identity_type=0
application/modify_resources=false application/modify_resources=false
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/file_version="2.3.6" application/file_version="2.3.7"
application/product_version="2.3.6" application/product_version="2.3.7"
application/company_name="DanchieGo" application/company_name="DanchieGo"
application/product_name="Tekton Armageddon" application/product_name="Tekton Armageddon"
application/file_description="" application/file_description=""
@@ -582,7 +582,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_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() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
+1 -1
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application] [application]
config/name="Tekton Dash Armageddon" config/name="Tekton Dash Armageddon"
config/version="2.3.6" config/version="2.3.7"
run/main_scene="res://scenes/ui/boot_screen.tscn" run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus") config/features=PackedStringArray("4.6", "Forward Plus")
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
+13
View File
@@ -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")
@@ -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
+31 -4
View File
@@ -86,6 +86,8 @@ var trapped_players: Dictionary = {} # player_id → true
var main_scene: Node = null var main_scene: Node = null
var gridmap: Node = null var gridmap: Node = null
var candy_cannon_scene: PackedScene = preload("res://scenes/candy_cannon.tscn")
var cannon_instance: Node3D = null
# HUD # HUD
var hud_layer: CanvasLayer var hud_layer: CanvasLayer
@@ -250,6 +252,14 @@ func _apply_arena_setup() -> void:
gridmap.update_grid_data() gridmap.update_grid_data()
gridmap.initialize_astar() 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" % [ print("[Gauntlet] Arena setup complete. Center NPC at (%d,%d), size %dx%d" % [
NPC_CENTER.x, NPC_CENTER.y, NPC_SIZE, NPC_SIZE NPC_CENTER.x, NPC_CENTER.y, NPC_SIZE, NPC_SIZE
]) ])
@@ -337,6 +347,13 @@ func _fire_volley() -> void:
if _can_rpc(): if _can_rpc():
rpc("sync_telegraph", targets) 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 # Wait telegraph duration, then apply impact
await get_tree().create_timer(telegraph_time).timeout await get_tree().create_timer(telegraph_time).timeout
@@ -443,18 +460,24 @@ func _check_all_players_trapped() -> void:
var all_players = get_tree().get_nodes_in_group("Players") var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players: for player in all_players:
var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1) 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) _trap_player(player)
func _trap_player(player: Node) -> void: 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 if pid == -1: return
trapped_players[pid] = true trapped_players[pid] = true
print("[Gauntlet] Player %d TRAPPED at %s" % [pid, str(player.current_position)]) print("[Gauntlet] Player %d TRAPPED at %s" % [pid, str(player.current_position)])
emit_signal("player_trapped", pid) emit_signal("player_trapped", pid)
# TODO: Apply movement lockout, score penalty, visual feedback # Apply visual feedback and notify
# For now, just mark as trapped — will be expanded in Task #4 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: func clear_sticky_cell(pos: Vector2i) -> void:
"""Used by Cleanser power-up to remove a sticky cell.""" """Used by Cleanser power-up to remove a sticky cell."""
@@ -462,6 +485,10 @@ func clear_sticky_cell(pos: Vector2i) -> void:
if gridmap: if gridmap:
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1) 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 # HUD
# ============================================================================= # =============================================================================
@@ -131,6 +131,18 @@ func simple_move_to(grid_position: Vector2i) -> bool:
if not try_push(grid_position, push_dir): if not try_push(grid_position, push_dir):
return false 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) # Check for Tekton interaction (Knock Mode)
# If moving into a Tekton's space while in Knock Mode, trigger knock # If moving into a Tekton's space while in Knock Mode, trigger knock
if player.get("is_knock_mode"): if player.get("is_knock_mode"):
@@ -142,6 +154,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
player.knock_tekton() player.knock_tekton()
return false # Don't move into the tile, just knock 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) 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 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) # 2. Apply freeze/stun effect (blue tint)
if _can_rpc(): if _can_rpc():
other_player.rpc("apply_stagger", 1.5) other_player.rpc("apply_stagger", 1.5)
+108
View File
@@ -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()
+41
View File
@@ -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 ===")