Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6971c27d77 | |||
| 7e6e0f7bef | |||
| 869f670605 | |||
| db061a7946 | |||
| 20cd2d08b8 | |||
| 1652630153 | |||
| 34039db92c | |||
| bf9ae51702 | |||
| 7a02eee277 | |||
| d6daed62b8 | |||
| 0548f54168 | |||
| 3fe8de2e32 | |||
| f40dae5a03 | |||
| ab3ffbbec8 | |||
| da5c319a5b | |||
| 5e5d0c8ecf | |||
| b5e22f3ca5 | |||
| 8abf07a0d4 | |||
| 350ae269f2 | |||
| e8604e2c02 | |||
| c9995f8578 | |||
| 4aa765c502 | |||
| 1d653bb7d0 | |||
| 4fe0378d1c | |||
| fb58e62fd9 | |||
| aa45bb0afd | |||
| d05ebdff05 | |||
| e984c1f8b5 | |||
| 99d38134b8 | |||
| 66bc1658a4 | |||
| 3cb8a606b5 | |||
| 19e7f619ab | |||
| 5ba7de3fd6 | |||
| a89e54783f | |||
| 376be28366 | |||
| 0415875128 | |||
| 03028413ca | |||
| 340ba13a48 | |||
| 04f7fdd2dd | |||
| edce1fa1fa | |||
| 6ac9acea35 | |||
| 9ddb1dd199 | |||
| 74de7f15ef | |||
| d40a242cbc | |||
| 419771c50b | |||
| e7a0717aff | |||
| 754fb0d0a3 | |||
| 2fd8130743 | |||
| 5ad9952750 | |||
| 720882f0dc | |||
| 456ca94786 | |||
| 78f8c5bf5e | |||
| 58b66bcb8f | |||
| 477c7ca7ec | |||
| ecb080e943 | |||
| e539a862d5 | |||
| 82763e4d5a | |||
| c30888b74c | |||
| 858bf08212 | |||
| 64630662ac | |||
| 187b530cbf | |||
| 6da55003e2 | |||
| 598065f255 | |||
| c471afaee4 | |||
| 0228fcfd92 | |||
| 8fc3c1b915 | |||
| 74c1e86c32 | |||
| 19f10a4486 | |||
| 8255b1f465 | |||
| 625ff5ec69 | |||
| ccd759da45 | |||
| a708da8836 | |||
| d2156c6d1a | |||
| 5b34f8b96f | |||
| b4ce7453c3 | |||
| 594a0ce84d | |||
| cc584c3251 | |||
| 3ca2ff3c0f |
@@ -0,0 +1,139 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & Release
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
steps:
|
||||
- name: Install tools
|
||||
run: apt-get update -qq && apt-get install -y -qq curl unzip
|
||||
|
||||
- name: Checkout Code
|
||||
run: |
|
||||
git config --global credential.helper store
|
||||
echo "http://adtpdn:${{ secrets.TEKTON_RELEASE_TOKEN }}@52.74.133.55:3000" > ~/.git-credentials
|
||||
git clone http://52.74.133.55:3000/danchie/tekton.git .
|
||||
git checkout $TAG_NAME
|
||||
|
||||
- name: Setup Godot (Cached)
|
||||
run: |
|
||||
apt-get install -y zip
|
||||
if [ ! -f /cache/godot_4.6 ]; then
|
||||
echo "Downloading Godot 4.6..."
|
||||
curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.6-stable/Godot_v4.6-stable_linux.x86_64.zip"
|
||||
unzip -q -o /tmp/godot.zip -d /cache/
|
||||
mv /cache/Godot_v4.6-stable_linux.x86_64 /cache/godot_4.6
|
||||
fi
|
||||
cp /cache/godot_4.6 /usr/local/bin/godot
|
||||
chmod +x /usr/local/bin/godot
|
||||
mkdir -p ~/.local/share/godot/export_templates/4.6.stable
|
||||
if [ ! -f /cache/Godot_v4.6-stable_export_templates.tpz ]; then
|
||||
echo "Downloading templates..."
|
||||
curl -sL -o /cache/Godot_v4.6-stable_export_templates.tpz \
|
||||
"https://github.com/godotengine/godot-builds/releases/download/4.6-stable/Godot_v4.6-stable_export_templates.tpz"
|
||||
fi
|
||||
cd ~/.local/share/godot/export_templates/4.6.stable
|
||||
unzip -q -o /cache/Godot_v4.6-stable_export_templates.tpz
|
||||
mv templates/* .
|
||||
rm -rf templates
|
||||
cd $GITHUB_WORKSPACE
|
||||
mkdir -p build
|
||||
|
||||
- name: Export Windows
|
||||
run: |
|
||||
mkdir -p build/windows
|
||||
cp addons/godotsteam/libgodotsteam* build/windows/ 2>/dev/null || true
|
||||
godot --headless --export-release "Windows Desktop" build/windows/tekton_armageddon_windows.exe || true
|
||||
cd build/windows && zip -r ../tekton_armageddon_windows_${TAG_NAME}.zip .
|
||||
|
||||
- name: Export Linux
|
||||
run: |
|
||||
mkdir -p build/linux
|
||||
godot --headless --export-release "Linux/X11" build/linux/tekton_armageddon_linux.x86_64 || true
|
||||
cd build/linux && zip -r ../tekton_armageddon_linux_${TAG_NAME}.zip .
|
||||
|
||||
- name: Export macOS
|
||||
run: |
|
||||
mkdir -p build/macos
|
||||
godot --headless --export-release "macOS" build/macos/tekton_armageddon_macos.zip 2>&1 | tail -5 || true
|
||||
if [ -f build/macos/tekton_armageddon_macos.zip ]; then
|
||||
mv build/macos/tekton_armageddon_macos.zip build/tekton_armageddon_macos_${TAG_NAME}.zip
|
||||
fi
|
||||
|
||||
- name: Create Gitea Release
|
||||
run: |
|
||||
set -e
|
||||
API="http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases"
|
||||
TAG="$TAG_NAME"
|
||||
echo "Checking existing release for $TAG..."
|
||||
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG" 2>/dev/null || echo "")
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*' || true)
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Creating new release for $TAG..."
|
||||
RELEASE_JSON=$(curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"draft\":true}" \
|
||||
"$API")
|
||||
echo "API response: $RELEASE_JSON"
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "FATAL: Could not create release"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "release_id=$RELEASE_ID"
|
||||
echo "$RELEASE_ID" > /tmp/release_id.txt
|
||||
|
||||
- name: Upload Windows asset
|
||||
run: |
|
||||
RELEASE_ID=$(cat /tmp/release_id.txt)
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@build/tekton_armageddon_windows_${TAG_NAME}.zip" \
|
||||
"http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||
echo "Windows uploaded"
|
||||
|
||||
- name: Upload Linux asset
|
||||
run: |
|
||||
RELEASE_ID=$(cat /tmp/release_id.txt)
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@build/tekton_armageddon_linux_${TAG_NAME}.zip" \
|
||||
"http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||
echo "Linux uploaded"
|
||||
|
||||
- name: Upload macOS asset
|
||||
run: |
|
||||
RELEASE_ID=$(cat /tmp/release_id.txt)
|
||||
if [ -f "build/tekton_armageddon_macos_${TAG_NAME}.zip" ]; then
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@build/tekton_armageddon_macos_${TAG_NAME}.zip" \
|
||||
"http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||
echo "macOS uploaded"
|
||||
else
|
||||
echo "macOS asset not built, skipping"
|
||||
fi
|
||||
|
||||
- name: Publish release
|
||||
run: |
|
||||
RELEASE_ID=$(cat /tmp/release_id.txt)
|
||||
curl -s -X PATCH \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"draft":false}' \
|
||||
"http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID"
|
||||
echo "Published: https://git.klud.top/danchie/tekton/releases/tag/$TAG_NAME"
|
||||
@@ -0,0 +1,149 @@
|
||||
name: Deploy Patch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Patch version (e.g., 2.4.2)'
|
||||
required: true
|
||||
type: string
|
||||
notes:
|
||||
description: 'Release notes'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-patch:
|
||||
name: Build Patch
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
ACTIONS_STEP_DEBUG: true
|
||||
steps:
|
||||
- name: Uji Koneksi Jaringan & SSH
|
||||
run: |
|
||||
echo "Test Ping ke IP Tailscale Gitea..."
|
||||
ping -c 3 100.79.174.108 || echo "PING GAGAL: Runner tidak bisa akses jaringan Tailscale."
|
||||
|
||||
echo "Test Koneksi Port SSH..."
|
||||
nc -zv 100.79.174.108 222 || echo "PORT GAGAL: Port 222 tertutup dari dalam runner."
|
||||
|
||||
- name: Setup SSH config for Gitea
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo -e "Host git.klud.top\n HostName 100.79.174.108\n Port 222\n StrictHostKeyChecking no" > ~/.ssh/config
|
||||
chmod 600 ~/.ssh/config
|
||||
|
||||
git config --global url."ssh://git@100.79.174.108:222/".insteadOf "git@git.klud.top:"
|
||||
git config --global url."ssh://git@100.79.174.108:222/".insteadOf "https://git.klud.top/"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ssh-key: ${{ secrets.TEKTON_SSH_KEY }}
|
||||
ssh-strict: false
|
||||
|
||||
- name: Setup Godot
|
||||
run: |
|
||||
if [ -f /cache/Godot_v4.6-stable_linux.x86_64.zip ]; then
|
||||
cp -a /cache/Godot_v4.6-stable_linux.x86_64.zip godot.zip
|
||||
else
|
||||
wget -q https://github.com/godotengine/godot-builds/releases/download/4.6-stable/Godot_v4.6-stable_linux.x86_64.zip -O godot.zip
|
||||
fi
|
||||
unzip -q godot.zip
|
||||
mv Godot_v4.6-stable_linux.x86_64 /usr/local/bin/godot
|
||||
chmod +x /usr/local/bin/godot
|
||||
|
||||
- name: Install export templates
|
||||
run: |
|
||||
mkdir -v -p ~/.local/share/godot/export_templates/4.6.stable
|
||||
if [ -f /cache/Godot_v4.6-stable_export_templates.tpz ]; then
|
||||
cp -a /cache/Godot_v4.6-stable_export_templates.tpz /tmp/godot-templates.tpz
|
||||
else
|
||||
wget -nv https://github.com/godotengine/godot-builds/releases/download/4.6-stable/Godot_v4.6-stable_export_templates.tpz -O /tmp/godot-templates.tpz
|
||||
fi
|
||||
unzip -o -q /tmp/godot-templates.tpz -d /tmp/godot-templates
|
||||
if [ -d /tmp/godot-templates/templates ]; then
|
||||
cp -a /tmp/godot-templates/templates/* ~/.local/share/godot/export_templates/4.6.stable/
|
||||
else
|
||||
cp -a /tmp/godot-templates/* ~/.local/share/godot/export_templates/4.6.stable/
|
||||
fi
|
||||
|
||||
- name: Run patch build script
|
||||
env:
|
||||
PATCH_VERSION: ${{ github.event.inputs.version }}
|
||||
PATCH_NOTES: ${{ github.event.inputs.notes }}
|
||||
run: |
|
||||
python3 patch_version.py "$PATCH_VERSION" "$PATCH_NOTES"
|
||||
|
||||
- name: Export Windows patch
|
||||
run: |
|
||||
mkdir -p build
|
||||
godot --headless --export-release "Windows Desktop" build/tekton_armageddon_patch_windows.exe
|
||||
|
||||
- name: Export Linux patch
|
||||
run: |
|
||||
mkdir -p build
|
||||
godot --headless --export-release "Linux/X11" build/tekton_armageddon_patch_linux.x86_64
|
||||
|
||||
- name: Upload patch artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tekton-patch-${{ github.event.inputs.version }}
|
||||
path: |
|
||||
build/tekton_armageddon_patch_windows.exe
|
||||
build/tekton_armageddon_patch_linux.x86_64
|
||||
retention-days: 30
|
||||
|
||||
deploy-patch:
|
||||
name: Deploy Patch
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: build-patch
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download patch artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tekton-patch-${{ github.event.inputs.version }}
|
||||
path: artifacts/patch
|
||||
|
||||
- name: Get or create Gitea patch release
|
||||
id: gitea_release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
||||
TAG_NAME: patch-${{ github.event.inputs.version }}
|
||||
RELEASE_NAME: "Patch ${{ github.event.inputs.version }}"
|
||||
RELEASE_NOTES: ${{ github.event.inputs.notes }}
|
||||
run: |
|
||||
API="https://git.klud.top/api/v1/repos/danchie/tekton/releases"
|
||||
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG_NAME")
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
PAYLOAD=$(python3 -c "import json, os; print(json.dumps({'tag_name': os.environ['TAG_NAME'], 'name': os.environ['RELEASE_NAME'], 'body': os.environ['RELEASE_NOTES'], 'prerelease': True}))")
|
||||
RELEASE_JSON=$(curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$API")
|
||||
RELEASE_ID=$(echo "$RELEASE_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
|
||||
fi
|
||||
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Windows patch asset
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TEKTON_RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@artifacts/patch/tekton_armageddon_patch_windows.exe" \
|
||||
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/${{ steps.gitea_release.outputs.release_id }}/assets"
|
||||
|
||||
- name: Upload Linux patch asset
|
||||
run: |
|
||||
curl -X POST \
|
||||
-H "Authorization: token ${{ secrets.TEKTON_RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "attachment=@artifacts/patch/tekton_armageddon_patch_linux.x86_64" \
|
||||
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/${{ steps.gitea_release.outputs.release_id }}/assets"
|
||||
@@ -1,128 +0,0 @@
|
||||
name: Build and Export
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to build (e.g., 2.4.0)'
|
||||
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.6.0'
|
||||
use-dotnet: false
|
||||
|
||||
- name: Setup Export Templates
|
||||
run: |
|
||||
TEMPLATES_DIR=~/.local/share/godot/export_templates/4.6.stable
|
||||
mkdir -p "$TEMPLATES_DIR"
|
||||
wget -q https://github.com/godotengine/godot/releases/download/4.6-stable/Godot_v4.6-stable_export_templates.tpz -O templates.tpz
|
||||
unzip -q templates.tpz -d "$TEMPLATES_DIR"
|
||||
mv "$TEMPLATES_DIR/templates/"* "$TEMPLATES_DIR/"
|
||||
rmdir "$TEMPLATES_DIR/templates"
|
||||
|
||||
- name: Export Windows Build
|
||||
run: |
|
||||
mkdir -p build
|
||||
godot --headless --export-release "Windows Desktop" build/tekton_armageddon_windows.exe
|
||||
|
||||
- name: Zip Windows Build
|
||||
run: cd build && zip tekton_armageddon_windows.zip tekton_armageddon_windows.exe
|
||||
|
||||
- name: Upload Windows Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
path: build/tekton_armageddon_windows.zip
|
||||
retention-days: 30
|
||||
|
||||
build-linux:
|
||||
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.6.0'
|
||||
use-dotnet: false
|
||||
|
||||
- name: Setup Export Templates
|
||||
run: |
|
||||
TEMPLATES_DIR=~/.local/share/godot/export_templates/4.6.stable
|
||||
mkdir -p "$TEMPLATES_DIR"
|
||||
wget -q https://github.com/godotengine/godot/releases/download/4.6-stable/Godot_v4.6-stable_export_templates.tpz -O templates.tpz
|
||||
unzip -q templates.tpz -d "$TEMPLATES_DIR"
|
||||
mv "$TEMPLATES_DIR/templates/"* "$TEMPLATES_DIR/"
|
||||
rmdir "$TEMPLATES_DIR/templates"
|
||||
|
||||
- name: Export Linux Build
|
||||
run: |
|
||||
mkdir -p build
|
||||
godot --headless --export-release "Linux/X11" build/tekton_armageddon_linux.x86_64
|
||||
|
||||
- name: Zip Linux Build
|
||||
run: cd build && zip tekton_armageddon_linux.zip tekton_armageddon_linux.x86_64
|
||||
|
||||
- name: Upload Linux Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-build
|
||||
path: build/tekton_armageddon_linux.zip
|
||||
retention-days: 30
|
||||
|
||||
create-release:
|
||||
needs: [build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Extract Version
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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.zip
|
||||
linux-build/tekton_armageddon_linux.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Mirror to tekton-updates
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PUBLIC_REPO_PAT }}
|
||||
run: |
|
||||
gh release create "v${{ steps.version.outputs.version }}" \
|
||||
--repo "${{ github.actor }}/tekton-updates" \
|
||||
--title "v${{ steps.version.outputs.version }}" \
|
||||
--notes "Mirror of https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}" \
|
||||
"windows-build/tekton_armageddon_windows.zip#Windows" \
|
||||
"linux-build/tekton_armageddon_linux.zip#Linux"
|
||||
@@ -1,177 +0,0 @@
|
||||
name: Build Platform Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to build (e.g., 2.4.0)'
|
||||
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.6.0'
|
||||
use-dotnet: false
|
||||
|
||||
- name: Setup Export Templates
|
||||
run: |
|
||||
TEMPLATES_DIR=~/.local/share/godot/export_templates/4.6.stable
|
||||
mkdir -p "$TEMPLATES_DIR"
|
||||
wget -q https://github.com/godotengine/godot/releases/download/4.6-stable/Godot_v4.6-stable_export_templates.tpz -O templates.tpz
|
||||
unzip -q templates.tpz -d "$TEMPLATES_DIR"
|
||||
mv "$TEMPLATES_DIR/templates/"* "$TEMPLATES_DIR/"
|
||||
rmdir "$TEMPLATES_DIR/templates"
|
||||
|
||||
- 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.6.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: Generate Changed Files List
|
||||
run: |
|
||||
find scripts/ scenes/ assets/ -type f > changed_files.txt
|
||||
echo "Patching $(wc -l < changed_files.txt) files"
|
||||
|
||||
- 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'
|
||||
@@ -1,130 +0,0 @@
|
||||
name: Build and Release Patch PCK
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'patch-release'
|
||||
paths:
|
||||
- 'scripts/**'
|
||||
- 'scenes/**'
|
||||
- 'assets/**'
|
||||
- 'CHANGELOG_DRAFT.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-deploy-patch:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Source Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# ── 1. Auto-generate version.json from CHANGELOG_DRAFT.md ────────────
|
||||
- name: Generate Version JSON & Bump Version
|
||||
run: python3 tools/generate_version_json.py
|
||||
|
||||
# ── 2. Commit bumped files back to the repo ───────────────────────────
|
||||
- name: Commit Version Bump
|
||||
run: |
|
||||
git config user.name "PatchBot"
|
||||
git config user.email "action@github.com"
|
||||
git add assets/data/version.json project.godot CHANGELOG_DRAFT.md
|
||||
git diff --staged --quiet || git commit -m "[AUTO] Version bump & changelog update"
|
||||
git push
|
||||
|
||||
# ── 3. Detect changed files for patch PCK ────────────────────────────
|
||||
- name: Generate Changed Files List
|
||||
run: |
|
||||
git diff --name-only HEAD^ HEAD -- 'scripts/**' 'scenes/**' 'assets/**' > changed_files.txt
|
||||
echo "Files to patch:"
|
||||
cat changed_files.txt
|
||||
|
||||
# ── 4. Build patch.pck ────────────────────────────────────────────────
|
||||
- name: Setup Godot
|
||||
uses: chickensoft-games/setup-godot@v1
|
||||
with:
|
||||
version: '4.6.0'
|
||||
use-dotnet: false
|
||||
|
||||
- name: Run Build Patch Script
|
||||
run: godot --headless -s tools/build_patch.gd
|
||||
|
||||
# ── 5. Generate checksums ─────────────────────────────────────────────────
|
||||
- name: Generate Checksums
|
||||
run: |
|
||||
sha256sum patch.pck > patch.pck.sha256
|
||||
sha256sum assets/data/version.json > version.json.sha256
|
||||
|
||||
# ── 6. Upload artifacts to GitHub ─────────────────────────────────────────
|
||||
- name: Upload Patch Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch-pck-${{ github.sha }}
|
||||
path: |
|
||||
patch.pck
|
||||
patch.pck.sha256
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload Version Manifest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: version-manifest-${{ github.sha }}
|
||||
path: |
|
||||
assets/data/version.json
|
||||
version.json.sha256
|
||||
retention-days: 90
|
||||
|
||||
# ── 7. Push patch.pck to public repo ─────────────────────────────────────
|
||||
- name: Push patch.pck to Public Repository
|
||||
uses: dmnemec/copy_file_to_another_repo_action@main
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
|
||||
with:
|
||||
source_file: 'patch.pck'
|
||||
destination_repo: '${{ github.actor }}/tekton-updates'
|
||||
destination_folder: 'latest'
|
||||
user_email: 'action@github.com'
|
||||
user_name: 'PatchBot'
|
||||
commit_message: '[AUTO] Pushed new patch.pck'
|
||||
|
||||
- name: Push patch checksum to Public Repository
|
||||
uses: dmnemec/copy_file_to_another_repo_action@main
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
|
||||
with:
|
||||
source_file: 'patch.pck.sha256'
|
||||
destination_repo: '${{ github.actor }}/tekton-updates'
|
||||
destination_folder: 'latest'
|
||||
user_email: 'action@github.com'
|
||||
user_name: 'PatchBot'
|
||||
commit_message: '[AUTO] Pushed patch checksum'
|
||||
|
||||
# ── 8. Push version.json to public repo ──────────────────────────────────
|
||||
- name: Push version.json to Public Repository
|
||||
uses: dmnemec/copy_file_to_another_repo_action@main
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
|
||||
with:
|
||||
source_file: 'assets/data/version.json'
|
||||
destination_repo: '${{ github.actor }}/tekton-updates'
|
||||
destination_folder: 'latest'
|
||||
user_email: 'action@github.com'
|
||||
user_name: 'PatchBot'
|
||||
commit_message: '[AUTO] Pushed new version.json'
|
||||
|
||||
- name: Push version checksum to Public Repository
|
||||
uses: dmnemec/copy_file_to_another_repo_action@main
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
|
||||
with:
|
||||
source_file: 'version.json.sha256'
|
||||
destination_repo: '${{ github.actor }}/tekton-updates'
|
||||
destination_folder: 'latest'
|
||||
user_email: 'action@github.com'
|
||||
user_name: 'PatchBot'
|
||||
commit_message: '[AUTO] Pushed version checksum'
|
||||
@@ -1,59 +0,0 @@
|
||||
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.6.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
|
||||
@@ -4,7 +4,10 @@
|
||||
.agent/
|
||||
_daily_basis/
|
||||
_daily_changes/
|
||||
tools/gitea-kanban
|
||||
build/
|
||||
label_mapping.json
|
||||
milestone_mapping.json
|
||||
|
||||
/android/
|
||||
.tmp
|
||||
|
||||
+11
-1
@@ -152,7 +152,17 @@
|
||||
- Fragment Craft system — collect drops to craft exclusive skins
|
||||
- Fixed boot screen stuck on "Checking versions..."
|
||||
|
||||
## [2.4.1] — $(date +"%Y-%m-%d")
|
||||
## [2.4.2] — 2026-07-03
|
||||
- Replaced the **Cleanser** mechanic in Gauntlet mode with a **Ghost powerup** sticky-bypass system.
|
||||
- Ghost (Invisible Mode) now lets players walk through sticky candy tiles in Gauntlet — no more hard block.
|
||||
- Players earn a Ghost powerup every 2 completed missions in Gauntlet (replaces Cleanser charge grants).
|
||||
- Ghost powerup tiles now spawn naturally on the Gauntlet arena (15% chance alongside common tiles).
|
||||
- Removed Cleanser HUD elements (icon, label, charge counter) from the Gauntlet overlay.
|
||||
- Removed `use_cleanser` input action — Ghost uses the existing powerup activation keybind.
|
||||
- Bots now activate Ghost powerup when boxed in by sticky tiles instead of using Cleanser.
|
||||
- Players pushed into sticky tiles while in Ghost mode are no longer slowed.
|
||||
|
||||
## [2.4.1] — 2026-06-28
|
||||
- Fixed Gauntlet map layout to remove red unpassable barrier blocks and center blocks. They are now standard walkable floors but act as hard blockers in physics so players cannot pass them.
|
||||
- Fixed Gauntlet mode to prevent powerups or sticky bubbles from spawning on boundary tiles or under the central cannon.
|
||||
- Center Candy Cannon now shoots actual projectiles that fly towards sticky cells and leave a VFX trail behind them.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Cloning from any tailnet machine
|
||||
|
||||
## Prerequisites
|
||||
- The remote machine must be on your tailnet (registered + can ping `100.85.x.x`)
|
||||
- You must have an account on the Gitea instance (`http://git.klud.top`)
|
||||
- You must have an SSH key on the remote machine (`ssh-keygen` if you don't)
|
||||
|
||||
## Step 1: Add your SSH key to Gitea
|
||||
1. Open `http://git.klud.top/user/settings/keys` in browser
|
||||
2. Paste the content of `~/.ssh/id_ed25519.pub` (or `id_rsa.pub`)
|
||||
3. Click "Add Key"
|
||||
|
||||
## Step 2: Test SSH reachability
|
||||
```bash
|
||||
ssh -T git@thunderobot -p 222
|
||||
# Expected output: "Hello from Gitea!\n\nPlease come over!"
|
||||
# If asked about fingerprint, type "yes" and press Enter
|
||||
```
|
||||
|
||||
## Step 3: Clone a repo
|
||||
```bash
|
||||
# Using tea CLI
|
||||
tea repos clone --git-protocol ssh danchie/tekton
|
||||
|
||||
# Or plain git
|
||||
git clone git@thunderobot:222/danchie/tekton.git
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
- `ssh: connect to host thunderobot port 222: Connection refused`
|
||||
→ Ensure the remote machine has `tailscale ping thunderobot` resolving to `100.93.226.13`
|
||||
→ Ensure the host's port `222` is reachable from the remote (it's bound to `0.0.0.0:222` in Docker)
|
||||
|
||||
- `Permission denied (publickey)`
|
||||
→ Your SSH key wasn't added via Gitea web UI
|
||||
→ Run `ssh-add ~/.ssh/id_ed25519` if you use an agent
|
||||
|
||||
- `Host key verification failed`
|
||||
→ Run `ssh -T git@thunderobot -p 222` interactively once to accept the fingerprint
|
||||
|
||||
## HTTPS is disabled
|
||||
`DISABLE_HTTP_GIT = true` is set on the server. All clones MUST use SSH.
|
||||
If you need to fetch via HTTPS for CI or other non-ssh environments, this must be remapped.
|
||||
@@ -1,141 +1,25 @@
|
||||
# Tekton Dash Armageddon
|
||||
|
||||
> Full developer documentation lives in this repo's **wiki** (sidebar link).
|
||||
>
|
||||
> See in particular: [Skin Creation Workflow](https://git.klud.top/danchie/tekton/wiki/Skin-Creation-Workflow), [Nakama Deployment](https://git.klud.top/danchie/tekton/wiki/Nakama-Deployment), and [Patch Release Workflow](https://git.klud.top/danchie/tekton/wiki/Patch-Release-Workflow).
|
||||
|
||||
## 🛠️ Developer Workflows
|
||||
## SSH setup for cloning
|
||||
|
||||
### Creating a Skin Material
|
||||
To create dynamic, color-maskable 3D materials for new character skins:
|
||||
- Open the **Skin Shader Generator** tool in the editor: `res://scenes/tools/skin_shader_generator.tscn`
|
||||
- Run the scene.
|
||||
- Import your base albedo and mask textures.
|
||||
- Use the UI to visualize UV overlays and adjust color channels (Red, Green, Blue, Alpha masks).
|
||||
- Export the configured material as a `.tres` file into the `assets/materials/skins/` directory.
|
||||
All clones **must use SSH** over Tailscale. HTTPS is disabled on this instance.
|
||||
|
||||
### Adding a Skin to the Shop
|
||||
Once your material is ready, you need to update the game's catalog and deploy the changes to the Nakama server.
|
||||
Guides by operating system:
|
||||
|
||||
#### Using the Catalog Editor Tool
|
||||
- Open the **Skin Catalog Editor** tool in the Godot Editor: `res://scenes/tools/skin_catalog_editor.tscn`
|
||||
- Press **F6** (or Right-click -> Run Current Scene).
|
||||
- **Manage Skins:**
|
||||
- Click **"+ New Skin"** to create a new entry.
|
||||
- Fill in the **ID**, **Name**, **Category**, and **Price** (Gold/Stars).
|
||||
- Assign the `.tres` material path generated in Step 1.
|
||||
- Click **"💾 Save & Generate"**. This automatically rewrites:
|
||||
- `res://scripts/managers/skin_manager.gd` (Local catalog)
|
||||
- `res://server/nakama/tekton_admin.js` (Server-side shop logic)
|
||||
- [Linux](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Linux)
|
||||
- [macOS](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-macOS)
|
||||
- [Windows](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Windows)
|
||||
|
||||
#### Nakama VPS Deployment
|
||||
After generating the updated `tekton_admin.js` locally, you must sync it with your remote server.
|
||||
|
||||
- **Copy the latest script:** Open `server/nakama/tekton_admin.js` locally and copy its updated contents (including your new skin).
|
||||
- **Connect to your VPS** via SSH.
|
||||
- **Create/Edit the file on the remote server:**
|
||||
```bash
|
||||
nano ~/tekton_admin.js
|
||||
# Or use micro (recommended): micro ~/tekton_admin.js
|
||||
```
|
||||
Paste the copied contents and save the file.
|
||||
- **Find your Nakama Container ID:**
|
||||
It is highly recommended to use **lazydocker** to manage containers.
|
||||
*(To install on Ubuntu: `curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash`)*
|
||||
Open lazydocker or run `docker ps` to find the Container ID for your Nakama server.
|
||||
- **Copy the file into the Nakama container:**
|
||||
```bash
|
||||
# Replace ed21ac5d442a with your actual Container ID
|
||||
docker cp ~/tekton_admin.js ed21ac5d442a:/nakama/data/modules/tekton_admin.js
|
||||
```
|
||||
- **Restart the container** (via lazydocker or `docker restart <Container ID>`) for Nakama to load the new modules. Live game clients will fetch this new catalog automatically upon booting.
|
||||
|
||||
#### 🎨 Skin Creation & Deployment Flow
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% Creation Phase
|
||||
subgraph phase1 [Skin Material Creation]
|
||||
A[Albedo & Mask Textures] --> B{Skin Shader Generator}
|
||||
B -->|Export| C[material.tres]
|
||||
end
|
||||
|
||||
%% Catalog Phase
|
||||
subgraph phase2 [Catalog Definition]
|
||||
C --> D{Skin Catalog Editor}
|
||||
D -->|Save & Generate| E[tekton_admin.js]
|
||||
D -->|Save & Generate| F[skin_manager.gd]
|
||||
end
|
||||
|
||||
%% Deployment Phase (Dual Path)
|
||||
subgraph phase3 ["Shop Backend (VPS)"]
|
||||
E -->|SSH & nano| G[VPS: ~/tekton_admin.js]
|
||||
G -->|docker cp| H[Nakama Container]
|
||||
H -->|Restart| I[Live Shop Logic Sync]
|
||||
end
|
||||
|
||||
subgraph phase4 ["Asset Delivery (CI/CD)"]
|
||||
C --> J[Git Push]
|
||||
F --> J
|
||||
J -->|GitHub Actions| K[patch.pck]
|
||||
K -->|Automatic Download| L[Player Client Assets Sync]
|
||||
end
|
||||
Quick verification after setup:
|
||||
```bash
|
||||
ssh -T git@thunderobot -p 222
|
||||
```
|
||||
|
||||
### Pushing a New Version (Automated Patching)
|
||||
When you're ready to deploy new features or assets to players:
|
||||
- Document your changes in `CHANGELOG_DRAFT.md` using player-friendly language.
|
||||
- Run the version generation script from the terminal:
|
||||
```bash
|
||||
python generate_version_json.py --bump patch
|
||||
```
|
||||
*(Use `--bump minor` or `--bump major` for larger updates.)*
|
||||
- Commit and push your changes to the `main` branch on GitHub:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Release version X.Y.Z"
|
||||
git push origin main
|
||||
```
|
||||
- The **GitHub Actions Workflow** (`deploy_patch.yml`) will automatically detect the push, build the patch manifest (`version.json`), and deploy it to the public `gh-pages` branch.
|
||||
- Live game clients will detect the new version on boot, download the updated files, and apply the patch seamlessly.
|
||||
|
||||
### Local Testing & Understanding the Patch System
|
||||
When a player (or you) downloads an in-game patch, Godot downloads a `patch.pck` file to the system's `user://` directory.
|
||||
- **Virtual File System:** Godot mounts this `.pck` file over the `res://` directory purely in memory. **It does not physically overwrite your local source files** (like `assets/data/version.json`).
|
||||
- **Editor Bypass:** When testing locally in the Godot Editor, the `BootScreen` is configured to skip the remote network download and instead automatically parse your *local* `assets/data/version.json`.
|
||||
- **Previewing Changelogs:** To preview how your changelog will look before pushing to GitHub:
|
||||
- Add your notes under the `## [NEXT]` section in `CHANGELOG_DRAFT.md`.
|
||||
- Run `py tools/generate_version_json.py` from the terminal.
|
||||
- Run the `BootScreen` scene in the Godot Editor. It will instantly display the updated local UI!
|
||||
- **Syncing:** After the GitHub Actions CI builds a release online, remember to run `git pull origin main` to sync your local project files with the CI-generated files.
|
||||
|
||||
#### 🗺️ Architecture Flowchart
|
||||
```mermaid
|
||||
flowchart TD
|
||||
%% Local Dev Flow
|
||||
subgraph Local Dev Environment
|
||||
A[📝 CHANGELOG_DRAFT.md] -->|py tools/generate_version_json.py| B[(📄 version.json)]
|
||||
B -.->|Test in Editor| C{BootScreen}
|
||||
C -- Editor Bypass --> D[Reads Local version.json directly]
|
||||
end
|
||||
|
||||
%% CI Pipeline
|
||||
subgraph "CI/CD Pipeline"
|
||||
B -->|git push| E[⚡ GitHub Actions]
|
||||
E -->|Compiles & Builds| F[📦 patch.pck]
|
||||
F -->|Deploys| G((🌍 Public Repository))
|
||||
end
|
||||
|
||||
%% Live Client Flow
|
||||
subgraph Player Client
|
||||
G -.->|HTTP Download on Boot| H[📂 user://patch.pck]
|
||||
H -->|ProjectSettings.load_resource_pack| I[🧠 Godot Virtual File System]
|
||||
I -.->|Overrides res:// in Memory| J[Game Starts Updated!]
|
||||
end
|
||||
Once verified, clone:
|
||||
```bash
|
||||
git clone git@thunderobot:222/danchie/tekton.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ongoing Features (Incoming)
|
||||
|
||||
### 🎰 Gacha System Backend Editor
|
||||
Currently in development: A dedicated backend editor tool (similar to `skin_catalog_editor.tscn`) specifically for managing the Gacha System.
|
||||
- **Nakama Syncing:** Will allow developers to push updated gacha pools, rates, and fragment costs directly to Nakama Storage.
|
||||
- **Dynamic Banners:** Will support updating specific slots on the gacha banner dynamically.
|
||||
- **Seasonal Rotations:** Will introduce automated scheduling so banners rotate based on active seasons and automatically remove themselves when the season ends.
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
{
|
||||
"latest_version": "2.4.1",
|
||||
"latest_version": "2.4.2",
|
||||
"minimum_app_version": "2.1.0",
|
||||
"releases": [
|
||||
{
|
||||
"version": "2.4.2",
|
||||
"date": "2026-07-03",
|
||||
"pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck",
|
||||
"pck_size": 0,
|
||||
"changelog": [
|
||||
"Replaced Cleanser mechanic in Gauntlet with Ghost powerup sticky-bypass system.",
|
||||
"Ghost (Invisible Mode) now lets players walk through sticky candy tiles in Gauntlet.",
|
||||
"Players earn a Ghost powerup every 2 completed missions in Gauntlet.",
|
||||
"Ghost powerup tiles now spawn naturally on the Gauntlet arena (15% chance).",
|
||||
"Removed Cleanser HUD elements from Gauntlet overlay.",
|
||||
"Bots now activate Ghost powerup when boxed in by sticky tiles.",
|
||||
"Players pushed into sticky tiles while in Ghost mode are no longer slowed."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "2.4.1",
|
||||
"date": "2026-06-28",
|
||||
|
||||
Binary file not shown.
+23
-16
@@ -1,14 +1,20 @@
|
||||
[runnable_presets]
|
||||
|
||||
"Windows Desktop"="Windows Desktop"
|
||||
Android="Android"
|
||||
macOS="macOS"
|
||||
Linux="Linux/X11"
|
||||
|
||||
[preset.0]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="build/tekton_armageddon_v2.4.1.exe"
|
||||
export_path="build/windows/tekton_armageddon_v2.4.2.exe"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
@@ -42,8 +48,8 @@ application/modify_resources=false
|
||||
application/icon=""
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version="2.4.1"
|
||||
application/product_version="2.4.1"
|
||||
application/file_version="2.4.2"
|
||||
application/product_version="2.4.2"
|
||||
application/company_name="DanchieGo"
|
||||
application/product_name="Tekton Armageddon"
|
||||
application/file_description=""
|
||||
@@ -74,13 +80,12 @@ Remove-Item -Recurse -Force '{temp_dir}'"
|
||||
|
||||
name="Android"
|
||||
platform="Android"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="build/tekton-dash-armageddon-v.2.4.1.apk"
|
||||
export_path="build/tekton-dash-armageddon-v.2.4.2.apk"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
@@ -111,7 +116,7 @@ architectures/arm64-v8a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=3
|
||||
version/name="2.4.1"
|
||||
version/name="2.4.2"
|
||||
package/unique_name="com.danchiego.$genname"
|
||||
package/name="Tekton Dash Armageddon"
|
||||
package/signed=true
|
||||
@@ -136,11 +141,12 @@ screen/support_normal=true
|
||||
screen/support_large=true
|
||||
screen/support_xlarge=true
|
||||
screen/background_color=Color(0, 0, 0, 1)
|
||||
splash_screen/disable_godot_boot_splash=false
|
||||
splash_screen/icon=""
|
||||
splash_screen/branding_image=""
|
||||
splash_screen/background_color=Color(0, 0, 0, 1)
|
||||
user_data_backup/allow=false
|
||||
command_line/extra_args=""
|
||||
apk_expansion/enable=false
|
||||
apk_expansion/SALT=""
|
||||
apk_expansion/public_key=""
|
||||
permissions/custom_permissions=PackedStringArray()
|
||||
permissions/access_checkin_properties=false
|
||||
permissions/access_coarse_location=false
|
||||
@@ -295,18 +301,20 @@ permissions/write_sms=false
|
||||
permissions/write_social_stream=false
|
||||
permissions/write_sync_settings=false
|
||||
permissions/write_user_dictionary=false
|
||||
apk_expansion/enable=false
|
||||
apk_expansion/SALT=""
|
||||
apk_expansion/public_key=""
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="macOS"
|
||||
platform="macOS"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="build/tekton_armageddon_v2.4.1.zip"
|
||||
export_path="build/tekton_armageddon_v2.4.2.zip"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
@@ -565,8 +573,8 @@ codesign/digest_algorithm=1
|
||||
codesign/identity_type=0
|
||||
application/modify_resources=false
|
||||
application/console_wrapper_icon=""
|
||||
application/file_version="2.4.1"
|
||||
application/product_version="2.4.1"
|
||||
application/file_version="2.4.2"
|
||||
application/product_version="2.4.2"
|
||||
application/company_name="DanchieGo"
|
||||
application/product_name="Tekton Armageddon"
|
||||
application/file_description=""
|
||||
@@ -576,13 +584,12 @@ application/trademarks=""
|
||||
|
||||
name="Linux/X11"
|
||||
platform="Linux"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path="build/tekton_armageddon_v2.4.1.x86_64"
|
||||
export_path="build/linux/tekton_armageddon_v2.4.22.x86_64"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@rpc("any_peer", "call_local")
|
||||
func remove_slow_effect():
|
||||
slow_timer = 0.0
|
||||
self.is_slowed = false
|
||||
if movement_manager:
|
||||
movement_manager.set_speed_multiplier(1.0)
|
||||
@@ -1,6 +0,0 @@
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
||||
sticky_cells.erase(pos)
|
||||
mark_cleansed(pos)
|
||||
if gridmap:
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
|
||||
@@ -1,68 +0,0 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="1_font"]
|
||||
|
||||
[node name="GauntletHUD" type="CanvasLayer"]
|
||||
layer = 5
|
||||
visible = false
|
||||
|
||||
[node name="TopContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_top = 70.0
|
||||
grow_horizontal = 2
|
||||
|
||||
[node name="SlowMoLabel" type="Label" parent="TopContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_colors/font_color = Color(0.3, 0.5, 1.0, 1)
|
||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/outline_size = 4
|
||||
theme_override_fonts/font = ExtResource("1_font")
|
||||
text = "SLOW-MO"
|
||||
horizontal_alignment = 1
|
||||
visible = false
|
||||
|
||||
[node name="BottomContainer" type="CenterContainer" parent="."]
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -120.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="BottomContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PhaseLabel" type="Label" parent="BottomContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 24
|
||||
theme_override_colors/font_color = Color(1, 0.6, 0.8, 1)
|
||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/outline_size = 6
|
||||
theme_override_fonts/font = ExtResource("1_font")
|
||||
text = "🍬 OPEN ARENA"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CleanserHBox" type="HBoxContainer" parent="BottomContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
alignment = 1
|
||||
|
||||
[node name="CleanserIcon" type="TextureRect" parent="BottomContainer/VBoxContainer/CleanserHBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(20, 20)
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CleanserLabel" type="Label" parent="BottomContainer/VBoxContainer/CleanserHBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/outline_size = 6
|
||||
theme_override_fonts/font = ExtResource("1_font")
|
||||
text = "[E] Cleanser (0)"
|
||||
horizontal_alignment = 1
|
||||
@@ -1,48 +0,0 @@
|
||||
func _spawn_cleanser_particles(pos: Vector2i) -> void:
|
||||
"""Spawn bright cleansing particles when sticky is cleared."""
|
||||
if not main_scene or not gridmap:
|
||||
return
|
||||
|
||||
var world_pos = Vector3(
|
||||
pos.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0,
|
||||
0.5,
|
||||
pos.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
|
||||
)
|
||||
|
||||
var particles = GPUParticles3D.new()
|
||||
particles.emitting = true
|
||||
particles.one_shot = true
|
||||
particles.amount = 12
|
||||
particles.lifetime = 0.6
|
||||
particles.explosiveness = 0.9
|
||||
|
||||
var material = ParticleProcessMaterial.new()
|
||||
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||
material.emission_sphere_radius = 0.3
|
||||
material.direction = Vector3(0, 1, 0)
|
||||
material.spread = 180.0
|
||||
material.initial_velocity_min = 3.0
|
||||
material.initial_velocity_max = 5.0
|
||||
material.gravity = Vector3(0, -5.0, 0)
|
||||
material.scale_min = 0.05
|
||||
material.scale_max = 0.15
|
||||
|
||||
var mesh = SphereMesh.new()
|
||||
mesh.radius = 0.2
|
||||
mesh.height = 0.4
|
||||
var spatial_mat = StandardMaterial3D.new()
|
||||
spatial_mat.albedo_color = Color(0.2, 1.0, 1.0) # Cyan/Blue for cleanser
|
||||
spatial_mat.emission_enabled = true
|
||||
spatial_mat.emission = Color(0.2, 1.0, 1.0)
|
||||
spatial_mat.emission_energy_multiplier = 3.0
|
||||
mesh.material = spatial_mat
|
||||
particles.draw_pass_1 = mesh
|
||||
|
||||
particles.process_material = material
|
||||
particles.position = world_pos
|
||||
|
||||
main_scene.add_child(particles)
|
||||
|
||||
await get_tree().create_timer(1.2).timeout
|
||||
if particles and is_instance_valid(particles):
|
||||
particles.queue_free()
|
||||
@@ -1,24 +0,0 @@
|
||||
func _find_valid_drop_position() -> Vector2i:
|
||||
# Try random adjacent cells
|
||||
var neighbors = enhanced_gridmap.get_neighbors(current_position, 0)
|
||||
neighbors.shuffle()
|
||||
|
||||
for neighbor in neighbors:
|
||||
var pos = neighbor.position
|
||||
# Check item layer
|
||||
var item_cell = Vector3i(pos.x, 1, pos.y)
|
||||
if enhanced_gridmap.get_cell_item(item_cell) == -1:
|
||||
if not is_position_occupied(pos):
|
||||
# Gauntlet Mode explicit overrides
|
||||
var gm = null
|
||||
var main_gauntlet = get_tree().root.get_node_or_null("Main")
|
||||
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):
|
||||
gm = main_gauntlet.gauntlet_manager
|
||||
if gm and gm.is_active:
|
||||
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:
|
||||
continue
|
||||
if gm._is_npc_zone(pos):
|
||||
continue
|
||||
return pos
|
||||
|
||||
return Vector2i(-1, -1)
|
||||
@@ -1,8 +0,0 @@
|
||||
@rpc("any_peer", "call_local")
|
||||
func remove_slow_effect():
|
||||
slow_timer = 0.0
|
||||
self.is_slowed = false
|
||||
if movement_manager:
|
||||
# INSTANT response: restore speed multiplier to 1.0 immediately
|
||||
movement_manager.set_speed_multiplier(1.0)
|
||||
print("Player %s slow effect removed early" % name)
|
||||
@@ -1,25 +0,0 @@
|
||||
/func _find_valid_drop_position/,/return Vector2i(-1, -1)/c\
|
||||
func _find_valid_drop_position() -> Vector2i:\
|
||||
# Try random adjacent cells\
|
||||
var neighbors = enhanced_gridmap.get_neighbors(current_position, 0)\
|
||||
neighbors.shuffle()\
|
||||
\
|
||||
for neighbor in neighbors:\
|
||||
var pos = neighbor.position\
|
||||
# Check item layer\
|
||||
var item_cell = Vector3i(pos.x, 1, pos.y)\
|
||||
if enhanced_gridmap.get_cell_item(item_cell) == -1:\
|
||||
if not is_position_occupied(pos):\
|
||||
# Gauntlet Mode explicit overrides\
|
||||
var gm = null\
|
||||
var main_gauntlet = get_tree().root.get_node_or_null("Main")\
|
||||
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):\
|
||||
gm = main_gauntlet.gauntlet_manager\
|
||||
if gm and gm.is_active:\
|
||||
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:\
|
||||
continue\
|
||||
if gm._is_npc_zone(pos):\
|
||||
continue\
|
||||
return pos\
|
||||
\
|
||||
return Vector2i(-1, -1)
|
||||
+14
-17
@@ -1,36 +1,33 @@
|
||||
import json
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: patch_version.py <version> [notes]")
|
||||
sys.exit(1)
|
||||
|
||||
version = sys.argv[1]
|
||||
notes = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
|
||||
with open("assets/data/version.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Find the 2.4.0 entry or just add 2.4.1 at the top
|
||||
changelog = [
|
||||
"Fixed Gauntlet map layout to remove red unpassable barrier blocks and center blocks.",
|
||||
"Fixed Gauntlet mode to prevent powerups or sticky bubbles from spawning on boundary tiles or under the central cannon.",
|
||||
"Center Candy Cannon now shoots actual projectiles that fly towards sticky cells and leave a VFX trail.",
|
||||
"Added new VFX to the Center Candy Cannon. It now has a glowing pink tank and spinning metallic rings.",
|
||||
"Fixed Gauntlet Cleanser to stack charges instead of capping at 1.",
|
||||
"Cleanser instantly clears a 3x3 AoE of sticky cells and frees any players inside immediately upon activation.",
|
||||
"Added VFX and SFX when purifying cells with the Cleanser (cyan burst particles).",
|
||||
"Added instant visual feedback indicator for Gauntlet Cleanser using popup text when consumed.",
|
||||
"Fixed Gauntlet Cleanser UI phase label layout to ensure it does not overlap with other UI elements."
|
||||
]
|
||||
changelog = [line.strip() for line in notes.splitlines() if line.strip()]
|
||||
|
||||
new_release = {
|
||||
"version": "2.4.1",
|
||||
"date": "2026-06-28",
|
||||
"version": version,
|
||||
"date": date.today().isoformat(),
|
||||
"pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck",
|
||||
"pck_size": 0,
|
||||
"changelog": changelog
|
||||
}
|
||||
|
||||
# Remove existing 2.4.1 if any
|
||||
data["releases"] = [r for r in data["releases"] if r.get("version") != "2.4.1"]
|
||||
# Remove existing entry for this version if any
|
||||
data["releases"] = [r for r in data["releases"] if r.get("version") != version]
|
||||
|
||||
# Insert at top
|
||||
data["releases"].insert(0, new_release)
|
||||
data["latest_version"] = "2.4.1"
|
||||
data["latest_version"] = version
|
||||
|
||||
with open("assets/data/version.json", "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent="\t")
|
||||
|
||||
+6
-9
@@ -17,7 +17,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
config/name="Tekton Dash Armageddon"
|
||||
config/version="2.4.1"
|
||||
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
|
||||
boot_splash/stretch_mode=0
|
||||
boot_splash/image="uid://b10e6kr508642"
|
||||
@@ -38,7 +38,7 @@ GoalManager="*res://scripts/managers/goal_manager.gd"
|
||||
PlayerManager="*res://scripts/managers/player_manager.gd"
|
||||
GoalsCycleManager="*res://scripts/managers/goals_cycle_manager.gd"
|
||||
Satori="*uid://b8vev00s34b7"
|
||||
SettingsManager="*uid://c1ouaaqnn0lrc"
|
||||
SettingsManager="*res://scripts/managers/settings_manager.gd"
|
||||
SfxManager="*res://scripts/managers/sfx_manager.gd"
|
||||
NameGenerator="*res://scripts/generators/name_generator.gd"
|
||||
MusicManager="*res://scripts/managers/music_manager.gd"
|
||||
@@ -67,8 +67,11 @@ enabled=PackedStringArray("res://addons/com.heroiclabs.nakama/plugin.cfg", "res:
|
||||
[file_customization]
|
||||
|
||||
folder_colors={
|
||||
"res://addons/": "pink",
|
||||
"res://assets/": "purple",
|
||||
"res://scenes/": "green"
|
||||
"res://scenes/": "green",
|
||||
"res://scripts/": "green",
|
||||
"res://server/": "blue"
|
||||
}
|
||||
|
||||
[input]
|
||||
@@ -140,12 +143,6 @@ use_powerup={
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
use_cleanser={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
action_grab_tekton={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
|
||||
|
||||
@@ -47,22 +47,3 @@ theme_override_constants/outline_size = 6
|
||||
theme_override_fonts/font = ExtResource("1_font")
|
||||
text = "🍬 OPEN ARENA"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CleanserHBox" type="HBoxContainer" parent="BottomContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
alignment = 1
|
||||
|
||||
[node name="CleanserIcon" type="TextureRect" parent="BottomContainer/VBoxContainer/CleanserHBox"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(20, 20)
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CleanserLabel" type="Label" parent="BottomContainer/VBoxContainer/CleanserHBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||
theme_override_constants/outline_size = 6
|
||||
theme_override_fonts/font = ExtResource("1_font")
|
||||
text = "[E] Cleanser (0)"
|
||||
horizontal_alignment = 1
|
||||
|
||||
+19
-22
@@ -210,9 +210,9 @@ func _run_ai_tick():
|
||||
print("[BotController] Action Taken: Attack Pursuit")
|
||||
return
|
||||
|
||||
# Priority 0.5: Gauntlet (#075) — burn Cleanser if boxed in
|
||||
if await _try_activate_cleanser():
|
||||
print("[BotController] Action Taken: Cleanser (trapped)")
|
||||
# Priority 0.5: Gauntlet (#075) — use Ghost powerup if boxed in
|
||||
if await _try_activate_ghost():
|
||||
print("[BotController] Action Taken: Ghost (trapped)")
|
||||
return
|
||||
|
||||
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
|
||||
@@ -260,27 +260,24 @@ func _run_ai_tick():
|
||||
return
|
||||
|
||||
# =============================================================================
|
||||
# Gauntlet (#075) — Cleanser + Sticky Avoidance wiring
|
||||
# Gauntlet (#075) — Ghost Powerup + Sticky Avoidance wiring
|
||||
# =============================================================================
|
||||
|
||||
func _try_activate_cleanser() -> bool:
|
||||
"""Activate Cleanser when the planner reports imminent danger.
|
||||
func _try_activate_ghost() -> bool:
|
||||
"""Activate Ghost powerup when the planner reports imminent danger.
|
||||
|
||||
Server-authoritative RPC; we only request it. Returns true if the request
|
||||
was sent successfully (not a guarantee it landed on a sticky cell)."""
|
||||
Uses the existing SpecialTilesManager to activate the held ghost powerup.
|
||||
Returns true if activation was triggered."""
|
||||
if not strategic_planner or not strategic_planner.is_gauntlet_mode():
|
||||
return false
|
||||
if not strategic_planner.should_activate_cleanser_now():
|
||||
if not strategic_planner.should_activate_ghost_now():
|
||||
return false
|
||||
var gm = strategic_planner._get_gauntlet_manager()
|
||||
if not gm:
|
||||
var stm = actor.get_node_or_null("SpecialTilesManager")
|
||||
if not stm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
if gm.has_method("rpc_activate_cleanser"):
|
||||
gm.rpc_activate_cleanser(pid)
|
||||
print("[BotController] %s requested Cleanser activation (trapped)" % actor.name)
|
||||
if stm.has_method("activate_effect"):
|
||||
stm.activate_effect(stm.SpecialEffect.INVISIBLE_MODE)
|
||||
print("[BotController] %s activated Ghost powerup (trapped)" % actor.name)
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -292,14 +289,14 @@ func _on_step_onto_unsafe() -> bool:
|
||||
var here = actor.current_position if "current_position" in actor else Vector2i(-1, -1)
|
||||
if here == Vector2i(-1, -1):
|
||||
return false
|
||||
# Post-move guard: if we somehow landed on a sticky without cleanser active,
|
||||
# burn Cleanser to clear ourselves out next tick.
|
||||
# Post-move guard: if we somehow landed on a sticky without ghost active,
|
||||
# burn Ghost powerup to phase through next tick.
|
||||
if strategic_planner.is_gauntlet_mode() and strategic_planner._is_overlay_unsafe(here):
|
||||
if not strategic_planner._is_bot_cleanser_active():
|
||||
if not strategic_planner._is_bot_ghost_active():
|
||||
var gm = strategic_planner._get_gauntlet_manager()
|
||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(here):
|
||||
print("[BotController] %s stepped onto sticky at %s — burning Cleanser" % [actor.name, here])
|
||||
return _try_activate_cleanser()
|
||||
print("[BotController] %s stepped onto sticky at %s — activating Ghost" % [actor.name, here])
|
||||
return _try_activate_ghost()
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -17,7 +17,7 @@ const GOAL_TILES = [7, 8, 9, 10] # Heart, Diamond, Star, Coin
|
||||
const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles
|
||||
|
||||
# Gauntlet overlay layer (v2 ground-growth model — sticky/telegraph on layer 2).
|
||||
# Bots must avoid these cells or burn a Cleanser charge to cross.
|
||||
# Bots must avoid these cells or use Ghost mode to cross.
|
||||
const GAUNTLET_OVERLAY_LAYER: int = 2
|
||||
const TILE_STICKY: int = 17
|
||||
const TILE_TELEGRAPH: int = 18
|
||||
@@ -61,23 +61,16 @@ func _get_gauntlet_manager() -> Node:
|
||||
return gm2
|
||||
return null
|
||||
|
||||
func _bot_has_cleanser_charge() -> bool:
|
||||
var gm = _get_gauntlet_manager()
|
||||
if not gm or not "player_cleansers" in gm:
|
||||
func _bot_has_ghost_powerup() -> bool:
|
||||
"""Check if the bot has a ghost powerup in its SpecialTilesManager inventory."""
|
||||
var stm = actor.get_node_or_null("SpecialTilesManager")
|
||||
if not stm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
return gm.player_cleansers.get(pid, 0) > 0
|
||||
return stm.inventory.get(stm.SpecialEffect.INVISIBLE_MODE, false)
|
||||
|
||||
func _is_bot_cleanser_active() -> bool:
|
||||
var gm = _get_gauntlet_manager()
|
||||
if not gm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
return gm.is_cleanser_active(pid)
|
||||
func _is_bot_ghost_active() -> bool:
|
||||
"""Check if the bot is currently in ghost (invisible) mode."""
|
||||
return actor.get("is_invisible") == true
|
||||
|
||||
func _is_overlay_unsafe(pos: Vector2i) -> bool:
|
||||
"""True if the cell carries a sticky or telegraphed overlay on layer 2."""
|
||||
@@ -88,10 +81,10 @@ func _is_overlay_unsafe(pos: Vector2i) -> bool:
|
||||
|
||||
func _is_cell_unsafe_in_gauntlet(pos: Vector2i) -> bool:
|
||||
"""Cell is unsafe in Gauntlet if it's sticky/telegraphed — unless the bot's
|
||||
Cleanser is active (grants temporary immunity)."""
|
||||
Ghost mode is active (grants sticky bypass)."""
|
||||
if not is_gauntlet_mode():
|
||||
return false
|
||||
if _is_bot_cleanser_active():
|
||||
if _is_bot_ghost_active():
|
||||
return false
|
||||
var gm = _get_gauntlet_manager()
|
||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(pos):
|
||||
@@ -106,13 +99,13 @@ func _count_unsafe_neighbors(pos: Vector2i) -> int:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
func should_activate_cleanser_now() -> bool:
|
||||
"""True if the bot is boxed in / about to be sealed and should burn Cleanser."""
|
||||
func should_activate_ghost_now() -> bool:
|
||||
"""True if the bot is boxed in / about to be sealed and should use Ghost powerup."""
|
||||
if not is_gauntlet_mode():
|
||||
return false
|
||||
if not _bot_has_cleanser_charge():
|
||||
if not _bot_has_ghost_powerup():
|
||||
return false
|
||||
if _is_bot_cleanser_active():
|
||||
if _is_bot_ghost_active():
|
||||
return false
|
||||
var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1)
|
||||
if here == Vector2i(-1, -1):
|
||||
@@ -623,7 +616,7 @@ func _is_valid_move_target(pos: Vector2i, ignore_players: bool = false) -> bool:
|
||||
# Gauntlet mode (#075): reject cells that are sticky or telegraphed —
|
||||
# stepping onto them either traps the bot or strands it within 1s.
|
||||
# Safety applies even when ignore_players is true (a sticky cell is unsafe
|
||||
# regardless of whether another player is on it). Cleanser-active bots are
|
||||
# regardless of whether another player is on it). Ghost-active bots are
|
||||
# exempt via the helper.
|
||||
if _is_cell_unsafe_in_gauntlet(pos):
|
||||
return false
|
||||
|
||||
@@ -128,8 +128,10 @@ func spawn_projectile(target_world_pos: Vector3, duration: float) -> void:
|
||||
# We need to wait for the X/Z tween to finish, but since it's parallel we can just use a separate timer or tween
|
||||
# to kill the projectile exactly when duration is reached, ensuring it doesn't get killed early by X/Z finishing 1 frame earlier than Y
|
||||
get_tree().create_timer(duration).timeout.connect(func():
|
||||
if is_instance_valid(spin_tween): spin_tween.kill()
|
||||
if is_instance_valid(projectile): projectile.queue_free()
|
||||
if is_instance_valid(spin_tween):
|
||||
spin_tween.kill()
|
||||
if is_instance_valid(projectile):
|
||||
projectile.queue_free()
|
||||
)
|
||||
|
||||
func can_rpc() -> bool:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
extends SceneTree
|
||||
|
||||
const SKIP_DIRS := [".git", ".godot", "addons"]
|
||||
|
||||
func _initialize() -> void:
|
||||
var errors: Array[String] = []
|
||||
_scan_dir("res://", errors)
|
||||
if errors.is_empty():
|
||||
print("Lint passed: no GDScript syntax errors found")
|
||||
quit(0)
|
||||
else:
|
||||
for e in errors:
|
||||
printerr(e)
|
||||
printerr("Lint failed: %d file(s) with syntax errors" % errors.size())
|
||||
quit(1)
|
||||
|
||||
func _scan_dir(path: String, errors: Array[String]) -> void:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return
|
||||
dir.list_dir_begin()
|
||||
var entry := dir.get_next()
|
||||
while entry != "":
|
||||
if entry == "." or entry == "..":
|
||||
entry = dir.get_next()
|
||||
continue
|
||||
var full_path := path.path_join(entry)
|
||||
if dir.current_is_dir():
|
||||
if not SKIP_DIRS.has(entry):
|
||||
_scan_dir(full_path, errors)
|
||||
elif entry.ends_with(".gd"):
|
||||
var script := GDScript.new()
|
||||
script.source_code = FileAccess.get_file_as_string(full_path)
|
||||
if script.reload() != OK:
|
||||
errors.append("%s: syntax error" % full_path)
|
||||
entry = dir.get_next()
|
||||
@@ -7,7 +7,7 @@ class_name GauntletManager
|
||||
signal phase_changed(phase_index: int, phase_name: String)
|
||||
signal growth_tick(cells: Array)
|
||||
signal player_trapped(player_id: int)
|
||||
signal cleanser_granted(player_id: int)
|
||||
signal ghost_granted(player_id: int)
|
||||
|
||||
# =============================================================================
|
||||
# Constants
|
||||
@@ -31,10 +31,9 @@ enum CellState {
|
||||
STICKY, # Covered in sticky candy, blocks + traps
|
||||
BUBBLE_GROWING, # Candy bubble growing, not yet exploded
|
||||
BLOCKED, # NPC zone or permanent obstacle
|
||||
CLEANSED, # Recently cleaned by Cleanser (temp protection)
|
||||
}
|
||||
|
||||
# Cells temporarily protected after a Cleanser pass (Vector2i -> time remaining).
|
||||
# Cells temporarily protected after Ghost-clearing (not used — kept for compat).
|
||||
var cleansed_cells: Dictionary = {}
|
||||
const CLEANSED_PROTECTION_TIME: float = 5.0
|
||||
|
||||
@@ -146,15 +145,10 @@ const SMACK_COOLDOWN: float = 8.0
|
||||
const SMACK_CHARGE_WINDOW: float = 3.0
|
||||
|
||||
# =============================================================================
|
||||
# Cleanser Tracking
|
||||
# Ghost Reward Tracking (replaces Cleanser)
|
||||
# =============================================================================
|
||||
|
||||
var player_mission_completions: Dictionary = {} # player_id → int
|
||||
var player_cleansers: Dictionary = {} # player_id → int (0 or 1)
|
||||
var cleanser_active: Dictionary = {} # player_id → true when immunity active
|
||||
var cleanser_cells_left: Dictionary = {} # player_id → int (cells remaining)
|
||||
const CLEANSER_MAX_CELLS: int = 5
|
||||
const CLEANSER_ACTIVATION_DELAY: float = 0.3
|
||||
|
||||
# =============================================================================
|
||||
# Trapped Players
|
||||
@@ -189,9 +183,6 @@ var pump_instance: Node3D = null
|
||||
# HUD
|
||||
var hud_layer: CanvasLayer
|
||||
var phase_label: Label
|
||||
var cleanser_label: Label
|
||||
var cleanser_icon: TextureRect
|
||||
var cleanser_count: int = 0
|
||||
var slowmo_label: Label
|
||||
var _gauntlet_hud_scene: PackedScene = preload("res://scenes/gauntlet_hud.tscn")
|
||||
|
||||
@@ -284,10 +275,6 @@ func _process(delta: float) -> void:
|
||||
elif not multiplayer.is_server():
|
||||
player.sync_modulate(Color.WHITE)
|
||||
|
||||
# Cleanser input (local player only)
|
||||
if Input.is_action_just_pressed("use_cleanser"):
|
||||
_try_use_cleanser()
|
||||
|
||||
# Slow-mo timer (all peers for visual consistency)
|
||||
if slowmo_active:
|
||||
slowmo_timer -= delta
|
||||
@@ -340,9 +327,12 @@ func _start_phase(phase: Phase) -> void:
|
||||
var phase_name = _phase_to_string(phase)
|
||||
print("[Gauntlet] Phase changed to: ", phase_name)
|
||||
|
||||
if _can_rpc():
|
||||
if _can_rpc() and multiplayer.is_server():
|
||||
rpc("sync_phase", int(phase), phase_name)
|
||||
|
||||
# Update phase explicitly with setup_arena
|
||||
_shrink_arena()
|
||||
|
||||
emit_signal("phase_changed", int(phase), phase_name)
|
||||
|
||||
func _phase_to_string(phase: Phase) -> String:
|
||||
@@ -361,6 +351,14 @@ func sync_phase(phase_index: int, phase_name: String) -> void:
|
||||
if not is_active:
|
||||
activate_client_side()
|
||||
current_phase = phase_index as Phase
|
||||
if not multiplayer.is_server():
|
||||
var bounds = get_arena_bounds()
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
if pos.x <= bounds.min or pos.x >= bounds.max or pos.y <= bounds.min or pos.y >= bounds.max:
|
||||
if not sticky_cells.has(pos):
|
||||
sticky_cells[pos] = true
|
||||
_update_hud_phase(phase_name)
|
||||
|
||||
# =============================================================================
|
||||
@@ -391,47 +389,46 @@ func sync_arena_setup() -> void:
|
||||
print("[Gauntlet] Client: Syncing Arena Setup (%dx%d)..." % [ARENA_COLUMNS, ARENA_ROWS])
|
||||
_apply_arena_setup()
|
||||
|
||||
func _apply_arena_setup() -> void:
|
||||
"""Shared arena layout logic for host + clients."""
|
||||
if not gridmap:
|
||||
gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
func _apply_arena_setup() -> void:
|
||||
"""Shared arena layout logic for host + clients."""
|
||||
if not gridmap:
|
||||
gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
# Resize grid (bypass setters that wipe the map)
|
||||
gridmap.set("columns", ARENA_COLUMNS)
|
||||
gridmap.set("rows", ARENA_ROWS)
|
||||
# Resize grid (bypass setters that wipe the map)
|
||||
gridmap.set("columns", ARENA_COLUMNS)
|
||||
gridmap.set("rows", ARENA_ROWS)
|
||||
|
||||
# Clear all
|
||||
gridmap.clear()
|
||||
# Clear all
|
||||
gridmap.clear()
|
||||
|
||||
# Build the 20x20 arena
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
# Build the 20x20 arena
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
|
||||
# Center 3x3 block: NPC obstacle (Candy Pump)
|
||||
if _is_npc_zone(pos):
|
||||
# Make the floor empty (-1) beneath the Candy Pump
|
||||
# We need to clear all possible layers just in case
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
continue
|
||||
# Center 3x3 block: NPC obstacle (Candy Pump)
|
||||
if x >= 8 and x <= 10 and z >= 8 and z <= 10:
|
||||
# Hardcode clear all possible layers beneath the Candy Pump
|
||||
for layer in range(5):
|
||||
gridmap.set_cell_item(Vector3i(x, layer, z), -1)
|
||||
continue
|
||||
|
||||
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
||||
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||
# Also make border walls visually walkable floors instead of red blocks
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
continue
|
||||
|
||||
# Interior: walkable floor
|
||||
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
||||
if pos.x <= 0 or pos.x >= 19 or pos.y <= 0 or pos.y >= 19:
|
||||
# Also make border walls visually walkable floors instead of red blocks
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), TILE_STICKY)
|
||||
sticky_cells[pos] = true
|
||||
continue
|
||||
|
||||
# Interior: walkable floor
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
gridmap.set_cell_item(Vector3i(x, 2, z), -1)
|
||||
|
||||
gridmap.diagonal_movement = true
|
||||
gridmap.update_grid_data()
|
||||
@@ -451,10 +448,7 @@ func sync_arena_setup() -> void:
|
||||
|
||||
func _is_npc_zone(pos: Vector2i) -> bool:
|
||||
"""Check if a position is within the center 3x3 NPC zone."""
|
||||
var half = NPC_SIZE / 2 # integer division = 1
|
||||
var min_coord = NPC_CENTER - Vector2i(half, half) # (8, 8)
|
||||
var max_coord = NPC_CENTER + Vector2i(half, half) # (10, 10)
|
||||
return pos.x >= min_coord.x and pos.x <= max_coord.x and pos.y >= min_coord.y and pos.y <= max_coord.y
|
||||
return pos.x >= 8 and pos.x <= 10 and pos.y >= 8 and pos.y <= 10
|
||||
|
||||
func get_spawn_points(player_count: int) -> Array[Vector2i]:
|
||||
"""Return spawn positions based on player count. Inside boundary walls."""
|
||||
@@ -508,13 +502,14 @@ func _spawn_mission_tiles() -> void:
|
||||
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
||||
var goal_items = [7, 8, 9, 10]
|
||||
var tiles_spawned: int = 0
|
||||
var main = get_node_or_null("/root/Main")
|
||||
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
|
||||
# Skip NPC pump zone (center 3x3)
|
||||
if _is_npc_zone(pos):
|
||||
if x >= 8 and x <= 10 and z >= 8 and z <= 10:
|
||||
continue
|
||||
|
||||
# Check base floor — don't spawn on void (or walls if they were still obstacles)
|
||||
@@ -540,7 +535,6 @@ func _spawn_mission_tiles() -> void:
|
||||
tiles_spawned += 1
|
||||
|
||||
# Sync to clients
|
||||
var main = get_node("/root/Main")
|
||||
if main:
|
||||
main.rpc("sync_grid_item", x, 1, z, tile_type)
|
||||
|
||||
@@ -858,6 +852,8 @@ func sync_growth_telegraph(cells: Array) -> void:
|
||||
|
||||
for cell in cells:
|
||||
var pos = cell as Vector2i
|
||||
if pos.x >= 8 and pos.x <= 10 and pos.y >= 8 and pos.y <= 10: continue
|
||||
|
||||
# Telegraph overlay tile on Layer 2 (still passable).
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
||||
_spawn_telegraph_highlight(pos)
|
||||
@@ -929,6 +925,8 @@ func sync_growth_apply(cells: Array) -> void:
|
||||
if not gridmap: return
|
||||
for cell in cells:
|
||||
var pos = cell as Vector2i
|
||||
if pos.x >= 8 and pos.x <= 10 and pos.y >= 8 and pos.y <= 10: continue
|
||||
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY)
|
||||
sticky_cells[pos] = true
|
||||
|
||||
@@ -1005,56 +1003,6 @@ func _spawn_impact_particles(targets: Array) -> void:
|
||||
if particles and is_instance_valid(particles):
|
||||
particles.queue_free()
|
||||
|
||||
# =============================================================================
|
||||
func _spawn_cleanser_particles(pos: Vector2i) -> void:
|
||||
"""Spawn bright cleansing particles when sticky is cleared."""
|
||||
if not main_scene or not gridmap:
|
||||
return
|
||||
|
||||
var world_pos = Vector3(
|
||||
pos.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0,
|
||||
0.5,
|
||||
pos.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
|
||||
)
|
||||
|
||||
var particles = GPUParticles3D.new()
|
||||
particles.emitting = true
|
||||
particles.one_shot = true
|
||||
particles.amount = 12
|
||||
particles.lifetime = 0.6
|
||||
particles.explosiveness = 0.9
|
||||
|
||||
var material = ParticleProcessMaterial.new()
|
||||
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||
material.emission_sphere_radius = 0.3
|
||||
material.direction = Vector3(0, 1, 0)
|
||||
material.spread = 180.0
|
||||
material.initial_velocity_min = 3.0
|
||||
material.initial_velocity_max = 5.0
|
||||
material.gravity = Vector3(0, -5.0, 0)
|
||||
material.scale_min = 0.05
|
||||
material.scale_max = 0.15
|
||||
|
||||
var mesh = SphereMesh.new()
|
||||
mesh.radius = 0.2
|
||||
mesh.height = 0.4
|
||||
var spatial_mat = StandardMaterial3D.new()
|
||||
spatial_mat.albedo_color = Color(0.2, 1.0, 1.0) # Cyan/Blue for cleanser
|
||||
spatial_mat.emission_enabled = true
|
||||
spatial_mat.emission = Color(0.2, 1.0, 1.0)
|
||||
spatial_mat.emission_energy_multiplier = 3.0
|
||||
mesh.material = spatial_mat
|
||||
particles.draw_pass_1 = mesh
|
||||
|
||||
particles.process_material = material
|
||||
particles.position = world_pos
|
||||
|
||||
main_scene.add_child(particles)
|
||||
|
||||
await get_tree().create_timer(1.2).timeout
|
||||
if particles and is_instance_valid(particles):
|
||||
particles.queue_free()
|
||||
|
||||
# =============================================================================
|
||||
# Sticky / Trap System
|
||||
|
||||
@@ -1066,12 +1014,15 @@ func is_cleansed_cell(pos: Vector2i) -> bool:
|
||||
|
||||
func cell_state(pos: Vector2i) -> CellState:
|
||||
"""Logical state of a playable cell (v2 ground-growth model)."""
|
||||
var b = get_arena_bounds()
|
||||
if pos.x <= b.min or pos.x >= b.max or pos.y <= b.min or pos.y >= b.max:
|
||||
return CellState.STICKY
|
||||
if _is_npc_zone(pos) or _is_boundary(pos):
|
||||
return CellState.BLOCKED
|
||||
if sticky_cells.has(pos):
|
||||
if is_sticky_cell(pos):
|
||||
return CellState.STICKY
|
||||
if cleansed_cells.has(pos):
|
||||
return CellState.CLEANSED
|
||||
return CellState.BLOCKED # Protected from regrowth temporarily
|
||||
if telegraphed_cells.has(pos):
|
||||
return CellState.TELEGRAPHED
|
||||
if bubble_cells.has(pos):
|
||||
@@ -1092,8 +1043,37 @@ func _tick_cleansed_cells(delta: float) -> void:
|
||||
for pos in expired:
|
||||
cleansed_cells.erase(pos)
|
||||
|
||||
func get_arena_bounds() -> Dictionary:
|
||||
match current_phase:
|
||||
Phase.OPEN_ARENA:
|
||||
return {"min": 0, "max": 19} # 20x20
|
||||
Phase.ROUTE_PRESSURE:
|
||||
return {"min": 1, "max": 18} # 18x18
|
||||
Phase.SURVIVAL_ENDGAME:
|
||||
return {"min": 6, "max": 12} # 7x7
|
||||
return {"min": 0, "max": 19}
|
||||
|
||||
func _shrink_arena() -> void:
|
||||
if not multiplayer.is_server(): return
|
||||
var b = get_arena_bounds()
|
||||
var new_sticky = []
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
if _is_npc_zone(pos) or _is_boundary(pos):
|
||||
continue
|
||||
if pos.x <= b.min or pos.x >= b.max or pos.y <= b.min or pos.y >= b.max:
|
||||
if not sticky_cells.has(pos):
|
||||
new_sticky.append(pos)
|
||||
|
||||
if new_sticky.size() > 0:
|
||||
if _can_rpc() and multiplayer.is_server():
|
||||
rpc("sync_growth_apply", new_sticky)
|
||||
else:
|
||||
sync_growth_apply(new_sticky)
|
||||
|
||||
func _is_boundary(pos: Vector2i) -> bool:
|
||||
return pos.x == 0 or pos.x == ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == ARENA_ROWS - 1
|
||||
return pos.x <= 0 or pos.x >= ARENA_COLUMNS - 1 or pos.y <= 0 or pos.y >= ARENA_ROWS - 1
|
||||
|
||||
# =============================================================================
|
||||
# Coverage tracking (v2 target: 70-75%, down from v1's 80%)
|
||||
@@ -1104,12 +1084,15 @@ const COVERAGE_TARGET_MAX: float = 0.75
|
||||
|
||||
func playable_cell_count() -> int:
|
||||
"""Number of cells that can ever become sticky (interior, minus NPC zone)."""
|
||||
var b = get_arena_bounds()
|
||||
var count := 0
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos := Vector2i(x, z)
|
||||
if _is_boundary(pos) or _is_npc_zone(pos):
|
||||
continue
|
||||
if pos.x <= b.min or pos.x >= b.max or pos.y <= b.min or pos.y >= b.max:
|
||||
continue
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@@ -1133,6 +1116,9 @@ const FORCED_TRAP_WINDOW: float = 30.0 # final seconds where trapping is allowed
|
||||
|
||||
func _is_cell_passable(pos: Vector2i, extra_sticky: Dictionary = {}) -> bool:
|
||||
"""Can a player stand on / move through this cell, given a hypothetical sticky set?"""
|
||||
var b = get_arena_bounds()
|
||||
if pos.x <= b.min or pos.x >= b.max or pos.y <= b.min or pos.y >= b.max:
|
||||
return false
|
||||
if _is_boundary(pos) or _is_npc_zone(pos):
|
||||
return false
|
||||
if sticky_cells.has(pos) or extra_sticky.has(pos):
|
||||
@@ -1334,11 +1320,11 @@ func _calculate_bubble_score(pos: Vector2i, player_cells: Array = []) -> float:
|
||||
return score
|
||||
|
||||
func _bubble_score_camping(pos: Vector2i) -> float:
|
||||
"""Reward targeting campers. +40 >5s, +60 >8s, +80 >10s-with-cleanser."""
|
||||
"""Reward targeting campers. +40 >5s, +60 >8s, +80 >10s-with-ghost."""
|
||||
var t := _camp_time_for_region(_region_of(pos))
|
||||
if t > 10.0:
|
||||
# Stronger only if a nearby player actually holds a cleanser.
|
||||
if _any_cleanser_holder_near(pos):
|
||||
# Stronger only if a nearby player is in ghost mode.
|
||||
if _any_ghost_player_near(pos):
|
||||
return 80.0
|
||||
return 60.0
|
||||
elif t > 8.0:
|
||||
@@ -1391,22 +1377,25 @@ func _bubble_score_unfair_trap(pos: Vector2i) -> float:
|
||||
func _bubble_blast_cells(center: Vector2i) -> Array:
|
||||
"""The 3x3 (radius 1) sticky cells a bubble at `center` would create,
|
||||
clipped to passable/playable cells."""
|
||||
var b = get_arena_bounds()
|
||||
var cells: Array = []
|
||||
for dx in range(-BUBBLE_EXPLOSION_RADIUS, BUBBLE_EXPLOSION_RADIUS + 1):
|
||||
for dz in range(-BUBBLE_EXPLOSION_RADIUS, BUBBLE_EXPLOSION_RADIUS + 1):
|
||||
var c := center + Vector2i(dx, dz)
|
||||
if _is_boundary(c) or _is_npc_zone(c):
|
||||
continue
|
||||
if c.x <= b.min or c.x >= b.max or c.y <= b.min or c.y >= b.max:
|
||||
continue
|
||||
cells.append(c)
|
||||
return cells
|
||||
|
||||
func _any_cleanser_holder_near(pos: Vector2i) -> bool:
|
||||
"""True if a player holding a Cleanser charge is within the camping region."""
|
||||
func _bubble_footprint(center: Vector2i) -> Array:
|
||||
return _bubble_blast_cells(center)
|
||||
|
||||
func _any_ghost_player_near(pos: Vector2i) -> bool:
|
||||
"""True if a player in ghost mode is within the camping region."""
|
||||
for player in get_tree().get_nodes_in_group("Players"):
|
||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if pid == -1:
|
||||
continue
|
||||
if player_cleansers.get(pid, 0) <= 0:
|
||||
if not player.get("is_invisible"):
|
||||
continue
|
||||
if "current_position" in player and player.current_position != null:
|
||||
if _region_of(player.current_position) == _region_of(pos):
|
||||
@@ -1502,7 +1491,7 @@ func _explode_bubble(center: Vector2i, cells: Array) -> void:
|
||||
if "current_position" in player and player.current_position != null:
|
||||
if blast.has(player.current_position):
|
||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if pid != -1 and is_cleanser_active(pid):
|
||||
if pid != -1 and player.get("is_invisible"):
|
||||
continue
|
||||
apply_sticky_slow(player)
|
||||
|
||||
@@ -1518,6 +1507,8 @@ func sync_bubble_spawn(center: Vector2i, cells: Array) -> void:
|
||||
# Telegraph-style warning overlay on the footprint (still passable).
|
||||
for c in cells:
|
||||
var pos = c as Vector2i
|
||||
if pos.x >= 8 and pos.x <= 10 and pos.y >= 8 and pos.y <= 10: continue
|
||||
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
||||
_spawn_bubble_visual(center)
|
||||
if SfxManager:
|
||||
@@ -1539,6 +1530,8 @@ func sync_bubble_explode(center: Vector2i, cells: Array) -> void:
|
||||
return
|
||||
for c in cells:
|
||||
var pos = c as Vector2i
|
||||
if pos.x >= 8 and pos.x <= 10 and pos.y >= 8 and pos.y <= 10: continue
|
||||
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY)
|
||||
sticky_cells[pos] = true
|
||||
# Medium shake — bubbles hit harder than a normal growth tick.
|
||||
@@ -1605,8 +1598,8 @@ func _check_all_players_trapped() -> void:
|
||||
var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1)
|
||||
if is_sticky_cell(pos):
|
||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if pid != -1 and is_cleanser_active(pid):
|
||||
continue # cleansing players are immune to the slow
|
||||
if pid != -1 and player.get("is_invisible"):
|
||||
continue # ghost players are immune to the slow
|
||||
apply_sticky_slow(player)
|
||||
|
||||
func apply_sticky_slow(player: Node) -> void:
|
||||
@@ -1638,25 +1631,22 @@ func _trap_player(player: Node) -> void:
|
||||
NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING)
|
||||
|
||||
func clear_sticky_cell(pos: Vector2i) -> void:
|
||||
"""Used by Cleanser power-up to remove a sticky cell."""
|
||||
"""Remove a sticky cell (used when ghost player walks through)."""
|
||||
if _can_rpc():
|
||||
if multiplayer.is_server():
|
||||
rpc("sync_clear_sticky_cell", pos)
|
||||
else:
|
||||
sync_clear_sticky_cell(pos) # Predictive local clear
|
||||
rpc("rpc_use_cleanser", pos)
|
||||
else:
|
||||
sync_clear_sticky_cell(pos)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
||||
sticky_cells.erase(pos)
|
||||
mark_cleansed(pos) # temporary regrowth protection (v2)
|
||||
mark_cleansed(pos) # temporary regrowth protection
|
||||
if gridmap:
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
|
||||
|
||||
# Play VFX and SFX
|
||||
_spawn_cleanser_particles(pos)
|
||||
if SfxManager:
|
||||
SfxManager.play("pick_up_power_tile")
|
||||
|
||||
@@ -1664,142 +1654,6 @@ func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
||||
if main_scene and main_scene.has_method("sync_grid_item"):
|
||||
main_scene.sync_grid_item(pos.x, 2, pos.y, -1)
|
||||
|
||||
func _try_use_cleanser() -> void:
|
||||
"""Local player attempts to activate Cleanser for 5-cell sticky immunity."""
|
||||
var local_pid = multiplayer.get_unique_id()
|
||||
var count = player_cleansers.get(local_pid, 0)
|
||||
if count <= 0:
|
||||
return
|
||||
|
||||
# Block activation during stun
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
var local_player = null
|
||||
for p in all_players:
|
||||
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
|
||||
if pid == local_pid:
|
||||
local_player = p
|
||||
break
|
||||
if not local_player:
|
||||
return
|
||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||
return
|
||||
# 0.3s activation delay
|
||||
await get_tree().create_timer(CLEANSER_ACTIVATION_DELAY).timeout
|
||||
|
||||
# Re-validate after delay
|
||||
if not is_instance_valid(local_player):
|
||||
return
|
||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||
return
|
||||
|
||||
# Consume cleanser from inventory (only if client, host relies on rpc)
|
||||
if not multiplayer.is_server():
|
||||
cleanser_active[local_pid] = true
|
||||
cleanser_cells_left[local_pid] = CLEANSER_MAX_CELLS
|
||||
player_cleansers[local_pid] = max(0, player_cleansers[local_pid] - 1)
|
||||
update_cleanser_ui(player_cleansers[local_pid])
|
||||
|
||||
NotificationManager.send_message(local_player, "Cleanser Used! (5 charges)", NotificationManager.MessageType.POWERUP)
|
||||
|
||||
# Sync to server/clients
|
||||
if not multiplayer.is_server() and _can_rpc():
|
||||
rpc("rpc_activate_cleanser", local_pid)
|
||||
elif multiplayer.is_server():
|
||||
# Call RPC logic directly for host (it will set active/cells_left/consume)
|
||||
rpc_activate_cleanser(local_pid)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func deactivate_cleanser(player_id: int) -> void:
|
||||
"""Deactivate cleanser immunity for a player."""
|
||||
cleanser_active.erase(player_id)
|
||||
cleanser_cells_left.erase(player_id)
|
||||
|
||||
func is_cleanser_active(player_id: int) -> bool:
|
||||
"""Check if a player has active cleanser immunity."""
|
||||
return cleanser_active.has(player_id)
|
||||
|
||||
func use_cleanser_cell(player_id: int) -> bool:
|
||||
"""Use one cleanser cell. Returns true if still active, false if exhausted."""
|
||||
if not cleanser_active.has(player_id):
|
||||
return false
|
||||
cleanser_cells_left[player_id] -= 1
|
||||
if cleanser_cells_left[player_id] <= 0:
|
||||
if _can_rpc():
|
||||
rpc("deactivate_cleanser", player_id)
|
||||
else:
|
||||
deactivate_cleanser(player_id)
|
||||
return false
|
||||
return true
|
||||
|
||||
func notify_movement_stopped(player_id: int, pos: Vector2i) -> void:
|
||||
"""Called from PlayerMovementManager when a move chain settles.
|
||||
Previously deactivated cleanser here, but now immunity persists
|
||||
until charges run out to allow repeated use across safe gaps."""
|
||||
pass
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_activate_cleanser(pid: int) -> void:
|
||||
"""RPC for clients to activate cleanser on server."""
|
||||
if multiplayer.is_server():
|
||||
# Verify they actually have a cleanser charge (prevents spam/cheats)
|
||||
if player_cleansers.get(pid, 0) <= 0:
|
||||
return
|
||||
|
||||
# Always apply the state and AoE, since this is the server authority
|
||||
cleanser_active[pid] = true
|
||||
cleanser_cells_left[pid] = CLEANSER_MAX_CELLS
|
||||
player_cleansers[pid] = max(0, player_cleansers[pid] - 1)
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", pid, player_cleansers[pid])
|
||||
|
||||
# NEW: Clear 3x3 area around player
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
var target_player = null
|
||||
for p in all_players:
|
||||
var target_pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
|
||||
if target_pid == pid:
|
||||
target_player = p
|
||||
break
|
||||
|
||||
if gridmap and is_instance_valid(target_player):
|
||||
var map_pos = gridmap.local_to_map(target_player.global_position)
|
||||
var center_pos = Vector2i(map_pos.x, map_pos.z)
|
||||
|
||||
# 3x3 neighborhood
|
||||
for dx in range(-1, 2):
|
||||
for dz in range(-1, 2):
|
||||
var check_pos = center_pos + Vector2i(dx, dz)
|
||||
if is_sticky_cell(check_pos):
|
||||
clear_sticky_cell(check_pos)
|
||||
|
||||
# Remove slow effect for any player in the cleansed area
|
||||
for p in all_players:
|
||||
if is_instance_valid(p) and p.has_method("remove_slow_effect"):
|
||||
if gridmap:
|
||||
var p_map_pos = gridmap.local_to_map(p.global_position)
|
||||
var p_cell_pos = Vector2i(p_map_pos.x, p_map_pos.z)
|
||||
if abs(p_cell_pos.x - center_pos.x) <= 1 and abs(p_cell_pos.y - center_pos.y) <= 1:
|
||||
if _can_rpc():
|
||||
p.rpc("remove_slow_effect")
|
||||
else:
|
||||
p.remove_slow_effect()
|
||||
|
||||
print("[Cleanser] Server cleared 3x3 area around %s for player %d" % [center_pos, pid])
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_use_cleanser(pos: Vector2i) -> void:
|
||||
"""RPC for clients to clear a sticky cell via Cleanser."""
|
||||
if multiplayer.is_server():
|
||||
clear_sticky_cell(pos)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_consume_cleanser(pid: int) -> void:
|
||||
"""RPC for clients to report Cleanser consumption to server."""
|
||||
if multiplayer.is_server():
|
||||
player_cleansers[pid] = 0
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", pid, 0)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func rpc_trigger_slowmo() -> void:
|
||||
"""RPC for clients to request slow-mo from server."""
|
||||
@@ -1886,25 +1740,7 @@ func _setup_hud() -> void:
|
||||
hud_layer.visible = false
|
||||
add_child(hud_layer)
|
||||
phase_label = hud_layer.get_node("BottomContainer/VBoxContainer/PhaseLabel")
|
||||
cleanser_icon = hud_layer.get_node("BottomContainer/VBoxContainer/CleanserHBox/CleanserIcon")
|
||||
cleanser_label = hud_layer.get_node("BottomContainer/VBoxContainer/CleanserHBox/CleanserLabel")
|
||||
slowmo_label = hud_layer.get_node_or_null("TopContainer/SlowMoLabel")
|
||||
_generate_cleanser_icon()
|
||||
|
||||
func _generate_cleanser_icon() -> void:
|
||||
var icon_img = Image.create(16, 16, false, Image.FORMAT_RGBA8)
|
||||
icon_img.fill(Color(0.4, 0.9, 1.0))
|
||||
icon_img.blend_rect(icon_img, Rect2i(2, 2, 12, 12), Vector2i(1, 1))
|
||||
for x in range(16):
|
||||
icon_img.set_pixel(x, 0, Color(0.2, 0.6, 0.7))
|
||||
icon_img.set_pixel(x, 15, Color(0.2, 0.6, 0.7))
|
||||
for y in range(16):
|
||||
icon_img.set_pixel(0, y, Color(0.2, 0.6, 0.7))
|
||||
icon_img.set_pixel(15, y, Color(0.2, 0.6, 0.7))
|
||||
for i in range(4, 12):
|
||||
icon_img.set_pixel(i, 7, Color(1.0, 1.0, 1.0, 0.8))
|
||||
icon_img.set_pixel(7, i, Color(1.0, 1.0, 1.0, 0.8))
|
||||
cleanser_icon.texture = ImageTexture.create_from_image(icon_img)
|
||||
|
||||
func _update_hud_phase(phase_name: String) -> void:
|
||||
if phase_label:
|
||||
@@ -1923,20 +1759,6 @@ func _update_hud_phase(phase_name: String) -> void:
|
||||
# Animate phase label with bounce effect
|
||||
_animate_phase_label()
|
||||
|
||||
func update_cleanser_ui(count: int) -> void:
|
||||
cleanser_count = count
|
||||
if cleanser_label:
|
||||
cleanser_label.text = "[E] Cleanser (%d)" % count
|
||||
# Show/hide icon based on availability
|
||||
if cleanser_icon:
|
||||
cleanser_icon.visible = count > 0
|
||||
if count > 0:
|
||||
# Pulse animation when cleanser is available
|
||||
var tween = create_tween()
|
||||
tween.set_loops(2)
|
||||
tween.tween_property(cleanser_icon, "modulate", Color(1.5, 1.5, 1.5, 1), 0.3)
|
||||
tween.tween_property(cleanser_icon, "modulate", Color.WHITE, 0.3)
|
||||
|
||||
func _animate_phase_label() -> void:
|
||||
"""Animate phase label with bounce effect."""
|
||||
if not phase_label:
|
||||
@@ -1961,7 +1783,7 @@ func _animate_phase_label() -> void:
|
||||
# =============================================================================
|
||||
|
||||
func _on_goal_count_updated(peer_id: int, count: int) -> void:
|
||||
"""Called when a player completes a goal cycle. Grant cleanser every 2 missions."""
|
||||
"""Called when a player completes a goal cycle. Grant ghost powerup every 2 missions."""
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
@@ -1970,34 +1792,29 @@ func _on_goal_count_updated(peer_id: int, count: int) -> void:
|
||||
player_mission_completions[peer_id] = 0
|
||||
player_mission_completions[peer_id] += 1
|
||||
|
||||
# Grant cleanser every 2 missions
|
||||
# Grant ghost powerup every 2 missions
|
||||
var completions = player_mission_completions[peer_id]
|
||||
if completions % 2 == 0:
|
||||
if not player_cleansers.has(peer_id):
|
||||
player_cleansers[peer_id] = 0
|
||||
_grant_ghost_powerup(peer_id)
|
||||
|
||||
# Allow stacking cleanser charges instead of capping at 1
|
||||
player_cleansers[peer_id] += 1
|
||||
emit_signal("cleanser_granted", peer_id)
|
||||
print("[Gauntlet] Player %d granted Cleanser (Total: %d) (mission %d)" % [peer_id, player_cleansers[peer_id], completions])
|
||||
|
||||
# Sync cleanser count to HUD
|
||||
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0))
|
||||
func _grant_ghost_powerup(peer_id: int) -> void:
|
||||
"""Grant the ghost (invisible mode) powerup to a player."""
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for p in all_players:
|
||||
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
|
||||
if pid == peer_id:
|
||||
var stm = p.get_node_or_null("SpecialTilesManager")
|
||||
if stm and stm.has_method("add_powerup_from_item"):
|
||||
stm.add_powerup_from_item(14) # 14 = Ghost / INVISIBLE_MODE
|
||||
emit_signal("ghost_granted", peer_id)
|
||||
print("[Gauntlet] Player %d granted Ghost powerup (mission %d)" % [peer_id, player_mission_completions[peer_id]])
|
||||
NotificationManager.send_message(p, "Ghost Power Earned!", NotificationManager.MessageType.POWERUP)
|
||||
break
|
||||
|
||||
func _on_score_updated(peer_id: int, new_score: int) -> void:
|
||||
"""Called when a player's score is updated."""
|
||||
pass # Score sync handled by GoalsCycleManager
|
||||
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_cleanser_count(peer_id: int, count: int) -> void:
|
||||
"""Sync cleanser count to HUD for specific player."""
|
||||
# Update local player's cleanser UI
|
||||
var local_pid = multiplayer.get_unique_id()
|
||||
if peer_id == local_pid:
|
||||
update_cleanser_ui(count)
|
||||
|
||||
# =============================================================================
|
||||
# Utility
|
||||
# =============================================================================
|
||||
|
||||
@@ -155,19 +155,15 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
player.knock_tekton()
|
||||
return false # Don't move into the tile, just knock
|
||||
|
||||
# If moving into a sticky cell: slow the player (unless cleanser active,
|
||||
# which clears the cell instead). Sticky no longer hard-traps.
|
||||
# If moving into a sticky cell: block movement unless player is in ghost
|
||||
# mode (is_invisible), which lets them bypass sticky tiles in gauntlet.
|
||||
if gm and gm.is_active and gm.is_sticky_cell(grid_position):
|
||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if pid != -1 and gm.is_cleanser_active(pid):
|
||||
# Cleanser immunity: clear sticky cell, use one cell, don't slow
|
||||
gm.clear_sticky_cell(grid_position)
|
||||
gm.use_cleanser_cell(pid)
|
||||
print("[Move] Cleanser cleared sticky cell at %s (%d cells left)" % [grid_position, gm.cleanser_cells_left.get(pid, 0)])
|
||||
if player.get("is_invisible"):
|
||||
# Ghost mode: walk through sticky tile freely
|
||||
print("[Move] Ghost mode bypassed sticky cell at %s" % grid_position)
|
||||
else:
|
||||
print("[Move] Player stepping into sticky cell at %s — slowed" % grid_position)
|
||||
if player.is_multiplayer_authority() or multiplayer.is_server():
|
||||
gm.apply_sticky_slow(player)
|
||||
print("[Move] Failed: Blocked by Gauntlet Sticky cell at %s" % grid_position)
|
||||
return false
|
||||
|
||||
rotate_towards_target(grid_position)
|
||||
|
||||
@@ -342,12 +338,9 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
if main_sticky and main_sticky.get("gauntlet_manager"):
|
||||
var gm_sticky = main_sticky.gauntlet_manager
|
||||
if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos):
|
||||
var push_pid = other_player.get("peer_id") if "peer_id" in other_player else -1
|
||||
if push_pid != -1 and gm_sticky.is_cleanser_active(push_pid):
|
||||
# Cleanser immunity: clear sticky cell, use one cell
|
||||
gm_sticky.clear_sticky_cell(pushed_to_pos)
|
||||
gm_sticky.use_cleanser_cell(push_pid)
|
||||
print("[Move] Cleanser cleared push-into-sticky at %s" % pushed_to_pos)
|
||||
if other_player.get("is_invisible"):
|
||||
# Ghost mode: pushed player bypasses sticky
|
||||
print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos)
|
||||
else:
|
||||
print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos)
|
||||
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
||||
@@ -407,17 +400,6 @@ func _on_movement_finished():
|
||||
emit_signal("movement_finished")
|
||||
else:
|
||||
current_move_direction = Vector2i.ZERO
|
||||
# Gauntlet (#072): a Cleanser ends early once the player rests on a safe
|
||||
# cell. Gated on gm.is_active so other game modes are never affected.
|
||||
var gm = null
|
||||
var main_node = player.get_tree().root.get_node_or_null("Main")
|
||||
if main_node and main_node.get("gauntlet_manager"):
|
||||
gm = main_node.gauntlet_manager
|
||||
if gm and gm.is_active and player.get("current_position") != null:
|
||||
var mpid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if mpid != -1 and gm.is_cleanser_active(mpid):
|
||||
if multiplayer.is_server() or player.is_multiplayer_authority():
|
||||
gm.notify_movement_stopped(mpid, player.current_position)
|
||||
emit_signal("movement_finished")
|
||||
|
||||
func move_to_clicked_position(grid_position: Vector2i) -> bool:
|
||||
|
||||
@@ -547,9 +547,11 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
|
||||
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request)
|
||||
item_id = rng.randi_range(7, 10)
|
||||
elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET):
|
||||
# Gauntlet mode: No power-up tile spawns from world.
|
||||
# Only common tiles (7-10) spawn; Smack/Cleanser are handled separately.
|
||||
item_id = rng.randi_range(7, 10)
|
||||
# Gauntlet mode: mostly common tiles, but ghost (14) can spawn too.
|
||||
if rng.randf() < 0.85:
|
||||
item_id = rng.randi_range(7, 10)
|
||||
else:
|
||||
item_id = 14 # Ghost powerup only
|
||||
else:
|
||||
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
|
||||
if rng.randf() < 0.8:
|
||||
|
||||
Reference in New Issue
Block a user