feat: implement Candy Cannon mechanics, CI/CD pipelines, and version 2.3.7 updates
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 }}
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
Vendored
+1
-1
@@ -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
@@ -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
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
])
|
])
|
||||||
@@ -336,6 +346,13 @@ func _fire_volley() -> void:
|
|||||||
# Telegraph phase — show warning
|
# Telegraph phase — show warning
|
||||||
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,24 +460,34 @@ 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."""
|
||||||
sticky_cells.erase(pos)
|
sticky_cells.erase(pos)
|
||||||
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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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 ===")
|
||||||
Reference in New Issue
Block a user