Compare commits
107 Commits
bullrush
...
experimental
| Author | SHA1 | Date | |
|---|---|---|---|
| 114748a54f | |||
| 0ab00afd37 | |||
| 878ad659db | |||
| 644f2f0f95 | |||
| dcb252f412 | |||
| bd8b81bbc1 | |||
| a2d008a480 | |||
| 260a448a70 | |||
| e73421c9b1 | |||
| 2dd3020d0b | |||
| fbca048aba | |||
| 4c6c8e1587 | |||
| 939d2b0ac2 | |||
| e845c1be25 | |||
| 1f6bdf4d06 | |||
| 36ae0d479d | |||
| c23c5f77bc | |||
| 1691d8ebc9 | |||
| ab93f047ab | |||
| b15e1153e0 | |||
| e6e4c7b6f6 | |||
| d524f23104 | |||
| 7762a82d18 | |||
| 0786638a98 | |||
| 08a527195f | |||
| 4a3637482c | |||
| 8414f48106 | |||
| 7c6f66e821 | |||
| 2643ef24fa | |||
| c47fadf152 | |||
| 4486e4d3fd | |||
| 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 |
@@ -0,0 +1,154 @@
|
|||||||
|
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 zip
|
||||||
|
|
||||||
|
- name: Checkout Code
|
||||||
|
run: |
|
||||||
|
git config --global credential.helper store
|
||||||
|
echo "http://god:${{ 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 update -qq && apt-get install -y -qq unzip curl
|
||||||
|
if [ ! -f /cache/godot_4.7 ]; then
|
||||||
|
echo "Downloading Godot 4.7..."
|
||||||
|
curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_linux.x86_64.zip"
|
||||||
|
unzip -q -o /tmp/godot.zip -d /cache/
|
||||||
|
mv /cache/Godot_v4.7-stable_linux.x86_64 /cache/godot_4.7
|
||||||
|
fi
|
||||||
|
cp /cache/godot_4.7 /usr/local/bin/godot
|
||||||
|
chmod +x /usr/local/bin/godot
|
||||||
|
mkdir -p ~/.local/share/godot/export_templates/4.7.stable
|
||||||
|
if [ ! -f /cache/Godot_v4.7-stable_export_templates.tpz ]; then
|
||||||
|
echo "Downloading templates..."
|
||||||
|
curl -sL -o /cache/Godot_v4.7-stable_export_templates.tpz \
|
||||||
|
"https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_export_templates.tpz"
|
||||||
|
fi
|
||||||
|
cd ~/.local/share/godot/export_templates/4.7.stable
|
||||||
|
unzip -q -o /cache/Godot_v4.7-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: Extract changelog
|
||||||
|
run: |
|
||||||
|
# Extract changelog for this tag version from CHANGELOG_DRAFT.md
|
||||||
|
V="${TAG_NAME#v}"
|
||||||
|
BODY=$(awk -v ver="[$V]" '
|
||||||
|
/^## / { if (found) exit }
|
||||||
|
/^## / && index($0, ver) { found=1; next }
|
||||||
|
found { print }
|
||||||
|
' CHANGELOG_DRAFT.md | sed 's/^ *//')
|
||||||
|
echo "CHANGELOG_BODY<<EOF" >> $GITHUB_ENV
|
||||||
|
echo "$BODY" >> $GITHUB_ENV
|
||||||
|
echo "EOF" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- 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..."
|
||||||
|
# Escape body for JSON
|
||||||
|
BODY_ESCAPED=$(echo "$CHANGELOG_BODY" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read().strip()))" 2>/dev/null || echo '""')
|
||||||
|
RELEASE_JSON=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":$BODY_ESCAPED,\"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,68 @@
|
|||||||
|
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-and-deploy:
|
||||||
|
name: Build & Deploy Patch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository (shallow)
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 http://god:$GITEA_TOKEN@52.74.133.55:3000/danchie/tekton.git .
|
||||||
|
git config user.name "god"
|
||||||
|
git config user.email "god@noreply.git.klud.top"
|
||||||
|
|
||||||
|
- name: Setup Godot (Cached)
|
||||||
|
run: |
|
||||||
|
if [ ! -f /cache/godot_4.7 ]; then
|
||||||
|
echo "Downloading Godot 4.7..."
|
||||||
|
apt-get update -qq && apt-get install -y -qq unzip curl
|
||||||
|
curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_linux.x86_64.zip"
|
||||||
|
unzip -q -o /tmp/godot.zip -d /cache/
|
||||||
|
mv /cache/Godot_v4.7-stable_linux.x86_64 /cache/godot_4.7
|
||||||
|
fi
|
||||||
|
cp /cache/godot_4.7 /usr/local/bin/godot
|
||||||
|
chmod +x /usr/local/bin/godot
|
||||||
|
mkdir -p build
|
||||||
|
|
||||||
|
- name: Generate version.json & bump version
|
||||||
|
env:
|
||||||
|
PATCH_VERSION: ${{ github.event.inputs.version }}
|
||||||
|
PATCH_NOTES: ${{ github.event.inputs.notes }}
|
||||||
|
run: |
|
||||||
|
python3 tools/generate_version_json.py --skip-changelog
|
||||||
|
|
||||||
|
- name: Export patch PCK
|
||||||
|
run: |
|
||||||
|
godot --headless --export-pack "Windows Desktop" build/patch.pck 2>&1 | tail -5
|
||||||
|
|
||||||
|
- name: Push to patches branch
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
mkdir -p patch-deploy
|
||||||
|
cp build/patch.pck patch-deploy/
|
||||||
|
cp assets/data/version.json patch-deploy/
|
||||||
|
cd patch-deploy
|
||||||
|
git init
|
||||||
|
git config user.name "god"
|
||||||
|
git config user.email "god@noreply.git.klud.top"
|
||||||
|
git remote add origin http://god:$GITEA_TOKEN@52.74.133.55:3000/danchie/tekton.git
|
||||||
|
git checkout -b patches
|
||||||
|
git add .
|
||||||
|
git commit -m "patch ${{ github.event.inputs.version }}"
|
||||||
|
git push -f origin patches
|
||||||
@@ -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/
|
.agent/
|
||||||
_daily_basis/
|
_daily_basis/
|
||||||
_daily_changes/
|
_daily_changes/
|
||||||
|
tools/gitea-kanban
|
||||||
build/
|
build/
|
||||||
|
label_mapping.json
|
||||||
|
milestone_mapping.json
|
||||||
|
|
||||||
/android/
|
/android/
|
||||||
.tmp
|
.tmp
|
||||||
|
|||||||
+28
-1
@@ -1,3 +1,20 @@
|
|||||||
|
## [NEXT]
|
||||||
|
- Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions.
|
||||||
|
- Fixed punch/smack SFX playing repeatedly when quickly attacking another player.
|
||||||
|
- Fixed currency split between Gacha and Shop — wallet balance now updates immediately after purchase without needing to reopen the panel.
|
||||||
|
- Fixed fatal crash in Multiplayer Gauntlet caused by missing `has_method` check on smack cooldown timers on remote peers.
|
||||||
|
- Upgraded engine from Godot 4.6 to 4.7 stable for better performance and stability.
|
||||||
|
- Migrated patch delivery from GitHub Pages to Gitea native raw endpoint — no more external dependencies for game updates.
|
||||||
|
- CI: optimized build cache for Godot binary, eliminating repeated 140MB downloads on every workflow run.
|
||||||
|
- CI: shallow repository checkout (--depth 1) for faster clone times.
|
||||||
|
- CI: removed unnecessary export template download from patch deployment workflow (--export-pack doesn't need them).
|
||||||
|
|
||||||
|
## [2.4.3] — 2026-07-04
|
||||||
|
- Fixed multiplayer desync issue where the playerboard UI would get stuck and not refresh properly.
|
||||||
|
- Fixed a bug where attacking or pushing another player would cause the smack/punch sound effect to play repeatedly.
|
||||||
|
- Fixed an issue where the Gacha panel's currency balance would not update immediately after purchasing currency in the Shop.
|
||||||
|
- Fixed a fatal client crash in Multiplayer Gauntlet mode caused by smack cooldown timers.
|
||||||
|
|
||||||
## [2.4.0] — 2026-06-18
|
## [2.4.0] — 2026-06-18
|
||||||
- Rebuilt the **Gauntlet** game mode from the ground up — new wave-based mechanics, arena redesign, and tighter difficulty scaling.
|
- Rebuilt the **Gauntlet** game mode from the ground up — new wave-based mechanics, arena redesign, and tighter difficulty scaling.
|
||||||
- Added **freeze-area VFX** — the freeze powerup now shows a visible icy floor spread across the affected tiles.
|
- Added **freeze-area VFX** — the freeze powerup now shows a visible icy floor spread across the affected tiles.
|
||||||
@@ -152,7 +169,17 @@
|
|||||||
- Fragment Craft system — collect drops to craft exclusive skins
|
- Fragment Craft system — collect drops to craft exclusive skins
|
||||||
- Fixed boot screen stuck on "Checking versions..."
|
- 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 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.
|
- 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.
|
- Center Candy Cannon now shoots actual projectiles that fly towards sticky cells and leave a VFX trail behind them.
|
||||||
|
|||||||
@@ -1,141 +1,37 @@
|
|||||||
# Tekton Dash Armageddon
|
# 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
|
## Clone
|
||||||
|
|
||||||
### Creating a Skin Material
|
SSH is preferred:
|
||||||
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.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
#### 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)
|
|
||||||
|
|
||||||
#### 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
|
```bash
|
||||||
nano ~/tekton_admin.js
|
git clone git@ssh.git.klud.top:danchie/tekton.git
|
||||||
# Or use micro (recommended): micro ~/tekton_admin.js
|
|
||||||
```
|
```
|
||||||
Paste the copied contents and save the file.
|
|
||||||
- **Find your Nakama Container ID:**
|
HTTPS also works:
|
||||||
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
|
```bash
|
||||||
# Replace ed21ac5d442a with your actual Container ID
|
git clone https://git.klud.top/danchie/tekton.git
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pushing a New Version (Automated Patching)
|
First-time SSH setup:
|
||||||
When you're ready to deploy new features or assets to players:
|
|
||||||
- Document your changes in `CHANGELOG_DRAFT.md` using player-friendly language.
|
- [Linux](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Linux)
|
||||||
- Run the version generation script from the terminal:
|
- [macOS](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-macOS)
|
||||||
|
- [Windows](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Windows)
|
||||||
|
|
||||||
|
SSH test:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python generate_version_json.py --bump patch
|
ssh -T git@ssh.git.klud.top
|
||||||
```
|
|
||||||
*(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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Expected output:
|
||||||
|
|
||||||
## 🚀 Ongoing Features (Incoming)
|
```text
|
||||||
|
Hi there, <yourname>! You've successfully authenticated with the key named <key-title>, but Gitea does not provide shell access.
|
||||||
### 🎰 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,174 +0,0 @@
|
|||||||
# AI Agent Workflow Guide for Tekton Dash
|
|
||||||
|
|
||||||
This document tells AI agents how to work on Tekton Dash tasks end-to-end.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Task Source: Notion MCP
|
|
||||||
|
|
||||||
All tasks live on the **"TektonDash - Armageddon PR Tasks"** Notion board.
|
|
||||||
|
|
||||||
https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36433be43b2980de8635000c0a910a0d
|
|
||||||
|
|
||||||
### Finding Tasks
|
|
||||||
|
|
||||||
**CRITICAL:** Always start by finding tasks from Notion. Priority order: P0 > P1 > P2 > P3. Status order: In Progress > To Do.
|
|
||||||
|
|
||||||
Example search for "Gauntlet" tasks:
|
|
||||||
```
|
|
||||||
Use: mcp_notion_API_post_search
|
|
||||||
query: "[Gauntlet]" or task name
|
|
||||||
filter: {"property": "object", "value": "page"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reading a Task
|
|
||||||
|
|
||||||
**CRITICAL:** The **Description** field contains the actual implementation requirements. TASKS.md defines workflow procedure only — each task's unique problem and solution are in Notion's Description field.
|
|
||||||
|
|
||||||
Always read full task details:
|
|
||||||
```
|
|
||||||
Use: mcp_notion_API_retrieve_a_page
|
|
||||||
page_id: "<task_page_id_from_search>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Each task page has these properties:
|
|
||||||
|
|
||||||
| Property | Type | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| **Name** | title | Task title, e.g. `[Gauntlet] #1 Game Mode Registration` |
|
|
||||||
| **Status** | select | `To Do` → `In Progress` → `Done` |
|
|
||||||
| **Priority** | select | `P0` (critical) / `P1` / `P2` / `P3` |
|
|
||||||
| **Effort** | select | `S - Small` / `M - Medium` / `L - Large` / `XL - Epic` |
|
|
||||||
| **Sprint** | select | `Alpha` / `Beta` / `Release` |
|
|
||||||
| **ProjectType** | select | `CORE` / `CLIENT` / `SERVER` / `INFRA` |
|
|
||||||
| **Description** | rich_text | Full task description — **read this to understand what to do** |
|
|
||||||
| **Acceptance** | checkbox | Check when task is verified complete |
|
|
||||||
| **DueDate** | date | Optional deadline |
|
|
||||||
| **UnitTest** | date | Optional test completion date |
|
|
||||||
|
|
||||||
### Task Lifecycle
|
|
||||||
|
|
||||||
```
|
|
||||||
To Do → In Progress → Done
|
|
||||||
```
|
|
||||||
|
|
||||||
**CRITICAL WORKFLOW:**
|
|
||||||
|
|
||||||
1. **Read task from Notion**: Use `mcp_notion_API_retrieve_a_page(page_id="...")` to get full task details
|
|
||||||
2. **Read Description field carefully**: This contains the actual implementation requirements — file names, function signatures, integration points, RPC patterns, etc.
|
|
||||||
3. **Implement exactly what Description specifies**: Don't invent your own approach — follow the Description's technical requirements
|
|
||||||
4. **Write unit tests**: Follow pattern in `tests/` directory (see GUT_SETUP_SKILLS.md)
|
|
||||||
5. **Update changelog**: Add entry to `CHANGELOG_DRAFT.md` (consumer-facing language, not technical jargon)
|
|
||||||
6. **Version management**: Check git diff for existing version changes (see Version Bumping section below)
|
|
||||||
7. **Mark complete in Notion**: Set `Status` → `Done`, check `Acceptance` ✅
|
|
||||||
|
|
||||||
```
|
|
||||||
Use: mcp_notion_API_patch_page
|
|
||||||
page_id: "<task_page_id>"
|
|
||||||
properties: {"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Code Structure
|
|
||||||
|
|
||||||
| Path | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `scripts/game_mode.gd` | GameMode enum + helpers (add new modes here) |
|
|
||||||
| `scripts/managers/` | All game mode managers (lobby, stop_n_go, portal, gauntlet) |
|
|
||||||
| `scenes/main.gd` | Central orchestrator — init, setup, game start routing |
|
|
||||||
| `tests/` | GUT unit tests — one file per task/feature |
|
|
||||||
|
|
||||||
### Adding a New Game Mode
|
|
||||||
|
|
||||||
1. Add enum to `scripts/game_mode.gd` → update `from_string()`, `mode_to_string()`, `get_all_modes()`, `is_restricted()`
|
|
||||||
2. Add mode name to `LobbyManager.available_game_modes` in `lobby_manager.gd`
|
|
||||||
3. Add arena name to `_update_available_areas()` in `lobby_manager.gd`
|
|
||||||
4. Add manager var + init branch in `main.gd` `_init_managers()`
|
|
||||||
5. Add setup branch in `_setup_host_game()` and `_setup_client_game()`
|
|
||||||
6. Add start branch in `_start_game()`
|
|
||||||
7. Add background in `_apply_arena_background()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Unit Testing
|
|
||||||
|
|
||||||
### Pattern
|
|
||||||
|
|
||||||
All tests extend `GutTest` and live in `tests/`. Naming: `test_<feature>.gd`
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
extends GutTest
|
|
||||||
|
|
||||||
func before_all():
|
|
||||||
gut.p("=== Feature Tests [Task ID] ===")
|
|
||||||
|
|
||||||
func test_something():
|
|
||||||
assert_eq(actual, expected, "Description")
|
|
||||||
|
|
||||||
func after_all():
|
|
||||||
gut.p("=== Feature Tests Complete ===")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
run_tests.cmd # all tests
|
|
||||||
run_tests.cmd test_gauntlet_registration # specific test
|
|
||||||
```
|
|
||||||
|
|
||||||
Reports saved to `test_reports/` with timestamps.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Version Bumping
|
|
||||||
|
|
||||||
**Before bumping, check git for existing uncommitted version changes:**
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
git diff --cached -- project.godot CHANGELOG_DRAFT.md
|
|
||||||
git diff -- project.godot CHANGELOG_DRAFT.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### If version changes already exist (staged or unstaged):
|
|
||||||
→ **APPEND** your changelog bullet to the existing version block in `CHANGELOG_DRAFT.md`
|
|
||||||
→ **DO NOT** bump `project.godot` or `export_presets.cfg` — you're joining an in-progress batch
|
|
||||||
|
|
||||||
### If NO version changes exist (clean state):
|
|
||||||
→ **BUMP** version (increment patch: `2.3.5` → `2.3.6`)
|
|
||||||
→ **UPDATE** all locations below
|
|
||||||
|
|
||||||
Version appears in **4 locations** — all must match:
|
|
||||||
|
|
||||||
| File | Field |
|
|
||||||
|---|---|
|
|
||||||
| `CHANGELOG_DRAFT.md` | `## [X.Y.Z] — YYYY-MM-DD` header |
|
|
||||||
| `project.godot` | `config/version="X.Y.Z"` |
|
|
||||||
| `export_presets.cfg` | `application/file_version` and `application/product_version` (per preset) |
|
|
||||||
| `export_presets.cfg` | `export_path` filenames containing version |
|
|
||||||
| `export_presets.cfg` | `version/name` (Android preset) |
|
|
||||||
|
|
||||||
### Changelog Style
|
|
||||||
|
|
||||||
Entries are **consumer-facing** (readable by players). No internal jargon.
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## [2.3.6] — 2026-05-22
|
|
||||||
- Added new game mode: Candy Cannon Survival
|
|
||||||
```
|
|
||||||
|
|
||||||
**Bad:** "Added GAUNTLET = 3 to GameMode.Mode enum"
|
|
||||||
**Good:** "Added new game mode: Candy Cannon Survival"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Key Conventions
|
|
||||||
|
|
||||||
- **Caveman Mode**: Be terse. No filler. Execute first, talk second.
|
|
||||||
- **Read task from Notion FIRST**: Always use `mcp_notion_API_retrieve_a_page` to get the Description field before implementing
|
|
||||||
- **Description field is the spec**: TASKS.md is workflow procedure only. Each task's unique requirements (file names, function signatures, RPC patterns, integration points) are in Notion's Description field
|
|
||||||
- **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files
|
|
||||||
- **Notion status flow**: `To Do` → `In Progress` → `Done` (never skip steps)
|
|
||||||
- **Test everything**: Every completed task gets a `test_<feature>.gd` in `tests/`
|
|
||||||
- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`
|
|
||||||
- **Version discipline**: Check `git diff -- project.godot CHANGELOG_DRAFT.md` before bumping version (see Version Bumping section)
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Tekton Game - TODO
|
||||||
|
|
||||||
|
## Bugs to fix (checked 2026-07-04)
|
||||||
|
|
||||||
|
- [x] **Playerboard desync** — doesn't refresh correctly, stuck (fixed sync ready order)
|
||||||
|
- [x] **Multiple punch SFX** — sounds play more than once per punch (fixed charged strike state clearance)
|
||||||
|
- [x] **Currency not shared** — gacha wallet and shop wallet are separate (fixed UI state sync)
|
||||||
|
- [x] **Multiplayer gauntlet mode broken** — single player works, multiplayer doesn't (fixed client crash from bad indentation)
|
||||||
|
|
||||||
|
## In Progress
|
||||||
|
|
||||||
|
- [ ] CI release pipeline (`docker network connect` approach pending verification)
|
||||||
|
|
||||||
|
## Done
|
||||||
|
|
||||||
|
- [x] Cleanup test runs 96-99
|
||||||
|
- [x] Remove test tag `v9.9.9-test`
|
||||||
|
- [x] Remove orphan Docker containers/networks/volumes
|
||||||
|
- [x] Prune dangling images (~7.7GB reclaimed)
|
||||||
|
- [x] Tailscale status check (peer traffic works; coordination-server sync is the only red)
|
||||||
@@ -55,8 +55,7 @@ func _ready():
|
|||||||
# Safety check: Don't auto-randomize if game mode manages its own arena
|
# Safety check: Don't auto-randomize if game mode manages its own arena
|
||||||
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
||||||
and get_node_or_null("/root/LobbyManager") \
|
and get_node_or_null("/root/LobbyManager") \
|
||||||
and (get_node("/root/LobbyManager").game_mode == "Stop n Go" \
|
and (get_node("/root/LobbyManager").game_mode == "Stop n Go")):
|
||||||
or get_node("/root/LobbyManager").game_mode == "Tekton Doors")):
|
|
||||||
randomize_grid()
|
randomize_grid()
|
||||||
validate_item_indices()
|
validate_item_indices()
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Bob.glb-b36d843833d2bf8fe73ce6b24284a2e6.scn"
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -50,3 +52,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Gatot.glb-7ed2e6cfe1354f044d634ce57f159a9a.sc
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -50,3 +52,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Masbro.glb-c019c78827ce632933ba37f4b2937305.s
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -50,3 +52,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Oldpop.glb-c0496f43d11bd79e0865e1e20da606da.s
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -50,3 +52,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/animation-0.glb-c294d3c96ec1222f9f04a65d47868
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -46,3 +48,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/animation.glb-d28e509f062b0ed9227a1d97e8075ed
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=0
|
gltf/naming_version=0
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/ted_mesh.glb-3a244082b66ad864a2330884dc3cfef0
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton_chuck.glb-635c14eb795891e496e379a5e8e8
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton_fishing_animation.glb-4469ef86e01e801d
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton_nest.glb-36c2a8dfaddd466203be329bdc5cb
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton_throwing_tiles.glb-ed040127419938b4e09
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
{
|
{
|
||||||
"latest_version": "2.4.1",
|
"latest_version": "2.4.3",
|
||||||
"minimum_app_version": "2.1.0",
|
"minimum_app_version": "2.1.0",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "2.4.3",
|
||||||
|
"date": "2026-07-04",
|
||||||
|
"pck_url": "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck",
|
||||||
|
"pck_size": 0,
|
||||||
|
"changelog": [
|
||||||
|
"Fixed multiplayer desync issue where the playerboard UI would get stuck and not refresh properly.",
|
||||||
|
"Fixed a bug where attacking or pushing another player would cause the smack/punch sound effect to play repeatedly.",
|
||||||
|
"Fixed an issue where the Gacha panel's currency balance would not update immediately after purchasing currency in the Shop.",
|
||||||
|
"Fixed a fatal client crash in Multiplayer Gauntlet mode caused by smack cooldown timers."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"version": "2.4.1",
|
||||||
"date": "2026-06-28",
|
"date": "2026-06-28",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Terrainv2.gltf-6439acacda69bfc8b96d1611ca8f5f
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Gauntlet terrain.gltf-2ceb7292a7914d9403e396e
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/Terrain.gltf-c727a544f574f1f4cd0808dd390e94ae
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/character_pointer.glb-104658ccca94ef9661e3256
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/block.glb-fb6bc8a4474a482c37edd6c5ac8ce3c9.sc
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -52,3 +54,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
Binary file not shown.
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton.glb-cf03c9120f9a286ceda76a1bd03efb7b.s
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tekton_walking.glb-4042e6c5856f09fa2cc375563a
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tile.glb-a2c57836b49962e6adb25601bd8d4a59.scn
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -52,3 +54,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
Binary file not shown.
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/tiles_armagedon.glb-f07b282b31fcdfdfd952d6096
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -123,3 +125,4 @@ _subresources={
|
|||||||
}
|
}
|
||||||
gltf/naming_version=1
|
gltf/naming_version=1
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/box_block.gltf-4e37264bb9b1903a1ad0284c855511
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/wall_animation.fbx-22f993a05720796858e5daa12a
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/wheat_block_0.glb-35fd252da9f0a42b79ed5759815
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/wheat_block_1.glb-5d996ad9d4004837507f80c51b7
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/stand_1.gltf-361364203c5fe17f6842f22d99f7d2d8
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ dest_files=["res://.godot/imported/stand_2.gltf-aa7bce8b5c8fdf784210ca33e5e3c47f
|
|||||||
nodes/root_type=""
|
nodes/root_type=""
|
||||||
nodes/root_name=""
|
nodes/root_name=""
|
||||||
nodes/root_script=null
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
nodes/apply_root_scale=true
|
nodes/apply_root_scale=true
|
||||||
nodes/root_scale=1.0
|
nodes/root_scale=1.0
|
||||||
nodes/import_as_skeleton_bones=false
|
nodes/import_as_skeleton_bones=false
|
||||||
@@ -40,3 +42,4 @@ materials/extract_path=""
|
|||||||
_subresources={}
|
_subresources={}
|
||||||
gltf/naming_version=2
|
gltf/naming_version=2
|
||||||
gltf/embedded_image_handling=1
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=0
|
||||||
|
|||||||
+140
-357
@@ -1,422 +1,205 @@
|
|||||||
# Steamworks Setup Guide for Tekton Armageddon
|
# Steamworks Setup Guide for Tekton Armageddon
|
||||||
|
|
||||||
This guide explains how to set up Steamworks for Windows, Mac, and Linux builds while using Nakama for mobile platforms (Android/iOS) for leaderboards, achievements, and shop functionality.
|
> **Current status: Steam builds are not active.** All platforms (Windows, Linux, macOS, Android) use Nakama as their sole backend. GodotSteam GDExtension exists in `addons/godotsteam/` but is **not enabled** in the project. This doc explains the current Nakama-only architecture and how to re-enable Steam if needed in the future.
|
||||||
|
|
||||||
## Overview
|
---
|
||||||
|
|
||||||
- **Desktop (Windows/Mac/Linux)**: Single build that detects Steam at runtime
|
## Current Architecture (Nakama-Only)
|
||||||
- If launched through Steam: Steam login available for Nakama registration; all features use Nakama
|
|
||||||
- If launched standalone: Uses Nakama for all features
|
|
||||||
- **Mobile (Android/iOS)**: Uses Nakama for all backend services
|
|
||||||
- **Unified Backend**: All platforms use Nakama for achievements, leaderboards, and shop
|
|
||||||
- **Steam Integration**: Steam is only used for authentication (auth session ticket for Nakama login)
|
|
||||||
|
|
||||||
## Prerequisites
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
### Steamworks Setup
|
│ Game Client │
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
1. **Steam Partner Account**
|
│ │ BackendService (autoload) │ │
|
||||||
- You need a Steam Partner account to access Steamworks
|
│ │ ┌─────────────┐ ┌──────────────────┐ │ │
|
||||||
- Apply at: https://partner.steamgames.com/
|
│ │ │ NakamaManager│ │steamworks_manager│ │ │
|
||||||
|
│ │ │ (active) │ │ (dormant) │ │ │
|
||||||
2. **Steam App ID**
|
│ │ └─────────────┘ └──────────────────┘ │ │
|
||||||
- Create a new app in Steamworks
|
│ └──────────────────────────────────────────┘ │
|
||||||
- Note your App ID (e.g., 480)
|
└──────────────────────┬───────────────────────────┘
|
||||||
- Update `steam_app_id` in `scripts/services/steamworks_manager.gd`
|
│
|
||||||
|
┌────────▼────────┐
|
||||||
3. **Steamworks SDK**
|
│ Nakama Server │
|
||||||
- Download the Steamworks SDK from Steamworks
|
│ (RPC backend) │
|
||||||
- The GodotSteam plugin includes the SDK, but you may need it for reference
|
│ achievements │
|
||||||
|
│ leaderboards │
|
||||||
## Installation Steps
|
│ shop/economy │
|
||||||
|
│ auth │
|
||||||
### 1. Install GodotSteam GDExtension
|
│ multiplayer │
|
||||||
|
└─────────────────┘
|
||||||
**Option A: Via Godot Asset Library (Recommended)**
|
|
||||||
1. Open Godot Editor
|
|
||||||
2. Go to `Project > Asset Library`
|
|
||||||
3. Search for "GodotSteam GDExtension 4.4+"
|
|
||||||
4. Download and install the plugin
|
|
||||||
5. Restart Godot Editor
|
|
||||||
|
|
||||||
**Option B: Manual Installation**
|
|
||||||
1. Download from: https://codeberg.org/godotsteam/godotsteam/releases
|
|
||||||
2. Download version 4.18.1 (compatible with Godot 4.6.2)
|
|
||||||
3. Extract to `addons/godotsteam/` in your project
|
|
||||||
4. Enable the plugin in `Project > Project Settings > Plugins`
|
|
||||||
|
|
||||||
### 2. Configure Steamworks in Project
|
|
||||||
|
|
||||||
1. **Enable the Plugin**
|
|
||||||
- Go to `Project > Project Settings > Plugins`
|
|
||||||
- Enable "GodotSteam"
|
|
||||||
|
|
||||||
2. **Set Steam App ID**
|
|
||||||
- Edit `scripts/services/steamworks_manager.gd`
|
|
||||||
- Change `steam_app_id` to your Steam App ID:
|
|
||||||
```gdscript
|
|
||||||
var steam_app_id: int = YOUR_APP_ID_HERE
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Add BackendService as Autoload**
|
- **All backends use Nakama** — achievements, leaderboards, shop, authentication, multiplayer.
|
||||||
- Go to `Project > Project Settings > Autoload`
|
- **Platform detection** (`BackendService._detect_platform()`) distinguishes:
|
||||||
- Add `BackendService` with path: `res://scripts/services/backend_service.gd`
|
- `MOBILE_NAKAMA` — Android/iOS (`OS.has_feature("android")` or `OS.has_feature("ios")`)
|
||||||
- Enable it as a singleton
|
- `DESKTOP_NAKAMA` — Windows/Linux/macOS (default when GodotSteam not loaded)
|
||||||
|
- `DESKTOP_STEAM` — only activated if `ClassDB.class_exists("Steam")` at runtime
|
||||||
|
- **GodotSteam GDExtension** is present in `addons/godotsteam/` but **not enabled** in Project Settings > Plugins. It is shipped in the CI export (Windows only, `|| true` on copy failure) but does not execute.
|
||||||
|
- **No Steam App ID configured.** The `steam_app_id` in `steamworks_manager.gd` defaults to `480` (generic test ID) via `ProjectSettings.get_setting("steam/initialization/app_id", 480)`.
|
||||||
|
- **No Steam Partner account, no Steamworks app, no Steam build pipelines** are active.
|
||||||
|
|
||||||
### 3. Configure Steamworks Features
|
---
|
||||||
|
|
||||||
#### Achievements
|
## Export Presets
|
||||||
|
|
||||||
1. **Define Achievements in Steamworks**
|
The project has 4 export presets in `export_presets.cfg`:
|
||||||
- Go to Steamworks > Your App > Achievements
|
|
||||||
- Create achievements with API names (e.g., "first_win", "level_10")
|
|
||||||
- Set display names, descriptions, and icons
|
|
||||||
|
|
||||||
2. **Use in Code**
|
| Preset | Name | Platform | Export Path |
|
||||||
```gdscript
|
|--------|------|----------|-------------|
|
||||||
# Unlock an achievement
|
| preset.0 | Windows Desktop | Windows | `build/windows/tekton_armageddon_v2.4.3.exe` |
|
||||||
BackendService.unlock_achievement("first_win")
|
| preset.1 | Android | Android | `build/tekton-dash-armageddon-v.2.4.3.apk` |
|
||||||
|
| preset.2 | macOS | macOS | `build/tekton_armageddon_v2.4.3.zip` |
|
||||||
|
| preset.3 | Linux/X11 | Linux | `build/linux/tekton_armageddon_v2.4.3.x86_64` |
|
||||||
|
|
||||||
# Set progress (for progress-based achievements)
|
No separate Steam vs. Non-Steam presets. All presets produce the same Nakama-only build.
|
||||||
BackendService.set_achievement_progress("kill_100_enemies", current_kills, 100)
|
|
||||||
|
|
||||||
# Check achievement status
|
### CI Export Flow (`.gitea/workflows/ci.yml`)
|
||||||
var progress = BackendService.get_achievement_progress("first_win")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Leaderboards
|
On tag push (`v*`), the CI pipeline:
|
||||||
|
|
||||||
1. **Define Leaderboards in Steamworks**
|
1. **Checkout** code at tag
|
||||||
- Go to Steamworks > Your App > Leaderboards
|
2. **Setup Godot 4.7** (cached)
|
||||||
- Create leaderboards with API names (e.g., "high_score", "fastest_time")
|
3. **Export Windows** → `build/windows/tekton_armageddon_windows.exe` (copies `libgodotsteam*` DLLs if present, failure ignored with `|| true`)
|
||||||
- Set sort order (ascending/descending) and display type
|
4. **Export Linux** → `build/linux/tekton_armageddon_linux.x86_64`
|
||||||
|
5. **Export macOS** → `build/macos/tekton_armageddon_macos.zip`
|
||||||
|
6. **Extract changelog** from `CHANGELOG_DRAFT.md`
|
||||||
|
7. **Create Gitea release** and upload all 3 platform zips
|
||||||
|
8. **Publish** release (draft → published)
|
||||||
|
|
||||||
2. **Use in Code**
|
The CI does **not** upload to Steam. All builds are distributed via Gitea releases (https://git.klud.top/danchie/tekton/releases).
|
||||||
```gdscript
|
|
||||||
# Submit a score
|
|
||||||
BackendService.submit_leaderboard_score("high_score", 1000)
|
|
||||||
|
|
||||||
# Get leaderboard entries
|
---
|
||||||
BackendService.leaderboard_entries_loaded.connect(_on_leaderboard_loaded)
|
|
||||||
BackendService.get_leaderboard_entries("high_score", 1, 10)
|
|
||||||
|
|
||||||
func _on_leaderboard_loaded(leaderboard_id: String, entries: Array):
|
## Platform Detection
|
||||||
for entry in entries:
|
|
||||||
print("Player: %s, Score: %d" % [entry.player_name, entry.score])
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Shop (Steam Inventory)
|
The `BackendService` autoload (`scripts/services/backend_service.gd`) detects platform at startup:
|
||||||
|
|
||||||
**Note**: Steam shop functionality requires additional setup with Steam Inventory Service or Steam Microtransactions. This is a complex feature that requires:
|
|
||||||
|
|
||||||
1. **Steam Inventory Service Setup**
|
|
||||||
- Define items in Steamworks > Your App > Inventory
|
|
||||||
- Set item types, prices, and properties
|
|
||||||
- Implement purchase callbacks in `steamworks_manager.gd`
|
|
||||||
|
|
||||||
2. **Alternative**: Use external payment processor for desktop and sync with Nakama
|
|
||||||
|
|
||||||
### 4. Export Presets
|
|
||||||
|
|
||||||
The project includes export presets for all platforms:
|
|
||||||
|
|
||||||
#### Desktop Builds (single build for Steam and standalone)
|
|
||||||
- **Windows Desktop** (preset.0) → `build/tekton_armageddon_v2.1.7.exe`
|
|
||||||
- **macOS** (preset.2) → `build/tekton_armageddon_v2.1.7.zip`
|
|
||||||
- **Linux/X11** (preset.3) → `build/tekton_armageddon_v2.1.7.x86_64`
|
|
||||||
|
|
||||||
#### Mobile Builds
|
|
||||||
- **Android** (preset.1) → `build/tekton-dash-armageddon-v.2.1.5.apk`
|
|
||||||
|
|
||||||
**Note**: Desktop builds are universal - the same executable works on both Steam and standalone. The game detects whether it's running through Steam at runtime and switches backends accordingly.
|
|
||||||
|
|
||||||
#### Configure macOS Export
|
|
||||||
|
|
||||||
1. **Code Signing** (for distribution)
|
|
||||||
- Get an Apple Developer certificate
|
|
||||||
- Update `codesign/identity` in export preset
|
|
||||||
- Set `codesign/enable` to `true`
|
|
||||||
|
|
||||||
2. **Architecture**
|
|
||||||
- Currently set to "universal" (Intel + Apple Silicon)
|
|
||||||
- Can be changed to "x86_64" or "arm64" if needed
|
|
||||||
|
|
||||||
#### Configure Linux Export
|
|
||||||
|
|
||||||
1. **Architecture**
|
|
||||||
- Currently set to "x86_64"
|
|
||||||
- Add ARM64 preset if needed for Linux ARM devices
|
|
||||||
|
|
||||||
### 5. Platform Detection
|
|
||||||
|
|
||||||
The `BackendService` automatically detects the platform and backend:
|
|
||||||
|
|
||||||
```gdscript
|
```gdscript
|
||||||
# Detection logic in BackendService
|
func _detect_platform() -> void:
|
||||||
if OS.has_feature("android") or OS.has_feature("ios"):
|
if OS.has_feature("android") or OS.has_feature("ios"):
|
||||||
# Mobile → Nakama
|
current_platform = Platform.MOBILE_NAKAMA
|
||||||
elif OS.has_feature("steam"):
|
|
||||||
# Desktop → Steamworks
|
|
||||||
else:
|
else:
|
||||||
# Desktop → Local storage (non-Steam builds)
|
if ClassDB.class_exists("Steam"):
|
||||||
|
current_platform = Platform.DESKTOP_STEAM
|
||||||
|
else:
|
||||||
|
current_platform = Platform.DESKTOP_NAKAMA
|
||||||
```
|
```
|
||||||
|
|
||||||
You can check the current platform:
|
Since GodotSteam is not enabled, desktop builds always hit `DESKTOP_NAKAMA`. The `steamworks_manager.gd` is only loaded when `DESKTOP_STEAM` is selected.
|
||||||
|
|
||||||
|
Check current platform at runtime:
|
||||||
```gdscript
|
```gdscript
|
||||||
print("Platform: %s" % BackendService.get_platform_name())
|
print("Platform: %s" % BackendService.get_platform_name())
|
||||||
print("Initialized: %s" % BackendService.is_initialized())
|
print("Initialized: %s" % BackendService.is_initialized())
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Platform Types
|
### Platform Types
|
||||||
|
|
||||||
- **DESKTOP_STEAM**: Running through Steam client (Steam login available, all features use Nakama)
|
| Value | Trigger | Backend |
|
||||||
- **DESKTOP_NAKAMA**: Desktop build not running through Steam (uses Nakama)
|
|-------|---------|---------|
|
||||||
- **MOBILE_NAKAMA**: Android/iOS (uses Nakama)
|
| `DESKTOP_NAKAMA` | Desktop build, GodotSteam not loaded | Nakama only |
|
||||||
|
| `DESKTOP_STEAM` | Desktop build, GodotSteam loaded + Steam running | Nakama + Steam auth |
|
||||||
|
| `MOBILE_NAKAMA` | Android/iOS feature detected | Nakama only |
|
||||||
|
|
||||||
#### Runtime Detection
|
---
|
||||||
|
|
||||||
The game automatically detects the launch method:
|
## Local Testing
|
||||||
- If `OS.has_feature("steam")` is true → Steam login available, all features use Nakama
|
|
||||||
- Otherwise → All features use Nakama
|
|
||||||
|
|
||||||
This means the same desktop build can be:
|
### Nakama Backend (All Platforms)
|
||||||
- Uploaded to Steam (Steam login enabled, all data stored in Nakama)
|
|
||||||
- Distributed standalone (all data stored in Nakama)
|
|
||||||
|
|
||||||
### Steam Login for Nakama
|
1. Ensure Nakama server is running locally (see `server/docker-compose.yaml`)
|
||||||
|
2. Launch the game from Godot Editor (press F5)
|
||||||
|
3. Log in with email/password or device ID
|
||||||
|
4. Test achievements, leaderboards, shop via game UI
|
||||||
|
5. Check console for `"BackendService: Initialized Nakama backend"`
|
||||||
|
|
||||||
When running through Steam, players can use their Steam account to register or log in to Nakama. This is the **only** Steam integration - all game features (achievements, leaderboards, shop) use Nakama.
|
### Force-Specific Platform for Testing
|
||||||
|
|
||||||
**How it works:**
|
Temporarily override platform in `BackendService._detect_platform()`:
|
||||||
1. Player clicks "Sign in with Steam" button on login screen
|
|
||||||
2. Game retrieves Steam auth session ticket via Steamworks
|
|
||||||
3. Auth ticket is sent to Nakama for authentication
|
|
||||||
4. Nakama validates the ticket with Steam backend
|
|
||||||
5. If valid, player is logged in/registered to Nakama
|
|
||||||
6. Player's Steam account is linked to their Nakama account
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- No password needed for Steam users
|
|
||||||
- Automatic account creation on first login
|
|
||||||
- Seamless cross-platform progression (all data in Nakama)
|
|
||||||
- Steam username is used as display name
|
|
||||||
- Unified backend across all platforms
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
- Nakama server must be configured with Steam API key
|
|
||||||
- Steamworks must be initialized (game launched through Steam)
|
|
||||||
- GodotSteam plugin must support `getAuthSessionTicket()`
|
|
||||||
|
|
||||||
**Configuration:**
|
|
||||||
Set your Steam API key in Nakama server configuration:
|
|
||||||
```yaml
|
|
||||||
nakama:
|
|
||||||
social:
|
|
||||||
steam:
|
|
||||||
api_key: "your_steam_api_key"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nakama Integration for Mobile
|
|
||||||
|
|
||||||
### Current Setup
|
|
||||||
|
|
||||||
Your project already has Nakama integrated via `addons/com.heroiclabs.nakama/` and `NakamaManager` autoload.
|
|
||||||
|
|
||||||
### Connecting to BackendService
|
|
||||||
|
|
||||||
The `BackendService` will automatically use Nakama on mobile. You need to implement Nakama-specific methods in `NakamaManager`:
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# In NakamaManager.gd, add these signals:
|
|
||||||
signal achievement_unlocked(achievement_id: String)
|
|
||||||
signal leaderboard_score_submitted(leaderboard_id: String, score: int, success: bool)
|
|
||||||
|
|
||||||
# Implement achievement methods
|
|
||||||
func unlock_achievement(achievement_id: String):
|
|
||||||
# Use Nakama's achievement system
|
|
||||||
var achievement = await client.write_storage_object_async(
|
|
||||||
session,
|
|
||||||
NakamaWriteStorageObject.new(
|
|
||||||
"achievements",
|
|
||||||
achievement_id,
|
|
||||||
{"unlocked": true, "timestamp": Time.get_unix_time_from_system()}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
achievement_unlocked.emit(achievement_id)
|
|
||||||
|
|
||||||
# Implement leaderboard methods
|
|
||||||
func submit_leaderboard_score(leaderboard_id: String, score: int):
|
|
||||||
# Use Nakama's leaderboard system
|
|
||||||
var result = await client.write_leaderboard_record_async(
|
|
||||||
session,
|
|
||||||
leaderboard_id,
|
|
||||||
score
|
|
||||||
)
|
|
||||||
leaderboard_score_submitted.emit(leaderboard_id, score, result != null)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Testing Steam Builds
|
|
||||||
|
|
||||||
1. **Export Steam Build**
|
|
||||||
- Use the "Windows Desktop (Steam)" preset (preset.0)
|
|
||||||
- Export to `build/steam/tekton_armageddon_v2.1.7.exe`
|
|
||||||
|
|
||||||
2. **Upload to Steam**
|
|
||||||
- Upload the exported build to Steamworks
|
|
||||||
- Set as the default build for your app
|
|
||||||
|
|
||||||
3. **Run through Steam**
|
|
||||||
- Launch the game via Steam (not directly)
|
|
||||||
- Steam must be running
|
|
||||||
- Check console for "SteamworksManager: Steam initialized successfully"
|
|
||||||
|
|
||||||
4. **Test Achievements**
|
|
||||||
- Call `BackendService.unlock_achievement("test_achievement")`
|
|
||||||
- Check Steam overlay (Shift+Tab) to see achievement unlock
|
|
||||||
|
|
||||||
5. **Test Leaderboards**
|
|
||||||
- Submit scores via `BackendService.submit_leaderboard_score()`
|
|
||||||
- View in Steamworks backend or Steam overlay
|
|
||||||
|
|
||||||
### Testing Non-Steam Builds
|
|
||||||
|
|
||||||
1. **Export Non-Steam Build**
|
|
||||||
- Use the "Windows Desktop (Non-Steam)" preset (preset.1)
|
|
||||||
- Export to `build/standalone/tekton_armageddon_v2.1.7.exe`
|
|
||||||
|
|
||||||
2. **Run Directly**
|
|
||||||
- Run the executable directly (not through Steam)
|
|
||||||
- Check console for "BackendService: Initialized Nakama backend"
|
|
||||||
|
|
||||||
3. **Test Leaderboards**
|
|
||||||
- Ensure Nakama server is accessible
|
|
||||||
- Open the leaderboard panel to fetch rankings
|
|
||||||
- Submit scores via `UserProfileManager.submit_to_leaderboard()`
|
|
||||||
- Leaderboards are global (same as mobile)
|
|
||||||
|
|
||||||
4. **Test Shop**
|
|
||||||
- Ensure Nakama server is accessible
|
|
||||||
- Open the shop panel to fetch catalog
|
|
||||||
- Purchase items via `UserProfileManager.purchase_item()`
|
|
||||||
- Shop functionality works the same as mobile
|
|
||||||
|
|
||||||
### Testing Nakama (Mobile)
|
|
||||||
|
|
||||||
1. **Run on Mobile Device**
|
|
||||||
- Export to Android/iOS using the Android preset (preset.2)
|
|
||||||
- The game will automatically use Nakama backend
|
|
||||||
- Check logs for "BackendService: Initialized Nakama backend"
|
|
||||||
|
|
||||||
2. **Test in Editor**
|
|
||||||
- To test Nakama in editor, temporarily modify `_detect_platform()`:
|
|
||||||
```gdscript
|
```gdscript
|
||||||
func _detect_platform() -> void:
|
func _detect_platform() -> void:
|
||||||
current_platform = Platform.MOBILE_NAKAMA # Force Nakama
|
current_platform = Platform.DESKTOP_NAKAMA # Force desktop Nakama mode
|
||||||
|
# ... or ...
|
||||||
|
current_platform = Platform.MOBILE_NAKAMA # Force mobile mode
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
### Export and Run Standalone
|
||||||
|
|
||||||
### Steamworks Not Initializing
|
```bash
|
||||||
|
# Windows
|
||||||
|
godot --headless --export-release "Windows Desktop" build/windows/standalone.exe
|
||||||
|
./build/windows/standalone.exe
|
||||||
|
|
||||||
**Problem**: "SteamworksManager: Failed to initialize Steam"
|
# Linux
|
||||||
|
godot --headless --export-release "Linux/X11" build/linux/standalone.x86_64
|
||||||
|
./build/linux/standalone.x86_64
|
||||||
|
|
||||||
**Solutions**:
|
# macOS
|
||||||
1. Ensure game is launched through Steam (not directly)
|
godot --headless --export-release "macOS" build/macos/standalone.zip
|
||||||
2. Check Steam is running
|
```
|
||||||
3. Verify `steam_app_id` is correct
|
|
||||||
4. Check GodotSteam plugin is enabled in Project Settings
|
|
||||||
5. Restart Godot Editor after installing plugin
|
|
||||||
|
|
||||||
### Achievements Not Unlocking
|
---
|
||||||
|
|
||||||
**Problem**: Achievements don't appear in Steam overlay
|
## Activating Steam Support (Future)
|
||||||
|
|
||||||
**Solutions**:
|
If Steam distribution is needed later, these are the steps:
|
||||||
1. Ensure achievement API names match Steamworks configuration
|
|
||||||
2. Check `steam.storeStats()` is called after setting achievements
|
|
||||||
3. Verify achievement is published in Steamworks (not in draft)
|
|
||||||
4. Test with Steam overlay open (Shift+Tab)
|
|
||||||
|
|
||||||
### Leaderboards Not Working
|
1. **Get a Steam Partner account** at https://partner.steamgames.com/
|
||||||
|
2. **Create Steam app** and get an App ID
|
||||||
|
3. **Enable GodotSteam plugin** in Project > Project Settings > Plugins
|
||||||
|
4. **Set App ID** in Project Settings > steam > initialization > app_id (or in `steamworks_manager.gd`)
|
||||||
|
5. **Configure Nakama** to accept Steam auth tickets (set Steam API key in Nakama config)
|
||||||
|
6. **Add a Steam-specific export preset** or modify existing presets to include Steam SDK redistributables
|
||||||
|
7. **Update CI pipeline** to upload builds to Steamworks SDK pipelines
|
||||||
|
8. **Test Steam auth flow**: launch through Steam, verify `"SteamworksManager: Steam initialized"`, verify Steam login → Nakama registration
|
||||||
|
|
||||||
**Problem**: Leaderboard scores not submitting
|
### What the Dormant Steam Code Does
|
||||||
|
|
||||||
**Solutions**:
|
The existing `steamworks_manager.gd` provides:
|
||||||
1. Ensure leaderboard exists in Steamworks
|
|
||||||
2. Check leaderboard API name matches
|
|
||||||
3. Verify leaderboard is published
|
|
||||||
4. Check console for error messages
|
|
||||||
|
|
||||||
### Platform Detection Issues
|
- `get_auth_session_ticket()` — retrieves Steam auth session ticket for Nakama login
|
||||||
|
- `get_steam_user_name()` — returns Steam persona name
|
||||||
|
- `get_steam_user_id()` — returns Steam ID
|
||||||
|
|
||||||
**Problem**: Wrong backend being used
|
All game features (achievements, leaderboards, shop) are already implemented purely via Nakama RPCs in `BackendService.api_rpc_async()`. Steam would only be used for **authentication** — the rest stays on Nakama.
|
||||||
|
|
||||||
**Solutions**:
|
---
|
||||||
1. Check OS features: `print(OS.get_supported_features())`
|
|
||||||
2. Manually override platform in `_detect_platform()` for testing
|
|
||||||
3. Ensure `BackendService` is added as autoload
|
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/services/
|
scripts/
|
||||||
├── backend_service.gd # Unified interface (autoload)
|
├── backend_service.gd # Unified interface (autoload) — Nakama-only default
|
||||||
└── steamworks_manager.gd # Steamworks implementation
|
├── steamworks_manager.gd # Steamworks implementation (dormant, loads only if GodotSteam present)
|
||||||
|
└── nakama_manager.gd # NakamaManager autoload (active)
|
||||||
|
|
||||||
export_presets.cfg # Export presets for all platforms
|
addons/godotsteam/ # GodotSteam GDExtension (installed but not enabled)
|
||||||
|
addons/com.heroiclabs.nakama/ # Nakama GDSDK (active)
|
||||||
|
|
||||||
|
export_presets.cfg # Export presets — all Nakama-only, no Steam presets
|
||||||
|
.gitea/workflows/ci.yml # CI pipeline — exports Win/Linux/macOS to Gitea releases
|
||||||
docs/STEAMWORKS_SETUP.md # This documentation
|
docs/STEAMWORKS_SETUP.md # This documentation
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Differences from Previous State
|
||||||
|
|
||||||
|
| Aspect | Before | Now |
|
||||||
|
|--------|--------|-----|
|
||||||
|
| Steam builds | Active, separate Steam/Non-Steam presets | Dormant, not shipped |
|
||||||
|
| Backend | Nakama + Steam | Nakama only |
|
||||||
|
| Export presets | 6+ (Steam + Non-Steam variants) | 4 (all Nakama-only) |
|
||||||
|
| Desktop platform detection | Steam if `OS.has_feature("steam")` | Nakama unless GodotSteam loaded |
|
||||||
|
| CI | Uploaded to Steamworks | Gitea releases only |
|
||||||
|
| Steam App ID | Configured | Default 480 (test), not set |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Additional Resources
|
## Additional Resources
|
||||||
|
|
||||||
- **GodotSteam Documentation**: https://godotsteam.com/
|
- **Nakama Documentation**: https://heroiclabs.com/docs/nakama/
|
||||||
|
- **GodotSteam Documentation**: https://godotsteam.com/ (for future reference)
|
||||||
- **GodotSteam GitHub**: https://codeberg.org/godotsteam/godotsteam
|
- **GodotSteam GitHub**: https://codeberg.org/godotsteam/godotsteam
|
||||||
- **Steamworks Documentation**: https://partner.steamgames.com/doc/home
|
- **Steamworks Documentation**: https://partner.steamgames.com/doc/home
|
||||||
- **Nakama Documentation**: https://heroiclabs.com/docs/nakama/
|
- **CI Workflow**: `.gitea/workflows/ci.yml`
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. Complete Steam Partner account setup
|
|
||||||
2. Create Steam app and get App ID
|
|
||||||
3. Install GodotSteam plugin
|
|
||||||
4. Configure achievements and leaderboards in Steamworks
|
|
||||||
5. Implement Nakama methods in `NakamaManager` for mobile
|
|
||||||
6. Test on all target platforms
|
|
||||||
7. Set up code signing for macOS distribution
|
|
||||||
8. Configure Steam Inventory Service if using in-game shop
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- **Steam builds** only work when launched through Steam client
|
|
||||||
- **Non-Steam builds** use Nakama for leaderboards and shop (same as mobile)
|
|
||||||
- **Shop functionality** is available on both Steam (via Steam Inventory) and non-Steam (via Nakama)
|
|
||||||
- **Non-Steam builds** sync to Nakama server, not Steam
|
|
||||||
- **Nakama** is already integrated for multiplayer, leaderboards, and shop
|
|
||||||
- Export presets are organized in separate folders: `build/steam/` and `build/standalone/`
|
|
||||||
|
|
||||||
## Build Workflow
|
|
||||||
|
|
||||||
### For Steam Distribution
|
|
||||||
|
|
||||||
1. Export using Steam presets (preset.0, preset.3, preset.5)
|
|
||||||
2. Upload builds to Steamworks
|
|
||||||
3. Configure achievements and leaderboards in Steamworks backend
|
|
||||||
4. Set build as default in Steamworks
|
|
||||||
5. Shop uses Steam Inventory Service (requires additional setup)
|
|
||||||
|
|
||||||
### For Standalone Distribution (itch.io, GOG, etc.)
|
|
||||||
|
|
||||||
1. Export using Non-Steam presets (preset.1, preset.4, preset.6)
|
|
||||||
2. Distribute the standalone executables
|
|
||||||
3. Ensure Nakama server is accessible to players
|
|
||||||
4. Players get global leaderboards and shop via Nakama
|
|
||||||
5. No Steam integration required
|
|
||||||
|
|
||||||
### For Mobile Stores
|
|
||||||
|
|
||||||
1. Export using Android preset (preset.2)
|
|
||||||
2. Upload to Google Play / App Store
|
|
||||||
3. Ensure Nakama server is configured and accessible
|
|
||||||
4. Leaderboards and shop work the same as non-Steam desktop
|
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# Versioning & Artifact Storage Strategy
|
|
||||||
|
|
||||||
## Version Format
|
|
||||||
|
|
||||||
Tekton Armageddon uses semantic versioning: `MAJOR.MINOR.PATCH`
|
|
||||||
|
|
||||||
- **MAJOR**: Breaking changes, incompatible API changes
|
|
||||||
- **MINOR**: New features, backward-compatible
|
|
||||||
- **PATCH**: Bug fixes, content updates, backward-compatible
|
|
||||||
|
|
||||||
Current version source: `project.godot` → `config/version`
|
|
||||||
|
|
||||||
## Artifact Types
|
|
||||||
|
|
||||||
### 1. Patch PCK Files
|
|
||||||
- **Purpose**: Hot-patch content updates without full client reinstall
|
|
||||||
- **Storage**: GitHub Actions artifacts (90 days) + tekton-updates repo
|
|
||||||
- **Naming**: `patch.pck` (latest), versioned in tekton-updates repo
|
|
||||||
- **Integrity**: SHA256 checksums generated and stored alongside
|
|
||||||
|
|
||||||
### 2. Platform Builds
|
|
||||||
- **Platforms**: Windows, Linux, Android
|
|
||||||
- **Storage**: GitHub Actions artifacts (90 days) + GitHub Releases (permanent)
|
|
||||||
- **Naming**: `tekton_armageddon_{Platform}_v{VERSION}.{ext}`
|
|
||||||
- **Trigger**: Git tags (`v*.*.*`) or manual workflow dispatch
|
|
||||||
- **Integrity**: SHA256 checksums for each platform build
|
|
||||||
|
|
||||||
### 3. Version Manifest
|
|
||||||
- **File**: `assets/data/version.json`
|
|
||||||
- **Purpose**: Client version checking, changelog delivery, patch URLs
|
|
||||||
- **Storage**: Embedded in builds + tekton-updates repo
|
|
||||||
- **Integrity**: SHA256 checksum
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
### Patch Deployment (`deploy_patch.yml`)
|
|
||||||
**Trigger**: Push to `patch-release` branch or manual dispatch
|
|
||||||
|
|
||||||
**Process**:
|
|
||||||
1. Auto-bump patch version from `project.godot`
|
|
||||||
2. Extract changelog from `CHANGELOG_DRAFT.md` [NEXT] section
|
|
||||||
3. Generate `version.json` with new release entry
|
|
||||||
4. Commit version bump back to repo
|
|
||||||
5. Build `patch.pck` from changed files
|
|
||||||
6. Generate SHA256 checksums
|
|
||||||
7. Upload artifacts to GitHub Actions (90-day retention)
|
|
||||||
8. Push to `tekton-updates` public repo (`latest/` folder)
|
|
||||||
|
|
||||||
**Artifacts**:
|
|
||||||
- `patch-pck-{SHA}`: patch.pck + checksum
|
|
||||||
- `version-manifest-{SHA}`: version.json + checksum
|
|
||||||
|
|
||||||
### Platform Builds (`build_artifacts.yml`)
|
|
||||||
**Trigger**: Git tag push (`v*.*.*`) or manual dispatch with version input
|
|
||||||
|
|
||||||
**Process**:
|
|
||||||
1. Matrix build for Windows, Linux, Android
|
|
||||||
2. Export using Godot export presets
|
|
||||||
3. Generate SHA256 checksums per platform
|
|
||||||
4. Upload artifacts to GitHub Actions (90-day retention)
|
|
||||||
5. Create GitHub Release with all platform builds + checksums
|
|
||||||
|
|
||||||
**Artifacts**:
|
|
||||||
- `tekton-{Platform}-v{VERSION}`: platform binary + checksum
|
|
||||||
|
|
||||||
## Artifact Retention
|
|
||||||
|
|
||||||
| Artifact Type | Storage Location | Retention | Purpose |
|
|
||||||
|--------------|------------------|-----------|---------|
|
|
||||||
| Patch PCK | GitHub Actions | 90 days | CI/CD history, rollback |
|
|
||||||
| Patch PCK | tekton-updates repo | Permanent | Client downloads |
|
|
||||||
| Platform Builds | GitHub Actions | 90 days | CI/CD history |
|
|
||||||
| Platform Builds | GitHub Releases | Permanent | Distribution |
|
|
||||||
| Version Manifest | tekton-updates repo | Permanent | Client version checks |
|
|
||||||
|
|
||||||
## Checksum Verification
|
|
||||||
|
|
||||||
All artifacts include SHA256 checksums for integrity verification:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify patch.pck
|
|
||||||
sha256sum -c patch.pck.sha256
|
|
||||||
|
|
||||||
# Verify platform build
|
|
||||||
sha256sum -c tekton_armageddon_Windows_v2.3.8.sha256
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version Compatibility
|
|
||||||
|
|
||||||
`version.json` includes `minimum_app_version` field:
|
|
||||||
- Clients below this version must reinstall full build
|
|
||||||
- Clients at or above can use patch system
|
|
||||||
|
|
||||||
## Changelog Management
|
|
||||||
|
|
||||||
**Source**: `CHANGELOG_DRAFT.md`
|
|
||||||
|
|
||||||
**Format**:
|
|
||||||
```markdown
|
|
||||||
## [NEXT]
|
|
||||||
- Feature or fix description
|
|
||||||
- Another change
|
|
||||||
|
|
||||||
## [2.3.7] — 2026-05-15
|
|
||||||
- Archived release notes
|
|
||||||
```
|
|
||||||
|
|
||||||
**Process**:
|
|
||||||
1. Developers add entries under `[NEXT]`
|
|
||||||
2. CI extracts `[NEXT]` entries on patch deployment
|
|
||||||
3. CI archives to versioned section
|
|
||||||
4. CI clears `[NEXT]` for next cycle
|
|
||||||
|
|
||||||
## Manual Version Bump
|
|
||||||
|
|
||||||
For local testing without CI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update versions but keep changelog
|
|
||||||
python3 tools/generate_version_json.py --local
|
|
||||||
|
|
||||||
# Skip changelog update entirely
|
|
||||||
python3 tools/generate_version_json.py --skip-changelog
|
|
||||||
```
|
|
||||||
|
|
||||||
## Release Process
|
|
||||||
|
|
||||||
### Patch Release
|
|
||||||
1. Merge changes to `patch-release` branch
|
|
||||||
2. CI auto-bumps version, builds, deploys
|
|
||||||
3. Clients auto-download on next launch
|
|
||||||
|
|
||||||
### Full Release
|
|
||||||
1. Tag commit: `git tag v2.4.0`
|
|
||||||
2. Push tag: `git push origin v2.4.0`
|
|
||||||
3. CI builds all platforms, creates GitHub Release
|
|
||||||
4. Distribute via Steam, Google Play, etc.
|
|
||||||
|
|
||||||
## Rollback Strategy
|
|
||||||
|
|
||||||
**Patch Rollback**:
|
|
||||||
1. Locate previous patch in GitHub Actions artifacts or tekton-updates repo
|
|
||||||
2. Manually push to `tekton-updates/latest/`
|
|
||||||
3. Update `version.json` to point to previous version
|
|
||||||
|
|
||||||
**Full Build Rollback**:
|
|
||||||
1. Download previous release from GitHub Releases
|
|
||||||
2. Re-tag or create hotfix branch
|
|
||||||
3. Redeploy via standard release process
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Automated rollback on failed health checks
|
|
||||||
- [ ] Delta patching for bandwidth optimization
|
|
||||||
- [ ] Multi-region CDN distribution
|
|
||||||
- [ ] Staged rollout (canary deployments)
|
|
||||||
- [ ] Automated compatibility testing matrix
|
|
||||||
@@ -1,1366 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Candy Cannon Survival — Technical Documentation</title>
|
|
||||||
<meta name="description" content="Technical implementation documentation for the Candy Cannon Survival (Gauntlet) game mode in Tekton Dash">
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--bg-primary: #0a0a0f;
|
|
||||||
--bg-secondary: #111118;
|
|
||||||
--bg-card: #16161f;
|
|
||||||
--bg-card-hover: #1c1c28;
|
|
||||||
--bg-code: #1a1a26;
|
|
||||||
--border: #2a2a3a;
|
|
||||||
--border-glow: #ff6bb5;
|
|
||||||
--text-primary: #e8e6f0;
|
|
||||||
--text-secondary: #8b89a0;
|
|
||||||
--text-muted: #5a586e;
|
|
||||||
--accent-pink: #ff6bb5;
|
|
||||||
--accent-pink-dim: #ff6bb540;
|
|
||||||
--accent-candy: #ff85c8;
|
|
||||||
--accent-purple: #a855f7;
|
|
||||||
--accent-blue: #6366f1;
|
|
||||||
--accent-cyan: #22d3ee;
|
|
||||||
--accent-green: #34d399;
|
|
||||||
--accent-yellow: #fbbf24;
|
|
||||||
--accent-orange: #fb923c;
|
|
||||||
--accent-red: #f87171;
|
|
||||||
--new-badge: #34d399;
|
|
||||||
--existing-badge: #6366f1;
|
|
||||||
--adapt-badge: #fbbf24;
|
|
||||||
--glass: rgba(22, 22, 31, 0.7);
|
|
||||||
--glass-border: rgba(255, 107, 181, 0.12);
|
|
||||||
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--accent-pink-dim) transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', -apple-system, sans-serif;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === AMBIENT BG === */
|
|
||||||
.ambient-bg {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.ambient-bg .orb {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(120px);
|
|
||||||
opacity: 0.15;
|
|
||||||
animation: orbFloat 20s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
.ambient-bg .orb:nth-child(1) {
|
|
||||||
width: 600px; height: 600px;
|
|
||||||
background: var(--accent-pink);
|
|
||||||
top: -200px; left: -100px;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
.ambient-bg .orb:nth-child(2) {
|
|
||||||
width: 500px; height: 500px;
|
|
||||||
background: var(--accent-purple);
|
|
||||||
bottom: -150px; right: -100px;
|
|
||||||
animation-delay: -7s;
|
|
||||||
}
|
|
||||||
.ambient-bg .orb:nth-child(3) {
|
|
||||||
width: 400px; height: 400px;
|
|
||||||
background: var(--accent-blue);
|
|
||||||
top: 50%; left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
animation-delay: -14s;
|
|
||||||
}
|
|
||||||
@keyframes orbFloat {
|
|
||||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
||||||
25% { transform: translate(30px, -40px) scale(1.05); }
|
|
||||||
50% { transform: translate(-20px, 20px) scale(0.95); }
|
|
||||||
75% { transform: translate(40px, 30px) scale(1.02); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === LAYOUT === */
|
|
||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === HERO === */
|
|
||||||
.hero {
|
|
||||||
padding: 80px 0 60px;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.hero-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: 100px;
|
|
||||||
background: var(--accent-pink-dim);
|
|
||||||
border: 1px solid var(--accent-pink);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1.5px;
|
|
||||||
color: var(--accent-candy);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.hero-badge .dot {
|
|
||||||
width: 6px; height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent-pink);
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.3; }
|
|
||||||
}
|
|
||||||
.hero h1 {
|
|
||||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
line-height: 1.1;
|
|
||||||
background: linear-gradient(135deg, #fff 0%, var(--accent-candy) 50%, var(--accent-purple) 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.hero .subtitle {
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
max-width: 640px;
|
|
||||||
margin: 0 auto 40px;
|
|
||||||
}
|
|
||||||
.hero-stats {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 32px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.hero-stat {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.hero-stat .value {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--accent-candy);
|
|
||||||
}
|
|
||||||
.hero-stat .label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === NAV === */
|
|
||||||
.sticky-nav {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
background: rgba(10,10,15,0.85);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding: 0;
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
.nav-inner {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 8px 0;
|
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
.nav-inner::-webkit-scrollbar { display: none; }
|
|
||||||
.nav-link {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.nav-link:hover, .nav-link.active {
|
|
||||||
background: var(--accent-pink-dim);
|
|
||||||
color: var(--accent-candy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === SECTIONS === */
|
|
||||||
section {
|
|
||||||
margin-bottom: 64px;
|
|
||||||
}
|
|
||||||
.section-header {
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
.section-header h2 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.section-header h2 .icon {
|
|
||||||
width: 36px; height: 36px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.section-header p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-top: 8px;
|
|
||||||
max-width: 700px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === CARDS === */
|
|
||||||
.card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 24px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
.card:hover {
|
|
||||||
border-color: var(--glass-border);
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
box-shadow: 0 0 30px var(--accent-pink-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === GLOSSARY === */
|
|
||||||
.glossary-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.glossary-item {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.glossary-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0;
|
|
||||||
width: 4px; height: 100%;
|
|
||||||
border-radius: 4px 0 0 4px;
|
|
||||||
}
|
|
||||||
.glossary-item.new::before { background: var(--new-badge); }
|
|
||||||
.glossary-item.adapt::before { background: var(--adapt-badge); }
|
|
||||||
.glossary-item.existing::before { background: var(--existing-badge); }
|
|
||||||
|
|
||||||
.glossary-item:hover {
|
|
||||||
border-color: var(--glass-border);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
|
|
||||||
}
|
|
||||||
.glossary-icon {
|
|
||||||
width: 44px; height: 44px;
|
|
||||||
min-width: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.glossary-item.new .glossary-icon { background: rgba(52,211,153,0.12); }
|
|
||||||
.glossary-item.adapt .glossary-icon { background: rgba(251,191,36,0.12); }
|
|
||||||
.glossary-item.existing .glossary-icon { background: rgba(99,102,241,0.12); }
|
|
||||||
|
|
||||||
.glossary-content h3 {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.glossary-content p {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
.glossary-content .code-ref {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
background: rgba(34,211,238,0.08);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 6px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === BADGES === */
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 10px;
|
|
||||||
border-radius: 100px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.8px;
|
|
||||||
}
|
|
||||||
.badge-new { background: rgba(52,211,153,0.15); color: var(--new-badge); }
|
|
||||||
.badge-adapt { background: rgba(251,191,36,0.15); color: var(--adapt-badge); }
|
|
||||||
.badge-existing { background: rgba(99,102,241,0.15); color: var(--existing-badge); }
|
|
||||||
|
|
||||||
/* === LEGEND === */
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
.legend-item {
|
|
||||||
display: flex; align-items: center; gap: 8px;
|
|
||||||
font-size: 13px; color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
.legend-dot {
|
|
||||||
width: 10px; height: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
.legend-dot.new { background: var(--new-badge); }
|
|
||||||
.legend-dot.adapt { background: var(--adapt-badge); }
|
|
||||||
.legend-dot.existing { background: var(--existing-badge); }
|
|
||||||
|
|
||||||
/* === ARCHITECTURE DIAGRAM === */
|
|
||||||
.arch-diagram {
|
|
||||||
background: var(--bg-code);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 32px;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
.arch-tree {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
.arch-tree .node { color: var(--accent-cyan); font-weight: 600; }
|
|
||||||
.arch-tree .new-node { color: var(--accent-green); font-weight: 600; }
|
|
||||||
.arch-tree .label { color: var(--text-muted); }
|
|
||||||
.arch-tree .connector { color: var(--text-muted); }
|
|
||||||
.arch-tree .tag-new {
|
|
||||||
font-size: 10px;
|
|
||||||
background: rgba(52,211,153,0.15);
|
|
||||||
color: var(--new-badge);
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-left: 4px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === TABLES === */
|
|
||||||
.table-wrap {
|
|
||||||
overflow-x: auto;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
thead th {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 14px 16px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
tbody td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid rgba(42,42,58,0.5);
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
tbody tr:last-child td { border-bottom: none; }
|
|
||||||
tbody tr:hover { background: rgba(255,107,181,0.03); }
|
|
||||||
tbody td:first-child { font-weight: 600; }
|
|
||||||
.td-code {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === REUSE TABLE === */
|
|
||||||
.reuse-direct { color: var(--accent-green); font-weight: 600; }
|
|
||||||
.reuse-heavy { color: var(--accent-cyan); font-weight: 600; }
|
|
||||||
.reuse-partial { color: var(--accent-yellow); font-weight: 600; }
|
|
||||||
.reuse-adapt { color: var(--accent-orange); font-weight: 600; }
|
|
||||||
.reuse-pattern { color: var(--accent-purple); font-weight: 600; }
|
|
||||||
|
|
||||||
/* === PHASE TIMELINE === */
|
|
||||||
.timeline {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) { .timeline { grid-template-columns: 1fr; } }
|
|
||||||
.phase-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 24px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.phase-card::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0; right: 0;
|
|
||||||
height: 3px;
|
|
||||||
}
|
|
||||||
.phase-card.phase-1::after { background: linear-gradient(90deg, var(--accent-green), var(--accent-cyan)); }
|
|
||||||
.phase-card.phase-2::after { background: linear-gradient(90deg, var(--accent-yellow), var(--accent-orange)); }
|
|
||||||
.phase-card.phase-3::after { background: linear-gradient(90deg, var(--accent-orange), var(--accent-red)); }
|
|
||||||
|
|
||||||
.phase-time {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.phase-card h3 {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.phase-card ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.phase-card li {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 4px 0;
|
|
||||||
padding-left: 16px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.phase-card li::before {
|
|
||||||
content: '›';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
color: var(--accent-pink);
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === FILE LISTS === */
|
|
||||||
.file-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.file-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.file-card:hover {
|
|
||||||
border-color: var(--glass-border);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
.file-icon {
|
|
||||||
width: 36px; height: 36px; min-width: 36px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.file-card.new-file .file-icon { background: rgba(52,211,153,0.12); }
|
|
||||||
.file-card.mod-file .file-icon { background: rgba(251,191,36,0.12); }
|
|
||||||
.file-info h4 {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
}
|
|
||||||
.file-info p {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === CODE BLOCKS === */
|
|
||||||
.code-block {
|
|
||||||
background: var(--bg-code);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px 24px;
|
|
||||||
overflow-x: auto;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 12.5px;
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
.code-block .kw { color: var(--accent-purple); }
|
|
||||||
.code-block .fn { color: var(--accent-cyan); }
|
|
||||||
.code-block .str { color: var(--accent-green); }
|
|
||||||
.code-block .cm { color: var(--text-muted); font-style: italic; }
|
|
||||||
.code-block .num { color: var(--accent-orange); }
|
|
||||||
.code-block .type { color: var(--accent-yellow); }
|
|
||||||
|
|
||||||
/* === RISK CARDS === */
|
|
||||||
.risk-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.risk-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.risk-indicator {
|
|
||||||
width: 36px; height: 36px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
background: rgba(248,113,113,0.12);
|
|
||||||
}
|
|
||||||
.risk-content h4 { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
|
|
||||||
.risk-content p { font-size: 12px; color: var(--text-secondary); }
|
|
||||||
|
|
||||||
/* === PRIORITY LIST === */
|
|
||||||
.priority-list {
|
|
||||||
counter-reset: priority;
|
|
||||||
}
|
|
||||||
.priority-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid rgba(42,42,58,0.4);
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.priority-item:last-child { border-bottom: none; }
|
|
||||||
.priority-num {
|
|
||||||
counter-increment: priority;
|
|
||||||
width: 36px; height: 36px; min-width: 36px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--accent-pink-dim);
|
|
||||||
color: var(--accent-candy);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
.priority-info h4 { font-size: 14px; font-weight: 700; }
|
|
||||||
.priority-info p {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === NETWORK TABLE ACCENTS === */
|
|
||||||
.sync-method {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === FOOTER === */
|
|
||||||
footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 48px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === SEARCH === */
|
|
||||||
.search-bar {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.search-bar input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px 12px 44px;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
.search-bar input:focus {
|
|
||||||
border-color: var(--accent-pink);
|
|
||||||
}
|
|
||||||
.search-bar input::placeholder {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
.search-bar .search-icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 14px; top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 16px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === FILTER BTNS === */
|
|
||||||
.filter-btns {
|
|
||||||
display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.filter-btn {
|
|
||||||
padding: 6px 14px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.filter-btn:hover, .filter-btn.active {
|
|
||||||
border-color: var(--accent-pink);
|
|
||||||
color: var(--accent-candy);
|
|
||||||
background: var(--accent-pink-dim);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === SCROLL TOP === */
|
|
||||||
.scroll-top {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 32px; right: 32px;
|
|
||||||
width: 44px; height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--accent-pink);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 18px;
|
|
||||||
display: none;
|
|
||||||
align-items: center; justify-content: center;
|
|
||||||
box-shadow: 0 4px 20px var(--accent-pink-dim);
|
|
||||||
transition: transform 0.2s;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
.scroll-top:hover { transform: translateY(-2px); }
|
|
||||||
.scroll-top.show { display: flex; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="ambient-bg">
|
|
||||||
<div class="orb"></div>
|
|
||||||
<div class="orb"></div>
|
|
||||||
<div class="orb"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="wrapper">
|
|
||||||
|
|
||||||
<!-- ============ HERO ============ -->
|
|
||||||
<header class="hero">
|
|
||||||
<div class="hero-badge"><span class="dot"></span> Technical Documentation</div>
|
|
||||||
<h1>Candy Cannon Survival</h1>
|
|
||||||
<p class="subtitle">Gauntlet Mode — Implementation blueprint mapping GDD mechanics to existing Tekton Dash systems</p>
|
|
||||||
<div class="hero-stats">
|
|
||||||
<div class="hero-stat"><div class="value">70%</div><div class="label">Code Reuse</div></div>
|
|
||||||
<div class="hero-stat"><div class="value">4</div><div class="label">New Files</div></div>
|
|
||||||
<div class="hero-stat"><div class="value">7</div><div class="label">Modified Files</div></div>
|
|
||||||
<div class="hero-stat"><div class="value">12</div><div class="label">New Terms</div></div>
|
|
||||||
<div class="hero-stat"><div class="value">10</div><div class="label">Reused Terms</div></div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- ============ NAV ============ -->
|
|
||||||
<div class="sticky-nav">
|
|
||||||
<nav class="nav-inner wrapper">
|
|
||||||
<a class="nav-link" href="#glossary">Glossary</a>
|
|
||||||
<a class="nav-link" href="#architecture">Architecture</a>
|
|
||||||
<a class="nav-link" href="#reuse">Reuse Map</a>
|
|
||||||
<a class="nav-link" href="#phases">Phases</a>
|
|
||||||
<a class="nav-link" href="#systems">Systems</a>
|
|
||||||
<a class="nav-link" href="#files">Files</a>
|
|
||||||
<a class="nav-link" href="#network">Network</a>
|
|
||||||
<a class="nav-link" href="#priority">Priority</a>
|
|
||||||
<a class="nav-link" href="#risks">Risks</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============ GLOSSARY ============ -->
|
|
||||||
<section id="glossary">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(255,107,181,0.12)">📖</span> Glossary</h2>
|
|
||||||
<p>All terms used in Gauntlet mode — categorized by whether they're new, adapted, or already implemented in Tekton Dash.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="legend">
|
|
||||||
<div class="legend-item"><div class="legend-dot new"></div> New — unique to Gauntlet</div>
|
|
||||||
<div class="legend-item"><div class="legend-dot adapt"></div> Adapted — modified from existing mechanic</div>
|
|
||||||
<div class="legend-item"><div class="legend-dot existing"></div> Existing — already in game, reused as-is</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-bar">
|
|
||||||
<span class="search-icon">🔍</span>
|
|
||||||
<input type="text" id="glossarySearch" placeholder="Search terms..." oninput="filterGlossary()">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-btns">
|
|
||||||
<button class="filter-btn active" onclick="setFilter('all', this)">All</button>
|
|
||||||
<button class="filter-btn" onclick="setFilter('new', this)">🟢 New Only</button>
|
|
||||||
<button class="filter-btn" onclick="setFilter('adapt', this)">🟡 Adapted</button>
|
|
||||||
<button class="filter-btn" onclick="setFilter('existing', this)">🔵 Existing</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-grid" id="glossaryGrid">
|
|
||||||
|
|
||||||
<!-- ===== NEW TERMS ===== -->
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="sticky cell">
|
|
||||||
<div class="glossary-icon">🍬</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Sticky Cell <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>A grid cell hit by the Candy Cannon that becomes impassable. Players stepping onto or pushed into a sticky cell are <em>trapped</em>. Remains until cleansed or round ends. Rendered as Layer 2 overlay (pink translucent mesh, ID 17).</p>
|
|
||||||
<span class="code-ref">TILE_STICKY = 17 → GridMap Layer 2</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="telegraph">
|
|
||||||
<div class="glossary-icon">⚡</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Telegraph <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>1-second warning before cannon impact. Target cell glows pink/candy color with a shadow preview and charge-up sound. Uses temporary overlay tile (ID 18) on Layer 2, animated alpha 0→1 over 0.8s, then replaced by Sticky Cell on impact.</p>
|
|
||||||
<span class="code-ref">TILE_TELEGRAPH = 18 → rpc("sync_telegraph")</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="candy cannon">
|
|
||||||
<div class="glossary-icon">💥</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Candy Cannon <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>Central NPC occupying a permanent 3×3 zone at arena center. Fires volleys of 5 candy shots every 5 seconds, creating sticky cells. Static body — cannot be grabbed, thrown, or interacted with. Not a Tekton — it's a dedicated hazard entity.</p>
|
|
||||||
<span class="code-ref">CandyCannonController → candy_cannon.tscn</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="volley">
|
|
||||||
<div class="glossary-icon">🎯</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Volley <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>A batch of 5 simultaneous cannon shots fired at different target cells. One volley fires every 5 seconds (36 total over 3 minutes = 180 impacts). Each shot in a volley has an independent impact size roll (1×1, 1×2, or 2×2).</p>
|
|
||||||
<span class="code-ref">_fire_volley() → cannon_interval = 5.0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="impact size">
|
|
||||||
<div class="glossary-icon">📐</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Impact Size <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>The footprint of each cannon shot. Three sizes: 1×1 (single cell), 1×2 (two adjacent), 2×2 (four cells square). Distribution changes per phase — early favors 1×1, endgame favors 2×2.</p>
|
|
||||||
<span class="code-ref">phase_weights[phase_idx]["2x2"]</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="trapped">
|
|
||||||
<div class="glossary-icon">🪤</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Trapped <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>Player state when standing on a sticky cell. Cannot move normally. Escape only via Cleanser power-up. Players can be trapped by stepping onto sticky, being pushed into sticky, or direct cannon hit. Trapped players keep their score but are out of active play.</p>
|
|
||||||
<span class="code-ref">trapped_players: Dict → rpc("sync_trapped")</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="cleanser">
|
|
||||||
<div class="glossary-icon">✨</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Cleanser <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>Power-up earned by completing 2 missions. Allows 5 cells of movement through sticky candy, cleansing traversed cells back to walkable. Inventory limit: 1. Cannot activate while stunned. 0.3s activation delay.</p>
|
|
||||||
<span class="code-ref">player_cleansers[peer_id] → GoalsCycleManager signal</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="clash">
|
|
||||||
<div class="glossary-icon">💫</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Clash <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>When two players activate Smack simultaneously (within 0.5s) and are in range of each other. Both get stunned for 1.0s, no push occurs, both smack bars are consumed. Server-authoritative timestamp comparison.</p>
|
|
||||||
<span class="code-ref">clash detection → 0.5s window, server authority</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="charged state">
|
|
||||||
<div class="glossary-icon">🔋</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Charged State <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>3-second window after Smack activation where the player model turns pink. If a target enters range during this window, the smack triggers. If no target is hit within 3s, energy is consumed with no effect.</p>
|
|
||||||
<span class="code-ref">smack_charged[player_id] → 3.0s window</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="anti-unfairness">
|
|
||||||
<div class="glossary-icon">⚖️</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Anti-Unfairness <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>Targeting rules preventing the cannon from feeling random/cheap. No same-player twice in a row, 2×2 never directly on player, path validation ensures escape routes exist (except final 30s). Uses AStar pathfinding.</p>
|
|
||||||
<span class="code-ref">last_targeted_player_id → EnhancedGridMap.initialize_astar()</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="route blocking">
|
|
||||||
<div class="glossary-icon">🚧</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Route Blocking <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>Cannon targeting strategy (25% chance) that places sticky cells on pathfinding bottlenecks — narrow corridors between sticky regions. Forces players to reroute. Calculated using EnhancedGridMap neighbor analysis.</p>
|
|
||||||
<span class="code-ref">_get_route_blocking_target() → 25% weight</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item new" data-type="new" data-name="gauntlet arena">
|
|
||||||
<div class="glossary-icon">🏟️</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Gauntlet Arena <span class="badge badge-new">New</span></h3>
|
|
||||||
<p>20×20 cell arena with 391 playable cells (400 minus 3×3 NPC zone). Players spawn at outer edges/corners. Target: 80% sticky coverage by round end (313 cells), leaving ~78 safe cells.</p>
|
|
||||||
<span class="code-ref">ARENA_SIZE = 20 → gauntlet.tscn</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ===== ADAPTED TERMS ===== -->
|
|
||||||
<div class="glossary-item adapt" data-type="adapt" data-name="smack">
|
|
||||||
<div class="glossary-icon">👊</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Smack <span class="badge badge-adapt">Adapted</span></h3>
|
|
||||||
<p>Gauntlet-specific melee push. Adapts existing <code>try_push()</code> from Attack Mode but replaces boost-meter gating with 8s auto-refill cooldown, adds 3s charged window, sticky landing trap, and clash detection. Push distance: 3 cells.</p>
|
|
||||||
<span class="code-ref">PlayerMovementManager.try_push() → smack_cooldowns</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item adapt" data-type="adapt" data-name="phase">
|
|
||||||
<div class="glossary-icon">⏱️</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Phase <span class="badge badge-adapt">Adapted</span></h3>
|
|
||||||
<p>Three escalation phases in Gauntlet: <strong>Open Arena</strong> (0–60s), <strong>Route Pressure</strong> (60–120s), <strong>Survival Endgame</strong> (120–180s). Adapts StopNGoManager's Go/Stop phase pattern but uses time-elapsed triggers instead of cycle signals.</p>
|
|
||||||
<span class="code-ref">enum Phase { OPEN_ARENA, ROUTE_PRESSURE, SURVIVAL_ENDGAME }</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item adapt" data-type="adapt" data-name="bot ai cannon avoidance">
|
|
||||||
<div class="glossary-icon">🤖</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Bot AI — Cannon Avoidance <span class="badge badge-adapt">Adapted</span></h3>
|
|
||||||
<p>Extends BotStrategicPlanner with Gauntlet-specific logic: telegraph awareness, sticky path planning, safe-zone pathfinding. Adapts existing bot movement heuristics to factor in shrinking arena.</p>
|
|
||||||
<span class="code-ref">BotStrategicPlanner → new evaluate_gauntlet()</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ===== EXISTING TERMS ===== -->
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="attack mode">
|
|
||||||
<div class="glossary-icon">⚔️</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Attack Mode <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Existing player state toggled via PowerUpManager when boost bar is full. In Gauntlet, <em>not used directly</em> — replaced by Smack mechanic. The push physics from <code>try_push()</code> are reused but the activation logic differs.</p>
|
|
||||||
<span class="code-ref">PowerUpManager.is_attack_mode → NOT used in Gauntlet</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="stagger">
|
|
||||||
<div class="glossary-icon">😵</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Stagger <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Existing 1.5s movement disable after being push-attacked. Gauntlet's Smack uses a shorter 1.0s stun, but the underlying <code>apply_stagger()</code> function is reused with a duration parameter.</p>
|
|
||||||
<span class="code-ref">PlayerMovementManager.apply_stagger(duration)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="mission / goals">
|
|
||||||
<div class="glossary-icon">🎯</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Mission / Goals <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>3×3 pattern-matching tile collection system. Reused as-is from GoalManager + GoalsCycleManager. In Gauntlet, completing every 2 missions also triggers Cleanser unlock (new hook on existing signal).</p>
|
|
||||||
<span class="code-ref">GoalManager → GoalsCycleManager.goal_count_updated</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="layer 2 overlay">
|
|
||||||
<div class="glossary-icon">🗂️</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Layer 2 Overlay <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>GridMap's Y=2 layer used for visual overlays (safe zones in Stop N Go, freeze in Freemode, highlights). Gauntlet uses it for Sticky Cell and Telegraph meshes. No conflict — modes are mutually exclusive.</p>
|
|
||||||
<span class="code-ref">GridMap.set_cell_item(Vector3i(x, 2, z), id)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="try push">
|
|
||||||
<div class="glossary-icon">🫸</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>try_push() <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Player push mechanic in PlayerMovementManager. Pushes target 3 cells backward. Gauntlet's Smack wraps this with direction-based push, sticky landing detection, and clash rules.</p>
|
|
||||||
<span class="code-ref">PlayerMovementManager.try_push(target, direction)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="screen shake">
|
|
||||||
<div class="glossary-icon">📳</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Screen Shake <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Camera shake effect triggered via RPC. Used on cannon impact with "medium" intensity. Already implemented system-wide.</p>
|
|
||||||
<span class="code-ref">player.rpc("trigger_screen_shake", "medium")</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="tekton projectile">
|
|
||||||
<div class="glossary-icon">🎪</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Tekton Projectile <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Arc-tween projectile from Tekton NPC. Candy Cannon reuses this exact visual pattern (spawn_projectile_rpc) — creating a mesh, arc-tweening position, then freeing on arrival.</p>
|
|
||||||
<span class="code-ref">tekton.gd → spawn_projectile_rpc(target, duration)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="rpc sync pattern">
|
|
||||||
<div class="glossary-icon">📡</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>RPC Sync Pattern <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Server-authoritative state sync via <code>@rpc("authority", "call_local", "reliable")</code>. All Gauntlet state changes (sticky, phase, trap, cleanser) use this identical pattern.</p>
|
|
||||||
<span class="code-ref">@rpc("authority", "call_local", "reliable")</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="timed match">
|
|
||||||
<div class="glossary-icon">⏰</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>Timed Match <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Global match timer from GoalsCycleManager. Gauntlet passes 180s duration. System handles countdown, HUD timer, and match-end trigger.</p>
|
|
||||||
<span class="code-ref">goals_cycle_manager.start_match(180.0)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="glossary-item existing" data-type="existing" data-name="special tiles manager">
|
|
||||||
<div class="glossary-icon">💎</div>
|
|
||||||
<div class="glossary-content">
|
|
||||||
<h3>SpecialTilesManager <span class="badge badge-existing">Existing</span></h3>
|
|
||||||
<p>Handles power-up tiles, inventory, and effects. Gauntlet restricts certain powerups (like Stop N Go restrictions) and adds Cleanser as a new inventory slot via the existing signal/slot system.</p>
|
|
||||||
<span class="code-ref">SpecialTilesManager.inventory → mode-based restrictions</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ ARCHITECTURE ============ -->
|
|
||||||
<section id="architecture">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(99,102,241,0.12)">🏗️</span> Architecture</h2>
|
|
||||||
<p>How GauntletManager slots into the existing manager tree, following the StopNGoManager pattern exactly.</p>
|
|
||||||
</div>
|
|
||||||
<div class="arch-diagram">
|
|
||||||
<div class="arch-tree">
|
|
||||||
<span class="node">main.gd</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="node">_init_managers()</span> <span class="label">← instantiate GauntletManager</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="node">_setup_host_game()</span> <span class="label">← arena setup branch</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="node">_start_game()</span> <span class="label">← start_game_mode() call</span><br>
|
|
||||||
<span class="connector">│</span><br>
|
|
||||||
<span class="new-node">GauntletManager</span> <span class="tag-new">NEW</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">_setup_arena()</span> <span class="label">← 20×20 grid, center 3×3 NPC zone</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">_setup_hud()</span> <span class="label">← mission label, cleanser indicator</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">start_game_mode()</span> <span class="label">← start cannon timer, spawn tiles</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">_process()</span> <span class="label">← cannon volley timer, phase escalation</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">CandyCannonController</span> <span class="tag-new">NEW</span> <span class="label">← targeting, volley fire</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">StickyCell system</span> <span class="tag-new">NEW</span> <span class="label">← Layer 2 overlay, trap logic</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">Cleanser system</span> <span class="tag-new">NEW</span> <span class="label">← powerup via missions</span><br>
|
|
||||||
<span class="connector">├──</span> <span class="new-node">Smack system</span> <span class="tag-new">NEW</span> <span class="label">← modified push with charge/cooldown</span><br>
|
|
||||||
<span class="connector">└──</span> <span class="new-node">Win condition</span> <span class="label">← highest score at timer end</span><br>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ REUSE MAP ============ -->
|
|
||||||
<section id="reuse">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(52,211,153,0.12)">♻️</span> Reuse Map</h2>
|
|
||||||
<p>How each GDD feature maps to existing systems — showing what's reused vs what's new.</p>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>GDD Feature</th><th>Existing System</th><th>Reuse</th><th>New Work</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Game Mode Registration</td><td class="td-code">GameMode.gd + LobbyManager</td><td><span class="reuse-direct">Direct</span></td><td>Add enum + strings</td></tr>
|
|
||||||
<tr><td>20×20 Arena</td><td class="td-code">StopNGoManager._setup_arena()</td><td><span class="reuse-heavy">Heavy</span></td><td>Custom layout, same API</td></tr>
|
|
||||||
<tr><td>Tile Collection / Scoring</td><td class="td-code">GoalsCycleManager</td><td><span class="reuse-direct">Direct</span></td><td>Reuse as-is</td></tr>
|
|
||||||
<tr><td>Mission System</td><td class="td-code">GoalManager + goals_cycle_manager</td><td><span class="reuse-direct">Direct</span></td><td>Same 3×3 pattern matching</td></tr>
|
|
||||||
<tr><td>Timed Match</td><td class="td-code">GoalsCycleManager.start_match()</td><td><span class="reuse-direct">Direct</span></td><td>Pass 180s duration</td></tr>
|
|
||||||
<tr><td>Player Movement</td><td class="td-code">PlayerMovementManager</td><td><span class="reuse-direct">Direct</span></td><td>No changes</td></tr>
|
|
||||||
<tr><td>Powerup System</td><td class="td-code">SpecialTilesManager</td><td><span class="reuse-partial">Partial</span></td><td>Cleanser = new type</td></tr>
|
|
||||||
<tr><td>Smack Mechanic</td><td class="td-code">PlayerMovementManager.try_push()</td><td><span class="reuse-adapt">Adapt</span></td><td>Modified push rules</td></tr>
|
|
||||||
<tr><td>Candy Cannon NPC</td><td class="td-code">tekton.gd + TektonController</td><td><span class="reuse-pattern">Pattern</span></td><td>New NPC, reuses projectile</td></tr>
|
|
||||||
<tr><td>Sticky Cells</td><td class="td-code">StopNGoManager safe zone overlay</td><td><span class="reuse-pattern">Pattern</span></td><td>New tile type, same layer</td></tr>
|
|
||||||
<tr><td>Telegraph VFX</td><td class="td-code">VFXManager / animation.gd</td><td><span class="reuse-pattern">Pattern</span></td><td>New animations, same system</td></tr>
|
|
||||||
<tr><td>HUD</td><td class="td-code">StopNGoManager._setup_hud()</td><td><span class="reuse-direct">Direct</span></td><td>Mode-specific labels</td></tr>
|
|
||||||
<tr><td>Network Sync</td><td class="td-code">RPC patterns</td><td><span class="reuse-direct">Direct</span></td><td>Same patterns</td></tr>
|
|
||||||
<tr><td>Lobby Settings</td><td class="td-code">LobbyManager signal/sync</td><td><span class="reuse-direct">Direct</span></td><td>Gauntlet settings</td></tr>
|
|
||||||
<tr><td>Bot AI</td><td class="td-code">BotController + BotStrategicPlanner</td><td><span class="reuse-adapt">Adapt</span></td><td>Cannon avoidance strategy</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ PHASES ============ -->
|
|
||||||
<section id="phases">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(251,191,36,0.12)">🌊</span> Phase Timeline</h2>
|
|
||||||
<p>Three escalation phases that control cannon intensity and impact size distribution.</p>
|
|
||||||
</div>
|
|
||||||
<div class="timeline">
|
|
||||||
<div class="phase-card phase-1">
|
|
||||||
<div class="phase-time">0:00 — 1:00</div>
|
|
||||||
<h3>Open Arena</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Collect tiles, learn the mission</li>
|
|
||||||
<li>Slow candy pressure</li>
|
|
||||||
<li>1×1 shots: <strong>60%</strong></li>
|
|
||||||
<li>1×2 shots: <strong>40%</strong></li>
|
|
||||||
<li>2×2 shots: <strong>0%</strong></li>
|
|
||||||
<li>~60 impacts total</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="phase-card phase-2">
|
|
||||||
<div class="phase-time">1:00 — 2:00</div>
|
|
||||||
<h3>Route Pressure</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Candy shapes arena topology</li>
|
|
||||||
<li>Smack becomes dangerous</li>
|
|
||||||
<li>1×1 shots: <strong>30%</strong></li>
|
|
||||||
<li>1×2 shots: <strong>55%</strong></li>
|
|
||||||
<li>2×2 shots: <strong>15%</strong></li>
|
|
||||||
<li>Cleanser used strategically</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="phase-card phase-3">
|
|
||||||
<div class="phase-time">2:00 — 3:00</div>
|
|
||||||
<h3>Survival Endgame</h3>
|
|
||||||
<ul>
|
|
||||||
<li>~80% arena is sticky</li>
|
|
||||||
<li>Safe zones limited, high tension</li>
|
|
||||||
<li>1×1 shots: <strong>15%</strong></li>
|
|
||||||
<li>1×2 shots: <strong>55%</strong></li>
|
|
||||||
<li>2×2 shots: <strong>30%</strong></li>
|
|
||||||
<li>Aggressive route-blocking allowed</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ CORE SYSTEMS ============ -->
|
|
||||||
<section id="systems">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(168,85,247,0.12)">⚙️</span> Core Systems</h2>
|
|
||||||
<p>Deep-dive into the four new systems and how they integrate.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sticky Cell -->
|
|
||||||
<div class="card" style="margin-bottom: 16px;">
|
|
||||||
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">🍬 Sticky Cell System</h3>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Feature</th><th>Implementation</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Visual</td><td>Layer 2 overlay — transparent candy-pink mesh (ID 17)</td></tr>
|
|
||||||
<tr><td>Movement Block</td><td class="td-code">PlayerMovementManager.simple_move_to() — add sticky check alongside wall check</td></tr>
|
|
||||||
<tr><td>Trap on Step</td><td class="td-code">GauntletManager._check_player_on_sticky() in _process()</td></tr>
|
|
||||||
<tr><td>Trap on Push</td><td class="td-code">PlayerMovementManager.try_push() — check landing cell</td></tr>
|
|
||||||
<tr><td>Cleanser Bypass</td><td>Temporary flag (like <code>is_invisible</code> wall bypass)</td></tr>
|
|
||||||
<tr><td>Network Sync</td><td class="td-code">main.rpc("sync_grid_item", x, 2, z, 17)</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Smack -->
|
|
||||||
<div class="card" style="margin-bottom: 16px;">
|
|
||||||
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">👊 Smack vs Attack Mode</h3>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Property</th><th>Current Attack Mode</th><th>Gauntlet Smack</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Charge Source</td><td>Boost bar fills to 100</td><td>8s auto-refill cooldown</td></tr>
|
|
||||||
<tr><td>Activation</td><td>Toggle <code>is_attack_mode</code></td><td>3s charged window (pink model)</td></tr>
|
|
||||||
<tr><td>Push Distance</td><td>3 cells backward</td><td>3 cells in push direction</td></tr>
|
|
||||||
<tr><td>Stagger Duration</td><td>1.5s <code>apply_stagger()</code></td><td>1.0s stun</td></tr>
|
|
||||||
<tr><td>Sticky Landing</td><td>N/A</td><td>Trapped on first sticky cell</td></tr>
|
|
||||||
<tr><td>Clash Rule</td><td>N/A</td><td>Both stunned, no push, bars consumed</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cleanser -->
|
|
||||||
<div class="card" style="margin-bottom: 16px;">
|
|
||||||
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">✨ Cleanser Power-Up</h3>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Property</th><th>Value</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Unlock Trigger</td><td class="td-code">GoalsCycleManager.goal_count_updated → count % 2 == 0</td></tr>
|
|
||||||
<tr><td>Storage</td><td class="td-code">GauntletManager.player_cleansers[peer_id] = 1</td></tr>
|
|
||||||
<tr><td>Effect</td><td>5 cells movement through sticky — crossed cells become passable</td></tr>
|
|
||||||
<tr><td>Sync</td><td class="td-code">rpc("sync_cleanser_state", peer_id, count)</td></tr>
|
|
||||||
<tr><td>Clear Sticky</td><td class="td-code">main.rpc("sync_grid_item", x, 2, z, -1)</td></tr>
|
|
||||||
<tr><td>Inventory Limit</td><td>1 per player</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cannon Targeting -->
|
|
||||||
<div class="card">
|
|
||||||
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">🎯 Cannon Targeting Intelligence</h3>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Roll %</th><th>Target Strategy</th><th>Purpose</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td><strong>60%</strong></td><td>Near a player (not same as last)</td><td>Direct pressure</td></tr>
|
|
||||||
<tr><td><strong>25%</strong></td><td>Route-blocking bottleneck</td><td>Cut escape paths</td></tr>
|
|
||||||
<tr><td><strong>10%</strong></td><td>Random non-sticky area</td><td>Spread coverage</td></tr>
|
|
||||||
<tr><td><strong>5%</strong></td><td>Previously sticky / chaos</td><td>Unpredictability</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ FILES ============ -->
|
|
||||||
<section id="files">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(34,211,238,0.12)">📁</span> File Changes</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 style="margin-bottom:12px; font-size:14px; color:var(--accent-green);">New Files</h3>
|
|
||||||
<div class="file-grid" style="margin-bottom:28px">
|
|
||||||
<div class="file-card new-file">
|
|
||||||
<div class="file-icon">📜</div>
|
|
||||||
<div class="file-info"><h4>gauntlet_manager.gd</h4><p>Core mode logic, phases, sticky cells, cleanser, smack</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card new-file">
|
|
||||||
<div class="file-icon">📜</div>
|
|
||||||
<div class="file-info"><h4>candy_cannon_controller.gd</h4><p>Cannon targeting, volley fire, telegraph</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card new-file">
|
|
||||||
<div class="file-icon">🎬</div>
|
|
||||||
<div class="file-info"><h4>gauntlet.tscn</h4><p>3D arena environment scene</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card new-file">
|
|
||||||
<div class="file-icon">🎬</div>
|
|
||||||
<div class="file-info"><h4>candy_cannon.tscn</h4><p>Candy Cannon NPC (3×3, static)</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 style="margin-bottom:12px; font-size:14px; color:var(--accent-yellow);">Modified Files</h3>
|
|
||||||
<div class="file-grid">
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>game_mode.gd</h4><p>Add GAUNTLET = 3 enum, string mappings</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>lobby_manager.gd</h4><p>Mode list, gauntlet settings, area mapping</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>main.gd</h4><p>Manager init, arena setup branch, start branch</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>player_movement_manager.gd</h4><p>Sticky check in move + push</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>goals_cycle_manager.gd</h4><p>Cleanser grant on 2nd goal</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>special_tiles_manager.gd</h4><p>Gauntlet powerup restrictions</p></div>
|
|
||||||
</div>
|
|
||||||
<div class="file-card mod-file">
|
|
||||||
<div class="file-icon">✏️</div>
|
|
||||||
<div class="file-info"><h4>MeshLibrary .tres</h4><p>Add TILE_STICKY (17) and TILE_TELEGRAPH (18)</p></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ NETWORK ============ -->
|
|
||||||
<section id="network">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(248,113,113,0.12)">📡</span> Network Sync</h2>
|
|
||||||
<p>All sync follows existing RPC patterns — no new networking paradigms needed.</p>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead><tr><th>Data</th><th>Sync Method</th><th>Existing Pattern</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Sticky Cells</td><td class="sync-method">main.rpc("sync_grid_item", x, 2, z, 17)</td><td>Safe zone / freeze overlay</td></tr>
|
|
||||||
<tr><td>Telegraph</td><td class="sync-method">rpc("sync_telegraph", targets_array)</td><td>StopNGoManager.sync_phase()</td></tr>
|
|
||||||
<tr><td>Phase Changes</td><td class="sync-method">rpc("sync_gauntlet_phase", idx, elapsed)</td><td>StopNGoManager.sync_phase()</td></tr>
|
|
||||||
<tr><td>Trap State</td><td class="sync-method">player.rpc("sync_trapped", true)</td><td>player.rpc("sync_stop_freeze")</td></tr>
|
|
||||||
<tr><td>Cleanser Grant</td><td class="sync-method">rpc("sync_cleanser", peer_id, count)</td><td>goals_cycle_manager.sync_goal_count()</td></tr>
|
|
||||||
<tr><td>Smack State</td><td class="sync-method">player.rpc("sync_smack_state", charged)</td><td>player.rpc("sync_modulate")</td></tr>
|
|
||||||
<tr><td>Cannon NPC</td><td colspan="2" style="color:var(--text-muted)">Static scene — no movement sync needed</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ PRIORITY ============ -->
|
|
||||||
<section id="priority">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(251,191,36,0.12)">📋</span> Implementation Priority</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="priority-list">
|
|
||||||
<div class="priority-item"><div class="priority-num">1</div><div class="priority-info"><h4>Game Mode Registration</h4><p>game_mode.gd, lobby_manager.gd, main.gd</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">2</div><div class="priority-info"><h4>Arena Setup</h4><p>gauntlet_manager._setup_arena(), 20×20 grid</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">3</div><div class="priority-info"><h4>Tile Spawning</h4><p>StopNGoManager._spawn_mission_tiles() pattern</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">4</div><div class="priority-info"><h4>Cannon Timer + Volley</h4><p>5s interval, 5 shots, 1×1 only</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">5</div><div class="priority-info"><h4>Sticky Cell System</h4><p>Layer 2 overlay, movement block, trap detection</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">6</div><div class="priority-info"><h4>Telegraph VFX</h4><p>Warning glow → impact transition</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">7</div><div class="priority-info"><h4>Impact Sizes</h4><p>1×2 and 2×2 shapes, phase weights</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">8</div><div class="priority-info"><h4>Smack Mechanic</h4><p>Modified push with cooldown/charge</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">9</div><div class="priority-info"><h4>Cleanser</h4><p>Unlock tracking, sticky bypass</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">10</div><div class="priority-info"><h4>Targeting Intelligence</h4><p>Player proximity, route blocking, anti-unfairness</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">11</div><div class="priority-info"><h4>Bot AI</h4><p>Cannon avoidance, sticky path planning</p></div></div>
|
|
||||||
<div class="priority-item"><div class="priority-num">12</div><div class="priority-info"><h4>Polish</h4><p>VFX, SFX, HUD animations, 3D scene</p></div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ============ RISKS ============ -->
|
|
||||||
<section id="risks">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2><span class="icon" style="background:rgba(248,113,113,0.12)">⚠️</span> Risk Assessment</h2>
|
|
||||||
</div>
|
|
||||||
<div class="risk-grid">
|
|
||||||
<div class="risk-card">
|
|
||||||
<div class="risk-indicator">🗂️</div>
|
|
||||||
<div class="risk-content">
|
|
||||||
<h4>Layer 2 Conflict</h4>
|
|
||||||
<p>GridMap Layer 2 used by freeze/safe overlays. <strong>Mitigated:</strong> Gauntlet mode is exclusive — no freeze/safe tiles exist.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="risk-card">
|
|
||||||
<div class="risk-indicator">📊</div>
|
|
||||||
<div class="risk-content">
|
|
||||||
<h4>20×20 Grid Performance</h4>
|
|
||||||
<p>400 cells + overlays. <strong>Mitigated:</strong> Existing 23×12 and 14×14 arenas work fine; 20×20 comparable.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="risk-card">
|
|
||||||
<div class="risk-indicator">🚫</div>
|
|
||||||
<div class="risk-content">
|
|
||||||
<h4>Impossible Arenas</h4>
|
|
||||||
<p>Cannon could seal all paths. <strong>Mitigated:</strong> AStar pathfinding check before each volley.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="risk-card">
|
|
||||||
<div class="risk-indicator">🔢</div>
|
|
||||||
<div class="risk-content">
|
|
||||||
<h4>MeshLibrary ID Collision</h4>
|
|
||||||
<p>IDs 17–18 might exist. <strong>Mitigated:</strong> Verify max ID in .tres before adding.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="risk-card">
|
|
||||||
<div class="risk-indicator">⏱️</div>
|
|
||||||
<div class="risk-content">
|
|
||||||
<h4>Smack Clash Timing</h4>
|
|
||||||
<p>Network latency affects clash detection. <strong>Mitigated:</strong> Server-authoritative timestamp, 0.5s window.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="wrapper">
|
|
||||||
Tekton Dash — Candy Cannon Survival Technical Docs · Generated from gauntlet-technical-implementation.md
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<button class="scroll-top" id="scrollTop" onclick="window.scrollTo({top:0,behavior:'smooth'})">↑</button>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Scroll-to-top button
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
document.getElementById('scrollTop').classList.toggle('show', window.scrollY > 400);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Active nav link
|
|
||||||
const sections = document.querySelectorAll('section[id]');
|
|
||||||
const navLinks = document.querySelectorAll('.nav-link');
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
let current = '';
|
|
||||||
sections.forEach(s => {
|
|
||||||
if (window.scrollY >= s.offsetTop - 120) current = s.id;
|
|
||||||
});
|
|
||||||
navLinks.forEach(l => {
|
|
||||||
l.classList.toggle('active', l.getAttribute('href') === '#' + current);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Glossary filter
|
|
||||||
let activeFilter = 'all';
|
|
||||||
function setFilter(type, btn) {
|
|
||||||
activeFilter = type;
|
|
||||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
filterGlossary();
|
|
||||||
}
|
|
||||||
function filterGlossary() {
|
|
||||||
const q = document.getElementById('glossarySearch').value.toLowerCase();
|
|
||||||
document.querySelectorAll('.glossary-item').forEach(item => {
|
|
||||||
const name = item.dataset.name;
|
|
||||||
const type = item.dataset.type;
|
|
||||||
const text = item.textContent.toLowerCase();
|
|
||||||
const matchFilter = activeFilter === 'all' || type === activeFilter;
|
|
||||||
const matchSearch = !q || text.includes(q);
|
|
||||||
item.style.display = matchFilter && matchSearch ? '' : 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,793 +0,0 @@
|
|||||||
# Candy Pump Survival (Gauntlet) — Technical Implementation Plan
|
|
||||||
|
|
||||||
## 1. Feasibility Summary
|
|
||||||
|
|
||||||
**Verdict: Feasible.** The existing codebase provides ~70% of the infrastructure needed. The game mode architecture is modular — each mode has its own manager (`StopNGoManager`, `PortalModeManager`) that handles arena setup, HUD, phase logic, and win conditions. A new `GauntletManager` follows this identical pattern.
|
|
||||||
|
|
||||||
### Reuse Breakdown
|
|
||||||
|
|
||||||
| GDD Feature | Existing System | Reuse Level | New Work |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Game Mode registration | `GameMode.gd` enum + `LobbyManager` | **Direct** | Already registered (`GAUNTLET = 3`) |
|
|
||||||
| 24×24 Arena setup | `StopNGoManager._setup_arena()` pattern | **Heavy** | Custom layout, same GridMap API |
|
|
||||||
| Tile collection / scoring | `GoalsCycleManager` | **Direct** | Reuse goal completion + scoring |
|
|
||||||
| Mission system (goals) | `GoalManager` + `goals_cycle_manager.gd` | **Direct** | Same 3×3 pattern matching |
|
|
||||||
| Timed match (3 min) | `GoalsCycleManager.start_match()` | **Direct** | Pass 180s duration |
|
|
||||||
| Player movement | `PlayerMovementManager` | **Direct** | Add sticky checks to `simple_move_to()` |
|
|
||||||
| Sticky cells | `StopNGoManager` safe zone overlay (Layer 2) | **Pattern** | New tile type, same GridMap layer approach |
|
|
||||||
| Telegraph VFX | Existing GauntletManager telegraph system | **Direct** | Adapt for growth ticks instead of cannon |
|
|
||||||
| Smack mechanic | Existing GauntletManager smack system | **Direct** | Already implemented |
|
|
||||||
| Cleanser power-up | Existing GauntletManager cleanser system | **Direct** | Already implemented |
|
|
||||||
| HUD | `StopNGoManager._setup_hud()` pattern | **Direct** | Mode-specific labels |
|
|
||||||
| Network sync | RPC patterns throughout codebase | **Direct** | Same `rpc()` / `sync_*` patterns |
|
|
||||||
| Bot AI | `BotController` + `BotStrategicPlanner` | **Adapt** | New strategy for sticky avoidance |
|
|
||||||
| Candy bubbles | **NEW** | **New** | Bubble spawn, grow, explode system |
|
|
||||||
| Candidate scoring | **NEW** | **New** | Cellular-automation growth algorithm |
|
|
||||||
| Movement buffers | **NEW** | **New** | Hidden safe zone detection/decay |
|
|
||||||
|
|
||||||
### What Changes from Current Implementation
|
|
||||||
|
|
||||||
The current `GauntletManager` uses a **cannon shooting** model (NPC fires projectiles at targets). The new GDD replaces this with a **ground growth** model (candy spreads from the ground via cellular-automation scoring). This requires:
|
|
||||||
|
|
||||||
1. **Remove** `_fire_volley()`, cannon timer, volley size, projectile spawning
|
|
||||||
2. **Add** growth tick timer, candidate scoring, weighted cell selection
|
|
||||||
3. **Add** candy bubble system (spawn, grow, explode)
|
|
||||||
4. **Add** movement buffer detection and decay
|
|
||||||
5. **Add** layer-based priority logic
|
|
||||||
6. **Change** arena from 20×20 to 24×24
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
main.gd
|
|
||||||
├── _init_managers() ← GauntletManager instantiation (existing)
|
|
||||||
├── _setup_host_game() ← GauntletManager._setup_arena()
|
|
||||||
├── _start_game() ← GauntletManager.start_game_mode()
|
|
||||||
│
|
|
||||||
GauntletManager (MODIFY EXISTING)
|
|
||||||
├── _setup_arena() ← 24×24 grid, center 3×3 NPC zone
|
|
||||||
├── _setup_hud() ← Mission label, cleanser indicator
|
|
||||||
├── start_game_mode() ← Start growth timer, spawn tiles
|
|
||||||
├── _process() ← Growth tick timer, bubble timer, phase escalation
|
|
||||||
├── GrowthTick system ← Candidate scoring, weighted selection, telegraph
|
|
||||||
├── CandyBubble system ← Bubble spawn, grow, explode
|
|
||||||
├── StickyCell system ← Layer 2 overlay, trap logic
|
|
||||||
├── MovementBuffer system ← Hidden safe zone detection, decay, camping override
|
|
||||||
├── Cleanser system ← Existing powerup
|
|
||||||
├── Smack system ← Existing modified push
|
|
||||||
└── Win condition ← Highest score at timer end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. File-by-File Implementation
|
|
||||||
|
|
||||||
### 3.1 Game Mode Registration — Already Done
|
|
||||||
|
|
||||||
The existing `game_mode.gd` already has:
|
|
||||||
```gdscript
|
|
||||||
enum Mode {
|
|
||||||
FREEMODE = 0,
|
|
||||||
STOP_N_GO = 1,
|
|
||||||
TEKTON_DOORS = 2,
|
|
||||||
GAUNTLET = 3 # Already registered
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And `LobbyManager` already has `"Candy Cannon Survival"` in `available_game_modes`. The mode name string can remain as-is or be updated to `"Candy Pump Survival"` if desired.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.2 Core Manager — `gauntlet_manager.gd` (MODIFY EXISTING)
|
|
||||||
|
|
||||||
**Location:** `scripts/managers/gauntlet_manager.gd`
|
|
||||||
|
|
||||||
**Major structural changes:**
|
|
||||||
|
|
||||||
#### Remove (cannon-based system):
|
|
||||||
|
|
||||||
```
|
|
||||||
var cannon_timer: float
|
|
||||||
var cannon_interval: float
|
|
||||||
var volley_size: int
|
|
||||||
var last_targeted_player_id: int
|
|
||||||
func _fire_volley()
|
|
||||||
func _select_targets()
|
|
||||||
func _get_near_player_target()
|
|
||||||
func _get_route_blocking_target()
|
|
||||||
func _get_random_non_sticky_target()
|
|
||||||
func _get_random_target()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Add (growth-based system):
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
class_name GauntletManager
|
|
||||||
extends Node
|
|
||||||
|
|
||||||
# Signals
|
|
||||||
signal phase_changed(phase_index: int)
|
|
||||||
signal growth_tick(targets: Array)
|
|
||||||
signal player_trapped(player_id: int)
|
|
||||||
signal cleanser_granted(player_id: int)
|
|
||||||
signal bubble_spawned(center: Vector2i)
|
|
||||||
signal bubble_exploded(center: Vector2i, area: Array[Vector2i])
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
const ARENA_SIZE = 24
|
|
||||||
const NPC_SIZE = 3
|
|
||||||
const NPC_CENTER = Vector2i(11, 11) # Center of 24×24
|
|
||||||
const TILE_STICKY = 17
|
|
||||||
const TILE_TELEGRAPH = 18
|
|
||||||
const TILE_WALKABLE = 0
|
|
||||||
const TILE_OBSTACLE = 4
|
|
||||||
|
|
||||||
# Phase timing
|
|
||||||
enum Phase { OUTER_PRESSURE, MIDDLE_PRESSURE, INNER_SURVIVAL }
|
|
||||||
var current_phase: Phase = Phase.OUTER_PRESSURE
|
|
||||||
var elapsed_time: float = 0.0
|
|
||||||
|
|
||||||
# Growth tick state
|
|
||||||
var growth_timer: float = 0.0
|
|
||||||
var growth_interval: float = 3.0
|
|
||||||
var telegraph_duration: float = 1.0
|
|
||||||
var sticky_cells: Dictionary = {} # Vector2i -> true
|
|
||||||
var telegraphed_cells: Dictionary = {} # Vector2i -> true
|
|
||||||
|
|
||||||
# Phase-based growth config
|
|
||||||
var phase_growth_config: Array = [
|
|
||||||
{"cells_per_tick": [4, 6], "distribution": {"outer": 0.75, "middle": 0.10, "inner": 0.00, "near_player": 0.10, "random": 0.05}},
|
|
||||||
{"cells_per_tick": [6, 8], "distribution": {"outer": 0.20, "middle": 0.50, "inner": 0.00, "near_player": 0.15, "sticky_expansion": 0.10, "random": 0.05}},
|
|
||||||
{"cells_per_tick": [8, 10], "distribution": {"outer": 0.10, "middle": 0.25, "inner": 0.35, "near_player": 0.15, "sticky_expansion": 0.15, "random": 0.10}},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Candy bubble state
|
|
||||||
var bubble_timer: float = 0.0
|
|
||||||
var bubbles_this_phase: int = 0
|
|
||||||
var max_bubbles_per_phase: Array = [0, 2, 3]
|
|
||||||
var active_bubbles: Array = [] # [{center, grow_timer, warning_area}]
|
|
||||||
var recent_bubble_positions: Array = [] # For RepetitionPenalty
|
|
||||||
|
|
||||||
# Movement buffer state
|
|
||||||
var movement_buffers: Dictionary = {} # Vector2i -> {penalty: float, created_at: float}
|
|
||||||
var camping_tracker: Dictionary = {} # player_id -> {position: Vector2i, since: float}
|
|
||||||
|
|
||||||
# Smack state (per-player) — unchanged
|
|
||||||
var smack_cooldowns: Dictionary = {}
|
|
||||||
var smack_charged: Dictionary = {}
|
|
||||||
|
|
||||||
# Cleanser tracking — unchanged
|
|
||||||
var player_mission_completions: Dictionary = {}
|
|
||||||
var player_cleansers: Dictionary = {}
|
|
||||||
|
|
||||||
# Trapped players — unchanged
|
|
||||||
var trapped_players: Dictionary = {}
|
|
||||||
|
|
||||||
# Arena layer cache
|
|
||||||
var arena_layers: Dictionary = {} # Vector2i -> "outer"/"middle"/"inner"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.3 Arena Setup — `_setup_arena()`
|
|
||||||
|
|
||||||
**Pattern source:** `StopNGoManager._setup_arena()`
|
|
||||||
|
|
||||||
Key changes from 20×20 to 24×24:
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _setup_arena():
|
|
||||||
if not multiplayer.is_server():
|
|
||||||
return
|
|
||||||
# Resize gridmap to 24×24
|
|
||||||
enhanced_gridmap.columns = ARENA_SIZE
|
|
||||||
enhanced_gridmap.rows = ARENA_SIZE
|
|
||||||
enhanced_gridmap.floors = 3
|
|
||||||
# Clear all layers
|
|
||||||
enhanced_gridmap.clear_floor(0)
|
|
||||||
enhanced_gridmap.clear_floor(1)
|
|
||||||
enhanced_gridmap.clear_floor(2)
|
|
||||||
# Fill Floor 0 with walkable tiles
|
|
||||||
for x in range(ARENA_SIZE):
|
|
||||||
for z in range(ARENA_SIZE):
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
|
||||||
# Block center 3×3 for Candy Pump NPC
|
|
||||||
for x in range(NPC_CENTER.x - 1, NPC_CENTER.x + 2):
|
|
||||||
for z in range(NPC_CENTER.y - 1, NPC_CENTER.y + 2):
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE)
|
|
||||||
# Build arena layer map
|
|
||||||
_build_arena_layers()
|
|
||||||
# Sync to clients
|
|
||||||
rpc("sync_arena_setup", ARENA_SIZE, NPC_CENTER)
|
|
||||||
enhanced_gridmap.initialize_astar()
|
|
||||||
enhanced_gridmap.update_astar_costs()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.4 Layer Calculation — `_build_arena_layers()`
|
|
||||||
|
|
||||||
**New method.** Precomputes the layer for every cell based on edge distance.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _build_arena_layers():
|
|
||||||
arena_layers.clear()
|
|
||||||
for x in range(ARENA_SIZE):
|
|
||||||
for z in range(ARENA_SIZE):
|
|
||||||
var edge_dist = mini(x, z, ARENA_SIZE - 1 - x, ARENA_SIZE - 1 - z)
|
|
||||||
var layer: String
|
|
||||||
if edge_dist <= 3:
|
|
||||||
layer = "outer"
|
|
||||||
elif edge_dist <= 7:
|
|
||||||
layer = "middle"
|
|
||||||
else:
|
|
||||||
layer = "inner"
|
|
||||||
arena_layers[Vector2i(x, z)] = layer
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.5 Growth Tick System — `_process_growth_tick()`
|
|
||||||
|
|
||||||
**Replaces** `_fire_volley()`. Called every 3 seconds.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _process_growth_tick():
|
|
||||||
if not multiplayer.is_server():
|
|
||||||
return
|
|
||||||
var config = phase_growth_config[current_phase]
|
|
||||||
var cell_count = randi_range(config.cells_per_tick[0], config.cells_per_tick[1])
|
|
||||||
var candidates = _generate_candidates()
|
|
||||||
var selected = _select_cells_weighted(candidates, cell_count)
|
|
||||||
# Path safety check
|
|
||||||
selected = _apply_path_safety(selected)
|
|
||||||
# Movement buffer check
|
|
||||||
selected = _apply_movement_buffer_check(selected)
|
|
||||||
# Telegraph
|
|
||||||
_telegraph_cells(selected)
|
|
||||||
# After telegraph_duration: apply sticky
|
|
||||||
get_tree().create_timer(telegraph_duration).timeout.connect(func():
|
|
||||||
_apply_sticky_cells(selected)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.6 Candidate Generation — `_generate_candidates()`
|
|
||||||
|
|
||||||
**New method.** Builds scored list of all SAFE cells.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _generate_candidates() -> Array:
|
|
||||||
var candidates: Array = []
|
|
||||||
var players = get_tree().get_nodes_in_group("Players")
|
|
||||||
for x in range(ARENA_SIZE):
|
|
||||||
for z in range(ARENA_SIZE):
|
|
||||||
var pos = Vector2i(x, z)
|
|
||||||
if not _is_cell_valid_for_growth(pos):
|
|
||||||
continue
|
|
||||||
var score = _calculate_candidate_score(pos, players)
|
|
||||||
candidates.append({"pos": pos, "score": score})
|
|
||||||
return candidates
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.7 Candidate Scoring — `_calculate_candidate_score()`
|
|
||||||
|
|
||||||
**New method.** Implements the full Candidate Score formula from the GDD.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _calculate_candidate_score(pos: Vector2i, players: Array) -> float:
|
|
||||||
var score: float = 0.0
|
|
||||||
|
|
||||||
# LayerPriority
|
|
||||||
var layer = arena_layers.get(pos, "outer")
|
|
||||||
var layer_scores = {
|
|
||||||
Phase.OUTER_PRESSURE: {"outer": 60.0, "middle": 15.0, "inner": -40.0},
|
|
||||||
Phase.MIDDLE_PRESSURE: {"outer": 20.0, "middle": 60.0, "inner": 5.0},
|
|
||||||
Phase.INNER_SURVIVAL: {"outer": 10.0, "middle": 35.0, "inner": 60.0},
|
|
||||||
}
|
|
||||||
score += layer_scores[current_phase].get(layer, 0.0)
|
|
||||||
|
|
||||||
# StickyNeighborScore (+8 per sticky neighbor, max +64)
|
|
||||||
var neighbors = _get_8_neighbors(pos)
|
|
||||||
for n in neighbors:
|
|
||||||
if sticky_cells.has(n):
|
|
||||||
score += 8.0
|
|
||||||
|
|
||||||
# InwardPressureScore
|
|
||||||
var center_dist = pos.distance_to(Vector2(NPC_CENTER))
|
|
||||||
var max_dist = Vector2(ARENA_SIZE, ARENA_SIZE).length() / 2.0
|
|
||||||
var inward_ratio = 1.0 - (center_dist / max_dist)
|
|
||||||
match current_phase:
|
|
||||||
Phase.OUTER_PRESSURE: score += lerpf(0.0, 10.0, inward_ratio)
|
|
||||||
Phase.MIDDLE_PRESSURE: score += lerpf(5.0, 20.0, inward_ratio)
|
|
||||||
Phase.INNER_SURVIVAL: score += lerpf(10.0, 30.0, inward_ratio)
|
|
||||||
|
|
||||||
# PlayerPressureScore
|
|
||||||
var min_player_dist = INF
|
|
||||||
for p in players:
|
|
||||||
var p_pos = Vector2i(p.grid_position.x, p.grid_position.z) if p.has_method("get_grid_position") else Vector2i(p.position.x, p.position.z)
|
|
||||||
var dist = pos.distance_to(p_pos)
|
|
||||||
min_player_dist = mini(min_player_dist, int(dist))
|
|
||||||
if min_player_dist >= 2 and min_player_dist <= 4:
|
|
||||||
score += 20.0
|
|
||||||
elif min_player_dist == 0:
|
|
||||||
if elapsed_time < 150.0: # Before final 30s
|
|
||||||
score -= 50.0
|
|
||||||
else:
|
|
||||||
score += 10.0
|
|
||||||
|
|
||||||
# ClusterGrowthScore
|
|
||||||
if _connects_sticky_clusters(pos):
|
|
||||||
score += 25.0
|
|
||||||
elif _expands_sticky_cluster(pos):
|
|
||||||
score += 15.0
|
|
||||||
|
|
||||||
# RoutePressureScore
|
|
||||||
if _is_high_traffic_route(pos):
|
|
||||||
score += randf_range(10.0, 25.0)
|
|
||||||
|
|
||||||
# CampingPressureScore
|
|
||||||
for pid in camping_tracker:
|
|
||||||
var camp = camping_tracker[pid]
|
|
||||||
if pos.distance_to(camp.position) <= 4:
|
|
||||||
var camp_duration = elapsed_time - camp.since
|
|
||||||
if camp_duration > 10.0 and player_cleansers.get(pid, 0) > 0:
|
|
||||||
score += 60.0
|
|
||||||
elif camp_duration > 8.0:
|
|
||||||
score += 40.0
|
|
||||||
elif camp_duration > 5.0:
|
|
||||||
score += 20.0
|
|
||||||
|
|
||||||
# RandomNoise
|
|
||||||
score += randf_range(-20.0, 20.0)
|
|
||||||
|
|
||||||
# MovementBufferPenalty
|
|
||||||
if movement_buffers.has(pos):
|
|
||||||
var buffer = movement_buffers[pos]
|
|
||||||
var penalty = _get_buffer_penalty(buffer.penalty)
|
|
||||||
score += penalty
|
|
||||||
|
|
||||||
# PathSafetyPenalty
|
|
||||||
if _would_trap_player(pos) and elapsed_time < 150.0:
|
|
||||||
score -= 100.0
|
|
||||||
elif _removes_last_exit(pos):
|
|
||||||
score -= 60.0
|
|
||||||
elif _makes_route_too_narrow(pos):
|
|
||||||
score -= 20.0
|
|
||||||
|
|
||||||
# RepetitionPenalty
|
|
||||||
if _was_recently_targeted(pos):
|
|
||||||
score -= 30.0
|
|
||||||
elif _region_targeted_repeatedly(pos):
|
|
||||||
score -= 15.0
|
|
||||||
|
|
||||||
return score
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.8 Weighted Cell Selection — `_select_cells_weighted()`
|
|
||||||
|
|
||||||
**New method.** Selects cells using weighted randomness from scored candidates.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _select_cells_weighted(candidates: Array, count: int) -> Array[Vector2i]:
|
|
||||||
# Sort by score descending
|
|
||||||
candidates.sort_custom(func(a, b): return a.score > b.score)
|
|
||||||
# Build weight array
|
|
||||||
var weights: Array[float] = []
|
|
||||||
var total_weight: float = 0.0
|
|
||||||
for c in candidates:
|
|
||||||
var w = maxf(c.score + 100.0, 1.0) # Offset to ensure positive weights
|
|
||||||
weights.append(w)
|
|
||||||
total_weight += w
|
|
||||||
# Weighted random selection without replacement
|
|
||||||
var selected: Array[Vector2i] = []
|
|
||||||
var available = candidates.duplicate()
|
|
||||||
var available_weights = weights.duplicate()
|
|
||||||
for i in range mini(count, available.size()):
|
|
||||||
var roll = randf() * total_weight
|
|
||||||
var cumulative = 0.0
|
|
||||||
for j in range(available.size()):
|
|
||||||
cumulative += available_weights[j]
|
|
||||||
if roll <= cumulative:
|
|
||||||
selected.append(available[j].pos)
|
|
||||||
total_weight -= available_weights[j]
|
|
||||||
available.remove_at(j)
|
|
||||||
available_weights.remove_at(j)
|
|
||||||
break
|
|
||||||
return selected
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.9 Candy Bubble System
|
|
||||||
|
|
||||||
#### Bubble Spawn Timer
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _process_bubbles(delta: float):
|
|
||||||
if not multiplayer.is_server():
|
|
||||||
return
|
|
||||||
# Tick active bubbles
|
|
||||||
for i in range(active_bubbles.size() - 1, -1, -1):
|
|
||||||
var bubble = active_bubbles[i]
|
|
||||||
bubble.grow_timer -= delta
|
|
||||||
if bubble.grow_timer <= 0:
|
|
||||||
_explode_bubble(bubble)
|
|
||||||
active_bubbles.remove_at(i)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Bubble Spawn Logic
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _try_spawn_bubble():
|
|
||||||
var max_bubbles = max_bubbles_per_phase[current_phase]
|
|
||||||
if bubbles_this_phase >= max_bubbles:
|
|
||||||
return
|
|
||||||
var candidates = _generate_bubble_candidates()
|
|
||||||
if candidates.is_empty():
|
|
||||||
return
|
|
||||||
# Weighted selection
|
|
||||||
var selected = _select_bubble_target(candidates)
|
|
||||||
_spawn_bubble(selected)
|
|
||||||
bubbles_this_phase += 1
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Bubble Candidate Scoring
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _generate_bubble_candidates() -> Array:
|
|
||||||
var candidates: Array = []
|
|
||||||
var players = get_tree().get_nodes_in_group("Players")
|
|
||||||
for x in range(ARENA_SIZE):
|
|
||||||
for z in range(ARENA_SIZE):
|
|
||||||
var pos = Vector2i(x, z)
|
|
||||||
if not _is_cell_valid_for_bubble(pos):
|
|
||||||
continue
|
|
||||||
var score = _calculate_bubble_score(pos, players)
|
|
||||||
candidates.append({"pos": pos, "score": score})
|
|
||||||
return candidates
|
|
||||||
|
|
||||||
func _calculate_bubble_score(pos: Vector2i, players: Array) -> float:
|
|
||||||
var score: float = 0.0
|
|
||||||
|
|
||||||
# CampingScore
|
|
||||||
for pid in camping_tracker:
|
|
||||||
var camp = camping_tracker[pid]
|
|
||||||
if pos.distance_to(camp.position) <= 4:
|
|
||||||
var camp_duration = elapsed_time - camp.since
|
|
||||||
if camp_duration > 10.0 and player_cleansers.get(pid, 0) > 0:
|
|
||||||
score += 80.0
|
|
||||||
elif camp_duration > 8.0:
|
|
||||||
score += 60.0
|
|
||||||
elif camp_duration > 5.0:
|
|
||||||
score += 40.0
|
|
||||||
|
|
||||||
# UntouchedAreaScore
|
|
||||||
if _is_near_untouched_cluster(pos):
|
|
||||||
score += 30.0
|
|
||||||
|
|
||||||
# PlayerClusterScore
|
|
||||||
var nearby_players = 0
|
|
||||||
for p in players:
|
|
||||||
var p_pos = Vector2i(p.position.x, p.position.z)
|
|
||||||
if pos.distance_to(p_pos) <= 5:
|
|
||||||
nearby_players += 1
|
|
||||||
if nearby_players >= 2:
|
|
||||||
score += 20.0
|
|
||||||
|
|
||||||
# MissionRouteScore
|
|
||||||
if _is_important_for_scoring(pos):
|
|
||||||
score += randf_range(10.0, 20.0)
|
|
||||||
|
|
||||||
# RandomNoise
|
|
||||||
score += randf_range(-20.0, 20.0)
|
|
||||||
|
|
||||||
# DirectHitPenalty
|
|
||||||
for p in players:
|
|
||||||
var p_pos = Vector2i(p.position.x, p.position.z)
|
|
||||||
if pos == p_pos:
|
|
||||||
score -= 60.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# RecentBubblePenalty
|
|
||||||
for recent in recent_bubble_positions:
|
|
||||||
if pos.distance_to(recent) <= 5:
|
|
||||||
score -= 50.0
|
|
||||||
break
|
|
||||||
|
|
||||||
# UnfairTrapPenalty
|
|
||||||
if _would_create_unfair_trap(pos):
|
|
||||||
score -= 100.0
|
|
||||||
|
|
||||||
return score
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Bubble Explosion
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _explode_bubble(bubble: Dictionary):
|
|
||||||
var center = bubble.center
|
|
||||||
var explosion_area: Array[Vector2i] = []
|
|
||||||
for dx in range(-1, 2):
|
|
||||||
for dz in range(-1, 2):
|
|
||||||
var pos = Vector2i(center.x + dx, center.y + dz)
|
|
||||||
if _is_cell_valid_for_growth(pos):
|
|
||||||
explosion_area.append(pos)
|
|
||||||
# Telegraph 3×3 area briefly, then apply sticky
|
|
||||||
_telegraph_cells(explosion_area)
|
|
||||||
get_tree().create_timer(0.5).timeout.connect(func():
|
|
||||||
_apply_sticky_cells(explosion_area)
|
|
||||||
rpc("sync_bubble_explode", center, explosion_area)
|
|
||||||
recent_bubble_positions.append(center)
|
|
||||||
if recent_bubble_positions.size() > 5:
|
|
||||||
recent_bubble_positions.remove_at(0)
|
|
||||||
)
|
|
||||||
rpc("sync_bubble_explode_vfx", center)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.10 Movement Buffer System
|
|
||||||
|
|
||||||
#### Buffer Detection
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _detect_movement_buffers():
|
|
||||||
# Find all connected clusters of SAFE cells
|
|
||||||
var visited: Dictionary = {}
|
|
||||||
var clusters: Array = []
|
|
||||||
for x in range(ARENA_SIZE):
|
|
||||||
for z in range(ARENA_SIZE):
|
|
||||||
var pos = Vector2i(x, z)
|
|
||||||
if visited.has(pos) or not _is_cell_safe(pos):
|
|
||||||
continue
|
|
||||||
var cluster = _flood_fill_safe_cluster(pos, visited)
|
|
||||||
clusters.append(cluster)
|
|
||||||
# Apply buffer penalties to clusters that are critical for movement
|
|
||||||
for cluster in clusters:
|
|
||||||
if _is_critical_for_movement(cluster):
|
|
||||||
for pos in cluster:
|
|
||||||
if not movement_buffers.has(pos):
|
|
||||||
movement_buffers[pos] = {"penalty": 1.0, "created_at": elapsed_time}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Buffer Decay
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _decay_movement_buffers():
|
|
||||||
var to_remove: Array = []
|
|
||||||
for pos in movement_buffers:
|
|
||||||
var buffer = movement_buffers[pos]
|
|
||||||
# Every 5 seconds: reduce penalty by 25%
|
|
||||||
var age = elapsed_time - buffer.created_at
|
|
||||||
var decay_cycles = int(age / 5.0)
|
|
||||||
buffer.penalty *= pow(0.75, decay_cycles)
|
|
||||||
# Phase change: reduce by 50%
|
|
||||||
# (Applied once at phase transition, tracked separately)
|
|
||||||
# Final 30s: remove most
|
|
||||||
if elapsed_time > 150.0:
|
|
||||||
buffer.penalty *= 0.1
|
|
||||||
if buffer.penalty < 0.05:
|
|
||||||
to_remove.append(pos)
|
|
||||||
for pos in to_remove:
|
|
||||||
movement_buffers.erase(pos)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Camping Detection
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _update_camping_tracker():
|
|
||||||
var players = get_tree().get_nodes_in_group("Players")
|
|
||||||
for p in players:
|
|
||||||
var pid = p.get_multiplayer_authority()
|
|
||||||
var p_pos = Vector2i(p.position.x, p.position.z)
|
|
||||||
if camping_tracker.has(pid):
|
|
||||||
var camp = camping_tracker[pid]
|
|
||||||
if p_pos == camp.position:
|
|
||||||
pass # Still camping
|
|
||||||
else:
|
|
||||||
camping_tracker[pid] = {"position": p_pos, "since": elapsed_time}
|
|
||||||
else:
|
|
||||||
camping_tracker[pid] = {"position": p_pos, "since": elapsed_time}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.11 Sticky Cell Application
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _apply_sticky_cells(positions: Array[Vector2i]):
|
|
||||||
for pos in positions:
|
|
||||||
if not _is_cell_valid_for_growth(pos):
|
|
||||||
continue
|
|
||||||
sticky_cells[pos] = true
|
|
||||||
telegraphed_cells.erase(pos)
|
|
||||||
# Set Layer 2 overlay
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY)
|
|
||||||
# Check if any player is now on sticky
|
|
||||||
_check_players_on_sticky()
|
|
||||||
# Update A* costs
|
|
||||||
enhanced_gridmap.update_astar_costs()
|
|
||||||
# Sync to clients
|
|
||||||
rpc("sync_sticky_cells", sticky_cells.keys())
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.12 Player Sticky Check
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _check_players_on_sticky():
|
|
||||||
var players = get_tree().get_nodes_in_group("Players")
|
|
||||||
for p in players:
|
|
||||||
var p_pos = Vector2i(p.position.x, p.position.z)
|
|
||||||
if sticky_cells.has(p_pos):
|
|
||||||
var pid = p.get_multiplayer_authority()
|
|
||||||
if is_cleanser_active(pid):
|
|
||||||
clear_sticky_cell(p_pos)
|
|
||||||
use_cleanser_cell(pid)
|
|
||||||
else:
|
|
||||||
_trap_player(p)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.13 Path Safety Check
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _apply_path_safety(selected: Array[Vector2i]) -> Array[Vector2i]:
|
|
||||||
if elapsed_time > 150.0: # Final 30s: softer rules
|
|
||||||
return selected
|
|
||||||
var players = get_tree().get_nodes_in_group("Players")
|
|
||||||
var result = selected.duplicate()
|
|
||||||
for p in players:
|
|
||||||
var pid = p.get_multiplayer_authority()
|
|
||||||
if trapped_players.has(pid):
|
|
||||||
continue
|
|
||||||
var p_pos = Vector2i(p.position.x, p.position.z)
|
|
||||||
# Temporarily apply selected cells
|
|
||||||
var temp_sticky = sticky_cells.duplicate()
|
|
||||||
for pos in result:
|
|
||||||
temp_sticky[pos] = true
|
|
||||||
# Check if player has reachable safe cells within 6–8 cells
|
|
||||||
var has_escape = _has_reachable_safe_cell(p_pos, temp_sticky, 8)
|
|
||||||
if not has_escape:
|
|
||||||
# Replace some cells with safer alternatives
|
|
||||||
result = _replace_with_safer_candidates(result, 2)
|
|
||||||
return result
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.14 Telegraph System (Modified)
|
|
||||||
|
|
||||||
The existing telegraph system works but needs adaptation for growth ticks instead of cannon volleys.
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
func _telegraph_cells(positions: Array[Vector2i]):
|
|
||||||
for pos in positions:
|
|
||||||
telegraphed_cells[pos] = true
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
|
||||||
rpc("sync_growth_telegraph", positions)
|
|
||||||
# Animate telegraph
|
|
||||||
_animate_growth_telegraph(positions)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reuse existing** `_animate_telegraph()` tween pattern from current GauntletManager.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.15 Network Sync
|
|
||||||
|
|
||||||
| Data | Sync Method | Pattern |
|
|
||||||
|---|---|---|
|
|
||||||
| Sticky cells | `rpc("sync_sticky_cells", positions)` | Same as `sync_grid_item` |
|
|
||||||
| Growth telegraph | `rpc("sync_growth_telegraph", positions)` | Same as `sync_telegraph` |
|
|
||||||
| Phase changes | `rpc("sync_gauntlet_phase", phase_idx, elapsed)` | Same as `sync_phase` |
|
|
||||||
| Bubble spawn | `rpc("sync_bubble_spawn", center, grow_duration)` | New RPC |
|
|
||||||
| Bubble explode | `rpc("sync_bubble_explode", center, area)` | New RPC |
|
|
||||||
| Trap state | `player.rpc("sync_trapped", true)` | Same as `sync_stop_freeze` |
|
|
||||||
| Cleanser grant | `rpc("sync_cleanser", peer_id, count)` | Same as `sync_goal_count` |
|
|
||||||
| Smack state | `player.rpc("sync_smack_state", charged)` | Same as `sync_modulate` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.16 Integration Points in `main.gd`
|
|
||||||
|
|
||||||
The existing integration in `main.gd` already handles GauntletManager. No changes needed unless the mode name string is updated.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. New Files Summary
|
|
||||||
|
|
||||||
| File | Type | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| (none) | — | All changes are modifications to existing `gauntlet_manager.gd` |
|
|
||||||
|
|
||||||
## 5. Modified Files Summary
|
|
||||||
|
|
||||||
| File | Changes |
|
|
||||||
|---|---|
|
|
||||||
| `scripts/managers/gauntlet_manager.gd` | **Major rewrite:** Replace cannon system with growth tick system, add candidate scoring, add candy bubble system, add movement buffer system, add layer calculation, change arena to 24×24 |
|
|
||||||
| `scripts/game_mode.gd` | Optionally rename string to `"Candy Pump Survival"` |
|
|
||||||
| `scripts/managers/lobby_manager.gd` | Optionally rename mode string; update settings (remove cannon_interval, volley_size; add growth_interval, cells_per_tick) |
|
|
||||||
| `scripts/mode_config.gd` | Update schema: remove `gauntlet_cannon_interval`, `gauntlet_volley_size`; add `gauntlet_growth_interval`, `gauntlet_cells_per_tick_phase1/2/3` |
|
|
||||||
| `scenes/main.gd` | Update mode string match if renamed |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Helper Methods Required
|
|
||||||
|
|
||||||
These utility methods need to be added to `gauntlet_manager.gd`:
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# Cell validation
|
|
||||||
func _is_cell_valid_for_growth(pos: Vector2i) -> bool
|
|
||||||
func _is_cell_valid_for_bubble(pos: Vector2i) -> bool
|
|
||||||
func _is_cell_safe(pos: Vector2i) -> bool
|
|
||||||
|
|
||||||
# Neighbor queries
|
|
||||||
func _get_8_neighbors(pos: Vector2i) -> Array[Vector2i]
|
|
||||||
func _flood_fill_safe_cluster(start: Vector2i, visited: Dictionary) -> Array[Vector2i]
|
|
||||||
|
|
||||||
# Cluster analysis
|
|
||||||
func _expands_sticky_cluster(pos: Vector2i) -> bool
|
|
||||||
func _connects_sticky_clusters(pos: Vector2i) -> bool
|
|
||||||
func _is_near_untouched_cluster(pos: Vector2i) -> bool
|
|
||||||
func _is_critical_for_movement(cluster: Array) -> bool
|
|
||||||
|
|
||||||
# Route analysis
|
|
||||||
func _is_high_traffic_route(pos: Vector2i) -> bool
|
|
||||||
func _is_important_for_scoring(pos: Vector2i) -> bool
|
|
||||||
func _would_trap_player(pos: Vector2i) -> bool
|
|
||||||
func _removes_last_exit(pos: Vector2i) -> bool
|
|
||||||
func _makes_route_too_narrow(pos: Vector2i) -> bool
|
|
||||||
func _would_create_unfair_trap(pos: Vector2i) -> bool
|
|
||||||
func _has_reachable_safe_cell(from: Vector2i, temp_sticky: Dictionary, radius: int) -> bool
|
|
||||||
|
|
||||||
# Repetition tracking
|
|
||||||
func _was_recently_targeted(pos: Vector2i) -> bool
|
|
||||||
func _region_targeted_repeatedly(pos: Vector2i) -> bool
|
|
||||||
|
|
||||||
# Bubble helpers
|
|
||||||
func _select_bubble_target(candidates: Array) -> Vector2i
|
|
||||||
func _replace_with_safer_candidates(selected: Array[Vector2i], count: int) -> Array[Vector2i]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Implementation Priority (Recommended Order)
|
|
||||||
|
|
||||||
1. **Update arena to 24×24** — Modify `_setup_arena()`, update `NPC_CENTER`, update `_build_arena_layers()`
|
|
||||||
2. **Replace cannon with growth tick** — Remove `_fire_volley()`, add `_process_growth_tick()`, `_generate_candidates()`, `_calculate_candidate_score()`
|
|
||||||
3. **Weighted cell selection** — `_select_cells_weighted()`, sticky application, A* cost update
|
|
||||||
4. **Movement buffer system** — `_detect_movement_buffers()`, `_decay_movement_buffers()`, buffer penalty in scoring
|
|
||||||
5. **Path safety check** — `_apply_path_safety()`, `_has_reachable_safe_cell()`, replace unsafe selections
|
|
||||||
6. **Candy bubble system** — Bubble timer, `_try_spawn_bubble()`, bubble scoring, `_explode_bubble()`
|
|
||||||
7. **Camping detection** — `_update_camping_tracker()`, camping score in candidate and bubble scoring
|
|
||||||
8. **Update HUD** — Growth tick indicator, bubble warning, phase label
|
|
||||||
9. **Network sync** — New RPCs for growth telegraph, bubble spawn/explode
|
|
||||||
10. **Bot AI** — Sticky avoidance, pathfinding through sticky, cleanser usage
|
|
||||||
11. **Polish** — VFX for growth ticks, bubble animations, screen shake on explosion, sound effects
|
|
||||||
12. **Update lobby settings** — Replace cannon/volley settings with growth settings in `lobby_manager.gd` and `mode_config.gd`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Risk Assessment
|
|
||||||
|
|
||||||
| Risk | Mitigation |
|
|
||||||
|---|---|
|
|
||||||
| GridMap Layer 2 conflict with existing freeze/safe overlays | Gauntlet mode is exclusive — no freeze/safe tiles in this mode |
|
|
||||||
| 24×24 grid performance (576 cells + scoring every 3s) | Scoring runs on server only; candidate list is max 567 cells; weighted selection is O(n log n) |
|
|
||||||
| Movement buffer creating invisible safe zones that feel unfair | Buffers decay aggressively; camping override removes them; final 30s removes most; players experience it as "uneven growth" not "protected zones" |
|
|
||||||
| Path safety check preventing any arena pressure | Only triggers when a player would be fully trapped; final 30s disables strict check |
|
|
||||||
| Bubble stacking creating unavoidable traps | RecentBubblePenalty (-50) prevents nearby bubbles; max 5 per round; UnfairTrapPenalty (-100) prevents instant failures |
|
|
||||||
| Candidate scoring feeling too complex to tune | Start with simple weights; each component is independent and tunable; playtest to adjust |
|
|
||||||
| A* pathfinding cost updates every 3s causing lag | `update_astar_costs()` is lightweight (updates existing AStar2D); only runs on server |
|
|
||||||
@@ -1,1209 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Candy Pump Survival — Gameplay Visualization</title>
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
background: #1a1a2e;
|
|
||||||
color: #eee;
|
|
||||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 12px 0 4px;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #ff99cc;
|
|
||||||
text-shadow: 0 0 12px #ff66b288;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #888;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
#top-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.stat-box {
|
|
||||||
background: #16213e;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 6px 14px;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
.stat-box .label { font-size: 10px; color: #888; text-transform: uppercase; }
|
|
||||||
.stat-box .value { font-size: 22px; font-weight: bold; }
|
|
||||||
.stat-box .value.timer { color: #ffcc33; }
|
|
||||||
.stat-box .value.phase1 { color: #ff99cc; }
|
|
||||||
.stat-box .value.phase2 { color: #ffcc33; }
|
|
||||||
.stat-box .value.phase3 { color: #ff4d4d; }
|
|
||||||
.stat-box .value.score { color: #61ff54; }
|
|
||||||
.stat-box .value.sticky { color: #ff66b2; }
|
|
||||||
|
|
||||||
#game-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
canvas {
|
|
||||||
border: 2px solid #333;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
#sidebar {
|
|
||||||
width: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
background: #16213e;
|
|
||||||
border: 1px solid #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
.panel h3 {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #ff99cc;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
.player-entry {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 3px 0;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.player-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid #000;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.player-name { flex: 1; }
|
|
||||||
.player-score { font-weight: bold; color: #61ff54; font-size: 15px; }
|
|
||||||
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
.legend-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
.legend-swatch {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #666;
|
|
||||||
margin-top: 4px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.controls kbd {
|
|
||||||
background: #222;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 1px 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bottom-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.log-entry {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #888;
|
|
||||||
padding: 1px 0;
|
|
||||||
}
|
|
||||||
.log-entry.trap { color: #ff4d4d; }
|
|
||||||
.log-entry.cleanse { color: #66e6ff; }
|
|
||||||
.log-entry.bubble { color: #ff99cc; }
|
|
||||||
.log-entry.score { color: #61ff54; }
|
|
||||||
.log-entry.phase { color: #ffcc33; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Candy Pump Survival</h1>
|
|
||||||
<div class="subtitle">Gauntlet Mode — Gameplay Visualization</div>
|
|
||||||
|
|
||||||
<div id="top-bar">
|
|
||||||
<div class="stat-box"><div class="label">Timer</div><div class="value timer" id="timer">3:00</div></div>
|
|
||||||
<div class="stat-box"><div class="label">Phase</div><div class="value phase1" id="phase-label">OUTER PRESSURE</div></div>
|
|
||||||
<div class="stat-box"><div class="label">Coverage</div><div class="value sticky" id="coverage">0%</div></div>
|
|
||||||
<div class="stat-box"><div class="label">Growth Tick</div><div class="value" id="tick-count" style="color:#ff99cc">0</div></div>
|
|
||||||
<div class="stat-box"><div class="label">Bubbles</div><div class="value" id="bubble-count" style="color:#ff66cc">0</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="game-container">
|
|
||||||
<canvas id="canvas"></canvas>
|
|
||||||
<div id="sidebar">
|
|
||||||
<div class="panel">
|
|
||||||
<h3>Players</h3>
|
|
||||||
<div id="player-list"></div>
|
|
||||||
</div>
|
|
||||||
<div class="panel">
|
|
||||||
<h3>Legend</h3>
|
|
||||||
<div class="legend">
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#4f4f4f"></div>Safe</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#d6268a"></div>Sticky</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#e8a020"></div>Telegraph</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#ff5722"></div>Bubble</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#cc3380"></div>NPC</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#d10000"></div>Wall</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#66e6ff"></div>Cleansed</div>
|
|
||||||
<div class="legend-item"><div class="legend-swatch" style="background:#e8ba38"></div>Mission</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel">
|
|
||||||
<h3>Controls</h3>
|
|
||||||
<div class="controls">
|
|
||||||
All 4 players are autonomous bots.<br>
|
|
||||||
<kbd>R</kbd> Restart simulation<br>
|
|
||||||
<kbd>P</kbd> Pause / resume<br>
|
|
||||||
<kbd>D</kbd> Toggle bot paths
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="bottom-bar">
|
|
||||||
<div class="panel" style="min-width:500px;max-width:700px">
|
|
||||||
<h3>Event Log</h3>
|
|
||||||
<div id="log" style="max-height:120px;overflow-y:auto;font-size:11px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// ── Constants ──
|
|
||||||
const ARENA = 24;
|
|
||||||
const NPC_CENTER = { x: 11, y: 11 };
|
|
||||||
const NPC_SIZE = 3;
|
|
||||||
const CELL_PX = 26;
|
|
||||||
const WALL = 4, WALKABLE = 0, STICKY = 17, TELEGRAPH = 18, NPC_BLOCK = 5;
|
|
||||||
const TOTAL_PLAYABLE = ARENA * ARENA - NPC_SIZE * NPC_SIZE;
|
|
||||||
|
|
||||||
const PHASE_NAMES = ['OUTER PRESSURE', 'MIDDLE PRESSURE', 'INNER SURVIVAL'];
|
|
||||||
const PHASE_CLASSES = ['phase1', 'phase2', 'phase3'];
|
|
||||||
const PHASE_GROWTH = [
|
|
||||||
{ min: 4, max: 6, bubbles: 0 },
|
|
||||||
{ min: 6, max: 8, bubbles: 2 },
|
|
||||||
{ min: 8, max: 10, bubbles: 3 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PLAYER_COLORS = ['#33cc33', '#4d80ff', '#ff9933', '#cc66ff'];
|
|
||||||
const PLAYER_NAMES = ['Pip', 'Dabro', 'Gatot', 'Copper'];
|
|
||||||
|
|
||||||
// ── Canvas ──
|
|
||||||
const canvas = document.getElementById('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = ARENA * CELL_PX;
|
|
||||||
canvas.height = ARENA * CELL_PX;
|
|
||||||
|
|
||||||
// ── Game State ──
|
|
||||||
let grid = []; // 0=walkable, 4=wall, 5=npc, 17=sticky, 18=telegraph, 6=cleansed
|
|
||||||
let cellScores = []; // candidate scores for debug overlay
|
|
||||||
let missionTiles = {}; // "x,y" -> true if has mission tile
|
|
||||||
let players = [];
|
|
||||||
let elapsed = 0;
|
|
||||||
let paused = false;
|
|
||||||
let growthTimer = 0;
|
|
||||||
let totalTicks = 0;
|
|
||||||
let bubblesSpawned = 0;
|
|
||||||
let activeBubbles = []; // {x, y, growTimer, maxGrow}
|
|
||||||
let recentBubbles = [];
|
|
||||||
let cleanserCount = [0, 0, 0, 0];
|
|
||||||
let cleanserActive = [false, false, false, false];
|
|
||||||
let cleanserCellsLeft = [0, 0, 0, 0];
|
|
||||||
let trapped = [false, false, false, false];
|
|
||||||
let scores = [0, 0, 0, 0];
|
|
||||||
let missions = [0, 0, 0, 0];
|
|
||||||
let logs = [];
|
|
||||||
let currentPhase = 0;
|
|
||||||
let showPaths = false;
|
|
||||||
|
|
||||||
// ── Initialization ──
|
|
||||||
function initGrid() {
|
|
||||||
grid = [];
|
|
||||||
cellScores = [];
|
|
||||||
for (let y = 0; y < ARENA; y++) {
|
|
||||||
grid[y] = [];
|
|
||||||
cellScores[y] = [];
|
|
||||||
for (let x = 0; x < ARENA; x++) {
|
|
||||||
// Walls on perimeter
|
|
||||||
if (x === 0 || y === 0 || x === ARENA - 1 || y === ARENA - 1) {
|
|
||||||
grid[y][x] = WALL;
|
|
||||||
}
|
|
||||||
// NPC center 3x3
|
|
||||||
else if (x >= NPC_CENTER.x - 1 && x <= NPC_CENTER.x + 1 &&
|
|
||||||
y >= NPC_CENTER.y - 1 && y <= NPC_CENTER.y + 1) {
|
|
||||||
grid[y][x] = NPC_BLOCK;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
grid[y][x] = WALKABLE;
|
|
||||||
}
|
|
||||||
cellScores[y][x] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initMissionTiles() {
|
|
||||||
missionTiles = {};
|
|
||||||
for (let y = 1; y < ARENA - 1; y++) {
|
|
||||||
for (let x = 1; x < ARENA - 1; x++) {
|
|
||||||
if (grid[y][x] === WALKABLE) {
|
|
||||||
missionTiles[`${x},${y}`] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPlayers() {
|
|
||||||
const spawns = [
|
|
||||||
{ x: 2, y: 2 }, { x: ARENA - 3, y: 2 },
|
|
||||||
{ x: 2, y: ARENA - 3 }, { x: ARENA - 3, y: ARENA - 3 }
|
|
||||||
];
|
|
||||||
players = [];
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
players.push({
|
|
||||||
x: spawns[i].x, y: spawns[i].y,
|
|
||||||
targetX: spawns[i].x, targetY: spawns[i].y,
|
|
||||||
moving: false, moveProgress: 0,
|
|
||||||
startX: spawns[i].x, startY: spawns[i].y
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetGame() {
|
|
||||||
elapsed = 0;
|
|
||||||
growthTimer = 0;
|
|
||||||
totalTicks = 0;
|
|
||||||
bubblesSpawned = 0;
|
|
||||||
activeBubbles = [];
|
|
||||||
recentBubbles = [];
|
|
||||||
currentPhase = 0;
|
|
||||||
cleanserCount = [0, 0, 0, 0];
|
|
||||||
cleanserActive = [false, false, false, false];
|
|
||||||
cleanserCellsLeft = [0, 0, 0, 0];
|
|
||||||
trapped = [false, false, false, false];
|
|
||||||
scores = [0, 0, 0, 0];
|
|
||||||
missions = [0, 0, 0, 0];
|
|
||||||
logs = [];
|
|
||||||
initGrid();
|
|
||||||
initMissionTiles();
|
|
||||||
initPlayers();
|
|
||||||
addLog('Game started! Collect tiles and survive the candy.', 'phase');
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Logging ──
|
|
||||||
function addLog(msg, cls = '') {
|
|
||||||
logs.unshift({ msg, cls, time: formatTime(elapsed) });
|
|
||||||
if (logs.length > 50) logs.pop();
|
|
||||||
renderLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderLog() {
|
|
||||||
const el = document.getElementById('log');
|
|
||||||
el.innerHTML = logs.slice(0, 15).map(l =>
|
|
||||||
`<div class="log-entry ${l.cls}">[${l.time}] ${l.msg}</div>`
|
|
||||||
).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Layer / Edge Distance ──
|
|
||||||
function getLayer(x, y) {
|
|
||||||
const edge = Math.min(x, y, ARENA - 1 - x, ARENA - 1 - y);
|
|
||||||
if (edge <= 3) return 'outer';
|
|
||||||
if (edge <= 7) return 'middle';
|
|
||||||
return 'inner';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEdgeDistance(x, y) {
|
|
||||||
return Math.min(x, y, ARENA - 1 - x, ARENA - 1 - y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Candidate Scoring ──
|
|
||||||
function isCellValidForGrowth(x, y) {
|
|
||||||
if (x < 0 || y < 0 || x >= ARENA || y >= ARENA) return false;
|
|
||||||
return grid[y][x] === WALKABLE || grid[y][x] === 6; // walkable or cleansed
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNeighbors8(x, y) {
|
|
||||||
const n = [];
|
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
|
||||||
if (dx === 0 && dy === 0) continue;
|
|
||||||
const nx = x + dx, ny = y + dy;
|
|
||||||
if (nx >= 0 && ny >= 0 && nx < ARENA && ny < ARENA) n.push({ x: nx, y: ny });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function countStickyNeighbors(x, y) {
|
|
||||||
return getNeighbors8(x, y).filter(n => grid[n.y][n.x] === STICKY).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function expandsStickyCluster(x, y) {
|
|
||||||
const neighbors = getNeighbors8(x, y);
|
|
||||||
return neighbors.filter(n => grid[n.y][n.x] === STICKY).length >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectsStickyClusters(x, y) {
|
|
||||||
// Check if placing sticky here connects two separate sticky regions
|
|
||||||
const neighbors = getNeighbors8(x, y).filter(n => grid[n.y][n.x] === STICKY);
|
|
||||||
if (neighbors.length < 2) return false;
|
|
||||||
// Simple heuristic: multiple sticky neighbors from different directions
|
|
||||||
const dirs = neighbors.map(n => Math.atan2(n.y - y, n.x - x));
|
|
||||||
const uniqueDirs = new Set(dirs.map(d => Math.round(d * 10) / 10));
|
|
||||||
return uniqueDirs.size >= 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
function distToCenter(x, y) {
|
|
||||||
return Math.sqrt((x - NPC_CENTER.x) ** 2 + (y - NPC_CENTER.y) ** 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function distToNearestPlayer(x, y) {
|
|
||||||
let minD = Infinity;
|
|
||||||
for (const p of players) {
|
|
||||||
if (!trapped[players.indexOf(p)]) {
|
|
||||||
const d = Math.sqrt((x - p.x) ** 2 + (y - p.y) ** 2);
|
|
||||||
if (d < minD) minD = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return minD;
|
|
||||||
}
|
|
||||||
|
|
||||||
function wouldTrapPlayer(x, y) {
|
|
||||||
// Simplified: check if any active player would have no safe neighbors
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (trapped[i]) continue;
|
|
||||||
const p = players[i];
|
|
||||||
const pdist = Math.sqrt((x - p.x) ** 2 + (y - p.y) ** 2);
|
|
||||||
if (pdist > 8) continue;
|
|
||||||
// Count safe neighbors of player
|
|
||||||
const safeNeighbors = getNeighbors8(Math.round(p.x), Math.round(p.y))
|
|
||||||
.filter(n => grid[n.y][n.x] === WALKABLE || grid[n.y][n.x] === 6);
|
|
||||||
if (safeNeighbors.length <= 1) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateCandidateScore(x, y) {
|
|
||||||
let score = 0;
|
|
||||||
const layer = getLayer(x, y);
|
|
||||||
|
|
||||||
// LayerPriority
|
|
||||||
const layerScores = [
|
|
||||||
{ outer: 60, middle: 15, inner: -40 },
|
|
||||||
{ outer: 20, middle: 60, inner: 5 },
|
|
||||||
{ outer: 10, middle: 35, inner: 60 },
|
|
||||||
];
|
|
||||||
score += layerScores[currentPhase][layer] || 0;
|
|
||||||
|
|
||||||
// StickyNeighborScore
|
|
||||||
score += countStickyNeighbors(x, y) * 8;
|
|
||||||
|
|
||||||
// InwardPressureScore
|
|
||||||
const centerDist = distToCenter(x, y);
|
|
||||||
const maxDist = distToCenter(1, 1);
|
|
||||||
const inwardRatio = 1 - centerDist / maxDist;
|
|
||||||
const inwardScores = [
|
|
||||||
[0, 10], [5, 20], [10, 30]
|
|
||||||
];
|
|
||||||
score += inwardScores[currentPhase][0] + (inwardScores[currentPhase][1] - inwardScores[currentPhase][0]) * inwardRatio;
|
|
||||||
|
|
||||||
// PlayerPressureScore
|
|
||||||
const playerDist = distToNearestPlayer(x, y);
|
|
||||||
if (playerDist >= 2 && playerDist <= 4) score += 20;
|
|
||||||
else if (playerDist < 1) score += elapsed > 150 ? 10 : -50;
|
|
||||||
|
|
||||||
// ClusterGrowthScore
|
|
||||||
if (connectsStickyClusters(x, y)) score += 25;
|
|
||||||
else if (expandsStickyCluster(x, y)) score += 15;
|
|
||||||
|
|
||||||
// RandomNoise
|
|
||||||
score += (Math.random() * 40 - 20);
|
|
||||||
|
|
||||||
// PathSafetyPenalty
|
|
||||||
if (wouldTrapPlayer(x, y) && elapsed < 150) score -= 100;
|
|
||||||
|
|
||||||
// RepetitionPenalty (simplified)
|
|
||||||
const neighbors = getNeighbors8(x, y);
|
|
||||||
const recentTargets = neighbors.filter(n => grid[n.y][n.x] === TELEGRAPH);
|
|
||||||
if (recentTargets.length > 0) score -= 30;
|
|
||||||
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Growth Tick ──
|
|
||||||
function processGrowthTick() {
|
|
||||||
const config = PHASE_GROWTH[currentPhase];
|
|
||||||
const cellCount = config.min + Math.floor(Math.random() * (config.max - config.min + 1));
|
|
||||||
|
|
||||||
// Generate candidates
|
|
||||||
const candidates = [];
|
|
||||||
for (let y = 1; y < ARENA - 1; y++) {
|
|
||||||
for (let x = 1; x < ARENA - 1; x++) {
|
|
||||||
if (!isCellValidForGrowth(x, y)) continue;
|
|
||||||
const score = calculateCandidateScore(x, y);
|
|
||||||
cellScores[y][x] = score;
|
|
||||||
candidates.push({ x, y, score: score + 100 }); // offset for positive weights
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.length === 0) return;
|
|
||||||
|
|
||||||
// Weighted random selection
|
|
||||||
candidates.sort((a, b) => b.score - a.score);
|
|
||||||
let totalWeight = candidates.reduce((s, c) => s + c.score, 0);
|
|
||||||
const selected = [];
|
|
||||||
const used = new Set();
|
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(cellCount, candidates.length); i++) {
|
|
||||||
let roll = Math.random() * totalWeight;
|
|
||||||
let cumulative = 0;
|
|
||||||
for (const c of candidates) {
|
|
||||||
if (used.has(`${c.x},${c.y}`)) continue;
|
|
||||||
cumulative += c.score;
|
|
||||||
if (roll <= cumulative) {
|
|
||||||
selected.push(c);
|
|
||||||
used.add(`${c.x},${c.y}`);
|
|
||||||
totalWeight -= c.score;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegraph phase
|
|
||||||
for (const c of selected) {
|
|
||||||
grid[c.y][c.x] = TELEGRAPH;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTicks++;
|
|
||||||
|
|
||||||
// After telegraph delay, apply sticky
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const c of selected) {
|
|
||||||
if (grid[c.y][c.x] === TELEGRAPH) {
|
|
||||||
grid[c.y][c.x] = STICKY;
|
|
||||||
// Check players on this cell
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (!trapped[i] && Math.round(players[i].x) === c.x && Math.round(players[i].y) === c.y) {
|
|
||||||
if (cleanserActive[i]) {
|
|
||||||
grid[c.y][c.x] = 6; // cleansed
|
|
||||||
cleanserCellsLeft[i]--;
|
|
||||||
if (cleanserCellsLeft[i] <= 0) {
|
|
||||||
cleanserActive[i] = false;
|
|
||||||
addLog(`${PLAYER_NAMES[i]}'s Cleanser wore off.`, 'cleanse');
|
|
||||||
}
|
|
||||||
addLog(`${PLAYER_NAMES[i]} cleansed a sticky cell!`, 'cleanse');
|
|
||||||
} else {
|
|
||||||
trapPlayer(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Clear telegraph visuals after impact
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const c of selected) {
|
|
||||||
if (grid[c.y][c.x] === TELEGRAPH) grid[c.y][c.x] = STICKY;
|
|
||||||
}
|
|
||||||
// Invalidate bot paths — grid changed
|
|
||||||
botPaths = [[], [], [], []];
|
|
||||||
}, 200);
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Candy Bubble System ──
|
|
||||||
function trySpawnBubble() {
|
|
||||||
const config = PHASE_GROWTH[currentPhase];
|
|
||||||
if (bubblesSpawned >= config.bubbles) return;
|
|
||||||
if (Math.random() > 0.3) return; // chance per tick
|
|
||||||
|
|
||||||
// Find valid bubble candidates
|
|
||||||
const candidates = [];
|
|
||||||
for (let y = 3; y < ARENA - 3; y++) {
|
|
||||||
for (let x = 3; x < ARENA - 3; x++) {
|
|
||||||
if (!isCellValidForGrowth(x, y)) continue;
|
|
||||||
// Check 3x3 area is mostly clear
|
|
||||||
let clear = true;
|
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
|
||||||
if (!isCellValidForGrowth(x + dx, y + dy)) { clear = false; break; }
|
|
||||||
}
|
|
||||||
if (!clear) break;
|
|
||||||
}
|
|
||||||
if (!clear) continue;
|
|
||||||
|
|
||||||
// Camping bonus
|
|
||||||
let score = 30;
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
const pdist = Math.sqrt((x - players[i].x) ** 2 + (y - players[i].y) ** 2);
|
|
||||||
if (pdist < 1.5) score -= 60; // don't spawn on player
|
|
||||||
else if (pdist <= 4) score += 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recent bubble penalty
|
|
||||||
for (const rb of recentBubbles) {
|
|
||||||
if (Math.sqrt((x - rb.x) ** 2 + (y - rb.y) ** 2) < 5) {
|
|
||||||
score -= 50;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Untouched area bonus
|
|
||||||
const clusterSize = floodFillCount(x, y, new Set());
|
|
||||||
if (clusterSize > 10) score += 30;
|
|
||||||
|
|
||||||
score += Math.random() * 40 - 20;
|
|
||||||
candidates.push({ x, y, score });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.length === 0) return;
|
|
||||||
|
|
||||||
candidates.sort((a, b) => b.score - a.score);
|
|
||||||
const target = candidates[0];
|
|
||||||
|
|
||||||
activeBubbles.push({
|
|
||||||
x: target.x, y: target.y,
|
|
||||||
growTimer: 2.5 + Math.random() * 0.5,
|
|
||||||
maxGrow: 2.5 + Math.random() * 0.5
|
|
||||||
});
|
|
||||||
bubblesSpawned++;
|
|
||||||
recentBubbles.push({ x: target.x, y: target.y });
|
|
||||||
if (recentBubbles.length > 8) recentBubbles.shift();
|
|
||||||
addLog(`Candy bubble appeared near (${target.x},${target.y})!`, 'bubble');
|
|
||||||
}
|
|
||||||
|
|
||||||
function floodFillCount(x, y, visited) {
|
|
||||||
const key = `${x},${y}`;
|
|
||||||
if (visited.has(key)) return 0;
|
|
||||||
if (!isCellValidForGrowth(x, y)) return 0;
|
|
||||||
visited.add(key);
|
|
||||||
let count = 1;
|
|
||||||
for (const n of getNeighbors8(x, y)) {
|
|
||||||
count += floodFillCount(n.x, n.y, visited);
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processBubbles(dt) {
|
|
||||||
for (let i = activeBubbles.length - 1; i >= 0; i--) {
|
|
||||||
const b = activeBubbles[i];
|
|
||||||
b.growTimer -= dt;
|
|
||||||
if (b.growTimer <= 0) {
|
|
||||||
// Explode into 3x3
|
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
|
||||||
const nx = b.x + dx, ny = b.y + dy;
|
|
||||||
if (isCellValidForGrowth(nx, ny)) {
|
|
||||||
grid[ny][nx] = STICKY;
|
|
||||||
// Check players
|
|
||||||
for (let j = 0; j < 4; j++) {
|
|
||||||
if (!trapped[j] && Math.round(players[j].x) === nx && Math.round(players[j].y) === ny) {
|
|
||||||
if (cleanserActive[j]) {
|
|
||||||
grid[ny][nx] = 6;
|
|
||||||
cleanserCellsLeft[j]--;
|
|
||||||
if (cleanserCellsLeft[j] <= 0) {
|
|
||||||
cleanserActive[j] = false;
|
|
||||||
addLog(`${PLAYER_NAMES[j]}'s Cleanser wore off.`, 'cleanse');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trapPlayer(j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addLog(`Bubble exploded at (${b.x},${b.y})!`, 'trap');
|
|
||||||
activeBubbles.splice(i, 1);
|
|
||||||
botPaths = [[], [], [], []];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Player Actions ──
|
|
||||||
function trapPlayer(idx) {
|
|
||||||
// Auto-use cleanser if available
|
|
||||||
if (cleanserCount[idx] > 0 && !cleanserActive[idx]) {
|
|
||||||
cleanserCount[idx]--;
|
|
||||||
cleanserActive[idx] = true;
|
|
||||||
cleanserCellsLeft[idx] = 5;
|
|
||||||
trapped[idx] = false;
|
|
||||||
addLog(`${PLAYER_NAMES[idx]} auto-activated Cleanser to escape!`, 'cleanse');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
trapped[idx] = true;
|
|
||||||
botPaths[idx] = [];
|
|
||||||
addLog(`${PLAYER_NAMES[idx]} is TRAPPED in sticky candy!`, 'trap');
|
|
||||||
scores[idx] = Math.max(0, scores[idx] - 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Movement ──
|
|
||||||
function canMoveTo(idx, x, y) {
|
|
||||||
if (x < 1 || y < 1 || x >= ARENA - 1 || y >= ARENA - 1) return false;
|
|
||||||
const cell = grid[y][x];
|
|
||||||
if (cell === WALL || cell === NPC_BLOCK) return false;
|
|
||||||
if (cell === STICKY && !cleanserActive[idx]) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function movePlayerStep(idx, dx, dy) {
|
|
||||||
if (trapped[idx]) return;
|
|
||||||
const p = players[idx];
|
|
||||||
if (p.moving) return; // already moving
|
|
||||||
const nx = Math.round(p.x) + dx;
|
|
||||||
const ny = Math.round(p.y) + dy;
|
|
||||||
if (!canMoveTo(idx, nx, ny)) return;
|
|
||||||
p.startX = Math.round(p.x);
|
|
||||||
p.startY = Math.round(p.y);
|
|
||||||
p.targetX = nx;
|
|
||||||
p.targetY = ny;
|
|
||||||
p.moveProgress = 0;
|
|
||||||
p.moving = true;
|
|
||||||
p.moveDir = { dx, dy };
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePlayerMovement(dt) {
|
|
||||||
const speed = 10; // cells per second — fast and responsive
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
const p = players[i];
|
|
||||||
if (!p.moving) continue;
|
|
||||||
p.moveProgress += dt * speed;
|
|
||||||
if (p.moveProgress >= 1) {
|
|
||||||
p.x = p.targetX;
|
|
||||||
p.y = p.targetY;
|
|
||||||
p.moving = false;
|
|
||||||
p.moveProgress = 0;
|
|
||||||
|
|
||||||
// Collect mission tile
|
|
||||||
const key = `${Math.round(p.x)},${Math.round(p.y)}`;
|
|
||||||
if (missionTiles[key]) {
|
|
||||||
delete missionTiles[key];
|
|
||||||
missions[i]++;
|
|
||||||
scores[i] += 1000;
|
|
||||||
addLog(`${PLAYER_NAMES[i]} collected a mission tile! (${missions[i]} total)`, 'score');
|
|
||||||
if (missions[i] % 2 === 0 && cleanserCount[i] < 1) {
|
|
||||||
cleanserCount[i]++;
|
|
||||||
addLog(`${PLAYER_NAMES[i]} earned a Cleanser!`, 'cleanse');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if landed on sticky
|
|
||||||
const gx = Math.round(p.x), gy = Math.round(p.y);
|
|
||||||
if (grid[gy][gx] === STICKY) {
|
|
||||||
if (cleanserActive[i]) {
|
|
||||||
grid[gy][gx] = 6; // cleansed
|
|
||||||
cleanserCellsLeft[i]--;
|
|
||||||
if (cleanserCellsLeft[i] <= 0) {
|
|
||||||
cleanserActive[i] = false;
|
|
||||||
addLog(`${PLAYER_NAMES[i]}'s Cleanser wore off.`, 'cleanse');
|
|
||||||
} else {
|
|
||||||
addLog(`${PLAYER_NAMES[i]} cleansed a cell (${cleanserCellsLeft[i]} left)`, 'cleanse');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trapPlayer(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.x = p.startX + (p.targetX - p.startX) * p.moveProgress;
|
|
||||||
p.y = p.startY + (p.targetY - p.startY) * p.moveProgress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Phase Management ──
|
|
||||||
function updatePhase() {
|
|
||||||
let newPhase = 0;
|
|
||||||
if (elapsed >= 120) newPhase = 2;
|
|
||||||
else if (elapsed >= 60) newPhase = 1;
|
|
||||||
|
|
||||||
if (newPhase !== currentPhase) {
|
|
||||||
currentPhase = newPhase;
|
|
||||||
addLog(`Phase changed: ${PHASE_NAMES[currentPhase]}`, 'phase');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Cooldowns ──
|
|
||||||
function updateCooldowns(_dt) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Bot AI ──
|
|
||||||
let botTimers = [0, 0, 0, 0];
|
|
||||||
let botPaths = [[], [], [], []];
|
|
||||||
|
|
||||||
function isWalkable(x, y) {
|
|
||||||
if (x < 0 || y < 0 || x >= ARENA || y >= ARENA) return false;
|
|
||||||
const c = grid[y][x];
|
|
||||||
return c === WALKABLE || c === 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
function astar(sx, sy, tx, ty) {
|
|
||||||
if (sx === tx && sy === ty) return [];
|
|
||||||
const k = (x, y) => `${x},${y}`;
|
|
||||||
const open = new Map();
|
|
||||||
const closed = new Set();
|
|
||||||
const h = (x, y) => Math.abs(x - tx) + Math.abs(y - ty);
|
|
||||||
open.set(k(sx, sy), { x: sx, y: sy, g: 0, f: h(sx, sy), p: null });
|
|
||||||
|
|
||||||
while (open.size > 0) {
|
|
||||||
let best = null;
|
|
||||||
for (const n of open.values()) {
|
|
||||||
if (!best || n.f < best.f || (n.f === best.f && n.g > best.g)) best = n;
|
|
||||||
}
|
|
||||||
if (best.x === tx && best.y === ty) {
|
|
||||||
const path = [];
|
|
||||||
let c = best;
|
|
||||||
while (c.p) { path.unshift({ x: c.x, y: c.y }); c = c.p; }
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
open.delete(k(best.x, best.y));
|
|
||||||
closed.add(k(best.x, best.y));
|
|
||||||
|
|
||||||
for (const [dx, dy] of [[0,-1],[0,1],[-1,0],[1,0]]) {
|
|
||||||
const nx = best.x + dx, ny = best.y + dy;
|
|
||||||
const nk = k(nx, ny);
|
|
||||||
if (closed.has(nk)) continue;
|
|
||||||
if (!(nx === tx && ny === ty) && !isWalkable(nx, ny)) continue;
|
|
||||||
const g = best.g + 1;
|
|
||||||
const ex = open.get(nk);
|
|
||||||
if (ex && g >= ex.g) continue;
|
|
||||||
open.set(nk, { x: nx, y: ny, g, f: g + h(nx, ny), p: best });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function findNearestMission(idx) {
|
|
||||||
const p = players[idx];
|
|
||||||
const cx = Math.round(p.x), cy = Math.round(p.y);
|
|
||||||
let best = null, bestDist = Infinity;
|
|
||||||
for (const key in missionTiles) {
|
|
||||||
const [mx, my] = key.split(',').map(Number);
|
|
||||||
const d = Math.abs(mx - cx) + Math.abs(my - cy);
|
|
||||||
if (d < bestDist) { bestDist = d; best = { x: mx, y: my }; }
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
function botStep(idx) {
|
|
||||||
if (trapped[idx]) return;
|
|
||||||
const p = players[idx];
|
|
||||||
if (p.moving) return;
|
|
||||||
|
|
||||||
const path = botPaths[idx];
|
|
||||||
if (path.length > 0) {
|
|
||||||
const next = path[0];
|
|
||||||
if (isWalkable(next.x, next.y)) {
|
|
||||||
movePlayerStep(idx, next.x - Math.round(p.x), next.y - Math.round(p.y));
|
|
||||||
if (!p.moving) {
|
|
||||||
path.shift();
|
|
||||||
// Stuck: couldn't move to next path node, invalidate
|
|
||||||
if (path.length > 0) botPaths[idx] = [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
path.shift();
|
|
||||||
botPaths[idx] = [];
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try A* to nearest mission
|
|
||||||
const target = findNearestMission(idx);
|
|
||||||
if (target) {
|
|
||||||
botPaths[idx] = astar(Math.round(p.x), Math.round(p.y), target.x, target.y);
|
|
||||||
if (botPaths[idx].length > 0) {
|
|
||||||
const next = botPaths[idx].shift();
|
|
||||||
movePlayerStep(idx, next.x - Math.round(p.x), next.y - Math.round(p.y));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FALLBACK: no path to any mission — move away from nearest sticky (survival)
|
|
||||||
escapeSticky(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeSticky(idx) {
|
|
||||||
const p = players[idx];
|
|
||||||
const cx = Math.round(p.x), cy = Math.round(p.y);
|
|
||||||
// Find nearest sticky cell
|
|
||||||
let nearestSticky = null, nearDist = Infinity;
|
|
||||||
for (let y = 0; y < ARENA; y++) {
|
|
||||||
for (let x = 0; x < ARENA; x++) {
|
|
||||||
if (grid[y][x] === STICKY) {
|
|
||||||
const d = Math.abs(x - cx) + Math.abs(y - cy);
|
|
||||||
if (d < nearDist) { nearDist = d; nearestSticky = { x, y }; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!nearestSticky) return;
|
|
||||||
// Move away from sticky: try the direction opposite to it
|
|
||||||
const dx = Math.sign(cx - nearestSticky.x);
|
|
||||||
const dy = Math.sign(cy - nearestSticky.y);
|
|
||||||
const dirs = [[dx, 0], [0, dy], [-dx, 0], [0, -dy], [1, 0], [-1, 0], [0, 1], [0, -1]];
|
|
||||||
for (const [mx, my] of dirs) {
|
|
||||||
if (mx === 0 && my === 0) continue;
|
|
||||||
if (canMoveTo(idx, cx + mx, cy + my)) {
|
|
||||||
movePlayerStep(idx, mx, my);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBots(dt) {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
botTimers[i] -= dt;
|
|
||||||
if (botTimers[i] <= 0) {
|
|
||||||
botStep(i);
|
|
||||||
botTimers[i] = 0.12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Scoring ──
|
|
||||||
function updateScoring() {
|
|
||||||
// Survival bonus at end
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Rendering ──
|
|
||||||
function draw() {
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
for (let y = 0; y < ARENA; y++) {
|
|
||||||
for (let x = 0; x < ARENA; x++) {
|
|
||||||
const px = x * CELL_PX;
|
|
||||||
const py = y * CELL_PX;
|
|
||||||
const cell = grid[y][x];
|
|
||||||
|
|
||||||
// Base cell color
|
|
||||||
let color;
|
|
||||||
switch (cell) {
|
|
||||||
case WALKABLE: color = '#4f4f4f'; break;
|
|
||||||
case WALL: color = '#d10000'; break;
|
|
||||||
case NPC_BLOCK: color = '#cc3380'; break;
|
|
||||||
case STICKY: color = '#d6268a'; break; // deep magenta — set candy
|
|
||||||
case TELEGRAPH: color = '#e8a020'; break; // amber/syrup — warning glow
|
|
||||||
case 6: color = '#66e6ff'; break; // cleansed
|
|
||||||
default: color = '#4f4f4f';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer highlight based on current pressure
|
|
||||||
if (cell === WALKABLE || cell === 6) {
|
|
||||||
const layer = getLayer(x, y);
|
|
||||||
const targetLayer = currentPhase === 0 ? 'outer' : currentPhase === 1 ? 'middle' : 'inner';
|
|
||||||
if (layer === targetLayer) {
|
|
||||||
// Subtle highlight for current pressure layer
|
|
||||||
const r = parseInt(color.slice(1, 3), 16);
|
|
||||||
const g = parseInt(color.slice(3, 5), 16);
|
|
||||||
const b = parseInt(color.slice(5, 7), 16);
|
|
||||||
color = `rgb(${Math.min(255, r + 12)},${Math.min(255, g + 8)},${Math.min(255, b + 5)})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(px, py, CELL_PX - 1, CELL_PX - 1);
|
|
||||||
|
|
||||||
// Draw grid lines
|
|
||||||
ctx.strokeStyle = '#222';
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.strokeRect(px, py, CELL_PX, CELL_PX);
|
|
||||||
|
|
||||||
// Mission tiles (fully fill cell)
|
|
||||||
if (missionTiles[`${x},${y}`] && (cell === WALKABLE || cell === 6)) {
|
|
||||||
ctx.fillStyle = '#e8ba38';
|
|
||||||
ctx.fillRect(px + 1, py + 1, CELL_PX - 2, CELL_PX - 2);
|
|
||||||
ctx.strokeStyle = '#b8901a';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.strokeRect(px + 1, py + 1, CELL_PX - 2, CELL_PX - 2);
|
|
||||||
// Shine highlight
|
|
||||||
ctx.fillStyle = 'rgba(255,255,255,0.2)';
|
|
||||||
ctx.fillRect(px + 3, py + 3, (CELL_PX - 6) * 0.4, (CELL_PX - 6) * 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegraph pulsing effect (amber/syrup glow)
|
|
||||||
if (cell === TELEGRAPH) {
|
|
||||||
const pulse = 0.35 + Math.sin(Date.now() / 120) * 0.3;
|
|
||||||
ctx.fillStyle = `rgba(232,160,32,${pulse})`;
|
|
||||||
ctx.fillRect(px, py, CELL_PX - 1, CELL_PX - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw layer borders (subtle)
|
|
||||||
for (let y = 1; y < ARENA - 1; y++) {
|
|
||||||
for (let x = 1; x < ARENA - 1; x++) {
|
|
||||||
if (getEdgeDistance(x, y) === 4 || getEdgeDistance(x, y) === 8) {
|
|
||||||
const px = x * CELL_PX;
|
|
||||||
const py = y * CELL_PX;
|
|
||||||
ctx.strokeStyle = 'rgba(255,255,255,0.08)';
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(px, py);
|
|
||||||
ctx.lineTo(px, py + CELL_PX);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw active bubbles (hot orange-red)
|
|
||||||
for (const b of activeBubbles) {
|
|
||||||
const px = b.x * CELL_PX;
|
|
||||||
const py = b.y * CELL_PX;
|
|
||||||
const progress = 1 - b.growTimer / b.maxGrow;
|
|
||||||
const size = CELL_PX * (0.3 + progress * 0.7);
|
|
||||||
|
|
||||||
// Warning 3x3 area
|
|
||||||
if (progress > 0.5) {
|
|
||||||
ctx.fillStyle = `rgba(255,87,34,${(progress - 0.5) * 0.25})`;
|
|
||||||
for (let dy = -1; dy <= 1; dy++) {
|
|
||||||
for (let dx = -1; dx <= 1; dx++) {
|
|
||||||
ctx.fillRect((b.x + dx) * CELL_PX, (b.y + dy) * CELL_PX, CELL_PX - 1, CELL_PX - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bubble itself
|
|
||||||
ctx.fillStyle = `rgba(255,87,34,${0.6 + progress * 0.4})`;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(px + CELL_PX / 2, py + CELL_PX / 2, size / 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.strokeStyle = '#ff8a50';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// Pulse ring
|
|
||||||
if (progress > 0.7) {
|
|
||||||
const pulseSize = size * (1 + Math.sin(Date.now() / 80) * 0.2);
|
|
||||||
ctx.strokeStyle = `rgba(255,138,80,${0.5})`;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(px + CELL_PX / 2, py + CELL_PX / 2, pulseSize / 2 + 4, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw players
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
const p = players[i];
|
|
||||||
const px = p.x * CELL_PX + CELL_PX / 2;
|
|
||||||
const py = p.y * CELL_PX + CELL_PX / 2;
|
|
||||||
const radius = CELL_PX / 2.5;
|
|
||||||
|
|
||||||
// Player body
|
|
||||||
ctx.fillStyle = trapped[i] ? '#666' : PLAYER_COLORS[i];
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// Trapped overlay
|
|
||||||
if (trapped[i]) {
|
|
||||||
ctx.strokeStyle = '#ff4d4d';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.setLineDash([3, 3]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(px, py, radius + 2, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
// Trapped X
|
|
||||||
ctx.strokeStyle = '#ff4d4d';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(px - 4, py - 4);
|
|
||||||
ctx.lineTo(px + 4, py + 4);
|
|
||||||
ctx.moveTo(px + 4, py - 4);
|
|
||||||
ctx.lineTo(px - 4, py + 4);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanser active indicator
|
|
||||||
if (cleanserActive[i]) {
|
|
||||||
ctx.strokeStyle = '#66e6ff';
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.setLineDash([4, 2]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(px, py, radius + 8, 0, Math.PI * 2);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player name
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.font = 'bold 9px sans-serif';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText(PLAYER_NAMES[i], px, py - radius - 6);
|
|
||||||
|
|
||||||
// Score
|
|
||||||
ctx.fillStyle = '#61ff54';
|
|
||||||
ctx.font = '8px sans-serif';
|
|
||||||
ctx.fillText(Math.round(scores[i]), px, py + radius + 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw bot paths (debug toggle with D)
|
|
||||||
if (showPaths) {
|
|
||||||
const pathColors = ['#33cc3355', '#4d80ff55', '#ff993355', '#cc66ff55'];
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
const path = botPaths[i];
|
|
||||||
if (path.length === 0) continue;
|
|
||||||
ctx.strokeStyle = pathColors[i];
|
|
||||||
ctx.lineWidth = 3;
|
|
||||||
ctx.setLineDash([4, 4]);
|
|
||||||
ctx.beginPath();
|
|
||||||
const sx = players[i].x * CELL_PX + CELL_PX / 2;
|
|
||||||
const sy = players[i].y * CELL_PX + CELL_PX / 2;
|
|
||||||
ctx.moveTo(sx, sy);
|
|
||||||
for (const node of path) {
|
|
||||||
ctx.lineTo(node.x * CELL_PX + CELL_PX / 2, node.y * CELL_PX + CELL_PX / 2);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.setLineDash([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw NPC center marker
|
|
||||||
const npcPx = NPC_CENTER.x * CELL_PX + CELL_PX / 2;
|
|
||||||
const npcPy = NPC_CENTER.y * CELL_PX + CELL_PX / 2;
|
|
||||||
ctx.fillStyle = '#cc3380';
|
|
||||||
ctx.font = 'bold 14px sans-serif';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText('PUMP', npcPx, npcPy - 6);
|
|
||||||
ctx.font = '10px sans-serif';
|
|
||||||
ctx.fillText('NPC', npcPx, npcPy + 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── UI Updates ──
|
|
||||||
function formatTime(t) {
|
|
||||||
const m = Math.floor(t / 60);
|
|
||||||
const s = Math.floor(t % 60);
|
|
||||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUI() {
|
|
||||||
document.getElementById('timer').textContent = formatTime(180 - elapsed);
|
|
||||||
|
|
||||||
const phaseEl = document.getElementById('phase-label');
|
|
||||||
phaseEl.textContent = PHASE_NAMES[currentPhase];
|
|
||||||
phaseEl.className = 'value ' + PHASE_CLASSES[currentPhase];
|
|
||||||
|
|
||||||
const stickyCount = grid.flat().filter(c => c === STICKY).length;
|
|
||||||
const coverage = Math.round((stickyCount / TOTAL_PLAYABLE) * 100);
|
|
||||||
document.getElementById('coverage').textContent = coverage + '%';
|
|
||||||
document.getElementById('tick-count').textContent = totalTicks;
|
|
||||||
document.getElementById('bubble-count').textContent = bubblesSpawned;
|
|
||||||
|
|
||||||
// Player list
|
|
||||||
const listEl = document.getElementById('player-list');
|
|
||||||
listEl.innerHTML = players.map((p, i) => `
|
|
||||||
<div class="player-entry" style="opacity:${trapped[i] ? 0.5 : 1}">
|
|
||||||
<div class="player-dot" style="background:${PLAYER_COLORS[i]}"></div>
|
|
||||||
<span class="player-name">${PLAYER_NAMES[i]}</span>
|
|
||||||
<span class="player-score">${Math.round(scores[i])}</span>
|
|
||||||
<span class="player-missions">×${missions[i]}</span>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Input ──
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'r': case 'R': resetGame(); break;
|
|
||||||
case 'p': case 'P': paused = !paused; break;
|
|
||||||
case 'd': case 'D': showPaths = !showPaths; break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Game Loop ──
|
|
||||||
let lastTime = 0;
|
|
||||||
|
|
||||||
function gameLoop(timestamp) {
|
|
||||||
const dt = Math.min((timestamp - lastTime) / 1000, 0.1);
|
|
||||||
lastTime = timestamp;
|
|
||||||
|
|
||||||
if (!paused && elapsed < 180) {
|
|
||||||
elapsed += dt;
|
|
||||||
|
|
||||||
// Growth tick
|
|
||||||
growthTimer += dt;
|
|
||||||
if (growthTimer >= 3.0) {
|
|
||||||
growthTimer -= 3.0;
|
|
||||||
processGrowthTick();
|
|
||||||
trySpawnBubble();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bubbles
|
|
||||||
processBubbles(dt);
|
|
||||||
|
|
||||||
// Player movement
|
|
||||||
updatePlayerMovement(dt);
|
|
||||||
|
|
||||||
// Phase
|
|
||||||
updatePhase();
|
|
||||||
|
|
||||||
// Cooldowns
|
|
||||||
updateCooldowns(dt);
|
|
||||||
|
|
||||||
// Bot AI — all non-selected players auto-pathfind
|
|
||||||
updateBots(dt);
|
|
||||||
|
|
||||||
// Score decay for trapped players
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (trapped[i]) {
|
|
||||||
scores[i] = Math.max(0, scores[i] - dt * 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passive score for surviving
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (!trapped[i]) {
|
|
||||||
scores[i] += dt * 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw();
|
|
||||||
if (Math.floor(elapsed * 4) % 2 === 0) updateUI(); // throttle UI updates
|
|
||||||
requestAnimationFrame(gameLoop);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Start ──
|
|
||||||
resetGame();
|
|
||||||
requestAnimationFrame(gameLoop);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Nakama JS → Lua Migration Audit
|
|
||||||
|
|
||||||
## JS RPC Registration (core.js.bak lines 12-72)
|
|
||||||
|
|
||||||
| # | JS RPC Name | JS Function | Lua Module | Lua RPC Name | Status |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| 1 | `admin_kick_player` | `rpcAdminKickPlayer` | admin.lua | `lua_admin_kick_player` | ✅ |
|
|
||||||
| 2 | `admin_ban_player` | `rpcAdminBanPlayer` | admin.lua | `lua_admin_ban_player` | ✅ |
|
|
||||||
| 3 | `admin_unban_player` | `rpcAdminUnbanPlayer` | admin.lua | `lua_admin_unban_player` | ✅ |
|
|
||||||
| 4 | `admin_get_ban_list` | `rpcAdminGetBanList` | admin.lua | `lua_admin_get_ban_list` | ✅ |
|
|
||||||
| 5 | `admin_get_server_stats` | `rpcAdminGetServerStats` | admin.lua | `lua_admin_get_server_stats` | ✅ |
|
|
||||||
| 6 | `admin_get_player_list` | `rpcAdminGetPlayerList` | admin.lua | `lua_admin_get_player_list` | ✅ Fixed |
|
|
||||||
| 7 | `admin_end_match` | `rpcAdminEndMatch` | admin.lua | `lua_admin_end_match` | ✅ |
|
|
||||||
| 8 | `admin_set_user_role` | `rpcAdminSetUserRole` | admin.lua | `lua_admin_set_user_role` | ✅ |
|
|
||||||
| 9 | `admin_list_users` | `rpcAdminListUsers` | admin.lua | `lua_admin_list_users` | ✅ |
|
|
||||||
| 10 | `admin_delete_users` | `rpcAdminDeleteUsers` | admin.lua | `lua_admin_delete_users` | ✅ Bug fixed |
|
|
||||||
| 11 | `admin_topup_gold` | `rpcAdminTopupGold` | admin.lua | `lua_admin_topup_gold` | ✅ |
|
|
||||||
| 12 | `admin_clear_global_chat` | `rpcAdminClearGlobalChat` | admin.lua | `lua_admin_clear_global_chat` | ✅ |
|
|
||||||
| 13 | `get_user_profile` | `rpcGetUserProfile` | user.lua | `lua_get_user_profile` | ✅ |
|
|
||||||
| 14 | `update_user_profile` | `rpcUpdateUserProfile` | user.lua | `lua_update_user_profile` | ✅ |
|
|
||||||
| 15 | `search_users` | `rpcSearchUsers` | user.lua | `lua_search_users` | ✅ |
|
|
||||||
| 16 | `get_leaderboard_stats` | `rpcGetLeaderboardStats` | leaderboard.lua | `lua_get_leaderboard_stats` | ✅ |
|
|
||||||
| 17 | `admin_update_stats` | `rpcAdminUpdateStats` | leaderboard.lua | `lua_admin_update_stats` | ✅ |
|
|
||||||
| 18 | `admin_delete_stats` | `rpcAdminDeleteStats` | leaderboard.lua | `lua_admin_delete_stats` | ✅ |
|
|
||||||
| 19 | `admin_sync_leaderboard` | `rpcAdminSyncLeaderboard` | leaderboard.lua | `lua_admin_sync_leaderboard` | ✅ |
|
|
||||||
| 20 | `submit_score` | `rpcSubmitScore` | leaderboard.lua | `lua_submit_score` | ✅ |
|
|
||||||
| 21 | `sync_leaderboard` | `rpcSyncLeaderboard` | leaderboard.lua | `lua_sync_leaderboard` | ✅ |
|
|
||||||
| 22 | `change_credentials` | `rpcChangeCredentials` | user.lua | `lua_change_credentials` | ✅ |
|
|
||||||
| 23 | `reset_stats` | `rpcResetStats` | leaderboard.lua | `lua_reset_stats` | ✅ |
|
|
||||||
| 24 | `send_lobby_invite` | `rpcSendLobbyInvite` | user.lua | `lua_send_lobby_invite` | ✅ |
|
|
||||||
| 25 | `send_friend_request` | `rpcSendFriendRequest` | user.lua | `lua_send_friend_request` | ✅ |
|
|
||||||
| 26 | `claim_daily_reward` | `rpcClaimDailyReward` | daily_rewards.lua | `lua_claim_daily_reward` | ✅ |
|
|
||||||
| 27 | `get_daily_reward_state` | `rpcGetDailyRewardState` | daily_rewards.lua | `lua_get_daily_reward_state` | ✅ |
|
|
||||||
| 28 | `set_daily_reward_config` | `rpcSetDailyRewardConfig` | daily_rewards.lua | `lua_set_daily_reward_config` | ✅ |
|
|
||||||
| 29 | `get_daily_reward_config_admin` | `rpcGetDailyRewardConfigAdmin` | daily_rewards.lua | `lua_get_daily_reward_config_admin` | ✅ |
|
|
||||||
| 30 | `admin_send_mail` | `rpcAdminSendMail` | inbox.lua | `lua_admin_send_mail` | ✅ |
|
|
||||||
| 31 | `admin_list_mail` | `rpcAdminListMail` | inbox.lua | `lua_admin_list_mail` | ✅ |
|
|
||||||
| 32 | `admin_update_mail` | `rpcAdminUpdateMail` | inbox.lua | `lua_admin_update_mail` | ✅ |
|
|
||||||
| 33 | `admin_delete_mail_server` | `rpcAdminDeleteMailServer` | inbox.lua | `lua_admin_delete_mail_server` | ✅ |
|
|
||||||
| 34 | `get_mail` | `rpcGetMail` | inbox.lua | `lua_get_mail` | ✅ |
|
|
||||||
| 35 | `claim_mail_reward` | `rpcClaimMailReward` | inbox.lua | `lua_claim_mail_reward` | ✅ |
|
|
||||||
| 36 | `delete_mail` | `rpcDeleteMail` | inbox.lua | `lua_delete_mail` | ✅ |
|
|
||||||
| 37 | `save_mail_state` | `rpcSaveMailState` | inbox.lua | `lua_save_mail_state` | ✅ |
|
|
||||||
| 38 | `purchase_item` | `rpcPurchaseItem` | economy.lua | `lua_purchase_item` | ✅ |
|
|
||||||
| 39 | `get_shop_catalog` | `rpcGetShopCatalog` | economy.lua | `lua_get_shop_catalog` | ✅ |
|
|
||||||
| 40 | `buy_currency` | `rpcBuyCurrency` | economy.lua | `lua_buy_currency` | ✅ |
|
|
||||||
| 41 | `admin_set_featured_banners` | `rpcAdminSetFeaturedBanners` | economy.lua | `lua_admin_set_featured_banners` | ✅ |
|
|
||||||
| 42 | `admin_get_featured_banners` | `rpcAdminGetFeaturedBanners` | economy.lua | `lua_admin_get_featured_banners` | ✅ |
|
|
||||||
|
|
||||||
## Hooks
|
|
||||||
|
|
||||||
| JS Hook | JS Function | Lua Module | Status |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `registerAfterAuthenticateSteam` | `afterAuthenticateSteam` | core.lua | ✅ Fixed |
|
|
||||||
|
|
||||||
## Non-RPC Items
|
|
||||||
|
|
||||||
| Item | Lua Module | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `leaderboardCreate("global_high_score")` | core.lua | ✅ |
|
|
||||||
| `ADMIN_ROLES` definition | utils.lua | ✅ |
|
|
||||||
| `isAdmin` / `isMatchHost` helpers | utils.lua | ✅ |
|
|
||||||
| `requireAdmin` | utils.lua | ✅ |
|
|
||||||
| `requireAdminOrHost` | utils.lua | ✅ |
|
|
||||||
|
|
||||||
## Bugs Fixed This Session
|
|
||||||
|
|
||||||
1. **`admin_get_player_list` RPC was missing** — Added to admin.lua
|
|
||||||
2. **`afterAuthenticateSteam` hook was missing** — Added to core.lua via `nk.register_req_after`
|
|
||||||
3. **`admin_delete_users` metadata bug** — `pcall(nk.json_decode, ...)` result was discarded, so the admin-role guard never worked. Fixed to assign decoded result.
|
|
||||||
4. **`core.lua` had stale duplicate stubs** — Duplicate `rpc_get_user_profile` and `rpc_admin_kick_player` registrations conflicted with user.lua and admin.lua. Removed.
|
|
||||||
|
|
||||||
## Final Module Map
|
|
||||||
|
|
||||||
```
|
|
||||||
server/nakama/
|
|
||||||
├── main.lua # Entrypoint - requires all modules
|
|
||||||
├── lua/
|
|
||||||
│ ├── utils.lua # Shared helpers (is_admin, require_admin, etc.)
|
|
||||||
│ ├── core.lua # Steam auth hook + leaderboard creation
|
|
||||||
│ ├── admin.lua # 12 admin RPCs
|
|
||||||
│ ├── user.lua # 6 user RPCs
|
|
||||||
│ ├── daily_rewards.lua # 4 daily reward RPCs
|
|
||||||
│ ├── leaderboard.lua # 7 leaderboard RPCs
|
|
||||||
│ ├── inbox.lua # 8 inbox/mail RPCs
|
|
||||||
│ └── economy.lua # 5 economy/shop RPCs
|
|
||||||
```
|
|
||||||
|
|
||||||
**Total: 42 RPCs + 1 auth hook = full 1:1 parity with core.js.bak** ✅
|
|
||||||
@@ -1,2191 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Knowledge Base: Multi-Platform & Regional Production Deployment Blueprint</title>
|
|
||||||
<!-- Tailwind CSS CDN -->
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
<script>tailwind.config = { darkMode: "class" }</script>
|
|
||||||
<!-- Lucide Icons -->
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<!-- Flag Icons CSS CDN (Free Country Flags in SVG) -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icons/7.2.3/css/flag-icons.min.css" />
|
|
||||||
<style>
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght=300;400;500;600;700;800&family=JetBrains+Mono:wght=400;500;600&display=swap');
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-font {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom styles to refine the flag layout */
|
|
||||||
.fi {
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-100 min-h-screen antialiased selection:bg-sky-500 selection:text-slate-900 dark:text-white">
|
|
||||||
|
|
||||||
<!-- Header Navigation Bar -->
|
|
||||||
<header class="border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-white/80 dark:bg-slate-950/80 backdrop-blur-md sticky top-0 z-50">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
|
||||||
<div class="flex items-center space-x-3">
|
|
||||||
<div class="bg-sky-500 p-2 rounded-lg text-slate-950">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/godot/default.svg" alt="Godot" class="w-6 h-6">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span
|
|
||||||
class="font-bold text-lg tracking-tight bg-gradient-to-r from-sky-400 to-indigo-400 bg-clip-text text-transparent">Nakama
|
|
||||||
x Godot 4</span>
|
|
||||||
<span class="text-xs block text-slate-500 dark:text-slate-400 font-mono">Production Blueprint</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
|
|
||||||
<span class="w-1.5 h-1.5 mr-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
|
||||||
Production Ready
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button id="theme-toggle" class="flex items-center justify-center p-2 rounded-lg text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:text-slate-600 dark:text-slate-400 dark:hover:bg-slate-200 dark:bg-slate-800 transition">
|
|
||||||
<i data-lucide="moon" class="w-5 h-5 hidden dark:block"></i>
|
|
||||||
<i data-lucide="sun" class="w-5 h-5 block dark:hidden"></i>
|
|
||||||
</button>
|
|
||||||
<button onclick="window.print()"
|
|
||||||
class="flex items-center space-x-2 bg-slate-200 dark:bg-slate-800 hover:bg-slate-300 dark:hover:bg-slate-700 text-slate-800 dark:text-slate-200 px-3.5 py-1.5 rounded-lg text-sm font-semibold transition border border-slate-700">
|
|
||||||
<i data-lucide="printer" class="w-4 h-4"></i>
|
|
||||||
<span>Export / Print</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Secondary Navigation Bar -->
|
|
||||||
<div class="border-t border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 hidden md:block">
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<nav class="flex space-x-8 overflow-x-auto py-3 text-xs font-medium text-slate-600 dark:text-slate-400">
|
|
||||||
<a href="#infrastructure" class="hover:text-sky-500 dark:hover:text-sky-400 whitespace-nowrap transition">1. Regional Infrastructure</a>
|
|
||||||
<a href="#storefront" class="hover:text-sky-500 dark:hover:text-sky-400 whitespace-nowrap transition">2. Storefront Commissions</a>
|
|
||||||
<a href="#monetization" class="hover:text-sky-500 dark:hover:text-sky-400 whitespace-nowrap transition">3. Monetization Architecture</a>
|
|
||||||
<a href="#architecture" class="hover:text-sky-500 dark:hover:text-sky-400 whitespace-nowrap transition">4. Core Architecture</a>
|
|
||||||
<a href="#priority-board" class="hover:text-sky-500 dark:hover:text-sky-400 whitespace-nowrap transition">5. Priority Board</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
|
||||||
|
|
||||||
<!-- Hero Section / Title -->
|
|
||||||
<div class="mb-12 border-b border-slate-200 dark:border-slate-800 pb-8">
|
|
||||||
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-extrabold text-slate-900 dark:text-white tracking-tight leading-none mb-4">
|
|
||||||
Knowledge Base & Production Deployment Blueprint
|
|
||||||
</h1>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 text-lg max-w-4xl">
|
|
||||||
A highly descriptive, production-centric strategy for engineering global, multi-store authentication,
|
|
||||||
transaction validation, and regulatory compliance networks utilizing Godot 4 and Heroic Labs Nakama.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-8">
|
|
||||||
<div class="bg-white dark:bg-slate-950 p-4 rounded-xl border border-slate-800 flex items-start space-x-3">
|
|
||||||
<div class="text-sky-400 p-2 bg-sky-500/5 rounded-lg"><i data-lucide="globe" class="w-5 h-5"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Core Jurisdictions
|
|
||||||
</h4>
|
|
||||||
<div class="flex items-center space-x-1.5 mt-1">
|
|
||||||
<span class="fi fi-eu" title="European Union"></span>
|
|
||||||
<span class="fi fi-cn" title="China"></span>
|
|
||||||
<span class="fi fi-hk" title="Hong Kong"></span>
|
|
||||||
<span class="fi fi-sg" title="Singapore"></span>
|
|
||||||
<span class="fi fi-jp" title="Japan"></span>
|
|
||||||
<span class="text-sm font-medium text-slate-700 dark:text-slate-300 ml-1">Europe, Mainland China Transit & APAC
|
|
||||||
Networks</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 p-4 rounded-xl border border-slate-800 flex items-start space-x-3">
|
|
||||||
<div class="text-emerald-400 p-2 bg-emerald-500/5 rounded-lg"><i data-lucide="credit-card"
|
|
||||||
class="w-5 h-5"></i></div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">IAP Validation</h4>
|
|
||||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Asynchronous, Server-to-Server, Ledger-Signed
|
|
||||||
Receipt Verification</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 p-4 rounded-xl border border-slate-800 flex items-start space-x-3">
|
|
||||||
<div class="text-indigo-400 p-2 bg-indigo-500/5 rounded-lg"><i data-lucide="shield-check"
|
|
||||||
class="w-5 h-5"></i></div>
|
|
||||||
<div>
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Store Integration</h4>
|
|
||||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Steamworks, Google Play, Apple App Store, TapSDK
|
|
||||||
(Zero-Commission)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section 1: Regional Infrastructures & Compliance Mapping -->
|
|
||||||
<section id="infrastructure" class="mb-16 scroll-mt-28">
|
|
||||||
<div class="flex items-center space-x-2 mb-6">
|
|
||||||
<i data-lucide="map" class="text-sky-500 w-6 h-6"></i>
|
|
||||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">1. Regional Infrastructure & Regulatory Compliance</h2>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
||||||
Deploying cross-border games requires careful partitioning of databases and game nodes to satisfy
|
|
||||||
extreme technical boundaries (e.g., latency issues caused by the Great Firewall) and data protection
|
|
||||||
statutes (e.g., GDPR, local municipal privacy mandates).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<!-- Europe card -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="fi fi-eu w-5 h-5 rounded-sm"></span>
|
|
||||||
<span
|
|
||||||
class="text-xs font-mono px-2.5 py-1 rounded bg-sky-500/10 text-sky-400 border border-sky-500/20 font-semibold uppercase">Europe
|
|
||||||
(EU) - Webdock</span>
|
|
||||||
</div>
|
|
||||||
<i data-lucide="shield" class="text-sky-400 w-5 h-5"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200 mb-3">GDPR & DMA Pipeline</h3>
|
|
||||||
<ul class="space-y-2.5 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>Central
|
|
||||||
Cluster:</strong> Host on high-density <strong>Webdock.io</strong> Ryzen-powered VPS
|
|
||||||
profiles located in Frankfurt or Vienna.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i>
|
|
||||||
<span><strong>Sovereignty:</strong> Webdock offers EU-owned infrastructure with strict
|
|
||||||
hardware insulation and zero overseas sub-processor data leaks.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>User
|
|
||||||
Control:</strong> Integrate explicit game-level telemetry opt-out flags which halt
|
|
||||||
outgoing Nakama analytics scripts instantly.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- China card (Repositioned to reflect China Transit priority routing) -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="fi fi-cn w-5 h-5 rounded-sm"></span>
|
|
||||||
<span class="fi fi-hk w-5 h-5 rounded-sm"></span>
|
|
||||||
<span
|
|
||||||
class="text-xs font-mono px-2.5 py-1 rounded bg-rose-500/10 text-rose-400 border border-rose-500/20 font-semibold uppercase">China
|
|
||||||
Transit - HostHatch</span>
|
|
||||||
</div>
|
|
||||||
<i data-lucide="lock" class="text-rose-400 w-5 h-5"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200 mb-3">HostHatch HK Edge Gateway</h3>
|
|
||||||
<ul class="space-y-2.5 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
<li class="flex items-start"><i data-lucide="alert-triangle"
|
|
||||||
class="text-amber-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>GFW
|
|
||||||
Proximity:</strong> Deploy regional proxy logic to <strong>HostHatch.com</strong>
|
|
||||||
Hong Kong nodes, featuring direct low-latency peering tunnels.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="alert-triangle"
|
|
||||||
class="text-amber-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>Regulatory
|
|
||||||
Separation:</strong> Standalone DB instances keep Mainland China data segregated
|
|
||||||
from Western clusters while resolving network hops near the target audience.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="alert-triangle"
|
|
||||||
class="text-amber-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>NPPA
|
|
||||||
Integrations:</strong> Route localized traffic from HK edge to Chinese validation
|
|
||||||
and anti-addiction registry backends efficiently.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Asia Ex-CN card (Now following CN, handling overflow and APAC routing) -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 hover:border-slate-300 dark:hover:border-slate-700 transition">
|
|
||||||
<div class="flex justify-between items-start mb-4">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<span class="fi fi-sg w-5 h-5 rounded-sm"></span>
|
|
||||||
<span class="fi fi-jp w-5 h-5 rounded-sm"></span>
|
|
||||||
<span class="fi fi-kr w-5 h-5 rounded-sm"></span>
|
|
||||||
<span
|
|
||||||
class="text-xs font-mono px-2.5 py-1 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold uppercase">Asia
|
|
||||||
(Ex-CN) - Following CN</span>
|
|
||||||
</div>
|
|
||||||
<i data-lucide="zap" class="text-emerald-400 w-5 h-5"></i>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200 mb-3">APAC Edge Arrays</h3>
|
|
||||||
<ul class="space-y-2.5 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>Transit
|
|
||||||
Integration:</strong> Set up edge nodes in Tokyo, Seoul, and Singapore configured to
|
|
||||||
capture traffic spills when cross-border HK connections are saturated.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>Latency
|
|
||||||
Optimization:</strong> Run high-performance CockroachDB clusters to maintain global
|
|
||||||
synchronization while serving nearby regional users under 45ms.</span></li>
|
|
||||||
<li class="flex items-start"><i data-lucide="check-circle-2"
|
|
||||||
class="text-emerald-500 w-4 h-4 mr-2 mt-0.5 shrink-0"></i> <span><strong>Data
|
|
||||||
Privacy:</strong> Satisfy local guidelines (Japan's APPI, South Korea's PIPA) using
|
|
||||||
explicit user data deletion interfaces inside Nakama profile routes.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Warning Callout on China Firewall -->
|
|
||||||
<div class="mt-6 bg-amber-500/5 border border-amber-500/20 rounded-xl p-4 flex items-start space-x-3">
|
|
||||||
<i data-lucide="alert-circle" class="text-amber-500 w-5 h-5 shrink-0 mt-0.5"></i>
|
|
||||||
<div class="text-sm text-amber-300">
|
|
||||||
<strong class="font-semibold block">Critical GFW Operational Warning:</strong>
|
|
||||||
Real-time WebSocket and UDP connection signals across the Great Firewall suffer massive packet
|
|
||||||
losses (≥ 25%). Incorporating HostHatch's Hong Kong node acts as an indispensable entry buffer;
|
|
||||||
however, complete logical segregation of the Chinese client backend instance is still mandatory.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section 2: Financial Strategy, Licenses, & Commissions -->
|
|
||||||
<section id="storefront" class="mb-16 scroll-mt-28">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<i data-lucide="dollar-sign" class="text-emerald-500 w-6 h-6"></i>
|
|
||||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">2. Storefront Commissions & Licensing Pipeline</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
||||||
Operating margins rely on optimizing each storefront's fee parameters. Before collecting gross revenue,
|
|
||||||
publishers must clear the initial platform entry fees.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Upfront Fee Settlement Grid -->
|
|
||||||
<div class="mb-8 bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6">
|
|
||||||
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200 mb-4 flex items-center">
|
|
||||||
<i data-lucide="wallet" class="text-sky-400 w-5 h-5 mr-2"></i>
|
|
||||||
Initial Publishing Costs & First Fee Settlement Grid
|
|
||||||
</h3>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 text-sm mb-6">
|
|
||||||
Below is the required capital breakdown needed to register your identity and prepare storefront
|
|
||||||
slots before pushing your initial Godot client build to production channels.
|
|
||||||
</p>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full text-left text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<thead class="bg-slate-50 dark:bg-slate-900 text-slate-600 dark:text-slate-400 text-xs font-mono uppercase tracking-wider">
|
|
||||||
<tr>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800">Platform</th>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800">First Fee Amount</th>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800 font-semibold text-center">Market Flag</th>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800">Fee Recurrence Type</th>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800">Refundability Status</th>
|
|
||||||
<th class="p-4 border-b border-slate-200 dark:border-slate-800">Settlement Methods</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-800">
|
|
||||||
<!-- Steam Row -->
|
|
||||||
<tr class="hover:bg-slate-50 dark:bg-slate-900/40">
|
|
||||||
<td class="p-4 font-semibold text-slate-900 dark:text-white">Steamworks (PC)</td>
|
|
||||||
<td class="p-4 font-mono text-emerald-400">$100.00 USD <span
|
|
||||||
class="text-slate-500 dark:text-slate-400 text-xs">(per product)</span></td>
|
|
||||||
<td class="p-4 text-center">
|
|
||||||
<span class="fi fi-us" title="United States / Global"></span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4">One-time per App Slot</td>
|
|
||||||
<td class="p-4"><span class="text-emerald-400 font-semibold">Yes</span> <span
|
|
||||||
class="text-xs text-slate-500 dark:text-slate-400">(Refunded after $1,000 in gross sales)</span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-xs">Credit Card, PayPal, Steam Wallet, Wire Transfer</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Itch Row -->
|
|
||||||
<tr class="hover:bg-slate-50 dark:bg-slate-900/40">
|
|
||||||
<td class="p-4 font-semibold text-slate-900 dark:text-white">Itch.io (PC)</td>
|
|
||||||
<td class="p-4 font-mono text-slate-600 dark:text-slate-400">$0.00 USD <span
|
|
||||||
class="text-slate-500 dark:text-slate-400 text-xs">(Zero entry fee)</span></td>
|
|
||||||
<td class="p-4 text-center">
|
|
||||||
<span class="fi fi-un" title="International / Open"></span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4">None</td>
|
|
||||||
<td class="p-4 text-slate-500 dark:text-slate-400">Not Applicable</td>
|
|
||||||
<td class="p-4 text-xs">None (Optional tax documentation verification)</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Google Row -->
|
|
||||||
<tr class="hover:bg-slate-50 dark:bg-slate-900/40">
|
|
||||||
<td class="p-4 font-semibold text-slate-900 dark:text-white">Google Play</td>
|
|
||||||
<td class="p-4 font-mono text-emerald-400">$25.00 USD</td>
|
|
||||||
<td class="p-4 text-center">
|
|
||||||
<span class="fi fi-us" title="United States / Global"></span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4">One-time per Developer Identity</td>
|
|
||||||
<td class="p-4 text-rose-400 font-semibold">No</td>
|
|
||||||
<td class="p-4 text-xs">Credit/Debit Card (requires Google Pay Profile)</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Apple Row -->
|
|
||||||
<tr class="hover:bg-slate-50 dark:bg-slate-900/40">
|
|
||||||
<td class="p-4 font-semibold text-slate-900 dark:text-white">Apple App Store</td>
|
|
||||||
<td class="p-4 font-mono text-emerald-400">$99.00 USD</td>
|
|
||||||
<td class="p-4 text-center">
|
|
||||||
<span class="fi fi-us" title="United States / Global"></span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4">Annual Subscription Renewal</td>
|
|
||||||
<td class="p-4 text-rose-400 font-semibold">No</td>
|
|
||||||
<td class="p-4 text-xs">Credit Card (linked to Apple ID Developer Account)</td>
|
|
||||||
</tr>
|
|
||||||
<!-- TapTap Row -->
|
|
||||||
<tr class="hover:bg-slate-50 dark:bg-slate-900/40">
|
|
||||||
<td class="p-4 font-semibold text-slate-900 dark:text-white">TapTap Developer</td>
|
|
||||||
<td class="p-4 font-mono text-slate-600 dark:text-slate-400">$0.00 USD <span
|
|
||||||
class="text-slate-500 dark:text-slate-400 text-xs">(Corporate validation)</span></td>
|
|
||||||
<td class="p-4 text-center">
|
|
||||||
<span class="fi fi-cn mr-0.5" title="China"></span>
|
|
||||||
<span class="fi fi-sg" title="Singapore / APAC"></span>
|
|
||||||
</td>
|
|
||||||
<td class="p-4">None</td>
|
|
||||||
<td class="p-4 text-slate-500 dark:text-slate-400">Not Applicable</td>
|
|
||||||
<td class="p-4 text-xs">Requires legal corporate identity / ICP verification</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 mb-8 text-center">
|
|
||||||
<span class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-widest font-mono">Net Yield Production
|
|
||||||
Formula</span>
|
|
||||||
<div class="text-2xl sm:text-3xl font-mono text-sky-400 my-3 select-all">
|
|
||||||
R_net = R_gross × (1 - C_store - T_tax) - F_fees
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 max-w-xl mx-auto">
|
|
||||||
Where <span class="text-sky-400">C_store</span> matches the target store platform commission, <span
|
|
||||||
class="text-sky-400">T_tax</span> matches regional withholding taxes, and <span
|
|
||||||
class="text-sky-400">F_fees</span> encapsulates external server transaction margins and API
|
|
||||||
query operations.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Platform Detail Tabs -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl overflow-hidden">
|
|
||||||
<div class="border-b border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/40 p-1 flex overflow-x-auto scrollbar-none"
|
|
||||||
id="store-tabs">
|
|
||||||
<button onclick="switchTab('steam')"
|
|
||||||
class="tab-btn active px-4 py-2 text-sm font-semibold rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white transition whitespace-nowrap bg-slate-200 dark:bg-slate-800 text-slate-900 dark:text-white flex items-center"
|
|
||||||
id="tab-steam">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/steam/default.svg" class="w-4 h-4 mr-1.5" alt="Steam">Steam (PC)
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('itch')"
|
|
||||||
class="tab-btn px-4 py-2 text-sm font-semibold rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white transition whitespace-nowrap flex items-center"
|
|
||||||
id="tab-itch">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/itchdotio/default.svg" class="w-4 h-4 mr-1.5 invert" alt="Itch.io">Itch.io (PC)
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('google')"
|
|
||||||
class="tab-btn px-4 py-2 text-sm font-semibold rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white transition whitespace-nowrap flex items-center"
|
|
||||||
id="tab-google">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/google-play/default.svg" class="w-4 h-4 mr-1.5" alt="Google Play">Google Play
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('apple')"
|
|
||||||
class="tab-btn px-4 py-2 text-sm font-semibold rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white transition whitespace-nowrap flex items-center"
|
|
||||||
id="tab-apple">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/apple/default.svg" class="w-4 h-4 mr-1.5 invert opacity-70" alt="Apple">App Store (iOS)
|
|
||||||
</button>
|
|
||||||
<button onclick="switchTab('taptap')"
|
|
||||||
class="tab-btn px-4 py-2 text-sm font-semibold rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white transition whitespace-nowrap flex items-center"
|
|
||||||
id="tab-taptap">
|
|
||||||
<span class="fi fi-cn mr-1"></span><span class="fi fi-sg mr-1.5"></span>TapTap (Mobile)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
|
||||||
<!-- Steam Tab Content -->
|
|
||||||
<div id="content-steam" class="tab-content">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Steamworks Implementation Matrix</h3>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Commission Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">30% (Default)</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Volume Scaling</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">25% at $10M | 20% at $50M</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">First Fee</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">$100 USD (Steam Direct
|
|
||||||
App Deposit)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Nakama Verification</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">Server Authenticated
|
|
||||||
Session Ticket</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/40 p-5 rounded-lg border border-slate-800">
|
|
||||||
<h4 class="text-xs font-semibold tracking-wider text-slate-500 dark:text-slate-400 uppercase mb-2">Technical
|
|
||||||
Execution</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed mb-3">
|
|
||||||
Initialize <code class="text-sky-400 font-mono text-xs">GodotSteam</code>
|
|
||||||
dynamically inside your client. The user retrieves their secure hex session ticket
|
|
||||||
locally, passing it down to Nakama using the native client driver hook to eliminate
|
|
||||||
credential spoofing risks.
|
|
||||||
</p>
|
|
||||||
<span class="text-xs font-mono text-slate-500 dark:text-slate-400 block">Endpoint:
|
|
||||||
client.authenticate_steam_async(ticket)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Itch Tab Content -->
|
|
||||||
<div id="content-itch" class="tab-content hidden">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Itch.io Standalone Blueprint</h3>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Commission Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">10% (Open Slider to 0%)</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">First Fee</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">$0.00 USD (Completely Open
|
|
||||||
Portal)</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Target Core Assets</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">No External Platform Hooks</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Nakama Verification</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">Device Unique ID /
|
|
||||||
Hardware Hash</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/40 p-5 rounded-lg border border-slate-800">
|
|
||||||
<h4 class="text-xs font-semibold tracking-wider text-slate-500 dark:text-slate-400 uppercase mb-2">Technical
|
|
||||||
Execution</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed mb-3">
|
|
||||||
Itch standalone builds must bypass Steam libraries completely. Query the hardware
|
|
||||||
hash utilizing Godot's <code
|
|
||||||
class="text-sky-400 font-mono text-xs">OS.get_unique_id()</code> call. Combine
|
|
||||||
with custom email authentication options inside your client UI for stable user
|
|
||||||
recovery.
|
|
||||||
</p>
|
|
||||||
<span class="text-xs font-mono text-slate-500 dark:text-slate-400 block">Endpoint:
|
|
||||||
client.authenticate_device_async(id)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Google Play Tab Content -->
|
|
||||||
<div id="content-google" class="tab-content hidden">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Google Play Commerce Setup</h3>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Commission Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">15% on first $1M USD annually
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Standard Tier Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">30% (If annual earnings exceed
|
|
||||||
$1M)</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">First Fee</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">$25 USD (One-time Console
|
|
||||||
Setup)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Nakama Verification</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">Google Play Games Server
|
|
||||||
Auth Code</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/40 p-5 rounded-lg border border-slate-800">
|
|
||||||
<h4 class="text-xs font-semibold tracking-wider text-slate-500 dark:text-slate-400 uppercase mb-2">Technical
|
|
||||||
Execution</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed mb-3">
|
|
||||||
Integrate the Google Play Games GDExtension inside Godot. Retrieve the user's secure
|
|
||||||
authentication code on startup and transfer it to the Nakama server backend to
|
|
||||||
provision accounts asynchronously.
|
|
||||||
</p>
|
|
||||||
<span class="text-xs font-mono text-slate-500 dark:text-slate-400 block">Endpoint:
|
|
||||||
client.authenticate_google_async(token)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Apple Tab Content -->
|
|
||||||
<div id="content-apple" class="tab-content hidden">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">Apple App Store Integration</h3>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Commission Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">15% via Small Business Program
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Standard Tier Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">30% (Standard Portfolio
|
|
||||||
Account)</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">First Fee</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">$99 USD (Annual
|
|
||||||
Registration Program)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Nakama Verification</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">Apple Identity Token or
|
|
||||||
Game Center</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/40 p-5 rounded-lg border border-slate-800">
|
|
||||||
<h4 class="text-xs font-semibold tracking-wider text-slate-500 dark:text-slate-400 uppercase mb-2">Technical
|
|
||||||
Execution</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed mb-3">
|
|
||||||
Process authentication loops using Apple ID identity payloads via Sign In with
|
|
||||||
Apple. For Game Center, extract authentication signatures, timestamps, and salt
|
|
||||||
packets to execute secure matching processes.
|
|
||||||
</p>
|
|
||||||
<span class="text-xs font-mono text-slate-500 dark:text-slate-400 block">Endpoint:
|
|
||||||
client.authenticate_apple_async(token)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- TapTap Tab Content -->
|
|
||||||
<div id="content-taptap" class="tab-content hidden">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-4">TapTap Developer Services Strategy</h3>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Commission Rate</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">0% (Completely Commission-Free)
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Target Core Markets</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">Mainland China & SEA Mobile
|
|
||||||
Communities</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="border-b border-slate-200 dark:border-slate-800">
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">First Fee</td>
|
|
||||||
<td class="py-2 text-slate-900 dark:text-white font-mono text-right">$0.00 USD (Entity verification
|
|
||||||
required)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="py-2 text-slate-500 dark:text-slate-400">Nakama Verification</td>
|
|
||||||
<td class="py-2 text-emerald-400 font-mono text-right">Custom Auth (via TapTap
|
|
||||||
OAuth Token)</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/40 p-5 rounded-lg border border-slate-800">
|
|
||||||
<h4 class="text-xs font-semibold tracking-wider text-slate-500 dark:text-slate-400 uppercase mb-2">Technical
|
|
||||||
Execution</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed mb-3">
|
|
||||||
TapTap operates without a direct native Nakama SDK connector. Retrieve the user's
|
|
||||||
OAuth access token using the TapSDK inside Godot. Execute authentication by passing
|
|
||||||
the token directly to Nakama's custom endpoint, routing background network
|
|
||||||
validations securely.
|
|
||||||
</p>
|
|
||||||
<span class="text-xs font-mono text-slate-500 dark:text-slate-400 block">Endpoint:
|
|
||||||
client.authenticate_custom_async(tap_token)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section 3: Secure Server-to-Server Purchase Validation -->
|
|
||||||
<section id="monetization" class="mb-16 scroll-mt-28">
|
|
||||||
<div class="flex items-center space-x-2 mb-6">
|
|
||||||
<i data-lucide="shield-alert" class="text-sky-500 w-6 h-6"></i>
|
|
||||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">3. Monetization Architecture & Secure IAP Loop</h2>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
||||||
Client-side reporting is inherently untrustworthy. Memory injectors (e.g., Lucky Patcher on Android,
|
|
||||||
memory editing tools on PC) can manipulate the client runtime to simulate successful purchases.
|
|
||||||
Implementing a robust, asynchronous verification cycle ensures validation is handled strictly by the
|
|
||||||
server.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Sequence Layout -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 lg:p-8">
|
|
||||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-6 flex items-center"><i data-lucide="git-branch"
|
|
||||||
class="text-sky-400 w-5 h-5 mr-2"></i> Asynchronous Verification Topology</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 relative">
|
|
||||||
<!-- Step 1 -->
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/50 p-5 rounded-lg border border-slate-800 relative">
|
|
||||||
<div
|
|
||||||
class="absolute -top-3 -left-3 w-8 h-8 rounded-full bg-sky-500 text-slate-950 flex items-center justify-center font-bold text-sm">
|
|
||||||
1</div>
|
|
||||||
<h4 class="text-sm font-semibold text-slate-800 dark:text-slate-200 mt-2 mb-2">Purchase & Tokenization</h4>
|
|
||||||
<p class="text-xs text-slate-600 dark:text-slate-400">The Godot client requests checkout. The player submits payment
|
|
||||||
to the store network, which issues an encrypted, signed platform transaction token.</p>
|
|
||||||
</div>
|
|
||||||
<!-- Step 2 -->
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/50 p-5 rounded-lg border border-slate-800 relative">
|
|
||||||
<div
|
|
||||||
class="absolute -top-3 -left-3 w-8 h-8 rounded-full bg-sky-500 text-slate-950 flex items-center justify-center font-bold text-sm">
|
|
||||||
2</div>
|
|
||||||
<h4 class="text-sm font-semibold text-slate-800 dark:text-slate-200 mt-2 mb-2">Nakama RPC Ingestion</h4>
|
|
||||||
<p class="text-xs text-slate-600 dark:text-slate-400">The client forwards the raw transaction token securely to
|
|
||||||
Nakama via an RPC function call: <code class="text-sky-400">verify_purchase</code>.</p>
|
|
||||||
</div>
|
|
||||||
<!-- Step 3 -->
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/50 p-5 rounded-lg border border-slate-800 relative">
|
|
||||||
<div
|
|
||||||
class="absolute -top-3 -left-3 w-8 h-8 rounded-full bg-sky-500 text-slate-950 flex items-center justify-center font-bold text-sm">
|
|
||||||
3</div>
|
|
||||||
<h4 class="text-sm font-semibold text-slate-800 dark:text-slate-200 mt-2 mb-2">Server Validation Check</h4>
|
|
||||||
<p class="text-xs text-slate-600 dark:text-slate-400">Nakama blocks immediate user manipulation. It connects
|
|
||||||
server-to-server with Google, Apple, or Steam APIs to verify status and signatures.</p>
|
|
||||||
</div>
|
|
||||||
<!-- Step 4 -->
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900/50 p-5 rounded-lg border border-slate-800 relative">
|
|
||||||
<div
|
|
||||||
class="absolute -top-3 -left-3 w-8 h-8 rounded-full bg-emerald-500 text-slate-950 flex items-center justify-center font-bold text-sm">
|
|
||||||
4</div>
|
|
||||||
<h4 class="text-sm font-semibold text-slate-800 dark:text-slate-200 mt-2 mb-2">Ledger Provisioning</h4>
|
|
||||||
<p class="text-xs text-slate-600 dark:text-slate-400">Upon verification, Nakama updates the persistent storage
|
|
||||||
wallet data and broadcasts confirmation back to the Godot client.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Section 4: Architecture Autoload Script Blueprint -->
|
|
||||||
<section id="architecture" class="mb-16 scroll-mt-28">
|
|
||||||
<div class="flex items-center space-x-2 mb-6">
|
|
||||||
<i data-lucide="network" class="text-sky-500 w-6 h-6"></i>
|
|
||||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">4. Core Architecture: Unified Identity Manager Decision Flow
|
|
||||||
Chart</h2>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-6">
|
|
||||||
The Unified Identity Manager dynamically discovers platforms at runtime, resolving features and
|
|
||||||
singletons without breaking compilations on platforms lacking those SDK wrappers. Click the platform
|
|
||||||
modes below to preview the path execution, safety hooks, and token routing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Dynamic Decision Flowchart Container -->
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-slate-800 rounded-xl p-6 lg:p-8">
|
|
||||||
<!-- Interactive Controls -->
|
|
||||||
<div
|
|
||||||
class="flex flex-wrap gap-2 mb-8 bg-slate-50 dark:bg-slate-900 p-1.5 rounded-xl border border-slate-800 max-w-2xl mx-auto justify-center">
|
|
||||||
<button onclick="highlightPath('all')"
|
|
||||||
class="path-ctrl-btn active px-4 py-2 text-xs font-semibold rounded-lg transition-all duration-300 bg-sky-500 text-slate-950 shadow-md flex items-center justify-center"
|
|
||||||
id="btn-path-all">
|
|
||||||
Show Full Tree
|
|
||||||
</button>
|
|
||||||
<button onclick="highlightPath('steam')"
|
|
||||||
class="path-ctrl-btn px-4 py-2 text-xs font-semibold rounded-lg transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white flex items-center justify-center"
|
|
||||||
id="btn-path-steam">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/steam/default.svg" class="w-3.5 h-3.5 mr-1.5" alt="Steam">PC (Steam)
|
|
||||||
</button>
|
|
||||||
<button onclick="highlightPath('android')"
|
|
||||||
class="path-ctrl-btn px-4 py-2 text-xs font-semibold rounded-lg transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white flex items-center justify-center"
|
|
||||||
id="btn-path-android">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/google-play/default.svg" class="w-3.5 h-3.5 mr-1.5" alt="Google Play">Android (Google/TapTap)
|
|
||||||
</button>
|
|
||||||
<button onclick="highlightPath('ios')"
|
|
||||||
class="path-ctrl-btn px-4 py-2 text-xs font-semibold rounded-lg transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white flex items-center justify-center"
|
|
||||||
id="btn-path-ios">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/apple/default.svg" class="w-3.5 h-3.5 mr-1.5 invert opacity-70" alt="Apple">iOS (Apple/TapTap)
|
|
||||||
</button>
|
|
||||||
<button onclick="highlightPath('fallback')"
|
|
||||||
class="path-ctrl-btn px-4 py-2 text-xs font-semibold rounded-lg transition-all duration-300 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:text-white flex items-center justify-center"
|
|
||||||
id="btn-path-fallback">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/itchdotio/default.svg" class="w-3.5 h-3.5 mr-1.5 invert" alt="Itch.io">Itch.io / Standalone PC
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Flowchart Graph Layout -->
|
|
||||||
<div class="flex flex-col space-y-8 relative">
|
|
||||||
|
|
||||||
<!-- Root Entry Point -->
|
|
||||||
<div class="flex justify-center transition-all duration-300" id="node-root">
|
|
||||||
<div
|
|
||||||
class="bg-slate-50 dark:bg-slate-900 border-2 border-slate-700 hover:border-sky-500 rounded-xl p-4 text-center max-w-xs shadow-lg transition-all duration-300">
|
|
||||||
<div class="text-xs font-mono font-bold text-sky-400 uppercase tracking-widest mb-1">Game
|
|
||||||
Initialization</div>
|
|
||||||
<h4 class="text-sm font-extrabold text-slate-900 dark:text-white">_ready() / dispatch_platform_auth()</h4>
|
|
||||||
<p class="text-xxs text-slate-500 dark:text-slate-400 mt-1">Queries environment architecture and singletons</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Connective Vertical Line from Root -->
|
|
||||||
<div class="flex justify-center h-6 -my-4 relative z-0">
|
|
||||||
<div class="w-0.5 bg-slate-200 dark:bg-slate-800 transition-all duration-300" id="line-root"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Platform Routing Split Container -->
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 relative z-10">
|
|
||||||
|
|
||||||
<!-- Steam Branch -->
|
|
||||||
<div class="flex flex-col items-center bg-slate-50 dark:bg-slate-900/40 border border-slate-800 rounded-xl p-4 transition-all duration-300"
|
|
||||||
id="branch-steam">
|
|
||||||
<div class="flex items-center space-x-1.5 mb-3">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/steam/default.svg" class="w-4 h-4" alt="Steam">
|
|
||||||
<span
|
|
||||||
class="text-xxs font-mono font-semibold px-2 py-0.5 rounded bg-sky-500/10 text-sky-400 border border-sky-500/10">PC
|
|
||||||
Steam Target</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full text-center space-y-3">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Class Check</div>
|
|
||||||
<code class="text-xxs text-amber-400">ClassDB.has_singleton("Steam")</code>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Ticket Retrieval</div>
|
|
||||||
<p class="text-xxs text-slate-600 dark:text-slate-400 mt-0.5">Grabs session hex ticket asynchronously
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-sky-500/30 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-sky-400">Nakama Endpoint</div>
|
|
||||||
<code class="text-xxs text-slate-600 dark:text-slate-400">authenticate_steam_async()</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Android Branch -->
|
|
||||||
<div class="flex flex-col items-center bg-slate-50 dark:bg-slate-900/40 border border-slate-800 rounded-xl p-4 transition-all duration-300"
|
|
||||||
id="branch-android">
|
|
||||||
<div class="flex items-center space-x-1.5 mb-3">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/google-play/default.svg" class="w-4 h-4" alt="Google Play">
|
|
||||||
<span
|
|
||||||
class="text-xxs font-mono font-semibold px-2 py-0.5 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/10">Android
|
|
||||||
OS Target</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full text-center space-y-3">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Feature Gating</div>
|
|
||||||
<code class="text-xxs text-emerald-400">OS.has_feature("taptap")</code>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center flex-row justify-around">
|
|
||||||
<span class="text-xxs text-emerald-500">Yes</span><span
|
|
||||||
class="text-xxs text-sky-500">No</span></div>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-1.5 shadow text-xxs">
|
|
||||||
<div class="font-bold text-emerald-400">TapSDK</div>
|
|
||||||
<p class="text-[9px] text-slate-500 dark:text-slate-400 mt-0.5">Fetch OAuth Token</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-1.5 shadow text-xxs">
|
|
||||||
<div class="font-bold text-sky-400">Google Play</div>
|
|
||||||
<p class="text-[9px] text-slate-500 dark:text-slate-400 mt-0.5">Fetch Auth Code</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-sky-500/30 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-sky-400">Nakama Endpoint</div>
|
|
||||||
<code
|
|
||||||
class="text-xxs text-slate-600 dark:text-slate-400">authenticate_custom_async()<br>OR<br>authenticate_google_async()</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- iOS Branch -->
|
|
||||||
<div class="flex flex-col items-center bg-slate-50 dark:bg-slate-900/40 border border-slate-800 rounded-xl p-4 transition-all duration-300"
|
|
||||||
id="branch-ios">
|
|
||||||
<div class="flex items-center space-x-1.5 mb-3">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/apple/default.svg" class="w-4 h-4 invert opacity-70" alt="Apple">
|
|
||||||
<span
|
|
||||||
class="text-xxs font-mono font-semibold px-2 py-0.5 rounded bg-violet-500/10 text-violet-400 border border-violet-500/10">iOS
|
|
||||||
OS Target</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full text-center space-y-3">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Feature Gating</div>
|
|
||||||
<code class="text-xxs text-violet-400">OS.has_feature("taptap")</code>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center flex-row justify-around">
|
|
||||||
<span class="text-xxs text-violet-500">Yes</span><span
|
|
||||||
class="text-xxs text-sky-500">No</span></div>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-1.5 shadow text-xxs">
|
|
||||||
<div class="font-bold text-violet-400">TapSDK</div>
|
|
||||||
<p class="text-[9px] text-slate-500 dark:text-slate-400 mt-0.5">Fetch OAuth Token</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-1.5 shadow text-xxs">
|
|
||||||
<div class="font-bold text-sky-400">Apple Auth</div>
|
|
||||||
<p class="text-[9px] text-slate-500 dark:text-slate-400 mt-0.5">Fetch Identity Token</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-sky-500/30 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-sky-400">Nakama Endpoint</div>
|
|
||||||
<code
|
|
||||||
class="text-xxs text-slate-600 dark:text-slate-400">authenticate_custom_async()<br>OR<br>authenticate_apple_async()</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Fallback / Itch.io Standalone Branch -->
|
|
||||||
<div class="flex flex-col items-center bg-slate-50 dark:bg-slate-900/40 border border-slate-800 rounded-xl p-4 transition-all duration-300"
|
|
||||||
id="branch-fallback">
|
|
||||||
<div class="flex items-center space-x-1.5 mb-3">
|
|
||||||
<img src="https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/itchdotio/default.svg" class="w-4 h-4 invert" alt="Itch.io">
|
|
||||||
<span
|
|
||||||
class="text-xxs font-mono font-semibold px-2 py-0.5 rounded bg-rose-500/10 text-rose-400 border border-rose-500/10">PC/Itch.io
|
|
||||||
Standalone</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full text-center space-y-3">
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Device Hardware ID</div>
|
|
||||||
<code class="text-xxs text-rose-400">OS.get_unique_id()</code>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-slate-50 dark:bg-slate-900 border border-slate-800 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-slate-800 dark:text-slate-200">Hardware Hashing</div>
|
|
||||||
<p class="text-xxs text-slate-600 dark:text-slate-400 mt-0.5">Generate hardware fingerprint</p>
|
|
||||||
</div>
|
|
||||||
<div class="text-slate-600 text-center flex justify-center"><i data-lucide="arrow-down"
|
|
||||||
class="w-4 h-4"></i></div>
|
|
||||||
<div class="bg-white dark:bg-slate-950 border border-sky-500/30 rounded-lg p-2.5 shadow text-xs">
|
|
||||||
<div class="font-bold text-sky-400">Nakama Endpoint</div>
|
|
||||||
<code class="text-xxs text-slate-600 dark:text-slate-400">authenticate_device_async()</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Connective Vertical Line to Target Server -->
|
|
||||||
<div class="flex justify-center h-6 -my-4 relative z-0">
|
|
||||||
<div class="w-0.5 bg-slate-200 dark:bg-slate-800 transition-all duration-300" id="line-terminal"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Unified Target Server Block -->
|
|
||||||
<div class="flex justify-center transition-all duration-300" id="node-terminal">
|
|
||||||
<div
|
|
||||||
class="bg-slate-50 dark:bg-slate-900 border-2 border-emerald-500/60 rounded-xl p-5 text-center max-w-sm shadow-xl transition-all duration-300">
|
|
||||||
<div
|
|
||||||
class="flex items-center justify-center space-x-2 text-emerald-400 text-xs font-mono font-bold uppercase tracking-wider mb-2">
|
|
||||||
<span class="relative flex h-2 w-2">
|
|
||||||
<span
|
|
||||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
||||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
|
||||||
</span>
|
|
||||||
<span>Nakama Server Response Node</span>
|
|
||||||
</div>
|
|
||||||
<h4 class="text-sm font-extrabold text-slate-900 dark:text-white">NakamaSession Established</h4>
|
|
||||||
<p class="text-xxs text-slate-600 dark:text-slate-400 mt-1.5 leading-relaxed">
|
|
||||||
Session token decoded & validated, persistent profiles resolved, and socket pipelines
|
|
||||||
opened. Global matching/telemetry gates unlocked.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Section 5: Project Management (PR) Board -->
|
|
||||||
|
|
||||||
<!-- Section 5: Project Management (PR) Board -->
|
|
||||||
<section id="priority-board" class="mb-16 scroll-mt-28">
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<i data-lucide="kanban-square" class="text-sky-500 w-6 h-6"></i>
|
|
||||||
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">5. Project Management (PR) Board & AI Checklist</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-slate-600 dark:text-slate-400 mb-8">
|
|
||||||
This section serves as a fully detailed tracking board. It merges production readiness, backend reconstruction, gameflow audit, and Steam depot release tasks.
|
|
||||||
<strong>Every task is fully expanded with checklists and automated testing criteria so you can track AI execution seamlessly.</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="bg-rose-50 dark:bg-rose-500/5 border border-rose-200 dark:border-rose-500/25 rounded-xl p-5 mb-8">
|
|
||||||
<h3 class="text-md font-bold text-rose-700 dark:text-rose-200 mb-3 flex items-center"><i data-lucide="flame" class="w-4 h-4 mr-2"></i>Priority Rule</h3>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed">
|
|
||||||
Do not spend release time on Steam depot upload, signing polish, or branch promotion until P0 backend authority
|
|
||||||
is fixed. Current audit found client-authoritative economy, gacha paths, and sync loopholes.
|
|
||||||
Those are launch blockers because they can corrupt wallet, inventory, match state, and account identity before first public build.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Detailed Task List (Expandable Accordions) -->
|
|
||||||
<div class="space-y-4">
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P0-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Economy Authority</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
server/nakama/tekton_admin.js, user_profile_manager.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Stop trusting client prices, categories, package IDs. Reconstruct server-authoritative economy.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Create server catalog mapping item IDs to category, price, currency type, stack rules.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Change purchase request so client sends only item ID, quantity, and optional idempotency key.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Validate balance and inventory capacity server-side before mutation.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Replace fake currency purchase with receipt verification placeholder interface per platform.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Write wallet/inventory mutation audit entry with user ID, request ID, before/after values.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P0-1">Analyze current Tekton economy flow in server/nakama/tekton_admin.js and Godot callers. Reconstruct shop purchase authority so the client no longer sends trusted price_gold, price_star, category, or reward data. Add a server-side item catalog and update rpcPurchaseItem to accept only item_id, quantity, and idempotency_key. Replace rpcBuyCurrency behavior with a receipt-verification-safe interface that records pending/verified transactions and never grants premium currency from package ID alone. Preserve existing profile/wallet behavior where possible. Add validation, normalized errors, and audit ledger writes. Update Godot callers to match new payload shape. Acceptance: no wallet or inventory mutation depends on client-submitted price/category/package intent; duplicate idempotency key does not duplicate grant; existing shop UI can still request purchases.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P0-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Call `rpcPurchaseItem` with modified price/category from client. Assert server rejects or ignores client price and uses catalog price. Assert duplicate idempotency keys return the exact same transaction result without deducting twice.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P0-1">**Completed [PRD-P0-1]: Economy Authority**
|
|
||||||
- **Goal:** Stop trusting client prices, categories, package IDs. Reconstruct server-authoritative economy.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: server/nakama/tekton_admin.js, user_profile_manager.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P0-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P0-2</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Gacha Authority</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
gacha_manager.gd, Nakama economy RPCs
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Move RNG, pity, cost consume, and rewards server-side.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add server RPC for gacha pull with banner ID, pull count, and idempotency key.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Store pity and banner state server-side.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Server consumes cost, rolls reward, writes item/fragment result, and returns canonical result.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Client only animates returned result; no local grant or deduction.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add migration note for existing local pity/fragment data.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P0-2">Refactor Tekton gacha so authority lives in Nakama. Read scripts/managers/gacha_manager.gd, user_profile_manager.gd, and server/nakama/tekton_admin.js before editing. Add server-side RPCs for gacha_pull and any needed banner/profile state. Server must own RNG, pity counter, cost deduction, reward choice, inventory/fragment writes, and audit/idempotency. Client must become presentation-only: it sends banner_id, pull_count, and idempotency_key, then animates the canonical server response. Remove local reward grant and local currency deduction from gacha_manager.gd. Acceptance: editing client RNG/pity code cannot change real rewards; duplicate pull request cannot duplicate rewards; profile refresh after pull shows server state.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P0-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Trigger `rpcGachaPull`. Assert client currency deduction happens only upon server response. Assert client cannot specify reward or manipulate RNG seed.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P0-2">**Completed [PRD-P0-2]: Gacha Authority**
|
|
||||||
- **Goal:** Move RNG, pity, cost consume, and rewards server-side.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: gacha_manager.gd, Nakama economy RPCs</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P0-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P0-3</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Auth & Secrets Lock</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
auth_manager.gd, nakama_manager.gd, project.godot
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Remove insecure Steam fallback, default App ID 480, hardcoded release secrets.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Replace production Steam App ID placeholder only when real ID exists.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Fail hard in Steam build if Steam ticket cannot be acquired.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove fallback email/custom auth from Steam release path.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Externalize server host, scheme, key, encryption key, and secrets.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Delete or environment-gate admin topup RPC and admin UI entry points.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P0-3">Audit and harden Tekton authentication and admin mutation paths. Read project.godot, scripts/services/steamworks_manager.gd, scripts/managers/auth_manager.gd, scripts/nakama_manager.gd, scripts/ui/admin_panel.gd, and server/nakama/tekton_admin.js. Remove insecure Steam release fallback behavior so Steam builds authenticate only with valid Steam tickets. Add clear release guards for Steam App ID 480 so production export fails or warns loudly if still using test ID. Externalize backend config and local encryption material away from hardcoded production defaults. Remove or environment-gate rpcAdminTopupGold and ensure admin panel scenes/scripts are not included in player exports unless explicitly feature-flagged. Acceptance: Steam build cannot silently fall back to insecure auth; test App ID 480 is blocked for production; admin mint path is unavailable in production runtime.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P0-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Export project with Steam features. Disconnect Steam client. Assert game fails to authenticate and does NOT fallback to custom/device auth. Assert admin UI is entirely hidden in release builds.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P0-3">**Completed [PRD-P0-3]: Auth & Secrets Lock**
|
|
||||||
- **Goal:** Remove insecure Steam fallback, default App ID 480, hardcoded release secrets.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: auth_manager.gd, nakama_manager.gd, project.godot</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P0-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P0-4</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Backend Deploy Safety</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
server/, Nakama runtime module
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Replace manual module copy/restart with staging/prod deploy, health check, rollback.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Separate dev/staging/prod Nakama config and secrets.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Script module package/copy/restart with version label.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add health check RPC after deploy.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Keep previous module artifact for rollback.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add smoke checklist: auth, profile, shop, mail, gacha, friends, leaderboard.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P0-4">Create a production-safe Nakama deployment workflow for Tekton. Review server/docker-compose.yaml, server/nakama/README.md, and current runtime module layout. Replace manual docker cp guidance with scripts or documented commands for staging and production deploys. Include environment-specific config/secrets, module version labeling, restart procedure, health check, smoke test commands, and rollback to previous module. Do not commit real secrets. Acceptance: a developer can deploy to staging, verify health, promote to production, and rollback using documented repeatable steps without manually editing containers.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P0-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Run deploy script. Assert Nakama server restarts without losing data. Trigger health check RPC to verify new module loaded successfully.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P0-4">**Completed [PRD-P0-4]: Backend Deploy Safety**
|
|
||||||
- **Goal:** Replace manual module copy/restart with staging/prod deploy, health check, rollback.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: server/, Nakama runtime module</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P0-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P0-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Spawn/Sync Authority Lock</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
main.gd, player.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Retain: deterministic pre-spawn sync. Remove: client-trusted teleport/update paths.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Keep deterministic pre-spawn strategy (client pre-creates lobby roster).</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove client-trusted position mutation path that can move authoritative state without server validation.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Introduce server-owned spawn_revision and state_revision integers.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Reject stale updates on client and server.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Ensure reconnect flow requests full player sync, then grid sync, then mode-specific sync.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P0-1">Audit and harden player spawn/sync authority in scenes/main.gd and scenes/player.gd. Keep deterministic pre-spawn strategy and existing server-authoritative item randomization pattern, but remove any client-trusted position mutation path that can move authoritative state without server validation. Introduce server-owned spawn_revision and state_revision integers sent with spawn and full-sync payloads. Reject stale updates on client and server. Ensure reconnect flow requests full player sync first, then full grid sync, then mode-specific sync (Stop n Go / Tekton Doors) with explicit ack step. Acceptance: client cannot force authoritative teleport; reconnecting client converges to identical player positions/goals/playerboards after one sync cycle; stale packets no longer overwrite newer state.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P0-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Simulate client sending stale spawn_revision. Assert server rejects. Reconnect mid-match, assert player converges to exact same grid position as before.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P0-1">**Completed [PRD-GF-P0-1]: Spawn/Sync Authority Lock**
|
|
||||||
- **Goal:** Retain: deterministic pre-spawn sync. Remove: client-trusted teleport/update paths.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: main.gd, player.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P0-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P0-2</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Lobby Start Gate Hardening</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
lobby.gd, lobby_manager.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Add preflight checklist RPC, check ready-state and host authority.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Preserve LAN/Nakama dual-mode behavior and tutorial fast path.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add preflight readiness checks before _on_game_starting transition.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Verify session valid, host authority true, all player records present, mode config validated.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add one typed preflight result object and render actionable errors.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P0-2">Rework lobby game-start gating in scenes/lobby.gd and scripts/managers/lobby_manager.gd. Preserve LAN/Nakama dual-mode behavior and tutorial fast path, but add preflight readiness checks before _on_game_starting transition: session valid (or explicit guest/LAN mode), host authority true, all required player records present, mode config validated, and scene dependencies reachable. Add one typed preflight result object and render actionable errors in connection_status/status_label. Acceptance: start button cannot trigger broken scene load with partial state; host and clients see same preflight result; loading screen transition only occurs after preflight pass.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P0-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Attempt to start match without full player records. Assert UI blocks start and shows specific error string from preflight check.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P0-2">**Completed [PRD-GF-P0-2]: Lobby Start Gate Hardening**
|
|
||||||
- **Goal:** Add preflight checklist RPC, check ready-state and host authority.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: lobby.gd, lobby_manager.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P0-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P0-3</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">RPC Sender Identity & Contract Clamp</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
main.gd, player.gd, lobby_manager.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Remove payload fields that claim identity. Validate sender natively.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Read all any_peer RPC entry points.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove payload fields that pretend to identify requester/authority (use get_remote_sender_id).</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Verify sender identity and authority explicitly for state-mutation RPCs.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Normalize RPC contracts to carry stable error codes.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P0-3">Clamp multiplayer RPC trust boundaries across scenes/main.gd, scenes/player.gd, and scripts/managers/lobby_manager.gd. Read all any_peer RPC entry points before editing. Keep fast RPC update flow, but remove payload fields that pretend to identify requester/authority when sender can be derived from multiplayer.get_remote_sender_id(). For room info, start flow, rematch, and state-mutation RPCs, verify sender identity and authority explicitly. Normalize RPC contracts so request payloads contain only data the caller is allowed to propose, and response payloads carry canonical server state plus stable error codes. Acceptance: spoofed requester IDs are ignored; unauthorized peers cannot mutate host/server-owned state; RPC errors are debuggable and consistent.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P0-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Call state-mutation RPC pretending to be another peer ID in payload. Assert server overrides payload with actual `get_remote_sender_id()` and blocks if unauthorized.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P0-3">**Completed [PRD-GF-P0-3]: RPC Sender Identity & Contract Clamp**
|
|
||||||
- **Goal:** Remove payload fields that claim identity. Validate sender natively.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: main.gd, player.gd, lobby_manager.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P0-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-rose-500/30 bg-rose-50 dark:bg-rose-500/10 text-rose-600 dark:text-rose-300 text-xs font-bold">P0</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P0-4</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Chat/DM Abuse Control</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
lobby.gd, Nakama chat
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Add moderation, throttling, sanitation, flood guard, and permission matrix.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Keep current channel UX, DM tabs, and history pull.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add per-user send cooldown and max payload length limits.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add command permission matrix (/clear admin only, all other slash commands explicit).</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Mark unsent/failed messages in UI with retry policy.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P0-4">Harden global chat and DM flow in scenes/lobby.gd and related Nakama chat policy. Keep current channel UX, DM tabs, and history pull, but add abuse controls: per-user send cooldown, max payload length, profanity/moderation hook placeholder, and command permission matrix (/clear admin only, all other slash commands explicit). Fix any DM append/state bug found during read-through. Prevent silent local-only divergence by marking unsent/failed messages in UI and retry policy. Acceptance: flood attempts are throttled; unauthorized command execution blocked server-side; message rendering sanitized and bounded; chat remains responsive under burst traffic.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P0-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Send 50 chat messages in 1 second. Assert Nakama throttles request and UI shows 'failed to send/cooldown' UI marker. Attempt `/clear` as non-admin, assert blocked.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P0-4">**Completed [PRD-GF-P0-4]: Chat/DM Abuse Control**
|
|
||||||
- **Goal:** Add moderation, throttling, sanitation, flood guard, and permission matrix.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: lobby.gd, Nakama chat</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P0-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P1-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Module Split & RPC Validation</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
tekton_admin.js
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Split monolith into auth, economy, admin, mail, social, leaderboard, validation helpers.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Refactor tekton_admin.js into domain modules without changing external RPC names.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Create modules for auth, economy, admin, mail, social, leaderboard, storage, validation.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add central validators for payload shape, types, limits.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add normalized error responses with stable error codes.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P1-1">Refactor server/nakama/tekton_admin.js into maintainable domain modules without changing external RPC names unless necessary. Create or plan modules for auth, economy, admin, mail, social, leaderboard, storage, and validation helpers. Add central validators for payload shape, types, limits, and allowed enum values. Add normalized error responses with stable error codes. Keep behavior compatible while moving code in small steps. Acceptance: RPC registration remains clear; each RPC validates payload before mutation; error responses are consistent; module split does not break existing smoke tests.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P1-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Call split RPC with missing payload fields. Assert central validator catches it and returns `INVALID_ARGUMENT` standard error code.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P1-1">**Completed [PRD-P1-1]: Module Split & RPC Validation**
|
|
||||||
- **Goal:** Split monolith into auth, economy, admin, mail, social, leaderboard, validation helpers.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: tekton_admin.js</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P1-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P1-2</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Ledger, Idempotency & Storage Model</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
Wallet, inventory, fragments, mail rewards
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Add mutation audit ledger, idempotency keys, and canonical fragment storage path.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Define one canonical fragment storage location and migration path.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add idempotency keys for mail claim, daily reward, purchase, gacha, and admin adjustments.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add audit records with source, user_id, mutation type, request_id.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Make mail claim transactional (claim, mark, return canonical state).</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P1-2">Implement a canonical mutation ledger and idempotency policy for Tekton live-service rewards. Read server/nakama/tekton_admin.js, scripts/managers/user_profile_manager.gd, scripts/managers/mail_manager.gd, and gacha/profile storage code. Define one canonical fragment storage location and migration path. Add idempotency keys for mail claim, daily reward, purchase, gacha, and admin adjustments. Add audit records with source, user_id, mutation type, request_id, before/after summary, and timestamp. Make mail claim transactional: claim rewards, mark claimed, and return canonical updated state in one server response. Acceptance: repeated claim/purchase/reward requests do not duplicate grants; fragments read/write from one canonical path; mail UI refreshes from server-returned state.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P1-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Send identical mail claim RPC twice simultaneously. Assert only one processes successfully and the second returns 'already claimed'.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P1-2">**Completed [PRD-P1-2]: Ledger, Idempotency & Storage Model**
|
|
||||||
- **Goal:** Add mutation audit ledger, idempotency keys, and canonical fragment storage path.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: Wallet, inventory, fragments, mail rewards</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P1-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P1-3</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Client Backend Facade</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
nakama_manager.gd, auth_manager.gd, backend_service.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Make one typed backend owner for session, socket, RPC calls, and central errors.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Decide whether BackendService becomes the sole typed backend facade or is deleted.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Implement one owner for client/session/socket.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add typed methods for RPCs, central error handling.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove direct UI RPC scatter for economy/auth/mail/gacha/social flows.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P1-3">Clean up Tekton client backend ownership. Read scripts/nakama_manager.gd, scripts/managers/auth_manager.gd, scripts/services/backend_service.gd, and UI/manager scripts that call NakamaManager.client.rpc_async directly. Decide whether BackendService becomes the sole typed backend facade or is deleted. Implement chosen direction in small steps: one owner for client/session/socket, typed methods for RPCs, central error handling, and no direct UI RPC scatter for economy/auth/mail/gacha/social flows. Acceptance: UI calls typed service/manager methods, not raw client.rpc_async; session/socket ownership is clear; duplicate auth/bootstrap code is removed or delegated.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P1-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Global search for `client.rpc_async` in `scripts/ui/`. Assert 0 results found (all go through facade).</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P1-3">**Completed [PRD-P1-3]: Client Backend Facade**
|
|
||||||
- **Goal:** Make one typed backend owner for session, socket, RPC calls, and central errors.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: nakama_manager.gd, auth_manager.gd, backend_service.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P1-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P1-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Tutorial Isolation Contract</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
tutorial_manager.gd
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Remove multiplayer-side effects during pause/freeze phases. Isolate tutorial boundaries.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Keep onboarding sequence and camera storytelling.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Enforce contract: no persistent wallet/profile mutation during tutorial.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Ensure no shared lobby state leakage.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Ensure clean bot/timer restore on exit, deterministic return-to-lobby handshake.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Replace broad pause/freeze side effects with scoped tutorial-state toggles.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P1-1">Isolate tutorial runtime from multiplayer/session side effects. Review scripts/managers/tutorial_manager.gd and match lifecycle hooks. Keep onboarding sequence and camera storytelling, but enforce tutorial contract: no persistent wallet/profile mutation, no shared lobby state leakage, clean bot/timer restore on exit, deterministic return-to-lobby handshake. Replace broad pause/freeze side effects with scoped tutorial-state toggles where possible. Acceptance: exiting tutorial leaves no stale bot freeze, no leaked paused systems, and no corrupted room/session flags.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P1-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Abort tutorial midway. Assert main game tree is fully unpaused, bots are reset, and no 'tutorial_active' flags leak into lobby.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P1-1">**Completed [PRD-GF-P1-1]: Tutorial Isolation Contract**
|
|
||||||
- **Goal:** Remove multiplayer-side effects during pause/freeze phases. Isolate tutorial boundaries.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: tutorial_manager.gd</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P1-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P1-2</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Mode Config Completeness</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
main.gd, lobby mode configs
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Remove duplicated/inconsistent option toggles. Add schema-driven validation.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Keep existing Stop n Go custom UI.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove duplicated/fragile control toggles.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Implement Tekton Doors options with same host-authoritative lock and sync callbacks.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Introduce schema-driven config validation shared by host, client, and bootstrap.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P1-2">Complete mode-configuration parity between Stop n Go and Tekton Doors in lobby and match bootstrap flow. Keep existing Stop n Go custom UI, but remove duplicated/fragile control toggles and implement Tekton Doors options with same host-authoritative lock and sync callbacks. Introduce schema-driven config validation shared by host, client display logic, and match bootstrap. Acceptance: both modes expose full validated config; non-host clients always mirror host values; invalid config rejected before match start.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P1-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Join as client, attempt to spoof mode config RPC. Assert host rejects invalid mode config changes and overrides client.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P1-2">**Completed [PRD-GF-P1-2]: Mode Config Completeness**
|
|
||||||
- **Goal:** Remove duplicated/inconsistent option toggles. Add schema-driven validation.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: main.gd, lobby mode configs</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P1-2').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P1-3</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Backend Facade & Flow Decoupling</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
backend_service.gd, UI panels
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Improve service ownership and typed errors. Add one backend facade.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Identify remaining UI/manager scripts calling client.rpc_async.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Migrate calls to the central BackendService or unified manager.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Implement central error mapping and retry policy.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Verify all gameflow-adjacent UI uses new typed methods.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P1-3">Finish client backend decoupling for gameflow-adjacent live-service features. Read scripts/services/backend_service.gd plus UI/manager scripts that still call NakamaManager.client.rpc_async directly (profile, social, leaderboard, daily reward, mail, admin, friend flows). Decide whether BackendService becomes real facade or is removed. Implement one typed backend owner for auth/session/socket/RPC calls, central error mapping, and retry policy. Acceptance: gameflow-adjacent UI does not call raw client.rpc_async directly for production paths; backend ownership is obvious; future auth/RPC changes touch one service layer first, not many UI panels.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P1-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Trigger network failure during profile fetch. Assert BackendService retry policy handles it gracefully without UI hard-crashing.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P1-3">**Completed [PRD-GF-P1-3]: Backend Facade & Flow Decoupling**
|
|
||||||
- **Goal:** Improve service ownership and typed errors. Add one backend facade.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: backend_service.gd, UI panels</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P1-3').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-amber-500/30 bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-300 text-xs font-bold">P1</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P1-4</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Versioning & Patch Integrity</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
tools/, export_presets.cfg, version.json
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Single release version source, checksums, compatibility rules, changelog archive.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Create one release version source (version.json or python script).</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Update project version, export versions, Android version deterministically.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Update patch manifest and changelog archive.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add patch integrity fields: checksum, size, minimum compatible app version.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P1-4">Rebuild Tekton versioning workflow. Review tools/generate_version_json.py, tools/build_patch.gd, export_presets.cfg, project.godot, assets/data/version.json, README.md, and CHANGELOG_DRAFT.md. Create one release version source and update all platform metadata deterministically: project version, export versions, Android version/code, patch manifest, changelog archive, and Git tag instructions. Add patch integrity fields such as checksum, size, minimum compatible app version, and signature placeholder if signing is not available yet. Acceptance: one command or documented flow bumps release version; generated metadata matches across files; patch manifest can reject incompatible or corrupted patch.pck.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P1-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Run version bump script. Assert export_presets.cfg Android version code increments correctly and patch manifest checksum is updated.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P1-4">**Completed [PRD-P1-4]: Versioning & Patch Integrity**
|
|
||||||
- **Goal:** Single release version source, checksums, compatibility rules, changelog archive.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: tools/, export_presets.cfg, version.json</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P1-4').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-violet-500/30 bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-300 text-xs font-bold">P2</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-P2-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Steam Depot & Store Packaging</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
tools/steam/, export presets
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Create SteamPipe VDFs, branch SOP, signing/notarization, platform filters.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Create tools/steam/app_build_<STEAM_APP_ID>.vdf and per-platform depot templates.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Document steamcmd upload command, branch promotion path.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Add guidance for Windows signing, macOS notarization, Android package name.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Configure store-specific export filters.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-P2-1">Add Steam and storefront release packaging workflow for Tekton after P0/P1 backend gates are complete. Review export_presets.cfg, docs/STEAMWORKS_SETUP.md, README.md, and current build output conventions. Create tools/steam/app_build_<STEAM_APP_ID>.vdf and per-platform depot VDF templates using placeholders only. Document steamcmd upload command, branch promotion path internal -> beta -> default, and smoke tests required before promotion. Add guidance for Windows signing, macOS bundle/team/notarization, Android final package name/version code, and store-specific export filters so Steam libraries are not shipped in non-Steam builds. Acceptance: no real IDs/secrets committed; SteamPipe templates exist; release checklist blocks default branch promotion until smoke tests pass.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-P2-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Trigger dry-run of SteamPipe VDF. Assert paths resolve to output directory without committing real credentials.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-P2-1">**Completed [PRD-P2-1]: Steam Depot & Store Packaging**
|
|
||||||
- **Goal:** Create SteamPipe VDFs, branch SOP, signing/notarization, platform filters.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: tools/steam/, export presets</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-P2-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="group bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden" open>
|
|
||||||
<summary class="flex items-center justify-between p-4 cursor-pointer bg-slate-50 dark:bg-slate-900/50 hover:bg-slate-100 dark:hover:bg-slate-900 transition list-none outline-none">
|
|
||||||
<div class="flex items-center space-x-4">
|
|
||||||
<span class="px-2 py-1 rounded border border-violet-500/30 bg-violet-50 dark:bg-violet-500/10 text-violet-600 dark:text-violet-300 text-xs font-bold">P2</span>
|
|
||||||
<span class="font-mono text-xs text-slate-500 dark:text-slate-400">PRD-GF-P2-1</span>
|
|
||||||
<h3 class="text-md font-bold text-slate-900 dark:text-white">Dead Path, Debug Gate & Telemetry Cleanup</h3>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center space-x-3 text-slate-400">
|
|
||||||
<span class="text-xs font-semibold uppercase tracking-wider hidden md:inline-block border border-slate-200 dark:border-slate-700 px-2 py-1 rounded">TODO</span>
|
|
||||||
<i data-lucide="chevron-down" class="w-5 h-5 transition-transform group-open:rotate-180"></i>
|
|
||||||
</div>
|
|
||||||
</summary>
|
|
||||||
<div class="p-5 border-t border-slate-200 dark:border-slate-800 grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<!-- Left Column: Details & Checklists -->
|
|
||||||
<div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Files / Areas</h4>
|
|
||||||
<div class="text-xs font-mono text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 border border-sky-200 dark:border-sky-500/20 px-2 py-1 rounded-md inline-block">
|
|
||||||
main.gd, player.gd, placeholders
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Goal / Risk</h4>
|
|
||||||
<p class="text-sm text-slate-700 dark:text-slate-300">Remove release-noisy debug hooks. Add safe-remove candidate matrix + SLO dashboard.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">Execution Checklist</h4>
|
|
||||||
<ul class="space-y-2 text-sm text-slate-700 dark:text-slate-300">
|
|
||||||
<li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Build matrix: keep, safe-remove, needs-runtime-proof, feature-flag.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Remove or feature-gate release-only noise (e.g., debug key hooks, excessive prints).</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Instrument events: room_joined, preflight_pass, loading_screen, match_sync.</span></li><li class="flex items-start"><i data-lucide="square" class="w-4 h-4 mr-2 mt-0.5 text-slate-400 shrink-0"></i> <span>Do not delete autoload/runtime-loaded scripts without proof.</span></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right Column: AI Prompt & Testing -->
|
|
||||||
<div class="flex flex-col space-y-4">
|
|
||||||
<div class="bg-indigo-50 dark:bg-indigo-500/5 border border-indigo-200 dark:border-indigo-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400 mb-2 flex items-center"><i data-lucide="bot" class="w-4 h-4 mr-2"></i> AI Execution Prompt</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-mono h-32 overflow-y-auto" id="prompt-PRD-GF-P2-1">Create dead-path/debug-path cleanup and telemetry gates for lobby-to-match lifecycle. Review main.gd, player.gd, login_screen.gd, backend_service.gd, and other placeholders/debug hooks. Build matrix with columns: keep, safe-remove, needs-runtime-proof, feature-flag. Remove or feature-gate release-only noise such as debug key hooks and excessive prints, but do not delete autoload/runtime-loaded scripts without proof. Instrument events: room_joined, preflight_pass/fail, loading_screen_start/finish, match_sync_complete, reconnect_success/fail, match_end_summary. Acceptance: safe-remove candidates are evidence-backed; release export excludes debug-only hooks; branch promotion can check match-start and reconnect SLO metrics.</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('prompt-PRD-GF-P2-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Prompt">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/20 rounded-lg p-4 mb-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400 mb-2 flex items-center"><i data-lucide="test-tube" class="w-4 h-4 mr-2"></i> Testing / Auto-Check</h4>
|
|
||||||
<p class="text-xs text-slate-700 dark:text-slate-300 font-mono leading-relaxed">AI AUTO-CHECK: Search codebase for `Input.is_key_pressed(KEY_F9)`. Assert wrapped in `OS.has_feature("debug")` or completely removed.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-sky-50 dark:bg-sky-500/5 border border-sky-200 dark:border-sky-500/20 rounded-lg p-4">
|
|
||||||
<h4 class="text-xs font-semibold uppercase tracking-wider text-sky-600 dark:text-sky-400 mb-2 flex items-center"><i data-lucide="message-square" class="w-4 h-4 mr-2"></i> MS Teams Daily Report</h4>
|
|
||||||
<div class="relative group">
|
|
||||||
<pre class="text-xs text-slate-700 dark:text-slate-300 whitespace-pre-wrap font-sans" id="teams-PRD-GF-P2-1">**Completed [PRD-GF-P2-1]: Dead Path, Debug Gate & Telemetry Cleanup**
|
|
||||||
- **Goal:** Remove release-noisy debug hooks. Add safe-remove candidate matrix + SLO dashboard.
|
|
||||||
- **Status:** Integrated & verified. Code changes applied to: main.gd, player.gd, placeholders</pre>
|
|
||||||
<button onclick="navigator.clipboard.writeText(document.getElementById('teams-PRD-GF-P2-1').textContent)" class="absolute top-0 right-0 p-1.5 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-md shadow-sm text-slate-500 hover:text-sky-500 opacity-0 group-hover:opacity-100 transition" title="Copy Teams Report">
|
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950 py-10 mt-20 text-center">
|
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
|
||||||
Blueprint released under MIT authorization. Secure server implementations are responsibility of the
|
|
||||||
deployment architecture team.
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- Interactive Script Operations -->
|
|
||||||
<script>
|
|
||||||
// Init Lucide Icons
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
// Highlight flowchart pipelines
|
|
||||||
function highlightPath(target) {
|
|
||||||
// Remove selection formatting from all controls
|
|
||||||
document.querySelectorAll('.path-ctrl-btn').forEach(btn => {
|
|
||||||
btn.classList.remove('bg-sky-500', 'text-slate-950', 'shadow-md');
|
|
||||||
btn.classList.add('text-slate-600 dark:text-slate-400', 'hover:text-slate-900 dark:text-white');
|
|
||||||
});
|
|
||||||
// Assign selection styles to current target control
|
|
||||||
const activeBtn = document.getElementById('btn-path-' + target);
|
|
||||||
if (activeBtn) {
|
|
||||||
activeBtn.classList.add('bg-sky-500', 'text-slate-950', 'shadow-md');
|
|
||||||
activeBtn.classList.remove('text-slate-600 dark:text-slate-400', 'hover:text-slate-900 dark:text-white');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Target elements to manipulate
|
|
||||||
const branches = {
|
|
||||||
steam: document.getElementById('branch-steam'),
|
|
||||||
android: document.getElementById('branch-android'),
|
|
||||||
ios: document.getElementById('branch-ios'),
|
|
||||||
fallback: document.getElementById('branch-fallback')
|
|
||||||
};
|
|
||||||
|
|
||||||
const root = document.getElementById('node-root');
|
|
||||||
const terminal = document.getElementById('node-terminal');
|
|
||||||
const lineRoot = document.getElementById('line-root');
|
|
||||||
const lineTerminal = document.getElementById('line-terminal');
|
|
||||||
|
|
||||||
// Apply opacity filters based on selection
|
|
||||||
if (target === 'all') {
|
|
||||||
Object.values(branches).forEach(b => {
|
|
||||||
b.classList.remove('opacity-25', 'border-sky-500/50');
|
|
||||||
b.classList.add('opacity-100');
|
|
||||||
});
|
|
||||||
root.classList.remove('opacity-50');
|
|
||||||
terminal.classList.remove('opacity-50');
|
|
||||||
lineRoot.classList.remove('bg-slate-200 dark:bg-slate-800/20');
|
|
||||||
lineTerminal.classList.remove('bg-slate-200 dark:bg-slate-800/20');
|
|
||||||
} else {
|
|
||||||
root.classList.remove('opacity-50');
|
|
||||||
terminal.classList.remove('opacity-50');
|
|
||||||
lineRoot.classList.remove('bg-slate-200 dark:bg-slate-800/20');
|
|
||||||
lineTerminal.classList.remove('bg-slate-200 dark:bg-slate-800/20');
|
|
||||||
|
|
||||||
Object.keys(branches).forEach(key => {
|
|
||||||
if (key === target) {
|
|
||||||
branches[key].classList.remove('opacity-25');
|
|
||||||
branches[key].classList.add('opacity-100', 'border-sky-500/50');
|
|
||||||
} else {
|
|
||||||
branches[key].classList.add('opacity-25');
|
|
||||||
branches[key].classList.remove('opacity-100', 'border-sky-500/50');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch Store Tabs UI
|
|
||||||
function switchTab(platform) {
|
|
||||||
// Hide all contents
|
|
||||||
document.querySelectorAll('.tab-content').forEach(content => {
|
|
||||||
content.classList.add('hidden');
|
|
||||||
});
|
|
||||||
// Show target
|
|
||||||
document.getElementById('content-' + platform).classList.remove('hidden');
|
|
||||||
|
|
||||||
// Deactivate all buttons
|
|
||||||
document.querySelectorAll('#store-tabs button').forEach(btn => {
|
|
||||||
btn.classList.remove('bg-slate-200 dark:bg-slate-800', 'text-slate-900 dark:text-white');
|
|
||||||
btn.classList.add('text-slate-600 dark:text-slate-400');
|
|
||||||
});
|
|
||||||
// Activate target button
|
|
||||||
const activeBtn = document.getElementById('tab-' + platform);
|
|
||||||
activeBtn.classList.remove('text-slate-600 dark:text-slate-400');
|
|
||||||
activeBtn.classList.add('bg-slate-200 dark:bg-slate-800', 'text-slate-900 dark:text-white');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Init Lucide Icons
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
// Theme logic
|
|
||||||
function toggleTheme() {
|
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init theme
|
|
||||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
||||||
if (localStorage.theme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
+24
-17
@@ -1,14 +1,20 @@
|
|||||||
|
[runnable_presets]
|
||||||
|
|
||||||
|
"Windows Desktop"="Windows Desktop"
|
||||||
|
Android="Android"
|
||||||
|
macOS="macOS"
|
||||||
|
Linux="Linux/X11"
|
||||||
|
|
||||||
[preset.0]
|
[preset.0]
|
||||||
|
|
||||||
name="Windows Desktop"
|
name="Windows Desktop"
|
||||||
platform="Windows Desktop"
|
platform="Windows Desktop"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton_armageddon_v2.4.1.exe"
|
export_path="build/windows/tekton_armageddon_v2.4.3.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -42,8 +48,8 @@ application/modify_resources=false
|
|||||||
application/icon=""
|
application/icon=""
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/file_version="2.4.1"
|
application/file_version="2.4.2"
|
||||||
application/product_version="2.4.1"
|
application/product_version="2.4.2"
|
||||||
application/company_name="DanchieGo"
|
application/company_name="DanchieGo"
|
||||||
application/product_name="Tekton Armageddon"
|
application/product_name="Tekton Armageddon"
|
||||||
application/file_description=""
|
application/file_description=""
|
||||||
@@ -74,13 +80,12 @@ Remove-Item -Recurse -Force '{temp_dir}'"
|
|||||||
|
|
||||||
name="Android"
|
name="Android"
|
||||||
platform="Android"
|
platform="Android"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton-dash-armageddon-v.2.4.1.apk"
|
export_path="build/tekton-dash-armageddon-v.2.4.3.apk"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -111,7 +116,7 @@ architectures/arm64-v8a=true
|
|||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=3
|
version/code=3
|
||||||
version/name="2.4.1"
|
version/name="2.4.2"
|
||||||
package/unique_name="com.danchiego.$genname"
|
package/unique_name="com.danchiego.$genname"
|
||||||
package/name="Tekton Dash Armageddon"
|
package/name="Tekton Dash Armageddon"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
@@ -136,11 +141,12 @@ screen/support_normal=true
|
|||||||
screen/support_large=true
|
screen/support_large=true
|
||||||
screen/support_xlarge=true
|
screen/support_xlarge=true
|
||||||
screen/background_color=Color(0, 0, 0, 1)
|
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
|
user_data_backup/allow=false
|
||||||
command_line/extra_args=""
|
command_line/extra_args=""
|
||||||
apk_expansion/enable=false
|
|
||||||
apk_expansion/SALT=""
|
|
||||||
apk_expansion/public_key=""
|
|
||||||
permissions/custom_permissions=PackedStringArray()
|
permissions/custom_permissions=PackedStringArray()
|
||||||
permissions/access_checkin_properties=false
|
permissions/access_checkin_properties=false
|
||||||
permissions/access_coarse_location=false
|
permissions/access_coarse_location=false
|
||||||
@@ -295,18 +301,20 @@ permissions/write_sms=false
|
|||||||
permissions/write_social_stream=false
|
permissions/write_social_stream=false
|
||||||
permissions/write_sync_settings=false
|
permissions/write_sync_settings=false
|
||||||
permissions/write_user_dictionary=false
|
permissions/write_user_dictionary=false
|
||||||
|
apk_expansion/enable=false
|
||||||
|
apk_expansion/SALT=""
|
||||||
|
apk_expansion/public_key=""
|
||||||
|
|
||||||
[preset.2]
|
[preset.2]
|
||||||
|
|
||||||
name="macOS"
|
name="macOS"
|
||||||
platform="macOS"
|
platform="macOS"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton_armageddon_v2.4.1.zip"
|
export_path="build/tekton_armageddon_v2.4.3.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -330,7 +338,7 @@ debug/export_console_wrapper=0
|
|||||||
application/liquid_glass_icon=""
|
application/liquid_glass_icon=""
|
||||||
application/icon=""
|
application/icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/bundle_identifier=""
|
application/bundle_identifier="com.danchiego.tektonarmageddon"
|
||||||
application/signature=""
|
application/signature=""
|
||||||
application/app_category="Games"
|
application/app_category="Games"
|
||||||
application/short_version=""
|
application/short_version=""
|
||||||
@@ -565,8 +573,8 @@ codesign/digest_algorithm=1
|
|||||||
codesign/identity_type=0
|
codesign/identity_type=0
|
||||||
application/modify_resources=false
|
application/modify_resources=false
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/file_version="2.4.1"
|
application/file_version="2.4.2"
|
||||||
application/product_version="2.4.1"
|
application/product_version="2.4.2"
|
||||||
application/company_name="DanchieGo"
|
application/company_name="DanchieGo"
|
||||||
application/product_name="Tekton Armageddon"
|
application/product_name="Tekton Armageddon"
|
||||||
application/file_description=""
|
application/file_description=""
|
||||||
@@ -576,13 +584,12 @@ application/trademarks=""
|
|||||||
|
|
||||||
name="Linux/X11"
|
name="Linux/X11"
|
||||||
platform="Linux"
|
platform="Linux"
|
||||||
runnable=true
|
|
||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton_armageddon_v2.4.1.x86_64"
|
export_path="build/linux/tekton_armageddon_v2.4.3.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
|
|||||||
@@ -1,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)
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import json
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
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."
|
|
||||||
]
|
|
||||||
|
|
||||||
new_release = {
|
|
||||||
"version": "2.4.1",
|
|
||||||
"date": "2026-06-28",
|
|
||||||
"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"]
|
|
||||||
|
|
||||||
# Insert at top
|
|
||||||
data["releases"].insert(0, new_release)
|
|
||||||
data["latest_version"] = "2.4.1"
|
|
||||||
|
|
||||||
with open("assets/data/version.json", "w", encoding="utf-8") as f:
|
|
||||||
json.dump(data, f, indent="\t")
|
|
||||||
f.write("\n")
|
|
||||||
+7
-10
@@ -15,9 +15,9 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Tekton Dash Armageddon"
|
config/name="Tekton Dash Armageddon"
|
||||||
config/version="2.4.1"
|
config/version="2.4.3"
|
||||||
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
|
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
|
||||||
boot_splash/stretch_mode=0
|
boot_splash/stretch_mode=0
|
||||||
boot_splash/image="uid://b10e6kr508642"
|
boot_splash/image="uid://b10e6kr508642"
|
||||||
@@ -38,7 +38,7 @@ GoalManager="*res://scripts/managers/goal_manager.gd"
|
|||||||
PlayerManager="*res://scripts/managers/player_manager.gd"
|
PlayerManager="*res://scripts/managers/player_manager.gd"
|
||||||
GoalsCycleManager="*res://scripts/managers/goals_cycle_manager.gd"
|
GoalsCycleManager="*res://scripts/managers/goals_cycle_manager.gd"
|
||||||
Satori="*uid://b8vev00s34b7"
|
Satori="*uid://b8vev00s34b7"
|
||||||
SettingsManager="*uid://c1ouaaqnn0lrc"
|
SettingsManager="*res://scripts/managers/settings_manager.gd"
|
||||||
SfxManager="*res://scripts/managers/sfx_manager.gd"
|
SfxManager="*res://scripts/managers/sfx_manager.gd"
|
||||||
NameGenerator="*res://scripts/generators/name_generator.gd"
|
NameGenerator="*res://scripts/generators/name_generator.gd"
|
||||||
MusicManager="*res://scripts/managers/music_manager.gd"
|
MusicManager="*res://scripts/managers/music_manager.gd"
|
||||||
@@ -67,8 +67,11 @@ enabled=PackedStringArray("res://addons/com.heroiclabs.nakama/plugin.cfg", "res:
|
|||||||
[file_customization]
|
[file_customization]
|
||||||
|
|
||||||
folder_colors={
|
folder_colors={
|
||||||
|
"res://addons/": "pink",
|
||||||
"res://assets/": "purple",
|
"res://assets/": "purple",
|
||||||
"res://scenes/": "green"
|
"res://scenes/": "green",
|
||||||
|
"res://scripts/": "green",
|
||||||
|
"res://server/": "blue"
|
||||||
}
|
}
|
||||||
|
|
||||||
[input]
|
[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)
|
, 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={
|
action_grab_tekton={
|
||||||
"deadzone": 0.5,
|
"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)
|
"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")
|
theme_override_fonts/font = ExtResource("1_font")
|
||||||
text = "🍬 OPEN ARENA"
|
text = "🍬 OPEN ARENA"
|
||||||
horizontal_alignment = 1
|
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
-6
@@ -86,12 +86,7 @@ var sng_go_option: OptionButton
|
|||||||
var sng_stop_option: OptionButton
|
var sng_stop_option: OptionButton
|
||||||
var sng_goals_option: OptionButton
|
var sng_goals_option: OptionButton
|
||||||
|
|
||||||
var doors_settings_container: HBoxContainer
|
# Gauntlet settings
|
||||||
var doors_swap_option: OptionButton
|
|
||||||
var doors_refresh_option: OptionButton
|
|
||||||
var doors_goals_option: OptionButton
|
|
||||||
|
|
||||||
# UI References - Player Slots
|
|
||||||
@onready var players_container = $LobbyPanel/PlayersContainer
|
@onready var players_container = $LobbyPanel/PlayersContainer
|
||||||
@onready var players_container2 = $LobbyPanel/PlayersContainer2
|
@onready var players_container2 = $LobbyPanel/PlayersContainer2
|
||||||
@onready var player_slots: Array[Control] = []
|
@onready var player_slots: Array[Control] = []
|
||||||
|
|||||||
+27
-183
@@ -12,10 +12,8 @@ var touch_controls
|
|||||||
var camera_context_manager
|
var camera_context_manager
|
||||||
var stop_n_go_manager
|
var stop_n_go_manager
|
||||||
var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode
|
var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode
|
||||||
var portal_mode_winner_id: int = -1
|
|
||||||
var is_match_ended: bool = false
|
var is_match_ended: bool = false
|
||||||
var obstacle_manager
|
var obstacle_manager
|
||||||
var portal_mode_manager
|
|
||||||
var gauntlet_manager
|
var gauntlet_manager
|
||||||
var vfx_manager
|
var vfx_manager
|
||||||
|
|
||||||
@@ -146,8 +144,6 @@ func _apply_arena_background():
|
|||||||
texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" # Fallback texture
|
texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" # Fallback texture
|
||||||
_instantiate_3d_arena("res://scenes/arena/freemode.tscn")
|
_instantiate_3d_arena("res://scenes/arena/freemode.tscn")
|
||||||
_hide_ground_tiles()
|
_hide_ground_tiles()
|
||||||
"Tekton Doors Arena":
|
|
||||||
texture_path = "res://assets/graphics/level_bg/placeholder_tekton_doors.jpg"
|
|
||||||
"Gauntlet Arena":
|
"Gauntlet Arena":
|
||||||
texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg"
|
texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg"
|
||||||
_instantiate_3d_arena("res://scenes/arena/gauntlet.tscn")
|
_instantiate_3d_arena("res://scenes/arena/gauntlet.tscn")
|
||||||
@@ -207,14 +203,6 @@ func _setup_effect_elevation():
|
|||||||
em.mesh_library = ml
|
em.mesh_library = ml
|
||||||
print("[Main] MeshLibrary elevation applied: Wall(4) and Freeze(5) at Y=0.8")
|
print("[Main] MeshLibrary elevation applied: Wall(4) and Freeze(5) at Y=0.8")
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
|
||||||
func sync_portal_configs(configs: Array):
|
|
||||||
if portal_mode_manager:
|
|
||||||
# Temporarily store the configs and trigger spawn
|
|
||||||
# Note: We use a custom property in manager to pass this
|
|
||||||
portal_mode_manager.set_meta("door_configs", configs)
|
|
||||||
portal_mode_manager._spawn_portal_doors()
|
|
||||||
|
|
||||||
# Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors
|
# Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors
|
||||||
var em = $EnhancedGridMap
|
var em = $EnhancedGridMap
|
||||||
if em:
|
if em:
|
||||||
@@ -245,12 +233,6 @@ func _init_managers():
|
|||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
# No direct initialize() yet, but we'll call start_game_mode later
|
# No direct initialize() yet, but we'll call start_game_mode later
|
||||||
|
|
||||||
# Portal manager for Tekton Doors mode
|
|
||||||
if LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
|
|
||||||
portal_mode_manager.name = "PortalModeManager"
|
|
||||||
add_child(portal_mode_manager)
|
|
||||||
portal_mode_manager.initialize(self , $EnhancedGridMap)
|
|
||||||
|
|
||||||
# Gauntlet manager for Candy Pump Survival mode
|
# Gauntlet manager for Candy Pump Survival mode
|
||||||
if LobbyManager.game_mode == "Candy Pump Survival":
|
if LobbyManager.game_mode == "Candy Pump Survival":
|
||||||
@@ -619,8 +601,6 @@ func _setup_host_game():
|
|||||||
# Spawning and arena setup
|
# Spawning and arena setup
|
||||||
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
||||||
stop_n_go_manager._setup_arena()
|
stop_n_go_manager._setup_arena()
|
||||||
elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager:
|
|
||||||
portal_mode_manager.setup_arena_locally()
|
|
||||||
elif LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager:
|
elif LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager:
|
||||||
gauntlet_manager._setup_arena()
|
gauntlet_manager._setup_arena()
|
||||||
else:
|
else:
|
||||||
@@ -725,10 +705,6 @@ func _setup_client_game():
|
|||||||
add_player_character(i, true)
|
add_player_character(i, true)
|
||||||
print("Client: Pre-spawned potential bot ", i)
|
print("Client: Pre-spawned potential bot ", i)
|
||||||
|
|
||||||
# Initialize arena locally for Tekton Doors
|
|
||||||
if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager:
|
|
||||||
portal_mode_manager.setup_arena_locally()
|
|
||||||
|
|
||||||
# Initialize arena locally for Candy Pump Survival
|
# Initialize arena locally for Candy Pump Survival
|
||||||
if LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager:
|
if LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager:
|
||||||
gauntlet_manager._apply_arena_setup()
|
gauntlet_manager._apply_arena_setup()
|
||||||
@@ -833,14 +809,10 @@ func _start_game():
|
|||||||
gauntlet_manager.setup_mission_tiles()
|
gauntlet_manager.setup_mission_tiles()
|
||||||
|
|
||||||
# Spawn Static Tektons and random tiles BEFORE countdown (Free Mode Only)
|
# Spawn Static Tektons and random tiles BEFORE countdown (Free Mode Only)
|
||||||
# Exclude for Stop n Go and Tekton Doors
|
# Exclude for Stop n Go and Candy Pump Survival
|
||||||
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors" and LobbyManager.game_mode != "Candy Pump Survival":
|
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Candy Pump Survival":
|
||||||
spawn_static_tektons()
|
spawn_static_tektons()
|
||||||
|
|
||||||
# Tekton Doors: Randomize connections BEFORE countdown so colors show
|
|
||||||
if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager:
|
|
||||||
portal_mode_manager._randomize_connections()
|
|
||||||
|
|
||||||
# STOP N GO: Rotate players to face East BEFORE countdown
|
# STOP N GO: Rotate players to face East BEFORE countdown
|
||||||
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager:
|
||||||
stop_n_go_manager.rotate_players_to_start()
|
stop_n_go_manager.rotate_players_to_start()
|
||||||
@@ -866,13 +838,6 @@ func _start_game():
|
|||||||
if goals_cycle_manager:
|
if goals_cycle_manager:
|
||||||
var match_duration = LobbyManager.get_match_duration()
|
var match_duration = LobbyManager.get_match_duration()
|
||||||
goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go
|
goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go
|
||||||
elif LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
if portal_mode_manager:
|
|
||||||
portal_mode_manager.start_game_mode()
|
|
||||||
|
|
||||||
if goals_cycle_manager:
|
|
||||||
var match_duration = LobbyManager.get_match_duration()
|
|
||||||
goals_cycle_manager.start_match(float(match_duration))
|
|
||||||
elif LobbyManager.game_mode == "Candy Pump Survival":
|
elif LobbyManager.game_mode == "Candy Pump Survival":
|
||||||
if gauntlet_manager:
|
if gauntlet_manager:
|
||||||
gauntlet_manager.start_game_mode()
|
gauntlet_manager.start_game_mode()
|
||||||
@@ -986,12 +951,6 @@ func _assign_random_spawn_positions():
|
|||||||
_assign_stop_n_go_spawn_positions(all_players)
|
_assign_stop_n_go_spawn_positions(all_players)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Tekton Doors Custom Spawn Logic
|
|
||||||
if LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
var all_players = get_tree().get_nodes_in_group("Players")
|
|
||||||
_assign_portal_mode_spawn_positions(all_players)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If static positions were not calculated yet, do it now to avoid players spawning in them
|
# If static positions were not calculated yet, do it now to avoid players spawning in them
|
||||||
if reserved_static_positions.is_empty() and LobbyManager.game_mode != "Stop n Go":
|
if reserved_static_positions.is_empty() and LobbyManager.game_mode != "Stop n Go":
|
||||||
if not static_tekton_manager:
|
if not static_tekton_manager:
|
||||||
@@ -1116,77 +1075,6 @@ func _assign_stop_n_go_spawn_positions(all_players: Array):
|
|||||||
|
|
||||||
print("[StopNGo] Assigned spawn %s to player %s" % [assigned_pos, player.name])
|
print("[StopNGo] Assigned spawn %s to player %s" % [assigned_pos, player.name])
|
||||||
|
|
||||||
func _assign_portal_mode_spawn_positions(all_players: Array):
|
|
||||||
"""Assigns spawns to different quadrants for Tekton Doors mode, avoiding stands and intersections."""
|
|
||||||
if not portal_mode_manager:
|
|
||||||
_assign_random_spawn_positions() # Fallback
|
|
||||||
return
|
|
||||||
|
|
||||||
# Sort players for deterministic assignment
|
|
||||||
all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int())
|
|
||||||
|
|
||||||
# Get baseline quadrant centers (3,3), (10,3), etc.
|
|
||||||
var base_spawn_points = portal_mode_manager.get_spawn_points()
|
|
||||||
var spawn_index = 0
|
|
||||||
var assigned_positions: Array[Vector2i] = []
|
|
||||||
|
|
||||||
for player in all_players:
|
|
||||||
var center_pos = base_spawn_points[spawn_index % base_spawn_points.size()]
|
|
||||||
var assigned_pos = center_pos # Fallback position
|
|
||||||
|
|
||||||
# Spiral search for a valid spot (walkable, not in stand zone, not occupied)
|
|
||||||
var found = false
|
|
||||||
for radius in range(0, 5): # Increase search radius
|
|
||||||
for dx in range(-radius, radius + 1):
|
|
||||||
for dz in range(-radius, radius + 1):
|
|
||||||
# Only check the "ring" at the current radius
|
|
||||||
if abs(dx) != radius and abs(dz) != radius and radius > 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
var test_pos = center_pos + Vector2i(dx, dz)
|
|
||||||
|
|
||||||
# 1. Check map bounds
|
|
||||||
var em = $EnhancedGridMap
|
|
||||||
if not em or test_pos.x < 0 or test_pos.x >= em.columns or test_pos.y < 0 or test_pos.y >= em.rows:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Check if walkable floor (Floor 0, ID 0)
|
|
||||||
if em.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) != 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 3. Check if reserved for a Static Tekton Stand (3x3 area, use 2-tile buffer)
|
|
||||||
var is_reserved = false
|
|
||||||
for reserved in reserved_static_positions:
|
|
||||||
if abs(test_pos.x - reserved.x) <= 2 and abs(test_pos.y - reserved.y) <= 2:
|
|
||||||
is_reserved = true
|
|
||||||
break
|
|
||||||
if is_reserved:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 4. Check if occupied by another already-assigned player
|
|
||||||
if assigned_positions.has(test_pos):
|
|
||||||
continue
|
|
||||||
|
|
||||||
assigned_pos = test_pos
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
if found: break
|
|
||||||
if found: break
|
|
||||||
|
|
||||||
assigned_positions.append(assigned_pos)
|
|
||||||
|
|
||||||
# Sync and place
|
|
||||||
player.position = player.grid_to_world(assigned_pos)
|
|
||||||
player.current_position = assigned_pos
|
|
||||||
player.is_player_moving = false
|
|
||||||
player.spawn_point_selected = true
|
|
||||||
|
|
||||||
if can_rpc():
|
|
||||||
player.rpc("set_spawn_position", assigned_pos)
|
|
||||||
|
|
||||||
spawn_index += 1
|
|
||||||
print("[PortalMode] Assigned Quadrant Pos %s to player %s" % [assigned_pos, player.name])
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Tekton NPC Management
|
# Tekton NPC Management
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -1639,9 +1527,6 @@ func sync_game_start(player_list: Array, is_turn_based: bool):
|
|||||||
stop_n_go_manager.name = "StopNGoManager"
|
stop_n_go_manager.name = "StopNGoManager"
|
||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
stop_n_go_manager.activate_client_side()
|
stop_n_go_manager.activate_client_side()
|
||||||
elif LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
if portal_mode_manager:
|
|
||||||
portal_mode_manager.activate_client_side()
|
|
||||||
|
|
||||||
# Initialize leaderboard for all peers (after a delay to ensure players loaded)
|
# Initialize leaderboard for all peers (after a delay to ensure players loaded)
|
||||||
call_deferred("_deferred_init_leaderboard")
|
call_deferred("_deferred_init_leaderboard")
|
||||||
@@ -1686,11 +1571,22 @@ func _deferred_set_player_goals(player_id: int, goals: Array):
|
|||||||
ui_manager.update_playerboard_ui()
|
ui_manager.update_playerboard_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_playerboard(player_id: int) -> Node:
|
||||||
|
var player = get_node_or_null(str(player_id))
|
||||||
|
var waited := 0.0
|
||||||
|
while (not player or player.playerboard.size() < 25) and waited < 2.0:
|
||||||
|
await get_tree().create_timer(0.1).timeout
|
||||||
|
waited += 0.1
|
||||||
|
player = get_node_or_null(str(player_id))
|
||||||
|
return player if player and player.playerboard.size() >= 25 else null
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func sync_playerboard(player_id: int, new_playerboard: Array):
|
func sync_playerboard(player_id: int, new_playerboard: Array):
|
||||||
# Find the player and update their playerboard
|
var player = await _wait_for_playerboard(player_id)
|
||||||
var player = get_node_or_null(str(player_id))
|
if not player:
|
||||||
if player:
|
push_warning("[sync_playerboard] Dropped update: player %s not ready" % player_id)
|
||||||
|
return
|
||||||
|
|
||||||
player.playerboard = new_playerboard.duplicate()
|
player.playerboard = new_playerboard.duplicate()
|
||||||
|
|
||||||
# Update UI for local player
|
# Update UI for local player
|
||||||
@@ -1702,9 +1598,16 @@ func sync_playerboard_slot(player_id: int, slot_index: int, item_id: int):
|
|||||||
"""Patch a single playerboard slot without touching other slots.
|
"""Patch a single playerboard slot without touching other slots.
|
||||||
Used by _execute_grab on grab confirmation to avoid overwriting concurrent
|
Used by _execute_grab on grab confirmation to avoid overwriting concurrent
|
||||||
in-flight optimistic grab updates on high-latency clients."""
|
in-flight optimistic grab updates on high-latency clients."""
|
||||||
var player = get_node_or_null(str(player_id))
|
var player = await _wait_for_playerboard(player_id)
|
||||||
if player and slot_index >= 0 and slot_index < player.playerboard.size():
|
if not player:
|
||||||
|
push_warning("[sync_playerboard_slot] Dropped update: player %s not ready" % player_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if slot_index >= 0 and slot_index < player.playerboard.size():
|
||||||
player.playerboard[slot_index] = item_id
|
player.playerboard[slot_index] = item_id
|
||||||
|
else:
|
||||||
|
push_warning("[sync_playerboard_slot] Ignored invalid slot %s for player %s" % [slot_index, player_id])
|
||||||
|
return
|
||||||
|
|
||||||
# Update UI for local player only
|
# Update UI for local player only
|
||||||
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
|
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
|
||||||
@@ -1860,7 +1763,7 @@ func randomize_item_at_position(grid_position: Vector2i):
|
|||||||
|
|
||||||
if is_ground:
|
if is_ground:
|
||||||
var get_mode_specific_tile = func():
|
var get_mode_specific_tile = func():
|
||||||
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors" and LobbyManager.game_mode != "Candy Pump Survival":
|
if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Candy Pump Survival":
|
||||||
# 60% Chance for Common (7-10), 40% for PowerUp
|
# 60% Chance for Common (7-10), 40% for PowerUp
|
||||||
if randf() <= 0.6:
|
if randf() <= 0.6:
|
||||||
return [7, 8, 9, 10].pick_random()
|
return [7, 8, 9, 10].pick_random()
|
||||||
@@ -1906,13 +1809,6 @@ func sync_grid_item(x: int, y: int, z: int, item: int):
|
|||||||
if f0 in [4, -1] or f1 == 4:
|
if f0 in [4, -1] or f1 == 4:
|
||||||
return
|
return
|
||||||
|
|
||||||
# TEKTON DOORS: Prevent placing items on portal doors
|
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
||||||
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
|
||||||
for door in doors:
|
|
||||||
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
|
||||||
if door_grid.x == x and door_grid.z == z:
|
|
||||||
return
|
|
||||||
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
||||||
# Force visual update
|
# Force visual update
|
||||||
@@ -2080,17 +1976,6 @@ func sync_grid_items_batch(data: Array):
|
|||||||
if f0 in [4, -1] or f1 == 4:
|
if f0 in [4, -1] or f1 == 4:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# TEKTON DOORS: Prevent placing items on portal doors
|
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS) and y == 1:
|
|
||||||
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
|
||||||
var on_door = false
|
|
||||||
for door in doors:
|
|
||||||
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
|
||||||
if door_grid.x == x and door_grid.z == z:
|
|
||||||
on_door = true
|
|
||||||
break
|
|
||||||
if on_door: continue
|
|
||||||
|
|
||||||
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
|
||||||
|
|
||||||
# Force visual update ONCE after batch
|
# Force visual update ONCE after batch
|
||||||
@@ -2098,7 +1983,7 @@ func sync_grid_items_batch(data: Array):
|
|||||||
enhanced_gridmap.update_grid_data()
|
enhanced_gridmap.update_grid_data()
|
||||||
|
|
||||||
func randomize_game_grid():
|
func randomize_game_grid():
|
||||||
if LobbyManager.game_mode == "Stop n Go" or LobbyManager.game_mode == "Tekton Doors":
|
if LobbyManager.game_mode == "Stop n Go":
|
||||||
return # These modes manage their own arena setup and item spawning
|
return # These modes manage their own arena setup and item spawning
|
||||||
|
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
@@ -2160,10 +2045,6 @@ func request_full_grid_sync():
|
|||||||
rpc_id(sender_id, "sync_full_grid_data", grid_data)
|
rpc_id(sender_id, "sync_full_grid_data", grid_data)
|
||||||
print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id)
|
print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id)
|
||||||
|
|
||||||
# If Tekton Doors, sync portal connections too
|
|
||||||
if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager:
|
|
||||||
portal_mode_manager.sync_to_client(sender_id)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_full_grid_data(data: PackedInt32Array):
|
func sync_full_grid_data(data: PackedInt32Array):
|
||||||
print("[Main] sync_full_grid_data received. Items: %d" % (data.size() / 3))
|
print("[Main] sync_full_grid_data received. Items: %d" % (data.size() / 3))
|
||||||
@@ -2179,13 +2060,6 @@ func sync_full_grid_data(data: PackedInt32Array):
|
|||||||
stop_n_go_manager.name = "StopNGoManager"
|
stop_n_go_manager.name = "StopNGoManager"
|
||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
stop_n_go_manager._apply_arena_setup()
|
stop_n_go_manager._apply_arena_setup()
|
||||||
elif LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
if not portal_mode_manager:
|
|
||||||
portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new()
|
|
||||||
portal_mode_manager.name = "PortalModeManager"
|
|
||||||
add_child(portal_mode_manager)
|
|
||||||
portal_mode_manager.initialize(self , enhanced_gridmap)
|
|
||||||
portal_mode_manager.setup_arena_locally()
|
|
||||||
else:
|
else:
|
||||||
# Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes)
|
# Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes)
|
||||||
for x in range(enhanced_gridmap.columns):
|
for x in range(enhanced_gridmap.columns):
|
||||||
@@ -2319,27 +2193,6 @@ func sync_game_end_stop_n_go(winner_id: int):
|
|||||||
# Trigger match end
|
# Trigger match end
|
||||||
_on_match_ended()
|
_on_match_ended()
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
|
||||||
func sync_game_end_portal_mode(winner_id: int):
|
|
||||||
print("[TEKTON DOORS] Game ended! Winner: ", winner_id)
|
|
||||||
portal_mode_winner_id = winner_id
|
|
||||||
|
|
||||||
var winner_name = "Player " + str(winner_id)
|
|
||||||
var player_node = get_node_or_null(str(winner_id))
|
|
||||||
if player_node:
|
|
||||||
winner_name = player_node.display_name
|
|
||||||
|
|
||||||
# Broadcast win
|
|
||||||
add_message_to_bar("MATCH COMPLETE", winner_name + " Wins with 8 Missions!", MessageType.GOAL)
|
|
||||||
|
|
||||||
# Stop logic
|
|
||||||
if portal_mode_manager:
|
|
||||||
if portal_mode_manager.swap_timer: portal_mode_manager.swap_timer.stop()
|
|
||||||
if portal_mode_manager.tile_refresh_timer: portal_mode_manager.tile_refresh_timer.stop()
|
|
||||||
|
|
||||||
# Trigger match end
|
|
||||||
_on_match_ended()
|
|
||||||
|
|
||||||
func _on_match_ended():
|
func _on_match_ended():
|
||||||
"""Called when the global match timer ends - show game over screen."""
|
"""Called when the global match timer ends - show game over screen."""
|
||||||
if is_match_ended:
|
if is_match_ended:
|
||||||
@@ -2404,9 +2257,6 @@ func _show_game_over_panel():
|
|||||||
if stop_n_go_manager and stop_n_go_manager.hud_layer:
|
if stop_n_go_manager and stop_n_go_manager.hud_layer:
|
||||||
stop_n_go_manager.hud_layer.hide()
|
stop_n_go_manager.hud_layer.hide()
|
||||||
|
|
||||||
if portal_mode_manager and portal_mode_manager.hud_layer:
|
|
||||||
portal_mode_manager.hud_layer.hide()
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Gather + sort player data
|
# Gather + sort player data
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -2428,12 +2278,6 @@ func _show_game_over_panel():
|
|||||||
if b.peer_id == stop_n_go_winner_id: return false
|
if b.peer_id == stop_n_go_winner_id: return false
|
||||||
return a.score > b.score
|
return a.score > b.score
|
||||||
)
|
)
|
||||||
elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_winner_id != -1:
|
|
||||||
all_player_scores.sort_custom(func(a, b):
|
|
||||||
if a.peer_id == portal_mode_winner_id: return true
|
|
||||||
if b.peer_id == portal_mode_winner_id: return false
|
|
||||||
return a.score > b.score
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
all_player_scores.sort_custom(func(a, b): return a.score > b.score)
|
all_player_scores.sort_custom(func(a, b): return a.score > b.score)
|
||||||
|
|
||||||
|
|||||||
@@ -2249,25 +2249,6 @@ text = "[b]Stop n Go[/b]
|
|||||||
- Your objective is to reach the mission tiles at the far end of the arena and safely carry them back to your starting zone.
|
- Your objective is to reach the mission tiles at the far end of the arena and safely carry them back to your starting zone.
|
||||||
- The first player to complete 8 missions and reach the finish floor wins."
|
- The first player to complete 8 missions and reach the finish floor wins."
|
||||||
|
|
||||||
[node name="Tekton Doors" type="MarginContainer" parent="HowToPlayPanel/Panel/VBox/TabContainer" unique_id=123456799]
|
|
||||||
visible = false
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_constants/margin_left = 10
|
|
||||||
theme_override_constants/margin_top = 10
|
|
||||||
theme_override_constants/margin_right = 10
|
|
||||||
theme_override_constants/margin_bottom = 10
|
|
||||||
metadata/_tab_index = 2
|
|
||||||
|
|
||||||
[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Tekton Doors" unique_id=123456800]
|
|
||||||
layout_mode = 2
|
|
||||||
bbcode_enabled = true
|
|
||||||
text = "[b]Tekton Doors[/b]
|
|
||||||
|
|
||||||
- Navigate a sprawling arena connected by color-coded portal doors.
|
|
||||||
- Grab tiles and match goal patterns to earn mission completions.
|
|
||||||
- Use doors to quickly teleport across rooms, but watch out for closures and traps.
|
|
||||||
- The first player to complete 8 missions and reach the finish room wins."
|
|
||||||
|
|
||||||
[node name="Controls" type="MarginContainer" parent="HowToPlayPanel/Panel/VBox/TabContainer" unique_id=123456805]
|
[node name="Controls" type="MarginContainer" parent="HowToPlayPanel/Panel/VBox/TabContainer" unique_id=123456805]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|||||||
@@ -1768,13 +1768,6 @@ func start_movement_along_path(path: Array, clear_visual: bool = true, force: bo
|
|||||||
if sng_manager.check_win_condition(name.to_int(), current_position):
|
if sng_manager.check_win_condition(name.to_int(), current_position):
|
||||||
sng_main.rpc("sync_game_end_stop_n_go", name.to_int())
|
sng_main.rpc("sync_game_end_stop_n_go", name.to_int())
|
||||||
|
|
||||||
# Tekton Doors Win Check
|
|
||||||
elif LobbyManager.game_mode == "Tekton Doors":
|
|
||||||
var main_node = get_tree().root.get_node_or_null("Main")
|
|
||||||
if main_node and main_node.portal_mode_manager:
|
|
||||||
if main_node.portal_mode_manager.check_win_condition(name.to_int(), current_position):
|
|
||||||
main_node.rpc("sync_game_end_portal_mode", name.to_int())
|
|
||||||
|
|
||||||
# FORCE SNAP: Update target visual position to the perfect grid center
|
# FORCE SNAP: Update target visual position to the perfect grid center
|
||||||
# This ensures that when interpolation resumes (in _process), it pulls to the correct spot
|
# This ensures that when interpolation resumes (in _process), it pulls to the correct spot
|
||||||
target_visual_position = grid_to_world(current_position)
|
target_visual_position = grid_to_world(current_position)
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
[gd_scene load_steps=8 format=3 uid="uid://portal_door_001"]
|
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/portal_door.gd" id="1_script"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_frame"]
|
|
||||||
size = Vector3(0.15, 2.2, 0.15)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_frame"]
|
|
||||||
albedo_color = Color(0.1, 0.5, 0.8, 1)
|
|
||||||
metallic = 0.8
|
|
||||||
roughness = 0.2
|
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="PlaneMesh_ground"]
|
|
||||||
size = Vector2(1.0, 1.0)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"]
|
|
||||||
transparency = 1
|
|
||||||
albedo_color = Color(1, 1, 1, 0.4)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(1, 1, 1, 1)
|
|
||||||
emission_energy_multiplier = 1.0
|
|
||||||
|
|
||||||
[sub_resource type="PlaneMesh" id="PlaneMesh_vortex"]
|
|
||||||
size = Vector2(1.4, 2.1)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vortex"]
|
|
||||||
transparency = 1
|
|
||||||
albedo_color = Color(0.0, 0.6, 1.0, 0.4)
|
|
||||||
emission_enabled = true
|
|
||||||
emission = Color(0.0, 0.4, 1.0, 1)
|
|
||||||
emission_energy_multiplier = 5.0
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_trigger"]
|
|
||||||
size = Vector3(1.4, 2.1, 0.8)
|
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_portal"]
|
|
||||||
properties/0/path = NodePath(":target_room_id")
|
|
||||||
properties/0/spawn = true
|
|
||||||
properties/0/replication_mode = 2
|
|
||||||
properties/1/path = NodePath(":target_door_id")
|
|
||||||
properties/1/spawn = true
|
|
||||||
properties/1/replication_mode = 2
|
|
||||||
properties/2/path = NodePath(":is_active")
|
|
||||||
properties/2/spawn = true
|
|
||||||
properties/2/replication_mode = 2
|
|
||||||
properties/3/path = NodePath(":portal_color")
|
|
||||||
properties/3/spawn = true
|
|
||||||
properties/3/replication_mode = 2
|
|
||||||
|
|
||||||
[node name="PortalDoor" type="StaticBody3D"]
|
|
||||||
script = ExtResource("1_script")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
|
|
||||||
shape = SubResource("BoxShape3D_trigger")
|
|
||||||
|
|
||||||
|
|
||||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_portal")
|
|
||||||
|
|
||||||
[node name="Frame_Left" type="MeshInstance3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.75, 1.1, 0)
|
|
||||||
mesh = SubResource("BoxMesh_frame")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_frame")
|
|
||||||
|
|
||||||
[node name="Frame_Right" type="MeshInstance3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.75, 1.1, 0)
|
|
||||||
mesh = SubResource("BoxMesh_frame")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_frame")
|
|
||||||
|
|
||||||
[node name="Frame_Top" type="MeshInstance3D" parent="."]
|
|
||||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 2.2, 0)
|
|
||||||
mesh = SubResource("BoxMesh_frame")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_frame")
|
|
||||||
|
|
||||||
[node name="Vortex" type="MeshInstance3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 1.1, 0)
|
|
||||||
mesh = SubResource("PlaneMesh_vortex")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_vortex")
|
|
||||||
|
|
||||||
[node name="GroundIndicator" type="MeshInstance3D" parent="."]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0.4)
|
|
||||||
mesh = SubResource("PlaneMesh_ground")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_ground")
|
|
||||||
|
|
||||||
[node name="Area3D" type="Area3D" parent="."]
|
|
||||||
collision_layer = 0
|
|
||||||
collision_mask = 2
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D"]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
|
|
||||||
shape = SubResource("BoxShape3D_trigger")
|
|
||||||
+2
-18
@@ -51,9 +51,6 @@ func _init(p_lobby: Control):
|
|||||||
LobbyManager.sng_go_duration_changed.connect(_on_sng_update)
|
LobbyManager.sng_go_duration_changed.connect(_on_sng_update)
|
||||||
LobbyManager.sng_stop_duration_changed.connect(_on_sng_update)
|
LobbyManager.sng_stop_duration_changed.connect(_on_sng_update)
|
||||||
LobbyManager.sng_required_goals_changed.connect(_on_sng_update)
|
LobbyManager.sng_required_goals_changed.connect(_on_sng_update)
|
||||||
LobbyManager.doors_swap_time_changed.connect(_on_doors_update)
|
|
||||||
LobbyManager.doors_refresh_time_changed.connect(_on_doors_update)
|
|
||||||
LobbyManager.doors_required_goals_changed.connect(_on_doors_update)
|
|
||||||
|
|
||||||
FriendManager.lobby_invite_received.connect(_on_lobby_invite_received)
|
FriendManager.lobby_invite_received.connect(_on_lobby_invite_received)
|
||||||
|
|
||||||
@@ -136,21 +133,8 @@ func _on_sng_update(_val: int = 0) -> void:
|
|||||||
if go_idx != -1: lobby.sng_go_option.selected = go_idx
|
if go_idx != -1: lobby.sng_go_option.selected = go_idx
|
||||||
var stop_idx = [3, 4, 5].find(LobbyManager.sng_stop_duration)
|
var stop_idx = [3, 4, 5].find(LobbyManager.sng_stop_duration)
|
||||||
if stop_idx != -1: lobby.sng_stop_option.selected = stop_idx
|
if stop_idx != -1: lobby.sng_stop_option.selected = stop_idx
|
||||||
var goals_idx = [5, 8, 12].find(LobbyManager.sng_required_goals)
|
var goals_idx2 = [5, 8, 12].find(LobbyManager.sng_required_goals)
|
||||||
if goals_idx != -1: lobby.sng_goals_option.selected = goals_idx
|
if goals_idx2 != -1: lobby.sng_goals_option.selected = goals_idx2
|
||||||
|
|
||||||
func _on_doors_update(_val: int = 0) -> void:
|
|
||||||
if not lobby.doors_swap_option: return
|
|
||||||
var swap_idx = [10, 15, 30].find(LobbyManager.doors_swap_time)
|
|
||||||
if swap_idx != -1: lobby.doors_swap_option.selected = swap_idx
|
|
||||||
var refresh_idx = [15, 25, 40].find(LobbyManager.doors_refresh_time)
|
|
||||||
if refresh_idx != -1: lobby.doors_refresh_option.selected = refresh_idx
|
|
||||||
var goals_idx = [5, 8, 12].find(LobbyManager.doors_required_goals)
|
|
||||||
if goals_idx != -1: lobby.doors_goals_option.selected = goals_idx
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LobbyManager Signal Handlers
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
func _on_room_joined(room_data: Dictionary) -> void:
|
func _on_room_joined(room_data: Dictionary) -> void:
|
||||||
lobby._show_panel("lobby")
|
lobby._show_panel("lobby")
|
||||||
|
|||||||
+19
-22
@@ -210,9 +210,9 @@ func _run_ai_tick():
|
|||||||
print("[BotController] Action Taken: Attack Pursuit")
|
print("[BotController] Action Taken: Attack Pursuit")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 0.5: Gauntlet (#075) — burn Cleanser if boxed in
|
# Priority 0.5: Gauntlet (#075) — use Ghost powerup if boxed in
|
||||||
if await _try_activate_cleanser():
|
if await _try_activate_ghost():
|
||||||
print("[BotController] Action Taken: Cleanser (trapped)")
|
print("[BotController] Action Taken: Ghost (trapped)")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
|
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
|
||||||
@@ -260,27 +260,24 @@ func _run_ai_tick():
|
|||||||
return
|
return
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Gauntlet (#075) — Cleanser + Sticky Avoidance wiring
|
# Gauntlet (#075) — Ghost Powerup + Sticky Avoidance wiring
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _try_activate_cleanser() -> bool:
|
func _try_activate_ghost() -> bool:
|
||||||
"""Activate Cleanser when the planner reports imminent danger.
|
"""Activate Ghost powerup when the planner reports imminent danger.
|
||||||
|
|
||||||
Server-authoritative RPC; we only request it. Returns true if the request
|
Uses the existing SpecialTilesManager to activate the held ghost powerup.
|
||||||
was sent successfully (not a guarantee it landed on a sticky cell)."""
|
Returns true if activation was triggered."""
|
||||||
if not strategic_planner or not strategic_planner.is_gauntlet_mode():
|
if not strategic_planner or not strategic_planner.is_gauntlet_mode():
|
||||||
return false
|
return false
|
||||||
if not strategic_planner.should_activate_cleanser_now():
|
if not strategic_planner.should_activate_ghost_now():
|
||||||
return false
|
return false
|
||||||
var gm = strategic_planner._get_gauntlet_manager()
|
var stm = actor.get_node_or_null("SpecialTilesManager")
|
||||||
if not gm:
|
if not stm:
|
||||||
return false
|
return false
|
||||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
if stm.has_method("activate_effect"):
|
||||||
if pid == null or pid < 0:
|
stm.activate_effect(stm.SpecialEffect.INVISIBLE_MODE)
|
||||||
return false
|
print("[BotController] %s activated Ghost powerup (trapped)" % actor.name)
|
||||||
if gm.has_method("rpc_activate_cleanser"):
|
|
||||||
gm.rpc_activate_cleanser(pid)
|
|
||||||
print("[BotController] %s requested Cleanser activation (trapped)" % actor.name)
|
|
||||||
return true
|
return true
|
||||||
return false
|
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)
|
var here = actor.current_position if "current_position" in actor else Vector2i(-1, -1)
|
||||||
if here == Vector2i(-1, -1):
|
if here == Vector2i(-1, -1):
|
||||||
return false
|
return false
|
||||||
# Post-move guard: if we somehow landed on a sticky without cleanser active,
|
# Post-move guard: if we somehow landed on a sticky without ghost active,
|
||||||
# burn Cleanser to clear ourselves out next tick.
|
# burn Ghost powerup to phase through next tick.
|
||||||
if strategic_planner.is_gauntlet_mode() and strategic_planner._is_overlay_unsafe(here):
|
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()
|
var gm = strategic_planner._get_gauntlet_manager()
|
||||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(here):
|
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])
|
print("[BotController] %s stepped onto sticky at %s — activating Ghost" % [actor.name, here])
|
||||||
return _try_activate_cleanser()
|
return _try_activate_ghost()
|
||||||
return false
|
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
|
const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles
|
||||||
|
|
||||||
# Gauntlet overlay layer (v2 ground-growth model — sticky/telegraph on layer 2).
|
# 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 GAUNTLET_OVERLAY_LAYER: int = 2
|
||||||
const TILE_STICKY: int = 17
|
const TILE_STICKY: int = 17
|
||||||
const TILE_TELEGRAPH: int = 18
|
const TILE_TELEGRAPH: int = 18
|
||||||
@@ -61,23 +61,16 @@ func _get_gauntlet_manager() -> Node:
|
|||||||
return gm2
|
return gm2
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func _bot_has_cleanser_charge() -> bool:
|
func _bot_has_ghost_powerup() -> bool:
|
||||||
var gm = _get_gauntlet_manager()
|
"""Check if the bot has a ghost powerup in its SpecialTilesManager inventory."""
|
||||||
if not gm or not "player_cleansers" in gm:
|
var stm = actor.get_node_or_null("SpecialTilesManager")
|
||||||
|
if not stm:
|
||||||
return false
|
return false
|
||||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
return stm.inventory.get(stm.SpecialEffect.INVISIBLE_MODE, false)
|
||||||
if pid == null or pid < 0:
|
|
||||||
return false
|
|
||||||
return gm.player_cleansers.get(pid, 0) > 0
|
|
||||||
|
|
||||||
func _is_bot_cleanser_active() -> bool:
|
func _is_bot_ghost_active() -> bool:
|
||||||
var gm = _get_gauntlet_manager()
|
"""Check if the bot is currently in ghost (invisible) mode."""
|
||||||
if not gm:
|
return actor.get("is_invisible") == true
|
||||||
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_overlay_unsafe(pos: Vector2i) -> bool:
|
func _is_overlay_unsafe(pos: Vector2i) -> bool:
|
||||||
"""True if the cell carries a sticky or telegraphed overlay on layer 2."""
|
"""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:
|
func _is_cell_unsafe_in_gauntlet(pos: Vector2i) -> bool:
|
||||||
"""Cell is unsafe in Gauntlet if it's sticky/telegraphed — unless the bot's
|
"""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():
|
if not is_gauntlet_mode():
|
||||||
return false
|
return false
|
||||||
if _is_bot_cleanser_active():
|
if _is_bot_ghost_active():
|
||||||
return false
|
return false
|
||||||
var gm = _get_gauntlet_manager()
|
var gm = _get_gauntlet_manager()
|
||||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(pos):
|
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
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
func should_activate_cleanser_now() -> bool:
|
func should_activate_ghost_now() -> bool:
|
||||||
"""True if the bot is boxed in / about to be sealed and should burn Cleanser."""
|
"""True if the bot is boxed in / about to be sealed and should use Ghost powerup."""
|
||||||
if not is_gauntlet_mode():
|
if not is_gauntlet_mode():
|
||||||
return false
|
return false
|
||||||
if not _bot_has_cleanser_charge():
|
if not _bot_has_ghost_powerup():
|
||||||
return false
|
return false
|
||||||
if _is_bot_cleanser_active():
|
if _is_bot_ghost_active():
|
||||||
return false
|
return false
|
||||||
var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1)
|
var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1)
|
||||||
if here == 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 —
|
# Gauntlet mode (#075): reject cells that are sticky or telegraphed —
|
||||||
# stepping onto them either traps the bot or strands it within 1s.
|
# 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
|
# 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.
|
# exempt via the helper.
|
||||||
if _is_cell_unsafe_in_gauntlet(pos):
|
if _is_cell_unsafe_in_gauntlet(pos):
|
||||||
return false
|
return false
|
||||||
|
|||||||
+3
-13
@@ -1,12 +1,7 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
class_name GameMode
|
class_name GameMode
|
||||||
|
|
||||||
enum Mode {
|
enum Mode { FREEMODE = 0, STOP_N_GO = 1, GAUNTLET = 2 }
|
||||||
FREEMODE = 0,
|
|
||||||
STOP_N_GO = 1,
|
|
||||||
TEKTON_DOORS = 2,
|
|
||||||
GAUNTLET = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
static func from_string(mode: String) -> Mode:
|
static func from_string(mode: String) -> Mode:
|
||||||
match mode:
|
match mode:
|
||||||
@@ -14,11 +9,8 @@ static func from_string(mode: String) -> Mode:
|
|||||||
return Mode.FREEMODE
|
return Mode.FREEMODE
|
||||||
"Stop n Go":
|
"Stop n Go":
|
||||||
return Mode.STOP_N_GO
|
return Mode.STOP_N_GO
|
||||||
"Tekton Doors":
|
|
||||||
return Mode.TEKTON_DOORS
|
|
||||||
"Candy Pump Survival":
|
"Candy Pump Survival":
|
||||||
return Mode.GAUNTLET
|
return Mode.GAUNTLET
|
||||||
_:
|
|
||||||
return Mode.FREEMODE
|
return Mode.FREEMODE
|
||||||
|
|
||||||
static func mode_to_string(mode: Mode) -> String:
|
static func mode_to_string(mode: Mode) -> String:
|
||||||
@@ -27,15 +19,13 @@ static func mode_to_string(mode: Mode) -> String:
|
|||||||
return "Freemode"
|
return "Freemode"
|
||||||
Mode.STOP_N_GO:
|
Mode.STOP_N_GO:
|
||||||
return "Stop n Go"
|
return "Stop n Go"
|
||||||
Mode.TEKTON_DOORS:
|
|
||||||
return "Tekton Doors"
|
|
||||||
Mode.GAUNTLET:
|
Mode.GAUNTLET:
|
||||||
return "Candy Pump Survival"
|
return "Candy Pump Survival"
|
||||||
_:
|
_:
|
||||||
return "Freemode"
|
return "Freemode"
|
||||||
|
|
||||||
static func is_restricted(mode: Mode) -> bool:
|
static func is_restricted(mode: Mode) -> bool:
|
||||||
return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET
|
return mode == Mode.STOP_N_GO or mode == Mode.GAUNTLET
|
||||||
|
|
||||||
static func get_all_modes() -> Array[String]:
|
static func get_all_modes() -> Array[String]:
|
||||||
return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival"]
|
return ["Freemode", "Stop n Go", "Candy Pump Survival"]
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cosvmn3gh5bxj
|
||||||
@@ -52,9 +52,6 @@ func _calculate_target_position() -> Vector3:
|
|||||||
bounds = bounds_gauntlet
|
bounds = bounds_gauntlet
|
||||||
elif mode == GameMode.Mode.STOP_N_GO:
|
elif mode == GameMode.Mode.STOP_N_GO:
|
||||||
bounds = bounds_stop_n_go
|
bounds = bounds_stop_n_go
|
||||||
elif mode == GameMode.Mode.TEKTON_DOORS:
|
|
||||||
bounds = bounds_doors
|
|
||||||
target_y = 32.3 # Doors uses a higher overlook
|
|
||||||
|
|
||||||
# Clamp X and Z
|
# Clamp X and Z
|
||||||
target_x = clamp(target_x, bounds.min_x, bounds.max_x)
|
target_x = clamp(target_x, bounds.min_x, bounds.max_x)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ signal patch_applied
|
|||||||
signal store_update_required(store_url: String)
|
signal store_update_required(store_url: String)
|
||||||
|
|
||||||
# Configuration - Update these URLs for your game
|
# Configuration - Update these URLs for your game
|
||||||
const VERSION_MANIFEST_URL := "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/version.json"
|
const VERSION_MANIFEST_URL := "https://git.klud.top/danchie/tekton/raw/branch/patches/version.json"
|
||||||
const ANDROID_STORE_URL := "https://play.google.com/store/apps/details?id=com.yourcompany.tekton"
|
const ANDROID_STORE_URL := "https://play.google.com/store/apps/details?id=com.yourcompany.tekton"
|
||||||
const IOS_STORE_URL := "https://apps.apple.com/app/tekton/id123456789"
|
const IOS_STORE_URL := "https://apps.apple.com/app/tekton/id123456789"
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ extends Node
|
|||||||
class_name GauntletManager
|
class_name GauntletManager
|
||||||
|
|
||||||
# GauntletManager - Handles Candy Pump Survival (Gauntlet) game mode
|
# GauntletManager - Handles Candy Pump Survival (Gauntlet) game mode
|
||||||
# Pattern: StopNGoManager + PortalModeManager
|
# Pattern: StopNGoManager + GauntletManager
|
||||||
|
|
||||||
signal phase_changed(phase_index: int, phase_name: String)
|
signal phase_changed(phase_index: int, phase_name: String)
|
||||||
signal growth_tick(cells: Array)
|
signal growth_tick(cells: Array)
|
||||||
signal player_trapped(player_id: int)
|
signal player_trapped(player_id: int)
|
||||||
signal cleanser_granted(player_id: int)
|
signal ghost_granted(player_id: int)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Constants
|
# Constants
|
||||||
@@ -31,10 +31,9 @@ enum CellState {
|
|||||||
STICKY, # Covered in sticky candy, blocks + traps
|
STICKY, # Covered in sticky candy, blocks + traps
|
||||||
BUBBLE_GROWING, # Candy bubble growing, not yet exploded
|
BUBBLE_GROWING, # Candy bubble growing, not yet exploded
|
||||||
BLOCKED, # NPC zone or permanent obstacle
|
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 = {}
|
var cleansed_cells: Dictionary = {}
|
||||||
const CLEANSED_PROTECTION_TIME: float = 5.0
|
const CLEANSED_PROTECTION_TIME: float = 5.0
|
||||||
|
|
||||||
@@ -146,15 +145,10 @@ const SMACK_COOLDOWN: float = 8.0
|
|||||||
const SMACK_CHARGE_WINDOW: float = 3.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_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
|
# Trapped Players
|
||||||
@@ -189,9 +183,6 @@ var pump_instance: Node3D = null
|
|||||||
# HUD
|
# HUD
|
||||||
var hud_layer: CanvasLayer
|
var hud_layer: CanvasLayer
|
||||||
var phase_label: Label
|
var phase_label: Label
|
||||||
var cleanser_label: Label
|
|
||||||
var cleanser_icon: TextureRect
|
|
||||||
var cleanser_count: int = 0
|
|
||||||
var slowmo_label: Label
|
var slowmo_label: Label
|
||||||
var _gauntlet_hud_scene: PackedScene = preload("res://scenes/gauntlet_hud.tscn")
|
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():
|
elif not multiplayer.is_server():
|
||||||
player.sync_modulate(Color.WHITE)
|
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)
|
# Slow-mo timer (all peers for visual consistency)
|
||||||
if slowmo_active:
|
if slowmo_active:
|
||||||
slowmo_timer -= delta
|
slowmo_timer -= delta
|
||||||
@@ -340,9 +327,12 @@ func _start_phase(phase: Phase) -> void:
|
|||||||
var phase_name = _phase_to_string(phase)
|
var phase_name = _phase_to_string(phase)
|
||||||
print("[Gauntlet] Phase changed to: ", phase_name)
|
print("[Gauntlet] Phase changed to: ", phase_name)
|
||||||
|
|
||||||
if _can_rpc():
|
if _can_rpc() and multiplayer.is_server():
|
||||||
rpc("sync_phase", int(phase), phase_name)
|
rpc("sync_phase", int(phase), phase_name)
|
||||||
|
|
||||||
|
# Update phase explicitly with setup_arena
|
||||||
|
_shrink_arena()
|
||||||
|
|
||||||
emit_signal("phase_changed", int(phase), phase_name)
|
emit_signal("phase_changed", int(phase), phase_name)
|
||||||
|
|
||||||
func _phase_to_string(phase: Phase) -> String:
|
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:
|
if not is_active:
|
||||||
activate_client_side()
|
activate_client_side()
|
||||||
current_phase = phase_index as Phase
|
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)
|
_update_hud_phase(phase_name)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -419,11 +417,12 @@ func _apply_arena_setup() -> void:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
# 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:
|
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
|
# 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, 0, z), TILE_WALKABLE)
|
||||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
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
|
continue
|
||||||
|
|
||||||
# Interior: walkable floor
|
# Interior: walkable floor
|
||||||
@@ -503,6 +502,7 @@ func _spawn_mission_tiles() -> void:
|
|||||||
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
||||||
var goal_items = [7, 8, 9, 10]
|
var goal_items = [7, 8, 9, 10]
|
||||||
var tiles_spawned: int = 0
|
var tiles_spawned: int = 0
|
||||||
|
var main = get_node_or_null("/root/Main")
|
||||||
|
|
||||||
for x in range(ARENA_COLUMNS):
|
for x in range(ARENA_COLUMNS):
|
||||||
for z in range(ARENA_ROWS):
|
for z in range(ARENA_ROWS):
|
||||||
@@ -535,7 +535,6 @@ func _spawn_mission_tiles() -> void:
|
|||||||
tiles_spawned += 1
|
tiles_spawned += 1
|
||||||
|
|
||||||
# Sync to clients
|
# Sync to clients
|
||||||
var main = get_node("/root/Main")
|
|
||||||
if main:
|
if main:
|
||||||
main.rpc("sync_grid_item", x, 1, z, tile_type)
|
main.rpc("sync_grid_item", x, 1, z, tile_type)
|
||||||
|
|
||||||
@@ -1004,56 +1003,6 @@ func _spawn_impact_particles(targets: Array) -> void:
|
|||||||
if particles and is_instance_valid(particles):
|
if particles and is_instance_valid(particles):
|
||||||
particles.queue_free()
|
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
|
# Sticky / Trap System
|
||||||
|
|
||||||
@@ -1065,12 +1014,15 @@ func is_cleansed_cell(pos: Vector2i) -> bool:
|
|||||||
|
|
||||||
func cell_state(pos: Vector2i) -> CellState:
|
func cell_state(pos: Vector2i) -> CellState:
|
||||||
"""Logical state of a playable cell (v2 ground-growth model)."""
|
"""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):
|
if _is_npc_zone(pos) or _is_boundary(pos):
|
||||||
return CellState.BLOCKED
|
return CellState.BLOCKED
|
||||||
if sticky_cells.has(pos):
|
if is_sticky_cell(pos):
|
||||||
return CellState.STICKY
|
return CellState.STICKY
|
||||||
if cleansed_cells.has(pos):
|
if cleansed_cells.has(pos):
|
||||||
return CellState.CLEANSED
|
return CellState.BLOCKED # Protected from regrowth temporarily
|
||||||
if telegraphed_cells.has(pos):
|
if telegraphed_cells.has(pos):
|
||||||
return CellState.TELEGRAPHED
|
return CellState.TELEGRAPHED
|
||||||
if bubble_cells.has(pos):
|
if bubble_cells.has(pos):
|
||||||
@@ -1091,8 +1043,37 @@ func _tick_cleansed_cells(delta: float) -> void:
|
|||||||
for pos in expired:
|
for pos in expired:
|
||||||
cleansed_cells.erase(pos)
|
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:
|
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%)
|
# Coverage tracking (v2 target: 70-75%, down from v1's 80%)
|
||||||
@@ -1103,12 +1084,15 @@ const COVERAGE_TARGET_MAX: float = 0.75
|
|||||||
|
|
||||||
func playable_cell_count() -> int:
|
func playable_cell_count() -> int:
|
||||||
"""Number of cells that can ever become sticky (interior, minus NPC zone)."""
|
"""Number of cells that can ever become sticky (interior, minus NPC zone)."""
|
||||||
|
var b = get_arena_bounds()
|
||||||
var count := 0
|
var count := 0
|
||||||
for x in range(ARENA_COLUMNS):
|
for x in range(ARENA_COLUMNS):
|
||||||
for z in range(ARENA_ROWS):
|
for z in range(ARENA_ROWS):
|
||||||
var pos := Vector2i(x, z)
|
var pos := Vector2i(x, z)
|
||||||
if _is_boundary(pos) or _is_npc_zone(pos):
|
if _is_boundary(pos) or _is_npc_zone(pos):
|
||||||
continue
|
continue
|
||||||
|
if pos.x <= b.min or pos.x >= b.max or pos.y <= b.min or pos.y >= b.max:
|
||||||
|
continue
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@@ -1132,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:
|
func _is_cell_passable(pos: Vector2i, extra_sticky: Dictionary = {}) -> bool:
|
||||||
"""Can a player stand on / move through this cell, given a hypothetical sticky set?"""
|
"""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):
|
if _is_boundary(pos) or _is_npc_zone(pos):
|
||||||
return false
|
return false
|
||||||
if sticky_cells.has(pos) or extra_sticky.has(pos):
|
if sticky_cells.has(pos) or extra_sticky.has(pos):
|
||||||
@@ -1333,11 +1320,11 @@ func _calculate_bubble_score(pos: Vector2i, player_cells: Array = []) -> float:
|
|||||||
return score
|
return score
|
||||||
|
|
||||||
func _bubble_score_camping(pos: Vector2i) -> float:
|
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))
|
var t := _camp_time_for_region(_region_of(pos))
|
||||||
if t > 10.0:
|
if t > 10.0:
|
||||||
# Stronger only if a nearby player actually holds a cleanser.
|
# Stronger only if a nearby player is in ghost mode.
|
||||||
if _any_cleanser_holder_near(pos):
|
if _any_ghost_player_near(pos):
|
||||||
return 80.0
|
return 80.0
|
||||||
return 60.0
|
return 60.0
|
||||||
elif t > 8.0:
|
elif t > 8.0:
|
||||||
@@ -1390,22 +1377,25 @@ func _bubble_score_unfair_trap(pos: Vector2i) -> float:
|
|||||||
func _bubble_blast_cells(center: Vector2i) -> Array:
|
func _bubble_blast_cells(center: Vector2i) -> Array:
|
||||||
"""The 3x3 (radius 1) sticky cells a bubble at `center` would create,
|
"""The 3x3 (radius 1) sticky cells a bubble at `center` would create,
|
||||||
clipped to passable/playable cells."""
|
clipped to passable/playable cells."""
|
||||||
|
var b = get_arena_bounds()
|
||||||
var cells: Array = []
|
var cells: Array = []
|
||||||
for dx in range(-BUBBLE_EXPLOSION_RADIUS, BUBBLE_EXPLOSION_RADIUS + 1):
|
for dx in range(-BUBBLE_EXPLOSION_RADIUS, BUBBLE_EXPLOSION_RADIUS + 1):
|
||||||
for dz 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)
|
var c := center + Vector2i(dx, dz)
|
||||||
if _is_boundary(c) or _is_npc_zone(c):
|
if _is_boundary(c) or _is_npc_zone(c):
|
||||||
continue
|
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)
|
cells.append(c)
|
||||||
return cells
|
return cells
|
||||||
|
|
||||||
func _any_cleanser_holder_near(pos: Vector2i) -> bool:
|
func _bubble_footprint(center: Vector2i) -> Array:
|
||||||
"""True if a player holding a Cleanser charge is within the camping region."""
|
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"):
|
for player in get_tree().get_nodes_in_group("Players"):
|
||||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
if not player.get("is_invisible"):
|
||||||
if pid == -1:
|
|
||||||
continue
|
|
||||||
if player_cleansers.get(pid, 0) <= 0:
|
|
||||||
continue
|
continue
|
||||||
if "current_position" in player and player.current_position != null:
|
if "current_position" in player and player.current_position != null:
|
||||||
if _region_of(player.current_position) == _region_of(pos):
|
if _region_of(player.current_position) == _region_of(pos):
|
||||||
@@ -1501,7 +1491,7 @@ func _explode_bubble(center: Vector2i, cells: Array) -> void:
|
|||||||
if "current_position" in player and player.current_position != null:
|
if "current_position" in player and player.current_position != null:
|
||||||
if blast.has(player.current_position):
|
if blast.has(player.current_position):
|
||||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
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
|
continue
|
||||||
apply_sticky_slow(player)
|
apply_sticky_slow(player)
|
||||||
|
|
||||||
@@ -1608,8 +1598,8 @@ func _check_all_players_trapped() -> void:
|
|||||||
var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1)
|
var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1)
|
||||||
if is_sticky_cell(pos):
|
if is_sticky_cell(pos):
|
||||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
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 # cleansing players are immune to the slow
|
continue # ghost players are immune to the slow
|
||||||
apply_sticky_slow(player)
|
apply_sticky_slow(player)
|
||||||
|
|
||||||
func apply_sticky_slow(player: Node) -> void:
|
func apply_sticky_slow(player: Node) -> void:
|
||||||
@@ -1641,25 +1631,22 @@ func _trap_player(player: Node) -> void:
|
|||||||
NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING)
|
NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING)
|
||||||
|
|
||||||
func clear_sticky_cell(pos: Vector2i) -> void:
|
func clear_sticky_cell(pos: Vector2i) -> void:
|
||||||
"""Used by Cleanser power-up to remove a sticky cell."""
|
"""Remove a sticky cell (used when ghost player walks through)."""
|
||||||
if _can_rpc():
|
if _can_rpc():
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
rpc("sync_clear_sticky_cell", pos)
|
rpc("sync_clear_sticky_cell", pos)
|
||||||
else:
|
else:
|
||||||
sync_clear_sticky_cell(pos) # Predictive local clear
|
sync_clear_sticky_cell(pos) # Predictive local clear
|
||||||
rpc("rpc_use_cleanser", pos)
|
|
||||||
else:
|
else:
|
||||||
sync_clear_sticky_cell(pos)
|
sync_clear_sticky_cell(pos)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
||||||
sticky_cells.erase(pos)
|
sticky_cells.erase(pos)
|
||||||
mark_cleansed(pos) # temporary regrowth protection (v2)
|
mark_cleansed(pos) # temporary regrowth protection
|
||||||
if gridmap:
|
if gridmap:
|
||||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
|
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
|
||||||
|
|
||||||
# Play VFX and SFX
|
|
||||||
_spawn_cleanser_particles(pos)
|
|
||||||
if SfxManager:
|
if SfxManager:
|
||||||
SfxManager.play("pick_up_power_tile")
|
SfxManager.play("pick_up_power_tile")
|
||||||
|
|
||||||
@@ -1667,142 +1654,6 @@ func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
|||||||
if main_scene and main_scene.has_method("sync_grid_item"):
|
if main_scene and main_scene.has_method("sync_grid_item"):
|
||||||
main_scene.sync_grid_item(pos.x, 2, pos.y, -1)
|
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")
|
@rpc("any_peer", "reliable")
|
||||||
func rpc_trigger_slowmo() -> void:
|
func rpc_trigger_slowmo() -> void:
|
||||||
"""RPC for clients to request slow-mo from server."""
|
"""RPC for clients to request slow-mo from server."""
|
||||||
@@ -1889,25 +1740,7 @@ func _setup_hud() -> void:
|
|||||||
hud_layer.visible = false
|
hud_layer.visible = false
|
||||||
add_child(hud_layer)
|
add_child(hud_layer)
|
||||||
phase_label = hud_layer.get_node("BottomContainer/VBoxContainer/PhaseLabel")
|
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")
|
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:
|
func _update_hud_phase(phase_name: String) -> void:
|
||||||
if phase_label:
|
if phase_label:
|
||||||
@@ -1926,20 +1759,6 @@ func _update_hud_phase(phase_name: String) -> void:
|
|||||||
# Animate phase label with bounce effect
|
# Animate phase label with bounce effect
|
||||||
_animate_phase_label()
|
_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:
|
func _animate_phase_label() -> void:
|
||||||
"""Animate phase label with bounce effect."""
|
"""Animate phase label with bounce effect."""
|
||||||
if not phase_label:
|
if not phase_label:
|
||||||
@@ -1964,7 +1783,7 @@ func _animate_phase_label() -> void:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _on_goal_count_updated(peer_id: int, count: int) -> 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():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1973,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] = 0
|
||||||
player_mission_completions[peer_id] += 1
|
player_mission_completions[peer_id] += 1
|
||||||
|
|
||||||
# Grant cleanser every 2 missions
|
# Grant ghost powerup every 2 missions
|
||||||
var completions = player_mission_completions[peer_id]
|
var completions = player_mission_completions[peer_id]
|
||||||
if completions % 2 == 0:
|
if completions % 2 == 0:
|
||||||
if not player_cleansers.has(peer_id):
|
_grant_ghost_powerup(peer_id)
|
||||||
player_cleansers[peer_id] = 0
|
|
||||||
|
|
||||||
# Allow stacking cleanser charges instead of capping at 1
|
func _grant_ghost_powerup(peer_id: int) -> void:
|
||||||
player_cleansers[peer_id] += 1
|
"""Grant the ghost (invisible mode) powerup to a player."""
|
||||||
emit_signal("cleanser_granted", peer_id)
|
var all_players = get_tree().get_nodes_in_group("Players")
|
||||||
print("[Gauntlet] Player %d granted Cleanser (Total: %d) (mission %d)" % [peer_id, player_cleansers[peer_id], completions])
|
for p in all_players:
|
||||||
|
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
|
||||||
# Sync cleanser count to HUD
|
if pid == peer_id:
|
||||||
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0))
|
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:
|
func _on_score_updated(peer_id: int, new_score: int) -> void:
|
||||||
"""Called when a player's score is updated."""
|
"""Called when a player's score is updated."""
|
||||||
pass # Score sync handled by GoalsCycleManager
|
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
|
# Utility
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ func mark_goal_complete(player_id: int):
|
|||||||
# Reset start time for next goal
|
# Reset start time for next goal
|
||||||
player_start_times[player_id] = Time.get_ticks_msec()
|
player_start_times[player_id] = Time.get_ticks_msec()
|
||||||
|
|
||||||
# print("Player %s completed goal in %.2fs" % [player_id, duration_sec])
|
|
||||||
|
|
||||||
func get_player_average_time(player_id: int) -> float:
|
func get_player_average_time(player_id: int) -> float:
|
||||||
if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty():
|
if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty():
|
||||||
return 10.0 # Default baseline (10 seconds)
|
return 10.0 # Default baseline (10 seconds)
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ signal sng_go_duration_changed(duration: int)
|
|||||||
signal sng_stop_duration_changed(duration: int)
|
signal sng_stop_duration_changed(duration: int)
|
||||||
signal sng_required_goals_changed(goals: int)
|
signal sng_required_goals_changed(goals: int)
|
||||||
|
|
||||||
# Tekton Doors settings signals
|
|
||||||
signal doors_swap_time_changed(time: int)
|
|
||||||
signal doors_refresh_time_changed(time: int)
|
|
||||||
signal doors_required_goals_changed(goals: int)
|
|
||||||
|
|
||||||
# Gauntlet settings signals
|
# Gauntlet settings signals
|
||||||
signal gauntlet_round_duration_changed(duration: int)
|
signal gauntlet_round_duration_changed(duration: int)
|
||||||
signal gauntlet_growth_interval_changed(interval: float)
|
signal gauntlet_growth_interval_changed(interval: float)
|
||||||
@@ -74,11 +69,6 @@ var sng_go_duration: int = 20
|
|||||||
var sng_stop_duration: int = 4
|
var sng_stop_duration: int = 4
|
||||||
var sng_required_goals: int = 8
|
var sng_required_goals: int = 8
|
||||||
|
|
||||||
# Tekton Doors settings
|
|
||||||
var doors_swap_time: int = 15
|
|
||||||
var doors_refresh_time: int = 25
|
|
||||||
var doors_required_goals: int = 8
|
|
||||||
|
|
||||||
# Gauntlet settings
|
# Gauntlet settings
|
||||||
var gauntlet_round_duration: int = 180
|
var gauntlet_round_duration: int = 180
|
||||||
var gauntlet_growth_interval: float = 3.0 # seconds between growth ticks
|
var gauntlet_growth_interval: float = 3.0 # seconds between growth ticks
|
||||||
@@ -522,37 +512,6 @@ func sync_sng_required_goals(goals: int) -> void:
|
|||||||
sng_required_goals = goals
|
sng_required_goals = goals
|
||||||
emit_signal("sng_required_goals_changed", goals)
|
emit_signal("sng_required_goals_changed", goals)
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Tekton Doors Settings
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
func set_doors_swap_time(time: int) -> void:
|
|
||||||
doors_swap_time = time
|
|
||||||
if is_host: rpc("sync_doors_swap_time", time)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
|
||||||
func sync_doors_swap_time(time: int) -> void:
|
|
||||||
doors_swap_time = time
|
|
||||||
emit_signal("doors_swap_time_changed", time)
|
|
||||||
|
|
||||||
func set_doors_refresh_time(time: int) -> void:
|
|
||||||
doors_refresh_time = time
|
|
||||||
if is_host: rpc("sync_doors_refresh_time", time)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
|
||||||
func sync_doors_refresh_time(time: int) -> void:
|
|
||||||
doors_refresh_time = time
|
|
||||||
emit_signal("doors_refresh_time_changed", time)
|
|
||||||
|
|
||||||
func set_doors_required_goals(goals: int) -> void:
|
|
||||||
doors_required_goals = goals
|
|
||||||
if is_host: rpc("sync_doors_required_goals", goals)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
|
||||||
func sync_doors_required_goals(goals: int) -> void:
|
|
||||||
doors_required_goals = goals
|
|
||||||
emit_signal("doors_required_goals_changed", goals)
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Gauntlet Settings
|
# Gauntlet Settings
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -740,8 +699,6 @@ func set_game_mode(mode: String) -> void:
|
|||||||
set_area("Free Mode Area")
|
set_area("Free Mode Area")
|
||||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||||
set_area("Stop n Go Area")
|
set_area("Stop n Go Area")
|
||||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
|
||||||
set_area("Tekton Doors Area")
|
|
||||||
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
||||||
set_area("Gauntlet Arena")
|
set_area("Gauntlet Arena")
|
||||||
|
|
||||||
@@ -756,8 +713,6 @@ func sync_game_mode(mode: String) -> void:
|
|||||||
selected_area = "Free Mode Area"
|
selected_area = "Free Mode Area"
|
||||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||||
selected_area = "Stop n Go Area"
|
selected_area = "Stop n Go Area"
|
||||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
|
||||||
selected_area = "Tekton Doors Area"
|
|
||||||
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
||||||
selected_area = "Gauntlet Arena"
|
selected_area = "Gauntlet Arena"
|
||||||
elif selected_area not in available_areas:
|
elif selected_area not in available_areas:
|
||||||
@@ -785,9 +740,6 @@ func start_game(force: bool = false) -> void:
|
|||||||
rpc("sync_sng_go_duration", sng_go_duration)
|
rpc("sync_sng_go_duration", sng_go_duration)
|
||||||
rpc("sync_sng_stop_duration", sng_stop_duration)
|
rpc("sync_sng_stop_duration", sng_stop_duration)
|
||||||
rpc("sync_sng_required_goals", sng_required_goals)
|
rpc("sync_sng_required_goals", sng_required_goals)
|
||||||
rpc("sync_doors_swap_time", doors_swap_time)
|
|
||||||
rpc("sync_doors_refresh_time", doors_refresh_time)
|
|
||||||
rpc("sync_doors_required_goals", doors_required_goals)
|
|
||||||
# Sync gauntlet settings
|
# Sync gauntlet settings
|
||||||
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
|
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||||
rpc("sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
rpc("sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||||
@@ -864,9 +816,6 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
|
|||||||
rpc_id(requester_id, "sync_sng_go_duration", sng_go_duration)
|
rpc_id(requester_id, "sync_sng_go_duration", sng_go_duration)
|
||||||
rpc_id(requester_id, "sync_sng_stop_duration", sng_stop_duration)
|
rpc_id(requester_id, "sync_sng_stop_duration", sng_stop_duration)
|
||||||
rpc_id(requester_id, "sync_sng_required_goals", sng_required_goals)
|
rpc_id(requester_id, "sync_sng_required_goals", sng_required_goals)
|
||||||
rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time)
|
|
||||||
rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time)
|
|
||||||
rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals)
|
|
||||||
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
|
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||||
rpc_id(requester_id, "sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
rpc_id(requester_id, "sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||||
rpc_id(requester_id, "sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick)
|
rpc_id(requester_id, "sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick)
|
||||||
@@ -1018,6 +967,3 @@ func reset() -> void:
|
|||||||
sng_go_duration = 20
|
sng_go_duration = 20
|
||||||
sng_stop_duration = 4
|
sng_stop_duration = 4
|
||||||
sng_required_goals = 8
|
sng_required_goals = 8
|
||||||
doors_swap_time = 15
|
|
||||||
doors_refresh_time = 25
|
|
||||||
doors_required_goals = 8
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func start_music():
|
|||||||
match game_mode:
|
match game_mode:
|
||||||
"Stop n Go":
|
"Stop n Go":
|
||||||
track_path = "res://assets/sounds/stop_n_go.wav"
|
track_path = "res://assets/sounds/stop_n_go.wav"
|
||||||
"Freemode", "Tekton Doors", _:
|
"Freemode", _:
|
||||||
track_path = "res://assets/sounds/level_bridge.wav"
|
track_path = "res://assets/sounds/level_bridge.wav"
|
||||||
|
|
||||||
play_track(track_path)
|
play_track(track_path)
|
||||||
|
|||||||
@@ -155,19 +155,15 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
player.knock_tekton()
|
player.knock_tekton()
|
||||||
return false # Don't move into the tile, just knock
|
return false # Don't move into the tile, just knock
|
||||||
|
|
||||||
# If moving into a sticky cell: slow the player (unless cleanser active,
|
# If moving into a sticky cell: block movement unless player is in ghost
|
||||||
# which clears the cell instead). Sticky no longer hard-traps.
|
# mode (is_invisible), which lets them bypass sticky tiles in gauntlet.
|
||||||
if gm and gm.is_active and gm.is_sticky_cell(grid_position):
|
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 player.get("is_invisible"):
|
||||||
if pid != -1 and gm.is_cleanser_active(pid):
|
# Ghost mode: walk through sticky tile freely
|
||||||
# Cleanser immunity: clear sticky cell, use one cell, don't slow
|
print("[Move] Ghost mode bypassed sticky cell at %s" % grid_position)
|
||||||
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)])
|
|
||||||
else:
|
else:
|
||||||
print("[Move] Player stepping into sticky cell at %s — slowed" % grid_position)
|
print("[Move] Failed: Blocked by Gauntlet Sticky cell at %s" % grid_position)
|
||||||
if player.is_multiplayer_authority() or multiplayer.is_server():
|
return false
|
||||||
gm.apply_sticky_slow(player)
|
|
||||||
|
|
||||||
rotate_towards_target(grid_position)
|
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"):
|
if main_sticky and main_sticky.get("gauntlet_manager"):
|
||||||
var gm_sticky = main_sticky.gauntlet_manager
|
var gm_sticky = main_sticky.gauntlet_manager
|
||||||
if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos):
|
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 other_player.get("is_invisible"):
|
||||||
if push_pid != -1 and gm_sticky.is_cleanser_active(push_pid):
|
# Ghost mode: pushed player bypasses sticky
|
||||||
# Cleanser immunity: clear sticky cell, use one cell
|
print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos)
|
print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos)
|
||||||
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
||||||
@@ -365,6 +358,10 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
|||||||
# Consume all available boost to force a full recharge cycle
|
# Consume all available boost to force a full recharge cycle
|
||||||
player.powerup_manager.consume_boost(100.0)
|
player.powerup_manager.consume_boost(100.0)
|
||||||
|
|
||||||
|
# NEW: Always clear charged strike state to prevent multi-hit spam
|
||||||
|
if player.get("is_charged_strike"):
|
||||||
|
player.set("is_charged_strike", false)
|
||||||
|
|
||||||
# SCORING: 200 Points for successful attack (ONLY in Free Mode)
|
# SCORING: 200 Points for successful attack (ONLY in Free Mode)
|
||||||
if player.is_multiplayer_authority():
|
if player.is_multiplayer_authority():
|
||||||
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
|
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
|
||||||
@@ -407,17 +404,6 @@ func _on_movement_finished():
|
|||||||
emit_signal("movement_finished")
|
emit_signal("movement_finished")
|
||||||
else:
|
else:
|
||||||
current_move_direction = Vector2i.ZERO
|
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")
|
emit_signal("movement_finished")
|
||||||
|
|
||||||
func move_to_clicked_position(grid_position: Vector2i) -> bool:
|
func move_to_clicked_position(grid_position: Vector2i) -> bool:
|
||||||
|
|||||||
@@ -230,10 +230,6 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not has_items:
|
if not has_items:
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
||||||
# Tekton Doors handles its own wall-aware refill in PortalModeManager
|
|
||||||
return
|
|
||||||
|
|
||||||
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
||||||
# Call randomize_floor on floor 1 using ScarcityController
|
# Call randomize_floor on floor 1 using ScarcityController
|
||||||
# ScarcityController is a global class, so we can pass its static function as a Callable
|
# ScarcityController is a global class, so we can pass its static function as a Callable
|
||||||
@@ -372,17 +368,6 @@ func auto_put_item() -> bool:
|
|||||||
var pos = neighbor.position
|
var pos = neighbor.position
|
||||||
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
||||||
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
||||||
# TEKTON DOORS: Avoid portal doors
|
|
||||||
var is_on_portal = false
|
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
||||||
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
|
||||||
for door in doors:
|
|
||||||
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
|
||||||
if Vector2i(door_grid.x, door_grid.z) == pos:
|
|
||||||
is_on_portal = true
|
|
||||||
break
|
|
||||||
|
|
||||||
if not is_on_portal:
|
|
||||||
valid_put_positions.append(pos)
|
valid_put_positions.append(pos)
|
||||||
|
|
||||||
if valid_put_positions.is_empty():
|
if valid_put_positions.is_empty():
|
||||||
|
|||||||
@@ -1,585 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
# PortalModeManager - Handles "Tekton Doors" mode logic
|
|
||||||
# Manages room partitioning, portal connections, and mode-specific timers.
|
|
||||||
|
|
||||||
var main: Node
|
|
||||||
var gridmap: Node
|
|
||||||
|
|
||||||
# Room layout config
|
|
||||||
const ROOM_COUNT = 4
|
|
||||||
const GRID_SIZE = 14
|
|
||||||
const ROOM_DIM = 7
|
|
||||||
|
|
||||||
# State
|
|
||||||
var connections = {} # room_id -> {door_id -> {target_room, target_door}}
|
|
||||||
var doors = [] # List of PortalDoor nodes
|
|
||||||
var swap_timer: Timer
|
|
||||||
var tile_refresh_timer: Timer
|
|
||||||
var finish_spawned: bool = false
|
|
||||||
var arena_setup_done: bool = false
|
|
||||||
var player_portal_cooldowns: Dictionary = {}
|
|
||||||
|
|
||||||
var hud_layer: CanvasLayer
|
|
||||||
var mission_label: Label
|
|
||||||
var _has_notified_mission_complete: bool = false
|
|
||||||
|
|
||||||
func initialize(p_main: Node, p_gridmap: Node):
|
|
||||||
main = p_main
|
|
||||||
gridmap = p_gridmap
|
|
||||||
|
|
||||||
if gridmap:
|
|
||||||
# Ensure walls (4) are strictly treated as non-walkable for all internal checks
|
|
||||||
# Use explicit type to avoid Array vs Array[int] mismatch error
|
|
||||||
var non_walkable: Array[int] = [4]
|
|
||||||
gridmap.non_walkable_items = non_walkable
|
|
||||||
|
|
||||||
# Create Stands container if it doesn't exist
|
|
||||||
print("[PortalModeManager] Initialized")
|
|
||||||
|
|
||||||
# Connection Swap Timer (15s)
|
|
||||||
swap_timer = Timer.new()
|
|
||||||
swap_timer.name = "PortalSwapTimer"
|
|
||||||
# Initial wait time; gets reset when started based on game mode settings
|
|
||||||
swap_timer.wait_time = 15.0
|
|
||||||
swap_timer.timeout.connect(_on_swap_timer_timeout)
|
|
||||||
add_child(swap_timer)
|
|
||||||
|
|
||||||
# Tile Refresh Timer (25s)
|
|
||||||
tile_refresh_timer = Timer.new()
|
|
||||||
tile_refresh_timer.name = "TileRefreshTimer"
|
|
||||||
# Initial wait time; gets reset when started based on game mode settings
|
|
||||||
tile_refresh_timer.wait_time = 25.0
|
|
||||||
tile_refresh_timer.timeout.connect(_on_tile_refresh_timer_timeout)
|
|
||||||
add_child(tile_refresh_timer)
|
|
||||||
|
|
||||||
# Connect to mission tracking
|
|
||||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
|
||||||
if gcm:
|
|
||||||
gcm.global_timer_updated.connect(_on_global_timer_updated)
|
|
||||||
gcm.goal_count_updated.connect(_on_goal_count_updated)
|
|
||||||
|
|
||||||
_setup_hud()
|
|
||||||
|
|
||||||
func _on_global_timer_updated(time_remaining: float):
|
|
||||||
if not multiplayer.is_server(): return
|
|
||||||
|
|
||||||
# Last 30 seconds: Reveal Finish Room
|
|
||||||
if time_remaining <= 30.0 and not finish_spawned:
|
|
||||||
_spawn_finish_room()
|
|
||||||
|
|
||||||
func start_game_mode():
|
|
||||||
if not multiplayer.is_server(): return
|
|
||||||
|
|
||||||
if arena_setup_done and not doors.is_empty():
|
|
||||||
print("[PortalModeManager] Arena already setup, starting timers and refresh only.")
|
|
||||||
else:
|
|
||||||
print("[PortalModeManager] Starting Portal Game Mode with full setup...")
|
|
||||||
setup_arena_locally()
|
|
||||||
_randomize_connections()
|
|
||||||
|
|
||||||
# Configure dynamic timings from LobbyManager before starting
|
|
||||||
swap_timer.wait_time = float(LobbyManager.doors_swap_time)
|
|
||||||
tile_refresh_timer.wait_time = float(LobbyManager.doors_refresh_time)
|
|
||||||
|
|
||||||
# Start Timers
|
|
||||||
if swap_timer.is_stopped():
|
|
||||||
swap_timer.start()
|
|
||||||
if tile_refresh_timer.is_stopped():
|
|
||||||
tile_refresh_timer.start()
|
|
||||||
|
|
||||||
# Initial Tile Spawn
|
|
||||||
_refresh_tiles()
|
|
||||||
|
|
||||||
# Show HUD
|
|
||||||
_activate_hud()
|
|
||||||
|
|
||||||
func _activate_hud():
|
|
||||||
if hud_layer:
|
|
||||||
hud_layer.visible = true
|
|
||||||
_update_hud_visuals()
|
|
||||||
|
|
||||||
func activate_client_side():
|
|
||||||
"""Called on clients to show HUD and prepare local state."""
|
|
||||||
print("[PortalModeManager] Activating client-side HUD")
|
|
||||||
_activate_hud()
|
|
||||||
# Initial update to catch any missed goal counts
|
|
||||||
_update_hud_visuals()
|
|
||||||
|
|
||||||
func setup_arena_locally():
|
|
||||||
"""Sets up GridMap size and walls. Called on host and clients."""
|
|
||||||
if arena_setup_done:
|
|
||||||
print("[PortalModeManager] Arena already setup locally, skipping.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("[PortalModeManager] Setting up arena locally...")
|
|
||||||
_setup_arena_size()
|
|
||||||
_setup_room_partitions()
|
|
||||||
_spawn_portal_doors()
|
|
||||||
|
|
||||||
# PRE-FILL TILES: Ensure all floor tiles have items before the countdown starts
|
|
||||||
if multiplayer.is_server():
|
|
||||||
_refresh_tiles()
|
|
||||||
|
|
||||||
arena_setup_done = true
|
|
||||||
|
|
||||||
func _setup_arena_size():
|
|
||||||
if not gridmap: return
|
|
||||||
gridmap.columns = GRID_SIZE
|
|
||||||
gridmap.rows = GRID_SIZE
|
|
||||||
gridmap.clear()
|
|
||||||
# Explicitly clear Floor 1 to prevent legacy tiles from previous rounds
|
|
||||||
if gridmap.has_method("clear_grid"):
|
|
||||||
gridmap.clear_grid(1)
|
|
||||||
|
|
||||||
# Fill Floor 0 with standard floor (Item ID 0)
|
|
||||||
for x in range(GRID_SIZE):
|
|
||||||
for z in range(GRID_SIZE):
|
|
||||||
gridmap.set_cell_item(Vector3i(x, 0, z), 0)
|
|
||||||
|
|
||||||
func get_spawn_points() -> Array[Vector2i]:
|
|
||||||
# One point per quadrant
|
|
||||||
return [
|
|
||||||
Vector2i(3, 3), # Room 0
|
|
||||||
Vector2i(10, 3), # Room 1
|
|
||||||
Vector2i(3, 10), # Room 2
|
|
||||||
Vector2i(10, 10) # Room 3
|
|
||||||
]
|
|
||||||
|
|
||||||
func _setup_hud():
|
|
||||||
hud_layer = CanvasLayer.new()
|
|
||||||
hud_layer.layer = 5
|
|
||||||
hud_layer.visible = false
|
|
||||||
add_child(hud_layer)
|
|
||||||
|
|
||||||
var bottom_container = CenterContainer.new()
|
|
||||||
bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
|
||||||
bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
|
||||||
bottom_container.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
||||||
bottom_container.offset_bottom = -50
|
|
||||||
hud_layer.add_child(bottom_container)
|
|
||||||
|
|
||||||
mission_label = Label.new()
|
|
||||||
mission_label.text = "GOALS (0/8)"
|
|
||||||
mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
||||||
|
|
||||||
var custom_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf")
|
|
||||||
if custom_font: mission_label.add_theme_font_override("font", custom_font)
|
|
||||||
|
|
||||||
mission_label.add_theme_font_size_override("font_size", 28)
|
|
||||||
mission_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
|
||||||
mission_label.add_theme_constant_override("outline_size", 8)
|
|
||||||
bottom_container.add_child(mission_label)
|
|
||||||
|
|
||||||
# Initial update
|
|
||||||
_update_hud_visuals()
|
|
||||||
|
|
||||||
func _update_hud_visuals():
|
|
||||||
if not mission_label: return
|
|
||||||
|
|
||||||
var my_id = multiplayer.get_unique_id()
|
|
||||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
|
||||||
var completed_count = gcm.player_goal_counts.get(my_id, 0) if gcm else 0
|
|
||||||
|
|
||||||
mission_label.text = "GOALS (%d/%d)" % [completed_count, LobbyManager.doors_required_goals]
|
|
||||||
|
|
||||||
if completed_count >= LobbyManager.doors_required_goals:
|
|
||||||
mission_label.text = "ALL GOALS COMPLETE!\nFIND THE FINISH ROOM!"
|
|
||||||
mission_label.add_theme_color_override("font_color", Color.GOLD)
|
|
||||||
|
|
||||||
if not _has_notified_mission_complete:
|
|
||||||
_has_notified_mission_complete = true
|
|
||||||
var player_node = main.get_node_or_null(str(my_id))
|
|
||||||
if player_node:
|
|
||||||
NotificationManager.send_message(player_node, "ALL GOALS COMPLETE!", NotificationManager.MessageType.GOAL)
|
|
||||||
else:
|
|
||||||
mission_label.add_theme_color_override("font_color", Color.WHITE)
|
|
||||||
_has_notified_mission_complete = false
|
|
||||||
|
|
||||||
func is_mission_complete(peer_id: int) -> bool:
|
|
||||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
|
||||||
if not gcm: return false
|
|
||||||
return gcm.player_goal_counts.get(peer_id, 0) >= LobbyManager.doors_required_goals
|
|
||||||
|
|
||||||
func check_win_condition(player_id: int, pos: Vector2i) -> bool:
|
|
||||||
# 1. Check if on finish tile
|
|
||||||
var tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
|
||||||
if tile != 3: return false
|
|
||||||
|
|
||||||
# 2. Check missions
|
|
||||||
return is_mission_complete(player_id)
|
|
||||||
|
|
||||||
func _setup_room_partitions():
|
|
||||||
for i in range(GRID_SIZE):
|
|
||||||
# Vertical wall (middle columns)
|
|
||||||
gridmap.set_cell_item(Vector3i(6, 0, i), 4) # Wall item
|
|
||||||
gridmap.set_cell_item(Vector3i(7, 0, i), 4)
|
|
||||||
|
|
||||||
# Horizontal wall (middle rows)
|
|
||||||
gridmap.set_cell_item(Vector3i(i, 0, 6), 4)
|
|
||||||
gridmap.set_cell_item(Vector3i(i, 0, 7), 4)
|
|
||||||
|
|
||||||
var _pending_sync_data = null
|
|
||||||
|
|
||||||
func _spawn_portal_doors():
|
|
||||||
# 1. Use synced configs if they exist (passed via main.rpc("sync_portal_configs"))
|
|
||||||
var door_configs = get_meta("door_configs") if has_meta("door_configs") else []
|
|
||||||
|
|
||||||
# 2. If no synced configs (e.g. Server start), generate base + extras
|
|
||||||
if door_configs.is_empty():
|
|
||||||
if not multiplayer.is_server():
|
|
||||||
print("[PortalModeManager] Client waiting for portal configs sync...")
|
|
||||||
return
|
|
||||||
|
|
||||||
door_configs = [
|
|
||||||
# BASE DOORS (2 per room)
|
|
||||||
{"room": 0, "pos": Vector2i(6, 2), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
|
||||||
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
|
||||||
{"room": 1, "pos": Vector2i(7, 2), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West
|
|
||||||
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
|
||||||
{"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
|
||||||
{"room": 2, "pos": Vector2i(6, 11), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
|
||||||
{"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
|
||||||
{"room": 3, "pos": Vector2i(7, 11), "rot": PI / 2, "offset": Vector2i(1, 0)} # West
|
|
||||||
]
|
|
||||||
|
|
||||||
# Server adds extras
|
|
||||||
var extra_options = [
|
|
||||||
{"room": 0, "pos": Vector2i(6, 5), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,2)
|
|
||||||
{"room": 1, "pos": Vector2i(7, 5), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West (Gap from 7,2)
|
|
||||||
{"room": 2, "pos": Vector2i(6, 8), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,11)
|
|
||||||
{"room": 3, "pos": Vector2i(7, 8), "rot": PI / 2, "offset": Vector2i(1, 0)} # West (Gap from 7,11)
|
|
||||||
]
|
|
||||||
extra_options.shuffle()
|
|
||||||
door_configs.append(extra_options[0])
|
|
||||||
door_configs.append(extra_options[1])
|
|
||||||
|
|
||||||
# Broadcast to clients
|
|
||||||
main.rpc("sync_portal_configs", door_configs)
|
|
||||||
|
|
||||||
# 3. Spawn the doors
|
|
||||||
if not doors.is_empty(): return # Guard against double spawn
|
|
||||||
|
|
||||||
print("[PortalModeManager] Spawning %d doors. Peer: %d" % [door_configs.size(), multiplayer.get_unique_id()])
|
|
||||||
|
|
||||||
var portal_scene = load("res://scenes/portal_door.tscn")
|
|
||||||
var stands_container = main.get_node_or_null("Stands")
|
|
||||||
if not stands_container:
|
|
||||||
stands_container = Node3D.new()
|
|
||||||
stands_container.name = "Stands"
|
|
||||||
main.add_child(stands_container)
|
|
||||||
|
|
||||||
for i in range(door_configs.size()):
|
|
||||||
var cfg = door_configs[i]
|
|
||||||
if not portal_scene:
|
|
||||||
print("[PortalModeManager] Error: Failed to load portal_door.tscn")
|
|
||||||
break
|
|
||||||
|
|
||||||
var door = portal_scene.instantiate()
|
|
||||||
door.name = "Portal_%d" % i
|
|
||||||
door.room_id = cfg["room"]
|
|
||||||
door.door_id = i
|
|
||||||
door.set_meta("spawn_offset", cfg["offset"]) # Store offset for teleport
|
|
||||||
|
|
||||||
# Position
|
|
||||||
var world_pos = gridmap.map_to_local(Vector3i(cfg["pos"].x, 0, cfg["pos"].y))
|
|
||||||
door.transform.origin = world_pos
|
|
||||||
door.rotation.y = cfg["rot"]
|
|
||||||
|
|
||||||
stands_container.add_child(door, true)
|
|
||||||
doors.append(door)
|
|
||||||
|
|
||||||
# Server-only interaction logic
|
|
||||||
if multiplayer.is_server():
|
|
||||||
door.player_entered_portal.connect(handle_portal_interaction)
|
|
||||||
|
|
||||||
gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor
|
|
||||||
|
|
||||||
print("[PortalModeManager] Finished spawning %d doors" % doors.size())
|
|
||||||
|
|
||||||
# Apply pending sync if it arrived early
|
|
||||||
if _pending_sync_data:
|
|
||||||
print("[PortalModeManager] Applying pending sync data...")
|
|
||||||
sync_portal_data(_pending_sync_data)
|
|
||||||
_pending_sync_data = null
|
|
||||||
|
|
||||||
const PORTAL_COLORS = [
|
|
||||||
Color(0, 1, 1), # Cyan
|
|
||||||
Color(1, 0, 1), # Magenta
|
|
||||||
Color(1, 0, 0), # Red
|
|
||||||
Color(0, 1, 0), # Green
|
|
||||||
Color(1, 0.5, 0) # Orange
|
|
||||||
]
|
|
||||||
|
|
||||||
func _randomize_connections():
|
|
||||||
if not multiplayer.is_server(): return
|
|
||||||
|
|
||||||
print("[PortalModeManager] Swapping portal connections...")
|
|
||||||
connections.clear()
|
|
||||||
|
|
||||||
var door_indices = []
|
|
||||||
for i in range(doors.size()):
|
|
||||||
door_indices.append(i)
|
|
||||||
|
|
||||||
# Shuffle and Validate: ensure no pairs are in the same room
|
|
||||||
var valid_pairing = false
|
|
||||||
var attempts = 0
|
|
||||||
while not valid_pairing and attempts < 100:
|
|
||||||
attempts += 1
|
|
||||||
door_indices.shuffle()
|
|
||||||
valid_pairing = true
|
|
||||||
for i in range(0, door_indices.size(), 2):
|
|
||||||
var a = door_indices[i]
|
|
||||||
var b = door_indices[i + 1]
|
|
||||||
if doors[a].room_id == doors[b].room_id:
|
|
||||||
valid_pairing = false
|
|
||||||
break
|
|
||||||
|
|
||||||
# Prepare sync data
|
|
||||||
var sync_data = [] # [[door_a_id, door_b_id, color], ...]
|
|
||||||
|
|
||||||
# Pair them up and assign colors
|
|
||||||
for i in range(0, door_indices.size(), 2):
|
|
||||||
var a = door_indices[i]
|
|
||||||
var b = door_indices[i + 1]
|
|
||||||
connections[a] = b
|
|
||||||
connections[b] = a
|
|
||||||
|
|
||||||
var color = PORTAL_COLORS[int(i / 2.0) % PORTAL_COLORS.size()]
|
|
||||||
sync_data.append([a, b, color])
|
|
||||||
|
|
||||||
doors[a].target_door_id = b
|
|
||||||
doors[a].portal_color = color
|
|
||||||
|
|
||||||
doors[b].target_door_id = a
|
|
||||||
doors[b].portal_color = color
|
|
||||||
|
|
||||||
# Sync to all clients
|
|
||||||
rpc("sync_portal_data", sync_data)
|
|
||||||
main.rpc("display_message", "PORTALS SWITCHED!")
|
|
||||||
|
|
||||||
func sync_to_client(peer_id: int):
|
|
||||||
"""Syncs current portal connections to a specific client."""
|
|
||||||
var sync_data = []
|
|
||||||
# connections is id -> id
|
|
||||||
# We need to rebuild the pair-based data for the RPC
|
|
||||||
var handled = []
|
|
||||||
for a_id in connections:
|
|
||||||
if a_id in handled: continue
|
|
||||||
var b_id = connections[a_id]
|
|
||||||
var color = doors[a_id].portal_color
|
|
||||||
sync_data.append([a_id, b_id, color])
|
|
||||||
handled.append(a_id)
|
|
||||||
handled.append(b_id)
|
|
||||||
|
|
||||||
rpc_id(peer_id, "sync_portal_data", sync_data)
|
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
|
||||||
func sync_portal_data(data: Array):
|
|
||||||
"""Syncs portal connections and colors to all clients."""
|
|
||||||
print("[PortalModeManager] Received portal sync data. Peed ID: ", multiplayer.get_unique_id())
|
|
||||||
|
|
||||||
# If doors array is empty on client, try to repopulate from Stands group
|
|
||||||
if doors.is_empty():
|
|
||||||
var stands = get_tree().get_nodes_in_group("PortalDoors")
|
|
||||||
# Sort by name to ensure consistent indexing
|
|
||||||
stands.sort_custom(func(a, b): return a.name < b.name)
|
|
||||||
doors = stands
|
|
||||||
|
|
||||||
# If still empty, defer sync until doors are spawned locally
|
|
||||||
if doors.is_empty():
|
|
||||||
print("[PortalModeManager] Doors not yet ready, deferring sync data...")
|
|
||||||
_pending_sync_data = data
|
|
||||||
return
|
|
||||||
|
|
||||||
connections.clear()
|
|
||||||
for pair in data:
|
|
||||||
var a_id = pair[0]
|
|
||||||
var b_id = pair[1]
|
|
||||||
var color = pair[2]
|
|
||||||
|
|
||||||
connections[a_id] = b_id
|
|
||||||
connections[b_id] = a_id
|
|
||||||
|
|
||||||
if a_id < doors.size() and b_id < doors.size():
|
|
||||||
if is_instance_valid(doors[a_id]):
|
|
||||||
doors[a_id].target_door_id = b_id
|
|
||||||
doors[a_id].portal_color = color
|
|
||||||
if is_instance_valid(doors[b_id]):
|
|
||||||
doors[b_id].target_door_id = a_id
|
|
||||||
doors[b_id].portal_color = color
|
|
||||||
else:
|
|
||||||
print("[PortalModeManager] Warning: Door index %d or %d out of range during sync" % [a_id, b_id])
|
|
||||||
|
|
||||||
|
|
||||||
func _on_global_goal_count_updated(_peer_id: int, _count: int):
|
|
||||||
# Mission requirement removed in favor of time-based finish reveal
|
|
||||||
pass
|
|
||||||
|
|
||||||
func _on_goal_count_updated(peer_id: int, _count: int):
|
|
||||||
# Update HUD if relevant (always check if it's the local player whose count changed)
|
|
||||||
if peer_id == multiplayer.get_unique_id():
|
|
||||||
_update_hud_visuals()
|
|
||||||
|
|
||||||
func _spawn_finish_room():
|
|
||||||
print("[PortalModeManager] Time is running out! Revealing Finish Room...")
|
|
||||||
finish_spawned = true
|
|
||||||
|
|
||||||
# Choose a random room quadrant index (0 to 3)
|
|
||||||
var room_idx = randi() % 4
|
|
||||||
|
|
||||||
# Determine center for the selected room quadrant (7x7 rooms)
|
|
||||||
var x_center = 3 if (room_idx == 0 or room_idx == 2) else 10
|
|
||||||
var z_center = 3 if (room_idx == 0 or room_idx == 1) else 10
|
|
||||||
|
|
||||||
# Determine 3x3 bounds around the center
|
|
||||||
var x_start = x_center - 1
|
|
||||||
var x_end = x_center + 2 # exclusive for range()
|
|
||||||
var z_start = z_center - 1
|
|
||||||
var z_end = z_center + 2 # exclusive for range()
|
|
||||||
|
|
||||||
print("[PortalModeManager] Converting 3x3 area in Room %d (X:%d-%d, Z:%d-%d) to Finish Tiles" % [room_idx, x_start, x_end-1, z_start, z_end-1])
|
|
||||||
|
|
||||||
# Iterate through the 3x3 area
|
|
||||||
for x in range(x_start, x_end):
|
|
||||||
for z in range(z_start, z_end):
|
|
||||||
# Only convert walkable floor tiles (Item ID 0) on Floor 0
|
|
||||||
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
||||||
if floor_0_item == 0:
|
|
||||||
# Change Floor 0 tile to Finish Tile (ID 3)
|
|
||||||
main.rpc("sync_grid_item", x, 0, z, 3)
|
|
||||||
|
|
||||||
# Clear any item on Floor 1 above this tile
|
|
||||||
main.rpc("sync_grid_item", x, 1, z, -1)
|
|
||||||
|
|
||||||
# Visual update for server
|
|
||||||
if gridmap.has_method("update_grid_data"):
|
|
||||||
gridmap.update_grid_data()
|
|
||||||
|
|
||||||
main.rpc("display_message", "[ALARM] THE FINISH ROOM HAS APPEARED!")
|
|
||||||
main.rpc("broadcast_message", "SYSTEM", "A 3x3 Finish Zone has appeared in Room %d!" % room_idx, 4) # 4 = MessageType.WARNING
|
|
||||||
|
|
||||||
func _get_room_index(pos: Vector2i) -> int:
|
|
||||||
if pos.x < 7 and pos.y < 7: return 0
|
|
||||||
if pos.x >= 7 and pos.y < 7: return 1
|
|
||||||
if pos.x < 7 and pos.y >= 7: return 2
|
|
||||||
return 3
|
|
||||||
|
|
||||||
func _on_swap_timer_timeout():
|
|
||||||
_randomize_connections()
|
|
||||||
|
|
||||||
func _on_tile_refresh_timer_timeout():
|
|
||||||
_refresh_tiles()
|
|
||||||
main.rpc("display_message", "TILES REPLENISHED!")
|
|
||||||
|
|
||||||
func _refresh_tiles():
|
|
||||||
# GridMap Floor 0 has the walls (ID 4) and floors (ID 0)
|
|
||||||
# GridMap Floor 1 should have the items (Heart, Star, etc)
|
|
||||||
# Cache door positions to avoid spawning under them
|
|
||||||
var door_positions = []
|
|
||||||
for door in doors:
|
|
||||||
if is_instance_valid(door):
|
|
||||||
var local_pos = gridmap.local_to_map(gridmap.to_local(door.global_position))
|
|
||||||
door_positions.append(Vector2i(local_pos.x, local_pos.z))
|
|
||||||
|
|
||||||
for x in range(GRID_SIZE):
|
|
||||||
for z in range(GRID_SIZE):
|
|
||||||
# 1. Check if Floor 0 is a wall or void
|
|
||||||
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
||||||
if floor_0_item in [4, -1]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 1.5. Prevent spawning directly under portal doors
|
|
||||||
if door_positions.has(Vector2i(x, z)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 2. Check if Floor 1 is already occupied
|
|
||||||
if gridmap.get_cell_item(Vector3i(x, 1, z)) != -1:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 3. Spawn a tile (60% chance per valid floor cell)
|
|
||||||
if randf() < 0.6:
|
|
||||||
var weights = ScarcityModel.get_tile_weights()
|
|
||||||
var tile_id = _pick_weighted_tile(weights)
|
|
||||||
# Update GridMap Floor 1 via RPC for sync
|
|
||||||
main.rpc("sync_grid_item", x, 1, z, tile_id)
|
|
||||||
|
|
||||||
func _pick_weighted_tile(weights: Dictionary) -> int:
|
|
||||||
var total_weight = 0
|
|
||||||
for w in weights.values(): total_weight += w
|
|
||||||
|
|
||||||
var r = randi() % total_weight
|
|
||||||
var cumulative = 0
|
|
||||||
for tile in weights:
|
|
||||||
cumulative += weights[tile]
|
|
||||||
if r < cumulative:
|
|
||||||
return tile
|
|
||||||
return 7 # Default Heart
|
|
||||||
|
|
||||||
func handle_portal_interaction(player, door):
|
|
||||||
if not multiplayer.is_server(): return
|
|
||||||
|
|
||||||
var current_time = Time.get_ticks_msec()
|
|
||||||
if player_portal_cooldowns.has(player.name):
|
|
||||||
# Reduce cooldown to 200ms (more responsive than 1s, but enough to avoid jitter)
|
|
||||||
if current_time - player_portal_cooldowns[player.name] < 200:
|
|
||||||
return
|
|
||||||
player_portal_cooldowns[player.name] = current_time
|
|
||||||
|
|
||||||
var source_id = door.door_id
|
|
||||||
if not connections.has(source_id): return
|
|
||||||
|
|
||||||
var target_id = connections[source_id]
|
|
||||||
var target_door = doors[target_id]
|
|
||||||
|
|
||||||
# Use stored offset to avoid infinite loop (spawn inside the target room)
|
|
||||||
var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0, 0)
|
|
||||||
|
|
||||||
var target_world = target_door.global_position
|
|
||||||
var target_grid_3d = gridmap.local_to_map(target_world)
|
|
||||||
var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset
|
|
||||||
|
|
||||||
# Check for overlaps at the target_grid
|
|
||||||
var final_target = target_grid
|
|
||||||
var all_players = get_tree().get_nodes_in_group("Players")
|
|
||||||
var is_occupied = true
|
|
||||||
var search_radius = 0
|
|
||||||
var max_search_radius = 2
|
|
||||||
|
|
||||||
while is_occupied and search_radius <= max_search_radius:
|
|
||||||
is_occupied = false
|
|
||||||
for p in all_players:
|
|
||||||
if p != player and p.current_position == final_target:
|
|
||||||
is_occupied = true
|
|
||||||
break
|
|
||||||
|
|
||||||
if is_occupied:
|
|
||||||
# Try to find an adjacent cell
|
|
||||||
search_radius += 1
|
|
||||||
var found_empty = false
|
|
||||||
# Check immediate neighbors first
|
|
||||||
var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1),
|
|
||||||
Vector2i(1, 1), Vector2i(-1, 1), Vector2i(1, -1), Vector2i(-1, -1)]
|
|
||||||
for offset_vec in offsets:
|
|
||||||
var test_pos = final_target + offset_vec
|
|
||||||
# Check if it's strictly a floor tile (ID 0) on Floor 0, not a wall
|
|
||||||
if gridmap.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) == 0:
|
|
||||||
# Verify no player is on this test_pos
|
|
||||||
var test_occupied = false
|
|
||||||
for p in all_players:
|
|
||||||
if p != player and p.current_position == test_pos:
|
|
||||||
test_occupied = true
|
|
||||||
break
|
|
||||||
if not test_occupied:
|
|
||||||
final_target = test_pos
|
|
||||||
found_empty = true
|
|
||||||
break
|
|
||||||
|
|
||||||
if found_empty:
|
|
||||||
is_occupied = false
|
|
||||||
|
|
||||||
print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, final_target, target_id])
|
|
||||||
|
|
||||||
# Snap player
|
|
||||||
if player.has_method("set_spawn_position"):
|
|
||||||
player.rpc("set_spawn_position", final_target)
|
|
||||||
@@ -547,19 +547,17 @@ 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)
|
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request)
|
||||||
item_id = rng.randi_range(7, 10)
|
item_id = rng.randi_range(7, 10)
|
||||||
elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET):
|
elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET):
|
||||||
# Gauntlet mode: No power-up tile spawns from world.
|
# Gauntlet mode: mostly common tiles, but ghost (14) can spawn too.
|
||||||
# Only common tiles (7-10) spawn; Smack/Cleanser are handled separately.
|
if rng.randf() < 0.85:
|
||||||
item_id = rng.randi_range(7, 10)
|
item_id = rng.randi_range(7, 10)
|
||||||
|
else:
|
||||||
|
item_id = 14 # Ghost powerup only
|
||||||
else:
|
else:
|
||||||
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
|
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
|
||||||
if rng.randf() < 0.8:
|
if rng.randf() < 0.8:
|
||||||
item_id = rng.randi_range(7, 10)
|
item_id = rng.randi_range(7, 10)
|
||||||
else:
|
else:
|
||||||
# 20% Chance for PowerUp
|
# 20% Chance for PowerUp
|
||||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
||||||
# Restrict to Speed (11) and Ghost (14) for Tekton Doors
|
|
||||||
item_id = [11, 14].pick_random()
|
|
||||||
else:
|
|
||||||
item_id = rng.randi_range(11, 14)
|
item_id = rng.randi_range(11, 14)
|
||||||
|
|
||||||
var cell = Vector3i(pos.x, 1, pos.y)
|
var cell = Vector3i(pos.x, 1, pos.y)
|
||||||
|
|||||||
@@ -18,12 +18,6 @@ const SCHEMA = {
|
|||||||
"sng_stop_duration": {"type": TYPE_INT, "default": 4, "min": 2, "max": 10},
|
"sng_stop_duration": {"type": TYPE_INT, "default": 4, "min": 2, "max": 10},
|
||||||
"sng_required_goals": {"type": TYPE_INT, "default": 8, "min": 3, "max": 20}
|
"sng_required_goals": {"type": TYPE_INT, "default": 8, "min": 3, "max": 20}
|
||||||
},
|
},
|
||||||
"Tekton Doors": {
|
|
||||||
"match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600},
|
|
||||||
"doors_swap_time": {"type": TYPE_INT, "default": 15, "min": 10, "max": 30},
|
|
||||||
"doors_refresh_time": {"type": TYPE_INT, "default": 25, "min": 15, "max": 40},
|
|
||||||
"doors_required_goals": {"type": TYPE_INT, "default": 8, "min": 5, "max": 12}
|
|
||||||
},
|
|
||||||
"Candy Pump Survival": {
|
"Candy Pump Survival": {
|
||||||
"match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600},
|
"match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600},
|
||||||
"gauntlet_growth_interval": {"type": TYPE_FLOAT, "default": 3.0, "min": 1.0, "max": 10.0},
|
"gauntlet_growth_interval": {"type": TYPE_FLOAT, "default": 3.0, "min": 1.0, "max": 10.0},
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
extends StaticBody3D
|
|
||||||
|
|
||||||
# PortalDoor.gd
|
|
||||||
# Specialized door for "Tekton Doors" mode.
|
|
||||||
# Teleports players to a target room/door when they step into it.
|
|
||||||
|
|
||||||
signal player_entered_portal(player_node, door_node)
|
|
||||||
|
|
||||||
@export var room_id: int = 0
|
|
||||||
@export var door_id: int = 0 # 0: North, 1: South, 2: East, 3: West
|
|
||||||
|
|
||||||
# State synced by PortalModeManager
|
|
||||||
var target_room_id: int = -1
|
|
||||||
var target_door_id: int = -1
|
|
||||||
var is_active: bool = true
|
|
||||||
var portal_color: Color = Color.WHITE: set = set_portal_color
|
|
||||||
|
|
||||||
func set_portal_color(value: Color):
|
|
||||||
portal_color = value
|
|
||||||
_update_visuals()
|
|
||||||
|
|
||||||
@onready var detection_area: Area3D = $Area3D
|
|
||||||
|
|
||||||
func _ready():
|
|
||||||
add_to_group("PortalDoors")
|
|
||||||
if detection_area:
|
|
||||||
detection_area.body_entered.connect(_on_body_entered)
|
|
||||||
|
|
||||||
# Visual feedback: indicate door is active
|
|
||||||
_update_visuals()
|
|
||||||
|
|
||||||
# Adjust GroundIndicator position based on spawn_offset metadata
|
|
||||||
_adjust_indicator_position()
|
|
||||||
|
|
||||||
func _on_body_entered(body: Node3D):
|
|
||||||
if not is_active: return
|
|
||||||
|
|
||||||
if body.is_in_group("Players") or body.get("is_bot"):
|
|
||||||
var current_time = Time.get_ticks_msec()
|
|
||||||
if body.has_meta("last_portal_time"):
|
|
||||||
# Reduce cooldown to 200ms to match manager logic and allow fast re-entry
|
|
||||||
if current_time - body.get_meta("last_portal_time") < 200:
|
|
||||||
return
|
|
||||||
|
|
||||||
body.set_meta("last_portal_time", current_time)
|
|
||||||
|
|
||||||
print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id])
|
|
||||||
emit_signal("player_entered_portal", body, self )
|
|
||||||
|
|
||||||
var _materials_initialized: bool = false
|
|
||||||
|
|
||||||
func _update_visuals():
|
|
||||||
# Removed is_node_ready() check to allow early setter calls to prepare variables,
|
|
||||||
# but we still need the nodes to exist to apply them.
|
|
||||||
if not is_inside_tree(): return
|
|
||||||
|
|
||||||
var vortex = get_node_or_null("Vortex")
|
|
||||||
var frame_left = get_node_or_null("Frame_Left")
|
|
||||||
|
|
||||||
# If children aren't there yet, we can't update visuals.
|
|
||||||
# This usually happens if called before or during early _ready.
|
|
||||||
if not vortex or not frame_left: return
|
|
||||||
|
|
||||||
if not _materials_initialized:
|
|
||||||
_initialize_unique_materials()
|
|
||||||
_materials_initialized = true
|
|
||||||
|
|
||||||
if vortex:
|
|
||||||
var mat = vortex.get_surface_override_material(0)
|
|
||||||
if mat:
|
|
||||||
mat.albedo_color = portal_color
|
|
||||||
mat.albedo_color.a = 0.5
|
|
||||||
if mat.has_method("set_emission"):
|
|
||||||
mat.set("emission", portal_color)
|
|
||||||
|
|
||||||
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
|
||||||
var frame = get_node_or_null(part_name)
|
|
||||||
if frame:
|
|
||||||
var mat = frame.get_surface_override_material(0)
|
|
||||||
if mat:
|
|
||||||
mat.albedo_color = portal_color.lerp(Color.BLACK, 0.4)
|
|
||||||
|
|
||||||
var ground = get_node_or_null("GroundIndicator")
|
|
||||||
if ground:
|
|
||||||
var mat = ground.get_surface_override_material(0)
|
|
||||||
if mat:
|
|
||||||
mat.albedo_color = portal_color
|
|
||||||
mat.albedo_color.a = 0.4
|
|
||||||
mat.emission = portal_color
|
|
||||||
mat.emission_energy_multiplier = 2.0
|
|
||||||
|
|
||||||
func _initialize_unique_materials():
|
|
||||||
var vortex = get_node_or_null("Vortex")
|
|
||||||
if vortex:
|
|
||||||
var mat = vortex.get_surface_override_material(0)
|
|
||||||
if not mat:
|
|
||||||
mat = vortex.mesh.surface_get_material(0)
|
|
||||||
|
|
||||||
if mat:
|
|
||||||
vortex.set_surface_override_material(0, mat.duplicate())
|
|
||||||
|
|
||||||
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
|
||||||
var frame = get_node_or_null(part_name)
|
|
||||||
if frame:
|
|
||||||
var mat = frame.get_surface_override_material(0)
|
|
||||||
if not mat:
|
|
||||||
mat = frame.mesh.surface_get_material(0)
|
|
||||||
|
|
||||||
if mat:
|
|
||||||
frame.set_surface_override_material(0, mat.duplicate())
|
|
||||||
|
|
||||||
var ground = get_node_or_null("GroundIndicator")
|
|
||||||
if ground:
|
|
||||||
var mat = ground.get_surface_override_material(0)
|
|
||||||
if not mat:
|
|
||||||
mat = ground.mesh.surface_get_material(0)
|
|
||||||
if mat:
|
|
||||||
ground.set_surface_override_material(0, mat.duplicate())
|
|
||||||
|
|
||||||
func _adjust_indicator_position():
|
|
||||||
# This uses the spawn_offset metadata set by PortalModeManager
|
|
||||||
# to push the ground indicator "into" the room.
|
|
||||||
if not has_meta("spawn_offset"): return
|
|
||||||
|
|
||||||
var ground = get_node_or_null("GroundIndicator")
|
|
||||||
if not ground: return
|
|
||||||
|
|
||||||
var offset_2d = get_meta("spawn_offset") # Vector2i
|
|
||||||
var offset_3d = Vector3(offset_2d.x, 0, offset_2d.y)
|
|
||||||
|
|
||||||
# Convert the global direction (into the room) to local coordinates
|
|
||||||
var local_dir = to_local(global_position + offset_3d).normalized()
|
|
||||||
|
|
||||||
# Nudge the indicator in that direction
|
|
||||||
ground.position = local_dir * 0.5 # Reduced from 0.6 to close the gap
|
|
||||||
ground.position.y = 0.05 # Keep it just above the floor
|
|
||||||
+1
-3
@@ -514,9 +514,7 @@ func spawn_tiles_around(count: int = 4):
|
|||||||
if LobbyManager:
|
if LobbyManager:
|
||||||
mode = LobbyManager.get_game_mode()
|
mode = LobbyManager.get_game_mode()
|
||||||
|
|
||||||
if LobbyManager and LobbyManager.get_game_mode() == GameMode.Mode.TEKTON_DOORS:
|
if mode == GameMode.Mode.FREEMODE:
|
||||||
item_id = [11, 14].pick_random()
|
|
||||||
elif mode == GameMode.Mode.FREEMODE:
|
|
||||||
item_id = rng.randi_range(7, 10) # No powerups in freemode either, just normal tiles
|
item_id = rng.randi_range(7, 10) # No powerups in freemode either, just normal tiles
|
||||||
else:
|
else:
|
||||||
item_id = rng.randi_range(11, 14)
|
item_id = rng.randi_range(11, 14)
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ func _ready() -> void:
|
|||||||
pull_10_btn.pressed.connect(func(): _do_pull(10))
|
pull_10_btn.pressed.connect(func(): _do_pull(10))
|
||||||
close_result_btn.pressed.connect(func(): result_panel.hide())
|
close_result_btn.pressed.connect(func(): result_panel.hide())
|
||||||
craft_btn.pressed.connect(_on_open_craft)
|
craft_btn.pressed.connect(_on_open_craft)
|
||||||
|
|
||||||
|
if UserProfileManager.profile_updated.connect(_refresh_ui) != OK:
|
||||||
|
pass
|
||||||
|
|
||||||
result_panel.hide()
|
result_panel.hide()
|
||||||
_switch_banner("star")
|
_switch_banner("star")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,1963 @@
|
|||||||
|
# Tekton Nakama Lua Server API Reference
|
||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> Auto-generated from source code. For AI agents -- call these RPCs without reading Lua.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
1. [Authentication & Core (core.lua)](#1-authentication--core-coreluau)
|
||||||
|
2. [Utilities (utils.lua)](#2-utilities-utilsluau)
|
||||||
|
3. [User Module (user.lua)](#3-user-module-userluau)
|
||||||
|
4. [Economy Module (economy.lua)](#4-economy-module-economyluau)
|
||||||
|
5. [Gacha Module (gacha.lua)](#5-gacha-module-gachaluau)
|
||||||
|
6. [Leaderboard Module (leaderboard.lua)](#6-leaderboard-module-leaderboardluau)
|
||||||
|
7. [Inbox/Mail Module (inbox.lua)](#7-inboxmail-module-inboxluau)
|
||||||
|
8. [Daily Rewards Module (daily_rewards.lua)](#8-daily-rewards-module-daily_rewardsluau)
|
||||||
|
9. [Admin Module (admin.lua)](#9-admin-module-adminluau)
|
||||||
|
10. [Storage Collections Reference](#10-storage-collections-reference)
|
||||||
|
11. [Leaderboard Config](#11-leaderboard-config)
|
||||||
|
12. [Error Strings Reference](#12-error-strings-reference)
|
||||||
|
13. [Permission Levels](#13-permission-levels)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Authentication & Core (`core.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### After-Hook: `after_authenticate_steam`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Trigger** | `AuthenticateSteam` |
|
||||||
|
| **Purpose** | On first Steam login: sets display_name from Steam username, default role to `"player"` |
|
||||||
|
| **Auth** | None (internal hook) |
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- If user has no `display_name`, sets it from `input.username` (fallback: `"SteamPlayer"`)
|
||||||
|
- If user metadata has no `role`, sets `metadata.role = "player"`
|
||||||
|
|
||||||
|
### Startup (not an RPC)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
nk.leaderboard_create("global_high_score", true, "desc", "best", nil, {})
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates native leaderboard `"global_high_score"` on module load (also created in `leaderboard.lua` -- pcall wraps both so duplicate is safe).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Utilities (`utils.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Shared helpers used by other modules.
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Constant | Value | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `ADMIN_ROLES` | `{admin=true, moderator=true, owner=true}` | Roles with admin privileges |
|
||||||
|
| `SYSTEM_USER_ID` | `"00000000-0000-0000-0000-000000000000"` | System/nil user for global storage |
|
||||||
|
| `CHANNEL_TYPE_ROOM` | `1` | Nakama channel type for rooms |
|
||||||
|
| `CHANNEL_TYPE_DIRECT` | `2` | Nakama channel type for DMs |
|
||||||
|
| `CHANNEL_TYPE_GROUP` | `3` | Nakama channel type for groups |
|
||||||
|
|
||||||
|
### `utils.is_admin(context)`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Checks if caller has admin/moderator/owner role.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `context` | table | Nakama RPC context |
|
||||||
|
|
||||||
|
**Returns:** `boolean`
|
||||||
|
|
||||||
|
### `utils.is_match_host(context, match_id)`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Checks if caller is the host of a match.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `context` | table | Nakama RPC context |
|
||||||
|
| `match_id` | string | Match ID to check |
|
||||||
|
|
||||||
|
**Returns:** `boolean`
|
||||||
|
**Note:** Reads `match.state` and checks `state.hostUserId` against `context.user_id`.
|
||||||
|
|
||||||
|
### `utils.require_admin(context)`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Errors with `"Admin privileges required"` if caller is not admin/moderator/owner.
|
||||||
|
|
||||||
|
### `utils.require_admin_or_host(context, match_id)`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Errors with `"Admin or host privileges required"` if caller is neither admin nor match host.
|
||||||
|
|
||||||
|
### `utils.resolve_channel_id(channel_id)`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Resolves a friendly room name (e.g. `"social_global"`) to a hashed Nakama channel ID.
|
||||||
|
|
||||||
|
| Param | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `channel_id` | string | Raw channel ID or friendly room name |
|
||||||
|
|
||||||
|
**Returns:** `string` -- resolved channel ID (passes through if already hashed, i.e. contains `"."`)
|
||||||
|
**Logic:** If no `"."` in value, calls `nk.channel_id_build("", name, CHANNEL_TYPE_ROOM)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. User Module (`user.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### RPC: `get_user_profile`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_get_user_profile` |
|
||||||
|
| **Auth** | None required (can view any profile) |
|
||||||
|
| **Purpose** | Get a user's public profile |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `user_id` | string | No | `context.user_id` | Target user ID |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | User ID |
|
||||||
|
| `username` | string | Username |
|
||||||
|
| `display_name` | string | Display name |
|
||||||
|
| `avatar_url` | string | Avatar URL |
|
||||||
|
| `create_time` | string | Account creation time |
|
||||||
|
| `role` | string | Role from metadata (`"player"` default) |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Account not found"` -- user doesn't exist
|
||||||
|
- `"Account banned until <time>. Reason: <reason>"` -- temporary ban still active
|
||||||
|
- `"Account permanently banned. Reason: <reason>"` -- permanent ban
|
||||||
|
|
||||||
|
**Ban logic:** Reads `metadata.banned`, `metadata.ban_expires` (Unix timestamp), `metadata.ban_reason`. If `ban_expires <= os.time()`, auto-clears the ban on read.
|
||||||
|
|
||||||
|
### RPC: `update_user_profile`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_update_user_profile` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Update own display name and/or avatar |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `display_name` | string | No | New display name |
|
||||||
|
| `avatar_url` | string | No | New avatar URL |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Failed to update profile"`
|
||||||
|
|
||||||
|
### RPC: `search_users`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_search_users` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Search users by username or display name |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `query` | string | No | `""` | Search term (ILike match on username/display_name). Empty = list all (limit 100). |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `users` | array | Array of user objects |
|
||||||
|
|
||||||
|
**User object:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | User ID |
|
||||||
|
| `username` | string | Username |
|
||||||
|
| `display_name` | string | Display name |
|
||||||
|
| `avatar_url` | string | Avatar URL from metadata |
|
||||||
|
|
||||||
|
**SQL:** Queries `users` table with `ILIKE`. Excludes system user `00000000-0000-0000-0000-000000000000`. Limit 100.
|
||||||
|
|
||||||
|
### RPC: `change_credentials`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_change_credentials` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Change email + password credentials |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `new_email` | string | No | New email |
|
||||||
|
| `new_password` | string | No | New password |
|
||||||
|
| `current_password` | string | If email set | Current password (required to unlink old email) |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Current password required"` -- account has email but no `current_password` provided
|
||||||
|
- `"Incorrect current password."` -- email auth re-verify failed
|
||||||
|
- `"Failed to set new credentials: <err>"` -- link_email failed (rolls back old link)
|
||||||
|
|
||||||
|
**Logic:** Verifies current password via `nk.authenticate_email`, unlinks old email, links new email+password. On failure, re-links old credentials.
|
||||||
|
|
||||||
|
### RPC: `send_lobby_invite`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_send_lobby_invite` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Send a lobby invitation notification |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `to_user_id` | string | Yes | Recipient user ID |
|
||||||
|
| `match_id` | string | Yes | Match/lobby ID to invite to |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Missing to_user_id or match_id"`
|
||||||
|
|
||||||
|
**Notification:** Code `1001`, persistent (`true`). Content: `{match_id, from_name}`.
|
||||||
|
|
||||||
|
### RPC: `send_friend_request`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_send_friend_request` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Send a friend request notification |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | Target user ID |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"user_id is required"`
|
||||||
|
- `"Cannot add yourself"`
|
||||||
|
|
||||||
|
**Notification:** Code `1002`, persistent (`true`). Content: `{from_user_id, from_name}`.
|
||||||
|
|
||||||
|
### RPC: `admin_get_user_history`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `user.rpc_admin_get_user_history` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Get wallet ledger, login history, and match history for a user |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | Target user ID |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `history.wallet_ledger` | array | Last 50 wallet ledger entries |
|
||||||
|
| `history.logins` | array | Login history (up to 20) |
|
||||||
|
| `history.matches` | array | Match history from storage collection `"matches"` |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Admin privileges required"` (via `utils.require_admin`)
|
||||||
|
- `"user_id is required"`
|
||||||
|
|
||||||
|
### After-Hook: `after_authenticate`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Trigger** | `AuthenticateDevice`, `AuthenticateEmail`, `AuthenticateCustom` |
|
||||||
|
| **Purpose** | Record login timestamp + IP to user storage |
|
||||||
|
|
||||||
|
**Storage write:** Collection `"history"`, key `"logins"`, permission_read=`0`, permission_write=`0`. Keeps last 20 entries. Each entry: `{time, ip}`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Economy Module (`economy.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Shop Catalog
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Hardcoded items available for purchase:
|
||||||
|
|
||||||
|
| ID | Name | Category | Gold | Star | Rarity | Character |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `oldpop-blue-hat` | Oldpop Blue Hat | head | 100 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-green-hat` | Oldpop Green Hat | head | 100 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-red-hat` | Oldpop Red Hat | head | 100 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-yellow-hat` | Oldpop Yellow Hat | head | 100 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-og-pant` | Copper OG Pant | costume | 0 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-grey-pant` | Copper Grey Pant | costume | 150 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-red-pant` | Copper Red Pant | costume | 150 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-yellow-pant` | Copper Yellow Pant | costume | 150 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-blue-gloves` | Oldpop Blue Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-green-gloves` | Oldpop Green Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-red-gloves` | Oldpop Red Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||||
|
| `oldpop-yellow-gloves` | Oldpop Yellow Gloves | glove | 75 | 0 | Common | Oldpop |
|
||||||
|
|
||||||
|
### RPC: `get_shop_catalog`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `economy.rpc_get_shop_catalog` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Get the shop catalog grouped by category, plus featured banners |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `catalog` | object | Grouped by category (`head`, `costume`, `glove`). Each entry: `{id, name, gold, star, rarity, character}` |
|
||||||
|
| `featured_banners` | array | Banners from `shop_config`/`featured_banners` (max 3 items, each: `{item_id, ...}`) |
|
||||||
|
|
||||||
|
**Errors:** `"Not authenticated"`
|
||||||
|
|
||||||
|
### RPC: `buy_currency`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `economy.rpc_buy_currency` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Purchase a currency package (IAP) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `package_id` | string | Yes | One of: `gold_100`, `gold_500`, `gold_1000`, `gold_2000`, `gold_5000`, `gold_10000`, `star_100`, `star_250`, `star_600` |
|
||||||
|
| `receipt` | string | For gold packages | Store receipt for validation |
|
||||||
|
| `idempotency_key` | string | Yes | Idempotency key (prevents double-claim) |
|
||||||
|
| `store_type` | string | No | `"google"`, `"apple"`, or `"test"` (default: `"test"`) |
|
||||||
|
|
||||||
|
**Package table:**
|
||||||
|
|
||||||
|
| Package ID | Gold Change | Star Change | Requires Verification |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `gold_100` | +100 | 0 | Yes |
|
||||||
|
| `gold_500` | +550 | 0 | Yes |
|
||||||
|
| `gold_1000` | +1150 | 0 | Yes |
|
||||||
|
| `gold_2000` | +2400 | 0 | Yes |
|
||||||
|
| `gold_5000` | +6250 | 0 | Yes |
|
||||||
|
| `gold_10000` | +13000 | 0 | Yes |
|
||||||
|
| `star_100` | -500 | +100 | No |
|
||||||
|
| `star_250` | -1100 | +250 | No |
|
||||||
|
| `star_600` | -2500 | +600 | No |
|
||||||
|
|
||||||
|
**Returns (JSON) -- success:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `status` | string | `"verified"` (receipt validated) or `"pending"` (no receipt, awaiting verification) |
|
||||||
|
| `package_id` | string | Package ID |
|
||||||
|
| `duplicate` | bool | (optional) `true` if idempotency key already used |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Package ID required"`
|
||||||
|
- `"Idempotency key required"`
|
||||||
|
- `"Invalid package ID"`
|
||||||
|
- `"InvalidReceipt"` -- IAP validation failed
|
||||||
|
- `"NotEnoughFunds"` -- wallet update failed (e.g. insufficient gold for star packages)
|
||||||
|
|
||||||
|
**Idempotency:** Checks `receipts` collection with idempotency_key. Returns existing result on duplicate.
|
||||||
|
|
||||||
|
**Receipt validation:** For `store_type="test"`, accepts `receipt == "mock_receipt_for_now"`.
|
||||||
|
|
||||||
|
### RPC: `purchase_item`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `economy.rpc_purchase_item` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Purchase an item from the shop catalog |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `item_id` | string | Yes | -- | Item ID from catalog |
|
||||||
|
| `quantity` | number | No | 1 | Quantity (min 1) |
|
||||||
|
| `idempotency_key` | string | Yes | -- | Idempotency key |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `item` | string | Item ID purchased |
|
||||||
|
| `duplicate` | bool | (optional) `true` if idempotency key already used |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Item ID required"`
|
||||||
|
- `"Invalid quantity"`
|
||||||
|
- `"Idempotency key required"`
|
||||||
|
- `"ItemNotFound"`
|
||||||
|
- `"NotEnoughFunds"` -- insufficient gold/star balance
|
||||||
|
- `"PurchaseFailed"` -- storage write failed
|
||||||
|
|
||||||
|
**Storage writes:**
|
||||||
|
- Collection `"inventory"`, key `item_id`: `{category, purchased_at, quantity}`
|
||||||
|
- Collection `"receipts"`, key `idempotencyKey`: `{type="item", item_id, quantity, cost, processed_at}`
|
||||||
|
|
||||||
|
### RPC: `admin_set_featured_banners`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `economy.rpc_admin_set_featured_banners` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Set up to 3 featured banner items for the shop |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `banners` | array | No | `[]` | Array of banner objects, each with `{item_id, ...}` (max 3) |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `banners` | array | Final banners array (max 3) |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Admin privileges required"`
|
||||||
|
- `"Item not found in catalog: <itemId>"` -- banner references a non-catalog item
|
||||||
|
|
||||||
|
**Storage:** Collection `"shop_config"`, key `"featured_banners"`, user `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||||
|
|
||||||
|
### RPC: `admin_get_featured_banners`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `economy.rpc_admin_get_featured_banners` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Get current featured banners config |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `banners` | array | Current banners (may be empty) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Gacha Module (`gacha.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Gacha Banners
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Banner ID | Name | Currency | Pull 1 Cost | Pull 10 Cost | Pity At |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `star` | Star Banner | star | 160 | 1440 | 90 |
|
||||||
|
| `gold` | Gold Banner | gold | 50 | 450 | 90 |
|
||||||
|
|
||||||
|
### Drop Rates
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Rarity | Probability | Pool Items |
|
||||||
|
|---|---|---|
|
||||||
|
| `real_prize` | 1.0% | `skin_gacha_rainbow_suit`, `skin_gacha_dragon_hat`, `skin_gacha_phantom_gloves`, `skin_gacha_neon_acc` |
|
||||||
|
| `rare` | 14.0% | `frag_rare` |
|
||||||
|
| `uncommon` | 25.0% | `frag_uncommon` |
|
||||||
|
| `common` | 60.0% | `frag_common` |
|
||||||
|
|
||||||
|
### Real Prize Catalog
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Item ID | Name | Category |
|
||||||
|
|---|---|---|
|
||||||
|
| `skin_gacha_rainbow_suit` | Rainbow Suit | costume |
|
||||||
|
| `skin_gacha_dragon_hat` | Dragon Hat | head |
|
||||||
|
| `skin_gacha_phantom_gloves` | Phantom Gloves | glove |
|
||||||
|
| `skin_gacha_neon_acc` | Neon Accessory | accessory |
|
||||||
|
|
||||||
|
### Fragment Items
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| ID | Name | Rarity |
|
||||||
|
|---|---|---|
|
||||||
|
| `frag_common` | Common Fragment | common |
|
||||||
|
| `frag_uncommon` | Uncommon Fragment | uncommon |
|
||||||
|
| `frag_rare` | Rare Fragment | rare |
|
||||||
|
|
||||||
|
### RPC: `perform_gacha_pull`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `gacha.rpc_perform_gacha_pull` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Perform 1 or 10 gacha pulls on a banner |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `banner_id` | string | Yes | -- | `"star"` or `"gold"` |
|
||||||
|
| `count` | number | No | 1 | Number of pulls (1 or 10) |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
| `results` | array | Array of pull results: `{id, rarity, name}` |
|
||||||
|
| `new_pity` | number | Updated pity counter for this banner |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Banner ID required"`
|
||||||
|
- `"Invalid count"`
|
||||||
|
- `"Unknown banner: <banner_id>"`
|
||||||
|
- `"Could not read account"`
|
||||||
|
- `"Insufficient currency"`
|
||||||
|
- `"Failed to update wallet"`
|
||||||
|
- `"Failed to write storage: <err>"`
|
||||||
|
|
||||||
|
**Pity system:** After `pity_at` (90) pulls without a `real_prize`, the next pull is guaranteed `real_prize`. Pity counter resets on `real_prize` drop. Pity counter persists per banner in storage.
|
||||||
|
|
||||||
|
**Storage reads/writes:**
|
||||||
|
- Collection `"profiles"`, key `"pity_counters"`: per-banner pity counts
|
||||||
|
- Collection `"profiles"`, key `"fragments"`: fragment inventory
|
||||||
|
- Collection `"inventory"`, key = item_id: real prize items
|
||||||
|
|
||||||
|
**Wallet:** Deducts currency cost. For `count=10`, uses `pull_10_cost` if available, otherwise `pull_1_cost * count`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Leaderboard Module (`leaderboard.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Leaderboard Config
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `"global_high_score"` |
|
||||||
|
| **Authoritative** | `true` |
|
||||||
|
| **Sort Order** | `"desc"` (highest first) |
|
||||||
|
| **Operator** | `"best"` (best score kept) |
|
||||||
|
| **Reset Schedule** | `nil` (never resets) |
|
||||||
|
|
||||||
|
### RPC: `get_leaderboard_stats`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_get_leaderboard_stats` |
|
||||||
|
| **Auth** | None |
|
||||||
|
| **Purpose** | Get top 50 leaderboard entries |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `leaderboard` | array | Top 50 leaderboard entries |
|
||||||
|
|
||||||
|
**Leaderboard entry object:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | Owner user ID |
|
||||||
|
| `username` | string | Username |
|
||||||
|
| `display_name` | string | Username (fallback -- actually `record.username`) |
|
||||||
|
| `avatar_url` | string | Avatar URL from metadata |
|
||||||
|
| `loadout_character` | string | Character from metadata (default `"Copper"`) |
|
||||||
|
| `high_score` | number | Score |
|
||||||
|
| `games_played` | number | From metadata |
|
||||||
|
| `games_won` | number | From metadata |
|
||||||
|
|
||||||
|
### RPC: `submit_score`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_submit_score` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Submit/update own score on the leaderboard |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `score` | number | No | 0 | Score to submit |
|
||||||
|
| `games_played` | number | No | 0 | Games played count |
|
||||||
|
| `games_won` | number | No | 0 | Games won count |
|
||||||
|
| `avatar_url` | string | No | From account | Avatar URL |
|
||||||
|
| `loadout_character` | string | No | `"Copper"` | Character selection |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Failed to submit score"`
|
||||||
|
|
||||||
|
### RPC: `sync_leaderboard`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_sync_leaderboard` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Bulk-sync all users' stats from `"stats"` and `"profiles"` collections to the native leaderboard |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | Always true |
|
||||||
|
| `synced` | number | Number of records written |
|
||||||
|
| `objects_found` | number | Stats objects found |
|
||||||
|
| `debug` | array | Error messages per user |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Sync failed: <err>"`
|
||||||
|
|
||||||
|
**Storage reads:** Collection `"stats"` (all users, limit 100), Collection `"profiles"` (all users, limit 100), key `"profile"`.
|
||||||
|
|
||||||
|
### RPC: `reset_stats`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_reset_stats` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Delete own leaderboard record and reset stats to zero |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
|
||||||
|
**Storage write:** Collection `"stats"`, key `"game_stats"` -- zeros out `games_played`, `games_won`, `high_score`, `total_kills`, `total_deaths`. permission_read=`2`, permission_write=`1`.
|
||||||
|
|
||||||
|
### RPC: `admin_update_stats`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_admin_update_stats` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Force-overwrite a user's stats and leaderboard record |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | Target user ID |
|
||||||
|
| `stats` | object | Yes | Stats object: `{high_score, games_played, games_won, loadout_character, ...}` |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Admin privileges required"`
|
||||||
|
- `"User ID and stats are required"`
|
||||||
|
|
||||||
|
**Storage write:** Collection `"stats"`, key `"game_stats"`, permission_read=`1`, permission_write=`0`.
|
||||||
|
|
||||||
|
### RPC: `admin_delete_stats`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_admin_delete_stats` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Delete a user's stats storage and leaderboard record |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | Target user ID |
|
||||||
|
|
||||||
|
**Storage deletes:** `{collection="stats", key="stats"}`, `{collection="stats", key="game_stats"}`, plus leaderboard record.
|
||||||
|
|
||||||
|
### RPC: `admin_sync_leaderboard`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `leaderboard.rpc_admin_sync_leaderboard` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Same as `sync_leaderboard` but requires admin |
|
||||||
|
|
||||||
|
**Delegates to:** `leaderboard.rpc_sync_leaderboard` (same params, same returns).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Inbox/Mail Module (`inbox.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### RPC: `admin_send_mail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_admin_send_mail` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Send a personal (targeted) or global mail |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `title` | string | No | `"Announcement"` | Mail title |
|
||||||
|
| `content` | string | No | `""` | Mail body |
|
||||||
|
| `start_date` | string | No | Current UTC | ISO8601 start date |
|
||||||
|
| `end_date` | string | No | `""` | ISO8601 end date (global only) |
|
||||||
|
| `rewards` | array/object | No | `{}` | Attached rewards (see claim_mail_reward for format) |
|
||||||
|
| `target_user_id` | string | No | `""` | If set -> personal mail to this user. Omit -> global mail |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `mail` | object | The created mail object |
|
||||||
|
|
||||||
|
**Mail object fields:** `id` (UUIDv4), `title`, `content`, `sender` (`"TEKTON DEV TEAM"`), `date`, `start_date`, `end_date`, `expiry_date` (30 days from now), `rewards`, `type` (`"personal"` or `"global"`).
|
||||||
|
|
||||||
|
**Personal storage:** Collection `"inbox"`, key `"personal"`, permission_read=`1`, permission_write=`0`.
|
||||||
|
**Global storage:** Collection `"config"`, key `"global_mail"`, user `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||||
|
|
||||||
|
### RPC: `get_mail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_get_mail` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Get all non-deleted, non-expired personal + global mail for the user |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `mails` | array | Filtered mail objects (excludes deleted, expired, not-yet-started, ended global) |
|
||||||
|
| `state` | object | `{claimed_ids, deleted_ids, read_ids}` |
|
||||||
|
|
||||||
|
**Filtering logic:** Excludes mail where:
|
||||||
|
- ID is in `state.deleted_ids`
|
||||||
|
- `expiry_date` is in the past
|
||||||
|
- `start_date` is in the future
|
||||||
|
- Global mail with `end_date` in the past
|
||||||
|
|
||||||
|
**Storage reads:**
|
||||||
|
- Collection `"inbox"`, key `"personal"`, user = caller
|
||||||
|
- Collection `"config"`, key `"global_mail"`, user = `SYSTEM_USER_ID`
|
||||||
|
- Collection `"inbox"`, key `"state"`, user = caller
|
||||||
|
|
||||||
|
### RPC: `claim_mail_reward`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_claim_mail_reward` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Claim rewards attached to a mail |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mail_id` | string | Yes | Mail ID to claim rewards from |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `claimed_ids` | array | Updated list of claimed mail IDs |
|
||||||
|
|
||||||
|
**Reward format (in the mail object):**
|
||||||
|
|
||||||
|
Legacy dict format: `{star=100, gold=50}`
|
||||||
|
Array format: `[{type="star", amount=100}, {type="gold", amount=50}, {type="frag_rare", id="frag_rare", amount=1}, {type="skin", id="skin_gacha_rainbow_suit"}]`
|
||||||
|
|
||||||
|
| Reward `type` | Behavior |
|
||||||
|
|---|---|
|
||||||
|
| `"star"` | Added to wallet |
|
||||||
|
| `"gold"` | Added to wallet |
|
||||||
|
| `"frag_"` prefix | Added to inventory fragments (key: `"fragments"`) |
|
||||||
|
| `"item"` | Added to inventory fragments |
|
||||||
|
| `"skin"` | Added to inventory (key = skin item ID) |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"mail_id required"`
|
||||||
|
- `"Reward already claimed"`
|
||||||
|
- `"Mail not found"`
|
||||||
|
|
||||||
|
**Storage writes:** Wallet update, then collection `"inventory"` (fragments or skins), then `"inbox"`/`"state"`.
|
||||||
|
|
||||||
|
### RPC: `delete_mail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_delete_mail` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Soft-delete a mail (adds ID to state.deleted_ids) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mail_id` | string | Yes | Mail ID to delete |
|
||||||
|
|
||||||
|
### RPC: `save_mail_state`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_save_mail_state` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Mark mails as read |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `read_ids` | array | No | Array of mail IDs to mark as read |
|
||||||
|
|
||||||
|
### RPC: `admin_list_mail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_admin_list_mail` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | List all mail (global + personal) across all users |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `mails` | array | All mails sorted by date descending. Personal mails have extra fields: `type="personal"`, `target_user_id` |
|
||||||
|
|
||||||
|
### RPC: `admin_update_mail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_admin_update_mail` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Edit or move a mail between users |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mail_id` | string | Yes | Mail ID to update |
|
||||||
|
| `type` | string | No | `"global"` or `"personal"` |
|
||||||
|
| `target_user_id` | string | If personal | Current owner (for lookup) |
|
||||||
|
| `new_target_user_id` | string | No | Transfer to a different user |
|
||||||
|
| `title` | string | No | New title |
|
||||||
|
| `content` | string | No | New content |
|
||||||
|
| `end_date` | string | No | New end date |
|
||||||
|
| `expiry_date` | string | No | New expiry date |
|
||||||
|
|
||||||
|
### RPC: `admin_delete_mail_server`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `inbox.rpc_admin_delete_mail_server` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Permanently delete mail from server storage |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mail_id` | string | Yes | Mail ID to delete |
|
||||||
|
| `type` | string | No | `"global"` or `"personal"` |
|
||||||
|
| `target_user_id` | string | If personal | Owner user ID |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Daily Rewards Module (`daily_rewards.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Default Reward Schedule
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
Days 1-31, each day gives `star` currency: `min(10 + (day-1)*5, 100)`.
|
||||||
|
|
||||||
|
| Day | Reward |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 10 star |
|
||||||
|
| 2 | 15 star |
|
||||||
|
| ... | ... |
|
||||||
|
| 19 | 100 star (capped) |
|
||||||
|
| 20-31 | 100 star each |
|
||||||
|
|
||||||
|
### RPC: `claim_daily_reward`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `daily_rewards.rpc_claim_daily_reward` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Claim today's daily reward |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `success` | bool | `true` |
|
||||||
|
| `reward_type` | string | `"star"`, `"gold"`, or a fragment ID (starts with `"frag_"`) |
|
||||||
|
| `reward_amount` | number | Amount rewarded |
|
||||||
|
| `day` | number | Day number (1-indexed) |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Not authenticated"`
|
||||||
|
- `"Already claimed today"` -- `last_claim_date == today`
|
||||||
|
- `"Already claimed all rewards for this month"` -- day index >= rewards array length
|
||||||
|
- `"Already claimed today's reward"` -- day index already in `claimed_days`
|
||||||
|
|
||||||
|
**Storage reads/writes:**
|
||||||
|
- Read: Collection `"daily_rewards"`, key `"state"` (per-user)
|
||||||
|
- Read: Collection `"config"`, key `"daily_rewards"`, user `SYSTEM_USER_ID` (config)
|
||||||
|
- Write: Collection `"daily_rewards"`, key `"state"` (updates claimed days)
|
||||||
|
- Write (if fragment): Collection `"inventory"`, key `"fragments"`
|
||||||
|
- Wallet update for star/gold rewards
|
||||||
|
|
||||||
|
**Reward types:** `"star"` or `"gold"` -> wallet update. `"frag_"` prefix -> inventory fragments.
|
||||||
|
|
||||||
|
### RPC: `get_daily_reward_state`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `daily_rewards.rpc_get_daily_reward_state` |
|
||||||
|
| **Auth** | Required |
|
||||||
|
| **Purpose** | Get current daily reward state and month config |
|
||||||
|
|
||||||
|
**Params:** None (ignore payload)
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `state` | object | `{claimed_days, last_claim_date, month}` |
|
||||||
|
| `month_rewards` | array | Reward config for current month (array of `{type, amount}`) |
|
||||||
|
| `can_claim_today` | bool | Whether user can claim today |
|
||||||
|
| `today_date` | string | Today's UTC date (YYYY-MM-DD) |
|
||||||
|
| `today_index` | number | Today's 0-based index |
|
||||||
|
| `server_month` | number | Current month number |
|
||||||
|
|
||||||
|
### RPC: `set_daily_reward_config` [Admin]
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `daily_rewards.rpc_set_daily_reward_config` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Set monthly daily reward configuration |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `config` | object | Yes | Map of month -> array of `{type, amount}` rewards. E.g. `{"01": [{type="star", amount=50}, ...]}` |
|
||||||
|
|
||||||
|
### RPC: `get_daily_reward_config_admin` [Admin]
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `daily_rewards.rpc_get_daily_reward_config_admin` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Get the current daily reward config |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Admin Module (`admin.lua`)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### RPC: `admin_kick_player`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_kick_player` |
|
||||||
|
| **Auth** | Admin **or** match host |
|
||||||
|
| **Purpose** | Kick a player from a match |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `match_id` | string | Yes | Match ID |
|
||||||
|
| `user_id` | string | Yes | Player to kick |
|
||||||
|
| `reason` | string | No | `"Kicked by admin"` |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Admin or host privileges required"`
|
||||||
|
- `"Cannot kick yourself"`
|
||||||
|
- `"Failed to kick player"`
|
||||||
|
|
||||||
|
**Mechanism:** Sends `nk.match_signal` with `{action="kick", user_id, reason}`.
|
||||||
|
|
||||||
|
### RPC: `admin_ban_player`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_ban_player` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Ban a player (sets metadata.banned, kicks from match) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | User to ban |
|
||||||
|
| `reason` | string | No | `"Banned by admin"` |
|
||||||
|
| `duration_hours` | number | No | Hours until ban expires (omitted = permanent) |
|
||||||
|
| `match_id` | string | No | Also kick from this match |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Admin privileges required"`
|
||||||
|
- `"Cannot ban yourself"`
|
||||||
|
- `"Target account not found"`
|
||||||
|
- `"Cannot ban an admin"`
|
||||||
|
|
||||||
|
**Storage:** Writes ban record to collection `"bans"`, key = `user_id`, user = `SYSTEM_USER_ID`, permission_read=`2`, permission_write=`0`.
|
||||||
|
|
||||||
|
### RPC: `admin_unban_player`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_unban_player` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Remove ban from a player |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | Yes |
|
||||||
|
|
||||||
|
### RPC: `admin_get_ban_list`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_get_ban_list` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | List all ban records |
|
||||||
|
|
||||||
|
### RPC: `admin_get_server_stats`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_get_server_stats` |
|
||||||
|
| **Auth** | Admin (or host if `match_id` provided) |
|
||||||
|
| **Purpose** | Get server stats (active matches, players) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `match_id` | string | No | If set, includes match-specific stats |
|
||||||
|
|
||||||
|
**Returns (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `active_matches` | number | Count of active matches |
|
||||||
|
| `total_players` | number | Total players in matches |
|
||||||
|
| `server_time` | number | `os.time()` |
|
||||||
|
| `match` | object | (optional) `{id, size, tick_rate, authoritative}` |
|
||||||
|
|
||||||
|
### RPC: `admin_end_match`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_end_match` |
|
||||||
|
| **Auth** | Admin **or** match host |
|
||||||
|
| **Purpose** | Signal a match to end |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `match_id` | string | Yes | Match ID |
|
||||||
|
| `reason` | string | No | `"Ended by admin"` |
|
||||||
|
|
||||||
|
### RPC: `admin_set_user_role`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_set_user_role` |
|
||||||
|
| **Auth** | **Owner only** (role must be `"owner"`) |
|
||||||
|
| **Purpose** | Set a user's role |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Valid Values |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | Target user |
|
||||||
|
| `role` | string | Yes | `"player"`, `"moderator"`, `"admin"` |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Only owners can modify user roles"`
|
||||||
|
- `"Invalid role"`
|
||||||
|
|
||||||
|
### RPC: `admin_topup_gold`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_topup_gold` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Add 999,999 gold to own wallet (dev quick-fill) |
|
||||||
|
|
||||||
|
**Wallet update:** `{gold = 999999}`
|
||||||
|
|
||||||
|
### RPC: `admin_clear_global_chat`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_clear_global_chat` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Delete all messages from a chat channel |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `channel_id` | string | Yes | Channel ID or room name |
|
||||||
|
|
||||||
|
**Returns (JSON):** `{success, deleted}`
|
||||||
|
|
||||||
|
### RPC: `admin_list_users`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_list_users` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | List all users (limit 500) with role and ban info |
|
||||||
|
|
||||||
|
**Returns (JSON):** `{users: [{user_id, username, display_name, create_time, role, banned, ban_reason}], count}`
|
||||||
|
|
||||||
|
### RPC: `admin_delete_users`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_delete_users` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Permanently delete user accounts |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `user_ids` | array | Yes | Array of user IDs to delete |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"Cannot delete your own account"`
|
||||||
|
- `"Cannot delete admin account"` (prevents deleting admin/moderator/owner)
|
||||||
|
|
||||||
|
**Returns (JSON):** `{success, deleted: [user_ids], failed: [{user_id, reason}]}`
|
||||||
|
|
||||||
|
### RPC: `admin_get_user_detail`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_get_user_detail` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Get comprehensive user details (profile, friends, purchases, storage, wallet ledger) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `user_id` | string | Yes | -- | Target user ID |
|
||||||
|
| `collections` | array | No | `["profiles","stats","inventory","receipts","history","matches","inbox"]` | Storage collections to include |
|
||||||
|
|
||||||
|
**Returns (JSON):** Rich object with `{user, friends, purchases, wallet_ledger, storage, subscription}`.
|
||||||
|
|
||||||
|
### RPC: `admin_update_user_identity`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_update_user_identity` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Update a user's identity fields (username, display_name, timezone, location, lang_tag, avatar_url, metadata) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | Yes |
|
||||||
|
| `username` | string | No |
|
||||||
|
| `display_name` | string | No |
|
||||||
|
| `timezone` | string | No |
|
||||||
|
| `location` | string | No |
|
||||||
|
| `lang_tag` | string | No |
|
||||||
|
| `avatar_url` | string | No |
|
||||||
|
| `metadata` | object | No (merged into existing metadata) |
|
||||||
|
|
||||||
|
### RPC: `admin_set_user_password`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_set_user_password` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Force-set a user's password (user must have email credential) |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required |
|
||||||
|
|---|---|---|
|
||||||
|
| `user_id` | string | Yes |
|
||||||
|
| `password` | string | Yes |
|
||||||
|
|
||||||
|
**Errors:**
|
||||||
|
- `"User has no email credential"`
|
||||||
|
- `"user_id and password are required"`
|
||||||
|
|
||||||
|
### RPC: `admin_get_player_list`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_get_player_list` |
|
||||||
|
| **Auth** | Admin **or** match host |
|
||||||
|
| **Purpose** | Get player list for a match (stub -- returns empty) |
|
||||||
|
|
||||||
|
### RPC: `admin_get_chat_config`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_get_chat_config` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Get lobby chat configuration |
|
||||||
|
|
||||||
|
**Returns (JSON):** `{config: {prefix, max_messages, max_age_days}}`
|
||||||
|
|
||||||
|
### RPC: `admin_set_chat_config`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_set_chat_config` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Set lobby chat configuration |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `prefix` | string | No | `""` | Chat prefix |
|
||||||
|
| `max_messages` | number | No | 50 | Max messages |
|
||||||
|
| `max_age_days` | number | No | 0 | Max message age |
|
||||||
|
|
||||||
|
**Storage:** Collection `"config"`, key `"lobby_chat"`, user `SYSTEM_USER_ID`.
|
||||||
|
|
||||||
|
### RPC: `admin_purge_old_messages`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_purge_old_messages` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Delete chat messages older than specified days |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `channel_id` | string | Yes | Channel ID or room name |
|
||||||
|
| `max_age_days` | number | Yes | Delete messages older than this |
|
||||||
|
|
||||||
|
### RPC: `admin_list_channel_messages`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_list_channel_messages` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | List messages in a chat channel |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required | Default | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `channel_id` | string | Yes | -- | Channel ID or room name |
|
||||||
|
| `limit` | number | No | 50 | Messages per page |
|
||||||
|
| `cursor` | string | No | `""` | Pagination cursor |
|
||||||
|
| `forward` | bool | No | `true` | Direction |
|
||||||
|
|
||||||
|
### RPC: `admin_delete_channel_message`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Function** | `admin.rpc_admin_delete_channel_message` |
|
||||||
|
| **Auth** | Admin required |
|
||||||
|
| **Purpose** | Delete a specific chat message |
|
||||||
|
|
||||||
|
**Params (JSON):**
|
||||||
|
|
||||||
|
| Field | Type | Required |
|
||||||
|
|---|---|---|
|
||||||
|
| `channel_id` | string | Yes |
|
||||||
|
| `message_id` | string | Yes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Storage Collections Reference
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Collection: `"shop_config"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `featured_banners` | SYSTEM | 2 (public) | 0 (owner) | `{banners: [{item_id, ...}]}` |
|
||||||
|
|
||||||
|
### Collection: `"receipts"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `<idempotency_key>` | Owner | 1 (owner-read) | 0 (owner) | `{type, package_id/ item_id, status, changeset, receipt, processed_at, ...}` |
|
||||||
|
|
||||||
|
### Collection: `"inventory"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `<item_id>` | Owner | 1 (owner-read) | 0 (owner) | `{category, purchased_at, quantity}` (or `{acquired_via, purchased_at}` for skins) |
|
||||||
|
| `"fragments"` | Owner | 1 (owner-read) | 0 (owner) | `{frag_common: N, frag_uncommon: N, frag_rare: N, ...}` |
|
||||||
|
|
||||||
|
### Collection: `"profiles"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"pity_counters"` | Owner | 1 (owner-read) | 0 (owner) | `{star: N, gold: N}` (per-banner pity) |
|
||||||
|
| `"fragments"` | Owner | 1 (owner-read) | 0 (owner) | Fragment counts (same as inventory fragments) |
|
||||||
|
| `"profile"` | Owner | 2 (public) | 0 (owner) | Profile data (avatar_url, loadout_character, etc.) |
|
||||||
|
|
||||||
|
### Collection: `"history"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"logins"` | Owner | 0 (no-read) | 0 (owner) | `{logins: [{time, ip}, ...]}` (max 20) |
|
||||||
|
|
||||||
|
### Collection: `"stats"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"game_stats"` | Owner | 1 (owner-read) or 2 (public) | 1 (owner-write) or 0 (owner) | `{games_played, games_won, high_score, total_kills, total_deaths, avatar_url, loadout_character}` |
|
||||||
|
| `"stats"` | Owner | Varies | 0 (owner) | Legacy/user-level stats |
|
||||||
|
|
||||||
|
### Collection: `"inbox"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"personal"` | Owner | 1 (owner-read) | 0 (owner) | `{mails: [{id, title, content, sender, date, start_date, end_date, expiry_date, rewards, type}]}` |
|
||||||
|
| `"state"` | Owner | 1 (owner-read) | 0 (owner) | `{claimed_ids: [string], deleted_ids: [string], read_ids: [string]}` |
|
||||||
|
|
||||||
|
### Collection: `"daily_rewards"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"state"` | Owner | 1 (owner-read) | 0 (owner) | `{claimed_days: [number], last_claim_date: "YYYY-MM-DD", month: "MM"}` |
|
||||||
|
|
||||||
|
### Collection: `"config"` (system-level)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `"daily_rewards"` | SYSTEM | 2 (public) | 0 (owner) | `{"MM": [{type, amount}, ...]}` (month->rewards map) |
|
||||||
|
| `"global_mail"` | SYSTEM | 2 (public) | 0 (owner) | `{mails: [{id, title, content, ...}]}` |
|
||||||
|
| `"lobby_chat"` | SYSTEM | 2 (public) | 0 (owner) | `{prefix, max_messages, max_age_days}` |
|
||||||
|
|
||||||
|
### Collection: `"bans"`
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission Read | Permission Write | Schema |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `<user_id>` | SYSTEM | 2 (public) | 0 (owner) | `{user_id, username, banned_by, banned_at, reason, expires}` |
|
||||||
|
|
||||||
|
### Collection: `"matches"` (per-user)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Key | User | Permission | Schema |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `<match_id>` | Owner | 0 (owner) | Match result data (keyed by match ID, varies) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Leaderboard Config
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| **ID** | `"global_high_score"` |
|
||||||
|
| **Authoritative** | `true` (can only be written server-side) |
|
||||||
|
| **Sort Order** | `"desc"` (highest -> lowest) |
|
||||||
|
| **Operator** | `"best"` (keep best score per user) |
|
||||||
|
| **Reset** | `nil` (never auto-resets) |
|
||||||
|
| **Created By** | Both `core.lua` and `leaderboard.lua` on module load |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Error Strings Reference
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Auth errors (raised by utils)
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Admin privileges required"` | Non-admin calls admin RPC |
|
||||||
|
| `"Admin or host privileges required"` | Non-admin, non-host calls admin/host RPC |
|
||||||
|
| `"Not authenticated"` | `context.user_id` is nil |
|
||||||
|
|
||||||
|
### Economy errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Package ID required"` | `buy_currency` called without `package_id` |
|
||||||
|
| `"Idempotency key required"` | Missing `idempotency_key` |
|
||||||
|
| `"Invalid package ID"` | Unknown `package_id` |
|
||||||
|
| `"InvalidReceipt"` | IAP receipt validation failed |
|
||||||
|
| `"NotEnoughFunds"` | Insufficient wallet balance for purchase |
|
||||||
|
| `"ItemNotFound"` | Item ID not in shop catalog |
|
||||||
|
| `"Invalid quantity"` | Quantity < 1 |
|
||||||
|
| `"PurchaseFailed"` | Storage write failed during item purchase |
|
||||||
|
| `"Item not found in catalog: <id>"` | Admin tried to banner a non-catalog item |
|
||||||
|
|
||||||
|
### User errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Account not found"` | User ID doesn't exist |
|
||||||
|
| `"Account banned until <time>. Reason: <reason>"` | Temporary ban |
|
||||||
|
| `"Account permanently banned. Reason: <reason>"` | Permanent ban |
|
||||||
|
| `"Failed to update profile"` | `nk.account_update_id` failed |
|
||||||
|
| `"Current password required"` | Email change without current password |
|
||||||
|
| `"Incorrect current password."` | Email re-auth failed |
|
||||||
|
| `"Failed to set new credentials: <err>"` | Link email failed |
|
||||||
|
| `"Missing to_user_id or match_id"` | Lobby invite missing fields |
|
||||||
|
| `"Cannot add yourself"` | Friend request to self |
|
||||||
|
| `"user_id is required"` | Missing target in friend request |
|
||||||
|
|
||||||
|
### Gacha errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Banner ID required"` | Missing `banner_id` |
|
||||||
|
| `"Invalid count"` | Count < 1 |
|
||||||
|
| `"Unknown banner: <id>"` | Invalid banner |
|
||||||
|
| `"Could not read account"` | Account lookup failed |
|
||||||
|
| `"Insufficient currency"` | Not enough star/gold |
|
||||||
|
| `"Failed to update wallet"` | Wallet update failed |
|
||||||
|
| `"Failed to write storage: <err>"` | Storage write failed |
|
||||||
|
|
||||||
|
### Leaderboard errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Failed to submit score"` | Leaderboard write failed |
|
||||||
|
| `"Sync failed: <err>"` | Storage list failed |
|
||||||
|
| `"User ID and stats are required"` | Missing params in admin_update_stats |
|
||||||
|
|
||||||
|
### Inbox errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"mail_id required"` | Missing mail ID |
|
||||||
|
| `"Reward already claimed"` | Mail already in claimed_ids |
|
||||||
|
| `"Mail not found"` | Mail ID not in any mailbox |
|
||||||
|
| `"Mail not found in global"` | Update/delete: mail not in global |
|
||||||
|
| `"Mail not found in personal inbox"` | Update/delete: mail not in personal |
|
||||||
|
| `"target_user_id required for personal mail"` | Missing owner for personal mail operations |
|
||||||
|
|
||||||
|
### Daily reward errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Already claimed today"` | `last_claim_date == today` |
|
||||||
|
| `"Already claimed all rewards for this month"` | Day index >= reward array length |
|
||||||
|
| `"Already claimed today's reward"` | Day index already in `claimed_days` |
|
||||||
|
|
||||||
|
### Admin errors
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Error String | Trigger |
|
||||||
|
|---|---|
|
||||||
|
| `"Cannot kick yourself"` | Self-kick attempt |
|
||||||
|
| `"Failed to kick player"` | Match signal failed |
|
||||||
|
| `"Cannot ban yourself"` | Self-ban attempt |
|
||||||
|
| `"Target account not found"` | Account lookup failed |
|
||||||
|
| `"Cannot ban an admin"` | Target is admin/moderator/owner |
|
||||||
|
| `"Only owners can modify user roles"` | Non-owner tries role change |
|
||||||
|
| `"Invalid role"` | Role not in `{player, moderator, admin}` |
|
||||||
|
| `"channel_id is required"` | Missing channel ID for chat operations |
|
||||||
|
| `"channel_id and message_id are required"` | Missing chat message params |
|
||||||
|
| `"Failed to delete message: <err>"` | Channel message remove failed |
|
||||||
|
| `"Failed to list messages: <err>"` | Channel list failed |
|
||||||
|
| `"max_age_days must be > 0"` | Invalid purge param |
|
||||||
|
| `"Match not found"` | Match lookup failed |
|
||||||
|
| `"User has no email credential"` | Cannot set password for non-email user |
|
||||||
|
| `"Cannot delete your own account"` | Self-deletion attempt |
|
||||||
|
| `"Cannot delete admin account"` | Deleting admin/moderator/owner |
|
||||||
|
| `"No user IDs provided"` | Empty `user_ids` array |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Permission Levels
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
### Role Hierarchy
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Role | Can Call Admin RPCs | Can Modify Roles | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `"player"` | No | No | Default role |
|
||||||
|
| `"moderator"` | Yes | No | Same as admin for auth checks |
|
||||||
|
| `"admin"` | Yes | No | Standard admin |
|
||||||
|
| `"owner"` | Yes | Yes (only owner) | Can set `player`, `moderator`, `admin` roles |
|
||||||
|
|
||||||
|
### Auth Check Functions Summary
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Check | What It Allows |
|
||||||
|
|---|---|
|
||||||
|
| `utils.require_admin` | Any RPC gated by this. Roles: admin, moderator, owner |
|
||||||
|
| `utils.require_admin_or_host` | Admin/moderator/owner, OR match host (state.hostUserId) |
|
||||||
|
| `metadata.role == "owner"` | Only `admin_set_user_role` |
|
||||||
|
|
||||||
|
### Storage Permission Bits
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `0` | Owner only (no one can read/write except server) |
|
||||||
|
| `1` | Owner read, server write |
|
||||||
|
| `2` | Public read, server write |
|
||||||
|
|
||||||
|
### System User ID
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
00000000-0000-0000-0000-000000000000
|
||||||
|
```
|
||||||
|
|
||||||
|
Used for global/shared storage collections (bans, config, global_mail, shop_config).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RPC Summary Table
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| RPC Name | Module | Auth | Admin? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_user_profile` | user | None | No |
|
||||||
|
| `update_user_profile` | user | Required | No |
|
||||||
|
| `search_users` | user | Required | No |
|
||||||
|
| `change_credentials` | user | Required | No |
|
||||||
|
| `send_lobby_invite` | user | Required | No |
|
||||||
|
| `send_friend_request` | user | Required | No |
|
||||||
|
| `admin_get_user_history` | user | Required | Yes |
|
||||||
|
| `get_shop_catalog` | economy | Required | No |
|
||||||
|
| `buy_currency` | economy | Required | No |
|
||||||
|
| `purchase_item` | economy | Required | No |
|
||||||
|
| `admin_set_featured_banners` | economy | Required | Yes |
|
||||||
|
| `admin_get_featured_banners` | economy | Required | Yes |
|
||||||
|
| `perform_gacha_pull` | gacha | Required | No |
|
||||||
|
| `get_leaderboard_stats` | leaderboard | None | No |
|
||||||
|
| `submit_score` | leaderboard | Required | No |
|
||||||
|
| `sync_leaderboard` | leaderboard | Required | No |
|
||||||
|
| `reset_stats` | leaderboard | Required | No |
|
||||||
|
| `admin_update_stats` | leaderboard | Required | Yes |
|
||||||
|
| `admin_delete_stats` | leaderboard | Required | Yes |
|
||||||
|
| `admin_sync_leaderboard` | leaderboard | Required | Yes |
|
||||||
|
| `admin_send_mail` | inbox | Required | Yes |
|
||||||
|
| `get_mail` | inbox | Required | No |
|
||||||
|
| `claim_mail_reward` | inbox | Required | No |
|
||||||
|
| `delete_mail` | inbox | Required | No |
|
||||||
|
| `save_mail_state` | inbox | Required | No |
|
||||||
|
| `admin_list_mail` | inbox | Required | Yes |
|
||||||
|
| `admin_update_mail` | inbox | Required | Yes |
|
||||||
|
| `admin_delete_mail_server` | inbox | Required | Yes |
|
||||||
|
| `claim_daily_reward` | daily_rewards | Required | No |
|
||||||
|
| `get_daily_reward_state` | daily_rewards | Required | No |
|
||||||
|
| `set_daily_reward_config` | daily_rewards | Required | Yes |
|
||||||
|
| `get_daily_reward_config_admin` | daily_rewards | Required | Yes |
|
||||||
|
| `admin_kick_player` | admin | Required | Admin or host |
|
||||||
|
| `admin_ban_player` | admin | Required | Yes |
|
||||||
|
| `admin_unban_player` | admin | Required | Yes |
|
||||||
|
| `admin_get_ban_list` | admin | Required | Yes |
|
||||||
|
| `admin_get_server_stats` | admin | Required | Admin or host |
|
||||||
|
| `admin_end_match` | admin | Required | Admin or host |
|
||||||
|
| `admin_set_user_role` | admin | Required | Owner only |
|
||||||
|
| `admin_topup_gold` | admin | Required | Yes |
|
||||||
|
| `admin_clear_global_chat` | admin | Required | Yes |
|
||||||
|
| `admin_list_users` | admin | Required | Yes |
|
||||||
|
| `admin_delete_users` | admin | Required | Yes |
|
||||||
|
| `admin_get_user_detail` | admin | Required | Yes |
|
||||||
|
| `admin_update_user_identity` | admin | Required | Yes |
|
||||||
|
| `admin_set_user_password` | admin | Required | Yes |
|
||||||
|
| `admin_get_player_list` | admin | Required | Admin or host |
|
||||||
|
| `admin_get_chat_config` | admin | Required | Yes |
|
||||||
|
| `admin_set_chat_config` | admin | Required | Yes |
|
||||||
|
| `admin_purge_old_messages` | admin | Required | Yes |
|
||||||
|
| `admin_list_channel_messages` | admin | Required | Yes |
|
||||||
|
| `admin_delete_channel_message` | admin | Required | Yes |
|
||||||
|
|
||||||
|
## Wallet Update Summary
|
||||||
|
|
||||||
|
[↑ Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
| Operation | Changeset | Context |
|
||||||
|
|---|---|---|
|
||||||
|
| IAP: gold packages (verified) | `{gold: +N}` | `buy_currency` |
|
||||||
|
| IAP: star packages | `{star: +N, gold: -N}` | `buy_currency` (costs gold) |
|
||||||
|
| Purchase item | `{gold: -N}` or `{star: -N}` | `purchase_item` |
|
||||||
|
| Gacha pull | `{<currency>: -cost}` | `perform_gacha_pull` |
|
||||||
|
| Claim mail rewards | `{star: +N}` and/or `{gold: +N}` | `claim_mail_reward` |
|
||||||
|
| Daily reward | `{star: +N}` or `{gold: +N}` | `claim_daily_reward` |
|
||||||
|
| Admin gold top-up | `{gold: 999999}` | `admin_topup_gold` |
|
||||||
|
|
||||||
|
All wallet updates use `nk.wallet_update(user_id, changeset, {}, true)` (third arg `true` = update only, no metadata).
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
extends SceneTree
|
||||||
|
func _init():
|
||||||
|
print("Testing gauntlet multiplayer")
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ddniv6k6aj2u
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/expect -f
|
||||||
|
spawn ssh admin@193.180.213.215 "ls -la /home/admin/nakama/data/modules/"
|
||||||
|
expect "password:"
|
||||||
|
send "Mieayamtelur17\r"
|
||||||
|
expect eof
|
||||||
@@ -18,11 +18,10 @@ func after_each():
|
|||||||
func test_gauntlet_enum_exists():
|
func test_gauntlet_enum_exists():
|
||||||
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be enum value 3")
|
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be enum value 3")
|
||||||
|
|
||||||
# Test 2: All 4 modes are present in enum
|
# Test 2: All 3 modes are present in enum
|
||||||
func test_all_modes_in_enum():
|
func test_all_modes_in_enum():
|
||||||
assert_eq(GameMode.Mode.FREEMODE, 0, "FREEMODE should be 0")
|
assert_eq(GameMode.Mode.FREEMODE, 0, "FREEMODE should be 0")
|
||||||
assert_eq(GameMode.Mode.STOP_N_GO, 1, "STOP_N_GO should be 1")
|
assert_eq(GameMode.Mode.STOP_N_GO, 1, "STOP_N_GO should be 1")
|
||||||
assert_eq(GameMode.Mode.TEKTON_DOORS, 2, "TEKTON_DOORS should be 2")
|
|
||||||
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be 3")
|
assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be 3")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -47,7 +46,7 @@ func test_round_trip_conversion():
|
|||||||
|
|
||||||
# Test 6: All existing modes still round-trip correctly
|
# Test 6: All existing modes still round-trip correctly
|
||||||
func test_existing_modes_round_trip():
|
func test_existing_modes_round_trip():
|
||||||
for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO, GameMode.Mode.TEKTON_DOORS]:
|
for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO]:
|
||||||
var s = GameMode.mode_to_string(mode)
|
var s = GameMode.mode_to_string(mode)
|
||||||
var back = GameMode.from_string(s)
|
var back = GameMode.from_string(s)
|
||||||
assert_eq(back, mode, "Round-trip failed for %s" % s)
|
assert_eq(back, mode, "Round-trip failed for %s" % s)
|
||||||
@@ -66,18 +65,17 @@ func test_get_all_modes_includes_gauntlet():
|
|||||||
var modes = GameMode.get_all_modes()
|
var modes = GameMode.get_all_modes()
|
||||||
assert_has(modes, "Candy Pump Survival", "get_all_modes should include 'Candy Pump Survival'")
|
assert_has(modes, "Candy Pump Survival", "get_all_modes should include 'Candy Pump Survival'")
|
||||||
|
|
||||||
# Test 9: get_all_modes returns exactly 4 entries
|
# Test 9: get_all_modes returns exactly 3 entries
|
||||||
func test_get_all_modes_count():
|
func test_get_all_modes_count():
|
||||||
var modes = GameMode.get_all_modes()
|
var modes = GameMode.get_all_modes()
|
||||||
assert_eq(modes.size(), 4, "get_all_modes should return 4 modes")
|
assert_eq(modes.size(), 3, "get_all_modes should return 3 modes")
|
||||||
|
|
||||||
# Test 10: get_all_modes order is correct
|
# Test 10: get_all_modes order is correct
|
||||||
func test_get_all_modes_order():
|
func test_get_all_modes_order():
|
||||||
var modes = GameMode.get_all_modes()
|
var modes = GameMode.get_all_modes()
|
||||||
assert_eq(modes[0], "Freemode", "First mode should be Freemode")
|
assert_eq(modes[0], "Freemode", "First mode should be Freemode")
|
||||||
assert_eq(modes[1], "Stop n Go", "Second mode should be Stop n Go")
|
assert_eq(modes[1], "Stop n Go", "Second mode should be Stop n Go")
|
||||||
assert_eq(modes[2], "Tekton Doors", "Third mode should be Tekton Doors")
|
assert_eq(modes[2], "Candy Pump Survival", "Third mode should be Candy Pump Survival")
|
||||||
assert_eq(modes[3], "Candy Pump Survival", "Fourth mode should be Candy Pump Survival")
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# is_restricted Tests
|
# is_restricted Tests
|
||||||
@@ -96,7 +94,6 @@ func test_freemode_not_restricted():
|
|||||||
# Test 13: All restricted modes are confirmed
|
# Test 13: All restricted modes are confirmed
|
||||||
func test_all_restricted_modes():
|
func test_all_restricted_modes():
|
||||||
assert_true(GameMode.is_restricted(GameMode.Mode.STOP_N_GO), "STOP_N_GO should be restricted")
|
assert_true(GameMode.is_restricted(GameMode.Mode.STOP_N_GO), "STOP_N_GO should be restricted")
|
||||||
assert_true(GameMode.is_restricted(GameMode.Mode.TEKTON_DOORS), "TEKTON_DOORS should be restricted")
|
|
||||||
assert_true(GameMode.is_restricted(GameMode.Mode.GAUNTLET), "GAUNTLET should be restricted")
|
assert_true(GameMode.is_restricted(GameMode.Mode.GAUNTLET), "GAUNTLET should be restricted")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ PROJECT_GODOT = "project.godot"
|
|||||||
CHANGELOG_DRAFT = "CHANGELOG_DRAFT.md"
|
CHANGELOG_DRAFT = "CHANGELOG_DRAFT.md"
|
||||||
VERSION_JSON = "assets/data/version.json"
|
VERSION_JSON = "assets/data/version.json"
|
||||||
EXPORT_PRESETS = "export_presets.cfg"
|
EXPORT_PRESETS = "export_presets.cfg"
|
||||||
MANIFEST_URL = "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck"
|
MANIFEST_URL = "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck"
|
||||||
|
|
||||||
# ─── Parse command line arguments ─────────────────────────────────────────────
|
# ─── Parse command line arguments ─────────────────────────────────────────────
|
||||||
parser = argparse.ArgumentParser(description="Generate version.json and update version numbers")
|
parser = argparse.ArgumentParser(description="Generate version.json and update version numbers")
|
||||||
|
|||||||
@@ -0,0 +1,2707 @@
|
|||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
# Tekton Armageddon - Client Architecture (Full Function Reference)
|
||||||
|
|
||||||
|
[Back to Home](./Home)
|
||||||
|
|
||||||
|
Complete per-function reference for the Godot 4.7 client codebase. Every script, signal, autoload dependency, and cross-file relationship documented.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
1. [Project Structure Overview](#1-project-structure-overview)
|
||||||
|
2. [Autoloads / Singletons Index](#2-autoloads--singletons-index)
|
||||||
|
3. [Service Layer](#3-service-layer)
|
||||||
|
- [3.1 NakamaManager](#31-nakamamanaager)
|
||||||
|
- [3.2 BackendService](#32-backendservice)
|
||||||
|
- [3.3 SteamworksManager](#33-steamworksmanager)
|
||||||
|
4. [Core Managers](#4-core-managers)
|
||||||
|
- [4.1 AuthManager](#41-authmanager)
|
||||||
|
- [4.2 LobbyManager](#42-lobbymanager)
|
||||||
|
- [4.3 GameStateManager](#43-gamestatemanager)
|
||||||
|
- [4.4 PlayerManager](#44-playermanager)
|
||||||
|
- [4.5 EventBus](#45-eventbus)
|
||||||
|
- [4.6 GameMode / ModeConfig](#46-gamemode--modeconfig)
|
||||||
|
5. [Player Subsystem Managers](#5-player-subsystem-managers)
|
||||||
|
- [5.1 PlayerMovementManager](#51-playermovementmanager)
|
||||||
|
- [5.2 PlayerInputManager](#52-playerinputmanager)
|
||||||
|
- [5.3 PlayerActionManager](#53-playeractionmanager)
|
||||||
|
- [5.4 PlayerboardManager](#54-playerboardmanager)
|
||||||
|
- [5.5 PowerupManager](#55-powerupmanager)
|
||||||
|
6. [Game Mode Managers](#6-game-mode-managers)
|
||||||
|
- [6.1 StopNGoManager](#61-stopngomanager)
|
||||||
|
- [6.2 GauntletManager](#62-gauntletmanager)
|
||||||
|
- [6.3 PortalModeManager](#63-portalmode_manager)
|
||||||
|
- [6.4 GoalManager](#64-goalmanager)
|
||||||
|
- [6.5 GoalsCycleManager](#65-goalscyclemanager)
|
||||||
|
- [6.6 PlayerRaceManager](#66-playerracemanager)
|
||||||
|
- [6.7 TurnManager](#67-turnmanager)
|
||||||
|
7. [Gameplay Managers](#7-gameplay-managers)
|
||||||
|
- [7.1 ObstacleManager](#71-obstaclemanager)
|
||||||
|
- [7.2 SpecialTilesManager](#72-specialtilesmanager)
|
||||||
|
- [7.3 StaticTektonManager](#73-statictektonmanager)
|
||||||
|
8. [UI / Presentation Managers](#8-ui--presentation-managers)
|
||||||
|
- [8.1 UIManager](#81-uimanager)
|
||||||
|
- [8.2 SfxManager](#82-sfxmanager)
|
||||||
|
- [8.3 MusicManager](#83-musicmanager)
|
||||||
|
- [8.4 NotificationManager](#84-notificationmanager)
|
||||||
|
- [8.5 ScreenShake](#85-screenshake)
|
||||||
|
- [8.6 CameraContextManager](#86-cameracontextmanager)
|
||||||
|
- [8.7 TouchControls](#87-touchcontrols)
|
||||||
|
- [8.8 TutorialManager / TutorialOverlay](#88-tutorialmanager--tutorialoverlay)
|
||||||
|
9. [Social / Economy Managers](#9-social--economy-managers)
|
||||||
|
- [9.1 UserProfileManager](#91-userprofilemanager)
|
||||||
|
- [9.2 GachaManager](#92-gachamanager)
|
||||||
|
- [9.3 SkinManager](#93-skinmanager)
|
||||||
|
- [9.4 ShopManager](#94-shopmanager)
|
||||||
|
- [9.5 JoinManager](#95-joinmanager)
|
||||||
|
- [9.6 FriendManager](#96-friendmanager)
|
||||||
|
- [9.7 MailManager](#97-mailmanager)
|
||||||
|
- [9.8 DailyRewardManager](#98-dailyrewardmanager)
|
||||||
|
- [9.9 AdminManager](#99-adminmanager)
|
||||||
|
10. [System Managers](#10-system-managers)
|
||||||
|
- [10.1 SettingsManager](#101-settingsmanager)
|
||||||
|
- [10.2 SessionManager](#102-sessionmanager)
|
||||||
|
- [10.3 GameUpdateManager](#103-gameupdatemanager)
|
||||||
|
11. [Core Scene Scripts](#11-core-scene-scripts)
|
||||||
|
- [11.1 main.gd (Main game scene controller)](#111-maingd-main-game-scene-controller)
|
||||||
|
- [11.2 player.gd](#112-playergd)
|
||||||
|
- [11.3 lobby.gd](#113-lobbygd)
|
||||||
|
- [11.4 animation.gd](#114-animationgd)
|
||||||
|
12. [UI Helper Classes (RefCounted)](#12-ui-helper-classes-refcounted)
|
||||||
|
- [12.1 LobbyMainMenu](#121-lobbymainmenu)
|
||||||
|
- [12.2 LobbyRoom](#122-lobbyroom)
|
||||||
|
- [12.3 LobbyRoomList](#123-lobbyroomlist)
|
||||||
|
- [12.4 LobbyChat](#124-lobbychat)
|
||||||
|
13. [Dependency Graph](#13-dependency-graph)
|
||||||
|
- [13.1 Manager Autoload Dependencies](#131-manager-autoload-dependencies)
|
||||||
|
- [13.2 Cross-Manager Signal Wiring](#132-cross-manager-signal-wiring)
|
||||||
|
14. [Scene Node Trees](#14-scene-node-trees)
|
||||||
|
- [14.1 main.tscn](#141-maintscn)
|
||||||
|
- [14.2 player.tscn](#142-playertscn)
|
||||||
|
- [14.3 lobby.tscn](#143-lobbytscn)
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Project Structure Overview
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/dev/tekton/
|
||||||
|
project.godot -- Godot 4.7 project file
|
||||||
|
scripts/
|
||||||
|
main.gd -- (NOT USED; logic lives in scenes/main.gd)
|
||||||
|
nakama_manager.gd -- Nakama network layer (autoload)
|
||||||
|
event_bus.gd -- Central observer pattern bus (autoload)
|
||||||
|
game_mode.gd -- GameMode enum + string utils (RefCounted)
|
||||||
|
mode_config.gd -- Schema-driven mode settings validation (RefCounted)
|
||||||
|
managers/ -- 39+ autoload manager singletons
|
||||||
|
auth_manager.gd
|
||||||
|
lobby_manager.gd
|
||||||
|
game_state_manager.gd
|
||||||
|
player_manager.gd
|
||||||
|
player_movement_manager.gd
|
||||||
|
player_input_manager.gd
|
||||||
|
player_action_manager.gd
|
||||||
|
user_profile_manager.gd
|
||||||
|
gacha_manager.gd
|
||||||
|
skin_manager.gd
|
||||||
|
ui_manager.gd
|
||||||
|
sfx_manager.gd
|
||||||
|
music_manager.gd
|
||||||
|
game_update_manager.gd
|
||||||
|
stop_n_go_manager.gd
|
||||||
|
gauntlet_manager.gd
|
||||||
|
portal_mode_manager.gd
|
||||||
|
turn_manager.gd
|
||||||
|
goal_manager.gd
|
||||||
|
goals_cycle_manager.gd
|
||||||
|
player_race_manager.gd
|
||||||
|
shop_manager.gd
|
||||||
|
join_manager.gd
|
||||||
|
powerup_manager.gd
|
||||||
|
notification_manager.gd
|
||||||
|
obstacle_manager.gd
|
||||||
|
friend_manager.gd
|
||||||
|
admin_manager.gd
|
||||||
|
mail_manager.gd
|
||||||
|
session_manager.gd
|
||||||
|
settings_manager.gd
|
||||||
|
tutorial_manager.gd
|
||||||
|
tutorial_overlay.gd
|
||||||
|
playerboard_manager.gd
|
||||||
|
camera_context_manager.gd
|
||||||
|
screen_shake.gd
|
||||||
|
special_tiles_manager.gd
|
||||||
|
static_tekton_manager.gd
|
||||||
|
touch_controls.gd
|
||||||
|
daily_reward_manager.gd
|
||||||
|
services/
|
||||||
|
backend_service.gd -- Unified RPC interface (autoload)
|
||||||
|
steamworks_manager.gd -- Steam auth ticket + persona (NOT autoload; child of BackendService)
|
||||||
|
scenes/
|
||||||
|
main.gd -- Core game scene controller (~2956 lines)
|
||||||
|
main.tscn -- Main game scene
|
||||||
|
player.gd -- Player character controller (~2751 lines)
|
||||||
|
player.tscn -- Player scene
|
||||||
|
lobby.gd -- Lobby/home screen controller (~583 lines)
|
||||||
|
lobby.tscn -- Lobby scene
|
||||||
|
animation.gd -- Stop n Go animation player (41 lines)
|
||||||
|
ui/
|
||||||
|
lobby_main_menu.gd -- RefCounted; main menu button wiring
|
||||||
|
lobby_room.gd -- RefCounted; room/player slot management
|
||||||
|
lobby_room_list.gd -- RefCounted; room list display + join
|
||||||
|
lobby_chat.gd -- RefCounted; global + DM chat
|
||||||
|
login_screen.tscn -- Login screen scene
|
||||||
|
boot_screen.tscn -- Boot splash scene
|
||||||
|
shop_panel.tscn -- Shop panel scene
|
||||||
|
gacha_panel.tscn -- Gacha panel scene
|
||||||
|
daily_reward_panel.tscn -- Daily reward panel scene
|
||||||
|
admin_panel.tscn -- Admin panel scene
|
||||||
|
profile_panel.tscn -- Profile panel scene
|
||||||
|
leaderboard_panel.tscn -- Leaderboard panel scene
|
||||||
|
mailbox_panel.tscn -- Mailbox panel scene
|
||||||
|
settings_menu.tscn -- Settings scene
|
||||||
|
lobby_invite_popup.tscn -- Invite popup scene
|
||||||
|
invite_friends_dialog.tscn -- Invite dialog scene
|
||||||
|
social_panel.tscn -- Social panel scene
|
||||||
|
game/
|
||||||
|
main.tscn -- (actual main game scene)
|
||||||
|
loading_screen/
|
||||||
|
loading_screen.tscn -- Level loading screen
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Autoloads / Singletons Index
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
All managers are registered as autoloads in project.godot and accessible globally via `/root/<ManagerName>`. The following are the configured autoloads:
|
||||||
|
|
||||||
|
| Autoload Name | File | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| AuthManager | res://scripts/managers/auth_manager.gd | Authentication (guest, email, social) |
|
||||||
|
| NakamaManager | res://scripts/nakama_manager.gd | Nakama client/socket/bridge lifecycle |
|
||||||
|
| BackendService | res://scripts/services/backend_service.gd | Unified RPC API wrapper |
|
||||||
|
| EventBus | res://scripts/event_bus.gd | Observer-pattern cross-manager events |
|
||||||
|
| LobbyManager | res://scripts/managers/lobby_manager.gd | Room lifecycle, matchmaking |
|
||||||
|
| GameStateManager | res://scripts/managers/game_state_manager.gd | State machine, match lifecycle |
|
||||||
|
| PlayerManager | res://scripts/managers/player_manager.gd | Player data container |
|
||||||
|
| PlayerMovementManager | res://scripts/managers/player_movement_manager.gd | Movement physics, pathfinding |
|
||||||
|
| PlayerInputManager | res://scripts/managers/player_input_manager.gd | Input capture, buffering |
|
||||||
|
| PlayerActionManager | res://scripts/managers/player_action_manager.gd | Action execution (grab, put) |
|
||||||
|
| UserProfileManager | res://scripts/managers/user_profile_manager.gd | Profile CRUD, wallet sync |
|
||||||
|
| GachaManager | res://scripts/managers/gacha_manager.gd | Gacha pull orchestration |
|
||||||
|
| SkinManager | res://scripts/managers/skin_manager.gd | Cosmetics, skins, loadout |
|
||||||
|
| UIManager | res://scripts/managers/ui_manager.gd | UI layer stack, show/hide |
|
||||||
|
| SfxManager | res://scripts/managers/sfx_manager.gd | Sound effect pool |
|
||||||
|
| MusicManager | res://scripts/managers/music_manager.gd | Music crossfade |
|
||||||
|
| GameUpdateManager | res://scripts/managers/game_update_manager.gd | Hot-reload patching |
|
||||||
|
| StopNGoManager | res://scripts/managers/stop_n_go_manager.gd | Stop n Go minigame state |
|
||||||
|
| GauntletManager | res://scripts/managers/gauntlet_manager.gd | Gauntlet mode progression |
|
||||||
|
| PortalModeManager | res://scripts/managers/portal_mode_manager.gd | Portal race mode |
|
||||||
|
| TurnManager | res://scripts/managers/turn_manager.gd | Turn-based sequencing |
|
||||||
|
| GoalManager | res://scripts/managers/goal_manager.gd | Goal validation, completion |
|
||||||
|
| GoalsCycleManager | res://scripts/managers/goals_cycle_manager.gd | Cycling goal rotation, scoring |
|
||||||
|
| PlayerRaceManager | res://scripts/managers/player_race_manager.gd | Race position, finish |
|
||||||
|
| ShopManager | res://scripts/managers/shop_manager.gd | Shop data layer |
|
||||||
|
| JoinManager | res://scripts/managers/join_manager.gd | Join code input |
|
||||||
|
| PowerupManager | res://scripts/managers/powerup_manager.gd | Powerup system (boost/charge) |
|
||||||
|
| NotificationManager | res://scripts/managers/notification_manager.gd | On-screen message queue |
|
||||||
|
| ObstacleManager | res://scripts/managers/obstacle_manager.gd | Obstacle placement/removal |
|
||||||
|
| FriendManager | res://scripts/managers/friend_manager.gd | Friends list, DMs |
|
||||||
|
| AdminManager | res://scripts/managers/admin_manager.gd | Admin panel state |
|
||||||
|
| MailManager | res://scripts/managers/mail_manager.gd | Mail CRUD |
|
||||||
|
| SessionManager | res://scripts/managers/session_manager.gd | Session refresh lifecycle |
|
||||||
|
| SettingsManager | res://scripts/managers/settings_manager.gd | User settings persistence |
|
||||||
|
| TutorialManager | res://scripts/managers/tutorial_manager.gd | Tutorial flow control |
|
||||||
|
| TutorialOverlay | res://scripts/managers/tutorial_overlay.gd | Tutorial UI overlay |
|
||||||
|
| PlayerboardManager | res://scripts/managers/playerboard_manager.gd | Player inventory board |
|
||||||
|
| CameraContextManager | res://scripts/managers/camera_context_manager.gd | Camera zoom/context |
|
||||||
|
| ScreenShake | res://scripts/managers/screen_shake.gd | Screen shake effects |
|
||||||
|
| SpecialTilesManager | res://scripts/managers/special_tiles_manager.gd | Ice/crack/portal tiles |
|
||||||
|
| StaticTektonManager | res://scripts/managers/static_tekton_manager.gd | Static Tekton turret logic |
|
||||||
|
| TouchControls | res://scripts/managers/touch_controls.gd | Mobile touch input overlay |
|
||||||
|
| DailyRewardManager | res://scripts/managers/daily_reward_manager.gd | Daily reward claims |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 3. Service Layer
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 3.1 NakamaManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/nakama_manager.gd` (330 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** NakamaManager
|
||||||
|
|
||||||
|
Central Nakama SDK integration. Manages the Nakama client, session, socket, and multiplayer bridge. All network communication flows through this singleton.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| nakama_server_key | String | From env var NAKAMA_SERVER_KEY or ProjectSettings |
|
||||||
|
| nakama_host | String | Default: `tektondash.vps.webdock.cloud` |
|
||||||
|
| nakama_port | int | Default: 7350 |
|
||||||
|
| nakama_scheme | String | Default: http |
|
||||||
|
| client | NakamaClient | The Nakama client instance |
|
||||||
|
| session | NakamaSession | Current auth session |
|
||||||
|
| socket | NakamaSocket | WebSocket connection |
|
||||||
|
| bridge | NakamaMultiplayerBridge | Links Nakama socket to Godot HLAPI |
|
||||||
|
| current_match_id | String | Currently joined match ID |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `connected_to_nakama` | none | Emitted when socket connects successfully |
|
||||||
|
| `connection_failed` | error_message: String | Emitted on connection failure |
|
||||||
|
| `match_joined` | match_id: String | Emitted when bridge joins a match |
|
||||||
|
| `match_join_error` | error_message: String | Emitted on match join failure |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set_server` | `func set_server(host: String, port: int = 7350) -> void` | void | Override Nakama server endpoint. Auto-detects scheme (https for .ts.net, http for 100.x IPs). Recreates client if no active session. |
|
||||||
|
| `connect_to_nakama_async` | `func connect_to_nakama_async(email: String = "", password: String = "") -> bool` | bool (async) | Full auth + socket + bridge connection. Empty email = device auth. Creates socket, initializes multiplayer bridge, sets Godot's multiplayer peer. |
|
||||||
|
| `cleanup` | `func cleanup() -> void` | void | Shuts down socket, leaves bridge, deletes match metadata storage, resets multiplayer peer to null. |
|
||||||
|
| `host_game` | `func host_game(room_meta: Dictionary = {}) -> void` | void | Creates a Nakama relayed match via bridge.create_match(). Optionally stores room metadata to Nakama storage. Has re-entry guard for double-click protection. |
|
||||||
|
| `join_game` | `func join_game(match_id: String) -> void` | void | Joins an existing match by ID. Leaves current match first if connected. |
|
||||||
|
| `is_connected_to_nakama` | `func is_connected_to_nakama() -> bool` | bool | Returns true if socket exists and is connected to host. |
|
||||||
|
| `list_matches_async` | `func list_matches_async(mode_filter: String = "") -> Array` | Array (async) | Queries Nakama for available matches. Batch-reads room metadata from storage. Returns array of room dicts. |
|
||||||
|
| `_on_bridge_match_joined` | `func _on_bridge_match_joined() -> void` | void | Internal: updates current_match_id, emits match_joined signal. |
|
||||||
|
| `_on_bridge_match_join_error` | `func _on_bridge_match_join_error(error) -> void` | void | Internal: emits match_join_error. |
|
||||||
|
|
||||||
|
**Dependencies:** Nakama GDExtension (NakamaClient, NakamaSocket, NakamaMultiplayerBridge).
|
||||||
|
**Depended by:** AuthManager, BackendService, LobbyManager, LobbyRoom, LobbyChat, LobbyMainMenu, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 3.2 BackendService
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/services/backend_service.gd` (247 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** BackendService
|
||||||
|
|
||||||
|
Unified typed interface for all Nakama Lua RPCs. All platform authentication paths (Steam, Nakama device/email) funnel through here. Provides retry logic with exponential backoff.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| current_platform | Platform (enum) | DESKTOP_STEAM, DESKTOP_NAKAMA, or MOBILE_NAKAMA |
|
||||||
|
| steamworks_manager | Node | Only for auth ticket retrieval |
|
||||||
|
| nakama_backend | Node | Reference to NakamaManager autoload |
|
||||||
|
|
||||||
|
**Enums:**
|
||||||
|
|
||||||
|
- `Platform { DESKTOP_STEAM, DESKTOP_NAKAMA, MOBILE_NAKAMA }`
|
||||||
|
- `ErrorCode { NONE, NETWORK_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR, UNKNOWN_ERROR, INSUFFICIENT_FUNDS }`
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Detects platform, initializes backend |
|
||||||
|
| `is_initialized` | `func is_initialized() -> bool` | bool | Checks nakama_backend is non-null |
|
||||||
|
| `get_platform_name` | `func get_platform_name() -> String` | String | Returns human-readable platform name |
|
||||||
|
| `get_steamworks_manager` | `func get_steamworks_manager() -> Node` | Node | Returns steamworks_manager child node |
|
||||||
|
| `api_rpc_async` | `func api_rpc_async(rpc_id: String, payload: String = "{}") -> Dictionary` | Dictionary (async) | Unified RPC with up to 3 retries, exponential backoff (0.5s base). Returns `{success, error, message, data}`. |
|
||||||
|
| `admin_clear_global_chat` | `func admin_clear_global_chat(payload: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_get_chat_config` | `func admin_get_chat_config() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_set_chat_config` | `func admin_set_chat_config(config: Dictionary) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_purge_old_messages` | `func admin_purge_old_messages(channel_id: String, max_age_days: int) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_list_channel_messages` | `func admin_list_channel_messages(channel_id: String, limit: int = 50, cursor: String = "", forward: bool = true) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_delete_channel_message` | `func admin_delete_channel_message(channel_id: String, message_id: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `send_friend_request` | `func send_friend_request(target_id: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `respond_friend_request` | `func respond_friend_request(target_id: String, accept: bool) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `perform_gacha_pull` | `func perform_gacha_pull(gacha_id: String, count: int) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `get_mail` | `func get_mail(payload: String = "{}") -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `claim_mail_reward` | `func claim_mail_reward(mail_id: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `delete_mail` | `func delete_mail(mail_id: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `send_mail` | `func send_mail(payload: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `change_avatar` | `func change_avatar(avatar_url: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `change_username` | `func change_username(new_username: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `change_status` | `func change_status(new_status: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `change_bio` | `func change_bio(new_bio: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `query_users` | `func query_users(payload: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `admin_give_currency` | `func admin_give_currency(payload: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `get_daily_reward_config_admin` | `func get_daily_reward_config_admin() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `set_daily_reward_config` | `func set_daily_reward_config(req: Dictionary) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `get_daily_reward_state` | `func get_daily_reward_state() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `claim_daily_reward` | `func claim_daily_reward() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `sync_leaderboard` | `func sync_leaderboard() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `get_leaderboard_stats` | `func get_leaderboard_stats() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `debug_add_exp` | `func debug_add_exp(exp_amount: int) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `reset_stats` | `func reset_stats() -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `search_users` | `func search_users(payload: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
| `send_lobby_invite` | `func send_lobby_invite(to_user_id: String, match_id: String) -> Dictionary` | Dictionary | RPC wrapper |
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager (autoload), SteamworksManager (child node).
|
||||||
|
**Depended by:** AuthManager, LobbyManager, LobbyChat, lobby.gd (admin), FriendManager, MailManager, GachaManager, DailyRewardManager, AdminManager, SkinManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 3.3 SteamworksManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/services/steamworks_manager.gd` (72 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**class_name:** SteamworksManager
|
||||||
|
|
||||||
|
NOT an autoload. Created as a child of BackendService. Provides Steam auth session tickets for Nakama login. GodotSteam GDExtension required.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| is_steam_initialized | bool | Whether Steam API initialized successfully |
|
||||||
|
| steam_app_id | int | From ProjectSettings or default 480 |
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Calls _initialize_steam |
|
||||||
|
| `is_initialized` | `func is_initialized() -> bool` | bool | Returns steam init status |
|
||||||
|
| `get_auth_session_ticket` | `func get_auth_session_ticket() -> String` | String | Gets Steam auth session ticket via Steam.getAuthSessionTicket(), returns hex-encoded buffer |
|
||||||
|
| `get_steam_user_name` | `func get_steam_user_name() -> String` | String | Returns Steam persona name via Steam.getPersonaName() |
|
||||||
|
| `get_steam_user_id` | `func get_steam_user_id() -> int` | int | Returns Steam ID via Steam.getSteamID() |
|
||||||
|
|
||||||
|
**Dependencies:** GodotSteam GDExtension (ClassDB.class_exists("Steam")).
|
||||||
|
**Depended by:** BackendService, AuthManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 4. Core Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.1 AuthManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/auth_manager.gd` (515 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** AuthManager
|
||||||
|
|
||||||
|
Centralized authentication handler. Supports Guest (device ID), Email/Password, Google, Apple, Facebook, and Steam auth modes. Persists sessions to encrypted file storage.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| current_user | Dictionary | {user_id, username, display_name, avatar_url, email} |
|
||||||
|
| is_authenticated | bool | Whether fully authenticated |
|
||||||
|
| is_guest | bool | Whether using guest mode |
|
||||||
|
| auth_mode | AuthMode (enum) | GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM |
|
||||||
|
|
||||||
|
**Enums:** `AuthMode { GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM }`
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `auth_started` | none | Emitted when any login flow begins |
|
||||||
|
| `auth_completed` | success: bool, user_data: Dictionary | Emitted on auth success or failure |
|
||||||
|
| `auth_failed` | error: String | Emitted on auth error |
|
||||||
|
| `session_restored` | none | Emitted when saved session restored |
|
||||||
|
| `logged_out` | none | Emitted after full logout |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Deferred call to _try_restore_session |
|
||||||
|
| `login_as_guest` | `func login_as_guest() -> bool` | bool (async) | Device ID guest auth. Generates/persists device ID. |
|
||||||
|
| `login_with_email` | `func login_with_email(email: String, password: String, remember: bool = true) -> bool` | bool (async) | Email/password authentication |
|
||||||
|
| `register_with_email` | `func register_with_email(email: String, password: String, username: String = "") -> bool` | bool (async) | Email registration (create if not exists) |
|
||||||
|
| `login_with_google` | `func login_with_google(id_token: String) -> bool` | bool (async) | Google auth via ID token |
|
||||||
|
| `login_with_apple` | `func login_with_apple(id_token: String) -> bool` | bool (async) | Apple auth via ID token |
|
||||||
|
| `login_with_facebook` | `func login_with_facebook(access_token: String) -> bool` | bool (async) | Facebook auth via access token |
|
||||||
|
| `login_with_steam` | `func login_with_steam() -> bool` | bool (async) | Steam ticket auth via BackendService.steamworks_manager |
|
||||||
|
| `link_email` | `func link_email(email: String, password: String) -> bool` | bool (async) | Link email to existing guest account |
|
||||||
|
| `link_google` | `func link_google(id_token: String) -> bool` | bool (async) | Link Google to existing account |
|
||||||
|
| `logout` | `func logout() -> void` | void | Full cleanup: NakamaManager.cleanup(), clear session files, reset state, emit logged_out |
|
||||||
|
| `clear_session` | `func clear_session() -> void` | void | Deletes SESSION_FILE and CREDENTIALS_FILE from user:// |
|
||||||
|
| `_try_restore_session` | internal | void | Attempts to load encrypted session file. Skips guest session auto-restore. |
|
||||||
|
| `_connect_socket` | internal | bool (async) | Creates Nakama socket, connects, initializes multiplayer bridge |
|
||||||
|
| `_load_user_profile` | internal | void (async) | Loads account data from Nakama into current_user |
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager, BackendService.
|
||||||
|
**Depended by:** LobbyMainMenu, lobby.gd, UserProfileManager, login_screen.tscn.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.2 LobbyManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/lobby_manager.gd` (1023 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** LobbyManager
|
||||||
|
|
||||||
|
Room/lobby lifecycle manager. Handles both Nakama (online) and LAN (direct ENet) modes. Manages room creation, joining, player list, ready states, game mode settings, and character/area selection.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| current_room | Dictionary | {} | Current room metadata |
|
||||||
|
| players_in_room | Array | [] | [{id, name, is_ready, character, nakama_id}] |
|
||||||
|
| available_rooms | Array | [] | Discovered rooms for room list |
|
||||||
|
| is_host | bool | false | Whether local player is room host |
|
||||||
|
| is_lan_mode | bool | false | Direct ENet (no Nakama) |
|
||||||
|
| LAN_PORT | const int | 7777 | ENet server port |
|
||||||
|
| LAN_DISCOVERY_PORT | const int | 7778 | UDP broadcast port |
|
||||||
|
| local_player_name | String | "Player" | Display name |
|
||||||
|
| is_tutorial_mode | bool | false | Tutorial mode flag |
|
||||||
|
| match_duration | int | 180 | Seconds (configurable by host) |
|
||||||
|
| randomize_spawn | bool | false | Randomize spawn positions |
|
||||||
|
| enable_cycle_timer | bool | false | Goal cycle timer |
|
||||||
|
| scarcity_mode | String | "Normal" | Item scarcity: Normal/Aggressive/Chaos |
|
||||||
|
| disconnect_reason | String | "" | UI feedback message |
|
||||||
|
| sng_go_duration | int | 20 | Stop n Go: GO phase seconds |
|
||||||
|
| sng_stop_duration | int | 4 | Stop n Go: STOP phase seconds |
|
||||||
|
| sng_required_goals | int | 8 | Goals needed for SNG win |
|
||||||
|
| doors_swap_time | int | 15 | Tekton Doors: swap interval |
|
||||||
|
| doors_refresh_time | int | 25 | Tekton Doors: refresh interval |
|
||||||
|
| doors_required_goals | int | 8 | Goals needed for Doors win |
|
||||||
|
| rematch_votes | Array | [] | Player IDs who voted for rematch |
|
||||||
|
| available_characters | Array[String] | [...] | ["Copper", "Dabro", "Gatot", "Pip", "Random"] |
|
||||||
|
| available_areas | Array[String] | [] | Mode-specific area list |
|
||||||
|
| available_game_modes | Array[String] | [...] | ["Freemode", "Stop n Go", "Candy Pump Survival"] |
|
||||||
|
| selected_area | String | "Freemode Arena" | Currently selected area |
|
||||||
|
| game_mode | String | "Freemode" | Current game mode |
|
||||||
|
| local_character_index | int | 0 | Local player's character index |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `room_list_updated` | rooms: Array | Room list refreshed |
|
||||||
|
| `room_joined` | room_data: Dictionary | Joined a room |
|
||||||
|
| `room_left` | none | Left current room |
|
||||||
|
| `player_joined` | player_data: Dictionary | Player entered room |
|
||||||
|
| `player_left` | player_id: int | Player left room |
|
||||||
|
| `ready_state_changed` | player_id: int, is_ready: bool | Player ready status changed |
|
||||||
|
| `all_players_ready` | none | All players ready |
|
||||||
|
| `host_disconnected` | none | Host left/disconnected |
|
||||||
|
| `game_starting` | none | Game countdown started |
|
||||||
|
| `match_duration_changed` | duration_seconds: int | Duration setting changed |
|
||||||
|
| `randomize_spawn_changed` | enabled: bool | Random spawn toggled |
|
||||||
|
| `character_changed` | player_id: int, character_name: String | Character selection changed |
|
||||||
|
| `area_changed` | area_name: String | Map area changed |
|
||||||
|
| `player_list_changed` | none | Player list should re-render |
|
||||||
|
| `rematch_votes_updated` | count: int, required: int | Rematch vote progress |
|
||||||
|
| `game_mode_changed` | mode: String | Game mode changed |
|
||||||
|
| `scarcity_mode_changed` | mode: String | Scarcity setting changed |
|
||||||
|
| `enable_cycle_timer_changed` | enabled: bool | Timer toggle changed |
|
||||||
|
| `sng_go_duration_changed` | duration: int | SNG Go duration changed |
|
||||||
|
| `sng_stop_duration_changed` | duration: int | SNG Stop duration changed |
|
||||||
|
| `sng_required_goals_changed` | goals: int | SNG required goals changed |
|
||||||
|
| `doors_swap_time_changed` | time: int | Doors swap interval changed |
|
||||||
|
| `doors_refresh_time_changed` | time: int | Doors refresh interval changed |
|
||||||
|
| `doors_required_goals_changed` | goals: int | Doors required goals changed |
|
||||||
|
| `gauntlet_round_duration_changed` | duration: int | Gauntlet round duration changed |
|
||||||
|
| `gauntlet_growth_interval_changed` | interval: float | Gauntlet growth interval changed |
|
||||||
|
| `gauntlet_cells_per_tick_changed` | cells: Dictionary | Cells per tick changed |
|
||||||
|
|
||||||
|
**Key Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_tutorial` | `func start_tutorial(mode: String = "Freemode") -> void` | void | Sets tutorial flags, calls create_room_lan("Tutorial") |
|
||||||
|
| `create_room` | `func create_room(room_name: String) -> void` | void | Hosts Nakama room: connects, calls NakamaManager.host_game |
|
||||||
|
| `join_room` | `func join_room(match_id: String) -> void` | void | Joins Nakama room by match ID |
|
||||||
|
| `create_room_lan` | `func create_room_lan(room_name: String = "LAN Game") -> bool` | bool | Creates ENet server on LAN_PORT, broadcasts UDP discovery |
|
||||||
|
| `join_room_lan` | `func join_room_lan(host_ip: String) -> bool` | bool | Creates ENet client to host IP:LAN_PORT |
|
||||||
|
| `leave_room` | -- | void | Leaves current room, cleans up peers |
|
||||||
|
| `start_game` | `func start_game(is_tutorial: bool = false) -> void` | void | Transitions from lobby to main game scene |
|
||||||
|
| `refresh_room_list` | `func refresh_room_list() -> void` | void | Queries Nakama for available rooms or broadcasts LAN |
|
||||||
|
| `set_ready` | `func set_ready(is_ready: bool) -> void` | void | Updates ready state via RPC |
|
||||||
|
| `set_match_duration` | `func set_match_duration(seconds: int) -> void` | void | Host sets match duration |
|
||||||
|
| `set_randomize_spawn` | `func set_randomize_spawn(enabled: bool) -> void` | void | Host toggles random spawn |
|
||||||
|
| `set_enable_cycle_timer` | `func set_enable_cycle_timer(enabled: bool) -> void` | void | Host toggles timer |
|
||||||
|
| `set_scarcity_mode` | `func set_scarcity_mode(mode: String) -> void` | void | Host sets scarcity |
|
||||||
|
| `set_game_mode` | `func set_game_mode(mode: String) -> void` | void | Host sets game mode |
|
||||||
|
| `cycle_character` | `func cycle_character(direction: int) -> void` | void | Change character selection |
|
||||||
|
| `cycle_area` | `func cycle_area(direction: int) -> void` | void | Change selected area |
|
||||||
|
| `get_players` | `func get_players() -> Array` | Array | Returns players_in_room |
|
||||||
|
| `is_all_ready` | `func is_all_ready() -> bool` | bool | All players ready check |
|
||||||
|
| `set_sng_go_duration` | -- | void | Host sets SNG go time |
|
||||||
|
| `set_sng_stop_duration` | -- | void | Host sets SNG stop time |
|
||||||
|
| `set_sng_required_goals` | -- | void | Host sets SNG goals |
|
||||||
|
| `get_selected_area` | `func get_selected_area() -> String` | String | Returns current area name |
|
||||||
|
| `get_game_mode` | `func get_game_mode() -> GameMode.Mode` | GameMode.Mode | Converts string to GameMode enum |
|
||||||
|
| `is_game_mode` | `func is_game_mode(mode: GameMode.Mode) -> bool` | bool | Mode comparison helper |
|
||||||
|
|
||||||
|
**Internal Functions:** `_on_match_joined`, `_on_peer_connected`, `_on_peer_disconnected`, `_on_server_disconnected`, `_update_available_areas`, `_start_lan_broadcast`, `_broadcast_lan_room`, `_stop_lan_broadcast`, `_update_lan_room_list`, `_listen_for_lan_discovery`, `_update_ready_state_rpc`, `_request_rematch`, `rpc_set_*`, `rpc_*`.
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager, GameStateManager.
|
||||||
|
**Depended by:** LobbyRoom, LobbyRoomList, LobbyMainMenu, main.gd, player.gd, lobby.gd, SceneManager (loading screen).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.3 GameStateManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/game_state_manager.gd` (66 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GameStateManager
|
||||||
|
|
||||||
|
Simple state machine and match configuration constants.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| current_state | GameState (enum) | LOBBY | Current application state |
|
||||||
|
| max_players | int | 8 | Max players in a match |
|
||||||
|
| enable_bots | bool | false | Bot fill toggle |
|
||||||
|
| local_player_id | int | 0 | Local peer ID |
|
||||||
|
|
||||||
|
**Enums:** `GameState { LOBBY, LOADING, GAME, RESULT }`
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `state_changed` | new_state: GameState | Emitted on state transition |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `change_state` | `func change_state(new_state: GameState) -> void` | void | Transitions state, emits state_changed |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** LobbyManager, main.gd, tutorial_manager.gd, many managers.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.4 PlayerManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/player_manager.gd` (37 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerManager
|
||||||
|
|
||||||
|
Lightweight data container for player metadata. Stores display name and peer ID for the local player. Used as a quick reference by various subsystems.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| display_name | String | Local player's display name |
|
||||||
|
| peer_id | int | Local player's multiplayer unique ID |
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:** None (data-only container).
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** UIManager, player.gd, various managers needing player identity.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.5 EventBus
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/event_bus.gd` (73 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** EventBus
|
||||||
|
|
||||||
|
Centralized observer pattern for inter-manager communication. Replaces direct cross-references between managers.
|
||||||
|
|
||||||
|
**Constants (event names):**
|
||||||
|
|
||||||
|
| Constant | Value | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| EVENT_PLAYER_JOINED | "player_joined" | Player entered match |
|
||||||
|
| EVENT_PLAYER_LEFT | "player_left" | Player left match |
|
||||||
|
| EVENT_PLAYER_READY | "player_ready" | Player ready state changed |
|
||||||
|
| EVENT_MATCH_STARTED | "match_started" | Match began |
|
||||||
|
| EVENT_MATCH_ENDED | "match_ended" | Match ended |
|
||||||
|
| EVENT_GAME_MODE_CHANGED | "game_mode_changed" | Game mode switched |
|
||||||
|
| EVENT_CURRENCY_CHANGED | "currency_changed" | Wallet balance changed |
|
||||||
|
| EVENT_ITEM_PURCHASED | "item_purchased" | Item bought from shop |
|
||||||
|
| EVENT_GACHA_PULL | "gacha_pull" | Gacha rolled |
|
||||||
|
| EVENT_PROFILE_LOADED | "profile_loaded" | Profile loaded from server |
|
||||||
|
| EVENT_PROFILE_UPDATED | "profile_updated" | Profile updated |
|
||||||
|
| EVENT_AVATAR_CHANGED | "avatar_changed" | Avatar changed |
|
||||||
|
| EVENT_SESSION_REFRESHED | "session_refreshed" | Nakama session refreshed |
|
||||||
|
| EVENT_SESSION_EXPIRED | "session_expired" | Nakama session expired |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `event_emitted` | event_name: String, data: Variant | Fired on every emit |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `emit` | `func emit(event_name: String, data: Variant = null) -> void` | void | Emit event to all registered listeners and the signal bus |
|
||||||
|
| `on` | `func on(event_name: String, callback: Callable) -> void` | void | Subscribe to event |
|
||||||
|
| `off` | `func off(event_name: String, callback: Callable) -> void` | void | Unsubscribe from event |
|
||||||
|
| `clear` | `func clear() -> void` | void | Remove all listeners (scene transition cleanup) |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** UserProfileManager, GachaManager, ShopManager, many managers for loose coupling.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 4.6 GameMode / ModeConfig
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/game_mode.gd` (41 lines)
|
||||||
|
**Extends:** RefCounted
|
||||||
|
**class_name:** GameMode
|
||||||
|
|
||||||
|
Enum and string conversion utilities for game modes.
|
||||||
|
|
||||||
|
**Enum:** `Mode { FREEMODE = 0, STOP_N_GO = 1, TEKTON_DOORS = 2, GAUNTLET = 3 }`
|
||||||
|
|
||||||
|
**Public Static Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `from_string` | `static func from_string(mode: String) -> Mode` | Mode | Converts "Freemode"/"Stop n Go"/"Tekton Doors"/"Candy Pump Survival" to enum |
|
||||||
|
| `mode_to_string` | `static func mode_to_string(mode: Mode) -> String` | String | Converts enum back to string |
|
||||||
|
| `is_restricted` | `static func is_restricted(mode: Mode) -> bool` | bool | Returns true for SNG, Doors, or Gauntlet |
|
||||||
|
| `get_all_modes` | `static func get_all_modes() -> Array[String]` | Array[String] | Returns all mode names |
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/mode_config.gd` (108 lines)
|
||||||
|
**Extends:** RefCounted
|
||||||
|
**class_name:** ModeConfig
|
||||||
|
|
||||||
|
Schema-driven validation for game mode settings. Consolidates duplicated/inconsistent option toggles.
|
||||||
|
|
||||||
|
**Public Static Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_defaults` | `static func get_defaults(mode: String) -> Dictionary` | Dictionary | Returns default config dict for mode |
|
||||||
|
| `validate_setting` | `static func validate_setting(mode: String, key: String, value: Variant) -> Dictionary` | Dictionary | Validates type, range, and allowed values for a single setting |
|
||||||
|
| `validate_config` | `static func validate_config(mode: String, config: Dictionary) -> Dictionary` | Dictionary | Validates entire config, returns errors array |
|
||||||
|
| `get_mode_settings` | `static func get_mode_settings(mode: String) -> Array` | Array | Returns list of setting keys for mode |
|
||||||
|
| `get_setting_schema` | `static func get_setting_schema(mode: String, key: String) -> Dictionary` | Dictionary | Returns schema for specific setting |
|
||||||
|
| `has_setting` | `static func has_setting(mode: String, key: String) -> bool` | bool | Checks if setting exists for mode |
|
||||||
|
| `get_supported_modes` | `static func get_supported_modes() -> Array` | Array | Returns all supported mode strings |
|
||||||
|
|
||||||
|
**Dependencies:** None (standalone utility classes).
|
||||||
|
**Depended by:** LobbyManager, LobbyRoom, mode-specific managers.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 5. Player Subsystem Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 5.1 PlayerMovementManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/player_movement_manager.gd` (33,053 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerMovementManager
|
||||||
|
|
||||||
|
Handles player movement physics, grid-based pathfinding, movement range highlighting, position syncing, and obstacle-aware navigation. Delegated from player.gd.
|
||||||
|
|
||||||
|
**Signals:** (custom signals listed; full list from code)
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `movement_started` | path: Array | Emitted when player begins moving |
|
||||||
|
| `movement_completed` | none | Emitted when movement tween finishes |
|
||||||
|
| `movement_interrupted` | none | Emitted when movement is cancelled |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `move_along_path` | `func move_along_path(player: Node, path: Array) -> void` | void | Tweens player along grid path |
|
||||||
|
| `find_path` | `func find_path(from: Vector2i, to: Vector2i, gridmap: Node) -> Array` | Array | A* or BFS pathfinding on grid |
|
||||||
|
| `highlight_movement_range` | `func highlight_movement_range(player: Node) -> void` | void | Shows reachable cells |
|
||||||
|
| `highlight_adjacent_cells` | `func highlight_adjacent_cells(player: Node) -> void` | void | Shows cardinal-adjacent cells |
|
||||||
|
| `rotate_towards_target` | `func rotate_towards_target(target_pos: Vector2i) -> void` | void | Smooth rotation to face target |
|
||||||
|
| `can_move_to` | `func can_move_to(pos: Vector2i, gridmap: Node) -> bool` | bool | Cell walkability check |
|
||||||
|
| `apply_stagger` | `func apply_stagger(duration: float) -> void` | void | Applies stun knockback |
|
||||||
|
| `sync_bump` | `func sync_bump(target_pos: Vector2i, is_soft: bool) -> void` | void | Visual bump animation |
|
||||||
|
| `set_player_moving` | `func set_player_moving(is_moving: bool) -> void` | void | Toggle movement state |
|
||||||
|
|
||||||
|
**Dependencies:** player.gd (node refs), ObstacleManager, SpecialTilesManager, EnhancedGridMap.
|
||||||
|
**Depended by:** player.gd, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 5.2 PlayerInputManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/player_input_manager.gd` (7,292 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerInputManager
|
||||||
|
|
||||||
|
Captures and buffers player input events. Supports keyboard, mouse, gamepad, and touch inputs. Provides input state query API.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `input_received` | event: InputEvent | Raw input forwarded |
|
||||||
|
| `action_pressed` | action: String | Action mapped press (grab, put, move) |
|
||||||
|
| `action_released` | action: String | Action released |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `is_action_held` | `func is_action_held(action: String) -> bool` | bool | Check if action is currently held |
|
||||||
|
| `get_movement_direction` | `func get_movement_direction() -> Vector2i` | Vector2i | Grid-aligned movement cardinal |
|
||||||
|
| `get_look_direction` | `func get_look_direction(camera: Camera3D) -> Vector2` | Vector2 | Mouse-world direction |
|
||||||
|
| `flush_buffer` | `func flush_buffer() -> void` | void | Clear input buffer |
|
||||||
|
| `is_touch_active` | `func is_touch_active() -> bool` | bool | Whether touch controls are in use |
|
||||||
|
|
||||||
|
**Dependencies:** TouchControls (autoload).
|
||||||
|
**Depended by:** player.gd, player_action_manager.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 5.3 PlayerActionManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/player_action_manager.gd` (8,828 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerActionManager
|
||||||
|
|
||||||
|
Action execution layer. Manages grab, put, arrange, tekton throw/knock actions. Handles action point consumption, cooldowns, and visual highlighting.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `action_executed` | action_type: String | Action performed |
|
||||||
|
| `action_failed` | reason: String | Action invalid |
|
||||||
|
| `action_points_changed` | points: int | AP updated |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `execute_grab` | `func execute_grab(player: Node, grid_pos: Vector2i) -> bool` | bool | Grab item from grid |
|
||||||
|
| `execute_put` | `func execute_put(player: Node, slot_index: int, grid_pos: Vector2i) -> bool` | bool | Put item from playerboard to grid |
|
||||||
|
| `execute_arrange` | `func execute_arrange(player: Node, from_slot: int, to_slot: int) -> bool` | bool | Rearrange playerboard slots |
|
||||||
|
| `consume_action_points` | `func consume_action_points(points: int) -> void` | void | Deduct action points |
|
||||||
|
| `can_afford_action` | `func can_afford_action() -> bool` | bool | Check AP > 0 |
|
||||||
|
| `after_action_completed` | `func after_action_completed() -> void` | void | Post-action cleanup: check win, cycle goals |
|
||||||
|
| `highlight_cells_if_authorized` | `func highlight_cells_if_authorized(cells: Array, item_id: int) -> void` | void | Show valid target cells |
|
||||||
|
| `highlight_empty_adjacent_cells` | `func highlight_empty_adjacent_cells() -> void` | void | Show empty adjacent cells for put |
|
||||||
|
| `highlight_occupied_playerboard_slots` | `func highlight_occupied_playerboard_slots() -> void` | void | Show occupied slots for grab |
|
||||||
|
| `highlight_random_valid_cells` | `func highlight_random_valid_cells() -> void` | void | Show random valid cells |
|
||||||
|
| `clear_highlights` | `func clear_highlights() -> void` | void | Remove all cell highlights |
|
||||||
|
| `clear_playerboard_highlights` | `func clear_playerboard_highlights() -> void` | void | Remove playerboard highlights |
|
||||||
|
|
||||||
|
**Dependencies:** PlayerboardManager, PlayerInputManager, GoalsCycleManager.
|
||||||
|
**Depended by:** player.gd, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 5.4 PlayerboardManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/playerboard_manager.gd` (22,790 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerboardManager
|
||||||
|
|
||||||
|
Manages each player's inventory board (2x5 or 3x5 grid of item slots). Handles slot selection, item placement, auto-arrange for goal matching, drag-and-drop, and visual updates.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `slot_selected` | slot_index: int | Slot clicked/selected |
|
||||||
|
| `slot_deselected` | none | Selection cleared |
|
||||||
|
| `item_placed` | slot_index: int, item_id: int | Item added to slot |
|
||||||
|
| `item_removed` | slot_index: int | Item removed from slot |
|
||||||
|
| `playerboard_updated` | player_id: int, board: Array | Full board synced |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `grab_item` | `func grab_item(grid_pos: Vector2i) -> bool` | bool | Auto-place grabbed item into best-fit slot |
|
||||||
|
| `auto_put_item` | `func auto_put_item() -> bool` | bool | Put goal-matching tile from board to adjacent grid |
|
||||||
|
| `handle_slot_clicked` | `func handle_slot_clicked(slot_index: int) -> void` | void | Process slot click event |
|
||||||
|
| `handle_playerboard_slot_selected` | `func handle_playerboard_slot_selected(slot_index: int) -> void` | void | Handle slot selection for action |
|
||||||
|
| `handle_put_slot_selected` | `func handle_put_slot_selected(slot_index: int) -> void` | void | Handle slot chosen for put action |
|
||||||
|
| `arrange_playerboard_item` | `func arrange_playerboard_item(slot_index: int) -> void` | void | Move item to better slot |
|
||||||
|
| `select_playerboard_slot` | `func select_playerboard_slot(slot_index: int) -> void` | void | Mark slot as selected |
|
||||||
|
| `deselect_playerboard_slot` | `func deselect_playerboard_slot() -> void` | void | Clear slot selection |
|
||||||
|
| `target_playerboard_slot` | `func target_playerboard_slot(slot_index: int) -> void` | void | Target a slot for move |
|
||||||
|
| `untarget_playerboard_slot` | `func untarget_playerboard_slot() -> void` | void | Clear target |
|
||||||
|
| `can_move_to_target_playerboard_slot` | `func can_move_to_target_playerboard_slot() -> bool` | bool | Check if target slot is valid |
|
||||||
|
| `bot_grab_item` | `func bot_grab_item(pos: Vector2i, slot: int, x: int, y: int, z: int) -> void` | void | Bot performs grab |
|
||||||
|
| `bot_put_item` | `func bot_put_item(pos: Vector2i, slot: int, x: int, y: int, z: int) -> void` | void | Bot performs put |
|
||||||
|
| `bot_arrange_item` | `func bot_arrange_item(from_slot: int, to_slot: int) -> void` | void | Bot rearranges board |
|
||||||
|
|
||||||
|
**Dependencies:** GoalsCycleManager, GoalManager, EnhancedGridMap (scene ref).
|
||||||
|
**Depended by:** player.gd, PlayerActionManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 5.5 PowerupManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/powerup_manager.gd` (9,417 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PowerupManager
|
||||||
|
|
||||||
|
Powerup/boost system. Tracks boost charge level, special ability availability, and consumes boost for charged actions.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `boost_changed` | amount: float | Boost level changed |
|
||||||
|
| `boost_full` | none | Boost reached 100% |
|
||||||
|
| `powerup_activated` | type: String | Powerup used |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `add_boost` | `func add_boost(amount: float) -> void` | void | Increment boost |
|
||||||
|
| `consume_boost` | `func consume_boost(amount: float) -> void` | void | Deduct boost |
|
||||||
|
| `can_use_special` | `func can_use_special() -> bool` | bool | Boost >= 100 |
|
||||||
|
| `get_boost_pct` | `func get_boost_pct() -> float` | float | 0.0 to 1.0 |
|
||||||
|
| `reset_boost` | `func reset_boost() -> void` | void | Set to 0 |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** player.gd (charged strike, knock), PlayerActionManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 6. Game Mode Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.1 StopNGoManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/stop_n_go_manager.gd` (21,884 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** StopNGoManager
|
||||||
|
|
||||||
|
State machine for the Stop n Go game mode. Alternates between GO (movement allowed) and STOP (frozen) phases. Tracks winner via first player to complete required goals during GO phases.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `phase_changed` | phase: String ("go"/"stop") | GO/STOP transition |
|
||||||
|
| `countdown_tick` | seconds: int | Phase countdown tick |
|
||||||
|
| `sng_winner` | player_id: int | Winner determined |
|
||||||
|
| `sng_ended` | none | Minigame concluded |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_sng` | `func start_sng(go_duration: int, stop_duration: int, required_goals: int) -> void` | void | Initialize SNG with params |
|
||||||
|
| `stop_sng` | `func stop_sng() -> void` | void | End SNG minigame |
|
||||||
|
| `start_go_phase` | `func start_go_phase() -> void` | void | Begin GO timer |
|
||||||
|
| `start_stop_phase` | `func start_stop_phase() -> void` | void | Begin STOP timer, freeze all |
|
||||||
|
| `freeze_player` | `func freeze_player(player_id: int) -> void` | void | Stop player movement |
|
||||||
|
| `unfreeze_player` | `func unfreeze_player(player_id: int) -> void` | void | Resume player movement |
|
||||||
|
| `check_winner` | `func check_winner() -> int` | int | Returns winner peer_id or -1 |
|
||||||
|
| `get_phase` | `func get_phase() -> String` | String | Current phase |
|
||||||
|
|
||||||
|
**Dependencies:** TurnManager, GoalManager, GoalsCycleManager, animation.gd (scene).
|
||||||
|
**Depended by:** main.gd, player.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.2 GauntletManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/gauntlet_manager.gd` (5,467 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GauntletManager
|
||||||
|
|
||||||
|
Manages the Candy Pump Survival / Gauntlet game mode. Handles round progression, danger zone growth (flood fill), and elimination.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `round_started` | round: int | New round began |
|
||||||
|
| `danger_zone_grown` | cells: Array | New tiles flooded |
|
||||||
|
| `player_eliminated` | player_id: int | Player fell off/eliminated |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_gauntlet` | `func start_gauntlet(duration: int, growth_interval: float) -> void` | void | Initialize gauntlet mode |
|
||||||
|
| `stop_gauntlet` | `func stop_gauntlet() -> void` | void | End gauntlet mode |
|
||||||
|
| `eliminate_player` | `func eliminate_player(player_id: int) -> void` | void | Mark player as eliminated |
|
||||||
|
| `get_alive_players` | `func get_alive_players() -> Array` | Array | Returns non-eliminated player IDs |
|
||||||
|
| `get_round` | `func get_round() -> int` | int | Current round number |
|
||||||
|
|
||||||
|
**Dependencies:** TurnManager, EnhancedGridMap.
|
||||||
|
**Depended by:** main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.3 PortalModeManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/portal_mode_manager.gd` (20,072 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PortalModeManager
|
||||||
|
|
||||||
|
Manages portal race mode (Tekton Doors variant). Tracks portal positions, door swapping, refresh cycles, and race completion.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `portals_swapped` | portal_pairs: Array | Doors swapped positions |
|
||||||
|
| `portals_refreshed` | portals: Array | New portal set spawned |
|
||||||
|
| `player_teleported` | player_id: int, from: Vector2i, to: Vector2i | Player used portal |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_portal_mode` | `func start_portal_mode(swap_time: int, refresh_time: int) -> void` | void | Initialize portal mode |
|
||||||
|
| `stop_portal_mode` | `func stop_portal_mode() -> void` | void | End portal mode |
|
||||||
|
| `teleport_player` | `func teleport_player(player: Node, portal_enter: Vector2i) -> void` | void | Teleport player through portal pair |
|
||||||
|
| `swap_portals` | `func swap_portals() -> void` | void | Randomize portal positions |
|
||||||
|
| `refresh_portals` | `func refresh_portals() -> void` | void | Spawn new portal set |
|
||||||
|
| `get_portal_pair` | `func get_portal_pair(portal_id: int) -> Array` | Array | Returns [entry, exit] positions |
|
||||||
|
|
||||||
|
**Dependencies:** SpecialTilesManager, EnhancedGridMap.
|
||||||
|
**Depended by:** main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.4 GoalManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/goal_manager.gd` (3,857 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GoalManager
|
||||||
|
|
||||||
|
Goal definitions, validation rules, and completion detection. Checks if a player's board arrangement matches the current goal pattern.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `goal_completed` | player_id: int, goal_id: int | Player completed a goal |
|
||||||
|
| `goal_failed` | player_id: int, reason: String | Goal became impossible |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `validate_goal` | `func validate_goal(player_board: Array, goal: Dictionary) -> bool` | bool | Check board matches goal pattern |
|
||||||
|
| `get_goal_type` | `func get_goal_type(goal: Dictionary) -> String` | String | Goal category (row, col, set, pattern) |
|
||||||
|
| `is_goal_possible` | `func is_goal_possible(player_board: Array, goal: Dictionary) -> bool` | bool | Whether goal is still achievable |
|
||||||
|
| `find_best_slot_for_item` | `func find_best_slot_for_item(board: Array, item: int, goal: Dictionary) -> int` | int | Auto-place item into best slot |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** GoalsCycleManager, PlayerboardManager, StopNGoManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.5 GoalsCycleManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/goals_cycle_manager.gd` (20,175 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GoalsCycleManager
|
||||||
|
|
||||||
|
Manages cycling goal rotation. Tracks per-player score, cycles active goals on timer or action trigger, and determines when a player reaches the goal threshold to win.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `goals_cycled` | new_goals: Array | Active goals changed |
|
||||||
|
| `player_scored` | player_id: int, points: int | Player earned points |
|
||||||
|
| `player_won` | player_id: int | Player reached win threshold |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_cycle` | `func start_cycle(timer_enabled: bool) -> void` | void | Begin goal cycling |
|
||||||
|
| `stop_cycle` | `func stop_cycle() -> void` | void | Stop cycling |
|
||||||
|
| `cycle_goals` | `func cycle_goals() -> void` | void | Generate new goal set |
|
||||||
|
| `add_score` | `func add_score(player_id: int, points: int) -> void` | void | Award points to player |
|
||||||
|
| `get_player_score` | `func get_player_score(player_id: int) -> int` | int | Get player's current score |
|
||||||
|
| `get_current_goals` | `func get_current_goals() -> Array` | Array | Get active goals |
|
||||||
|
| `set_goal_threshold` | `func set_goal_threshold(goals_needed: int) -> void` | void | Set goals to win |
|
||||||
|
|
||||||
|
**Dependencies:** GoalManager, TurnManager, Timer (scene).
|
||||||
|
**Depended by:** main.gd, PlayerActionManager, StopNGoManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.6 PlayerRaceManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/player_race_manager.gd` (4,757 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** PlayerRaceManager
|
||||||
|
|
||||||
|
Race-specific logic. Tracks player race position, finish locations, lap progression, and race completion.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `position_changed` | player_id: int, pos: int | Player moved in race order |
|
||||||
|
| `lap_completed` | player_id: int, lap: int | Player finished a lap |
|
||||||
|
| `race_completed` | results: Array | Final standings [{id, position}] |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_race` | `func start_race() -> void` | void | Initialize race state |
|
||||||
|
| `end_race` | `func end_race() -> void` | void | Finalize race |
|
||||||
|
| `on_race_completed` | `func on_race_completed(final_pos: int) -> void` | void | Player crossed finish line |
|
||||||
|
| `get_current_finish_locations` | `func get_current_finish_locations() -> Array` | Array | Active finish positions |
|
||||||
|
| `update_finish_availability` | `func update_finish_availability() -> void` | void | Recalculate finish positions |
|
||||||
|
| `get_player_position` | `func get_player_position(player_id: int) -> int` | int | Current race order index |
|
||||||
|
| `add_second_lap_goals` | `func add_second_lap_goals(goals: Array) -> void` | void | Set lap 2 goals |
|
||||||
|
|
||||||
|
**Dependencies:** GoalsCycleManager.
|
||||||
|
**Depended by:** player.gd, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 6.7 TurnManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/turn_manager.gd` (849 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** TurnManager
|
||||||
|
|
||||||
|
Turn-based sequencing for game modes that use round-robin or ordered turns (e.g., Stop n Go, Tekton Doors).
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| current_turn | int | Index in turn order |
|
||||||
|
| turn_order | Array | Player peer IDs in sequence |
|
||||||
|
| is_my_turn | bool | Whether local player is active |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `turn_changed` | player_id: int | Active turn changed |
|
||||||
|
| `turn_order_set` | order: Array | Turn order established |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set_turn_order` | `func set_turn_order(order: Array) -> void` | void | Establish turn sequence |
|
||||||
|
| `next_turn` | `func next_turn() -> void` | void | Advance to next player |
|
||||||
|
| `get_current_player` | `func get_current_player() -> int` | int | Current player peer ID |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** StopNGoManager, GauntletManager, GoalsCycleManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 7. Gameplay Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 7.1 ObstacleManager
|
||||||
|
|
||||||
|
[Back to top](#up)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/obstacle_manager.gd` (5,662 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** ObstacleManager
|
||||||
|
|
||||||
|
Obstacle placement and removal on the game grid. Handles wall tiles, blocking tiles, and destructible barriers.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `obstacle_placed` | cell: Vector3i, item_id: int | New obstacle added |
|
||||||
|
| `obstacle_removed` | cell: Vector3i | Obstacle destroyed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `place_obstacle` | `func place_obstacle(cell: Vector3i, item_id: int) -> void` | void | Set obstacle on grid layer |
|
||||||
|
| `remove_obstacle` | `func remove_obstacle(cell: Vector3i) -> void` | void | Clear obstacle |
|
||||||
|
| `is_cell_blocked` | `func is_cell_blocked(cell: Vector3i, gridmap: Node) -> bool` | bool | Check if cell has blocking tile |
|
||||||
|
| `get_blocked_cells` | `func get_blocked_cells(gridmap: Node) -> Array` | Array | All blocked cells |
|
||||||
|
| `clear_all_obstacles` | `func clear_all_obstacles() -> void` | void | Remove all obstacles |
|
||||||
|
|
||||||
|
**Dependencies:** EnhancedGridMap (scene ref).
|
||||||
|
**Depended by:** PlayerMovementManager, SpecialTilesManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 7.2 SpecialTilesManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/special_tiles_manager.gd` (23,090 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** SpecialTilesManager
|
||||||
|
|
||||||
|
Manages special floor tiles: ice (slippery), crack (breakable), portal tiles, teleporters, and other interactive terrain.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `tile_activated` | pos: Vector2i, tile_type: String | Tile effect triggered |
|
||||||
|
| `ice_slide_started` | player_id: int | Player started sliding |
|
||||||
|
| `crack_broke` | pos: Vector2i | Crack tile collapsed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `apply_tile_effect` | `func apply_tile_effect(player: Node, pos: Vector2i) -> void` | void | Activate tile effect on player |
|
||||||
|
| `get_tile_at` | `func get_tile_at(pos: Vector2i, gridmap: Node) -> int` | int | Item ID at position |
|
||||||
|
| `set_tile` | `func set_tile(pos: Vector2i, item_id: int, gridmap: Node) -> void` | void | Set tile item |
|
||||||
|
| `is_ice_tile` | `func is_ice_tile(item_id: int) -> bool` | bool | Check ice type |
|
||||||
|
| `is_crack_tile` | `func is_crack_tile(item_id: int) -> bool` | bool | Check crack type |
|
||||||
|
| `is_portal_tile` | `func is_portal_tile(item_id: int) -> bool` | bool | Check portal type |
|
||||||
|
| `spawn_portal_pair` | `func spawn_portal_pair(pos_a: Vector2i, pos_b: Vector2i) -> void` | void | Create portal entry/exit |
|
||||||
|
| `remove_portal_pair` | `func remove_portal_pair(pos_a: Vector2i, pos_b: Vector2i) -> void` | void | Remove portal tiles |
|
||||||
|
|
||||||
|
**Dependencies:** EnhancedGridMap, ObstacleManager, PortalModeManager.
|
||||||
|
**Depended by:** PlayerMovementManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 7.3 StaticTektonManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/static_tekton_manager.gd` (7,416 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** StaticTektonManager
|
||||||
|
|
||||||
|
Manages stationary Tekton turret behavior. Handles targeting, projectile spawning, and stun zones.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `turret_fired` | turret_id: int, target_pos: Vector2i | Turret shot |
|
||||||
|
| `turret_stunned` | turret_id: int | Turret disabled |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `activate_turret` | `func activate_turret(turret: Node) -> void` | void | Start turret behavior |
|
||||||
|
| `deactivate_turret` | `func deactivate_turret(turret: Node) -> void` | void | Stop turret |
|
||||||
|
| `fire_at_player` | `func fire_at_player(turret: Node, target: Vector2i) -> void` | void | Fire projectile at grid pos |
|
||||||
|
|
||||||
|
**Dependencies:** EnhancedGridMap, ObstacleManager.
|
||||||
|
**Depended by:** main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 8. UI / Presentation Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.1 UIManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/ui_manager.gd` (21,645 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** UIManager
|
||||||
|
|
||||||
|
Manages the UI layer stack: show/hide panels, overlay management, HUD elements, and dynamic UI creation.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `panel_opened` | panel_name: String | Panel shown |
|
||||||
|
| `panel_closed` | panel_name: String | Panel hidden |
|
||||||
|
| `hud_updated` | data: Dictionary | HUD refresh |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `show_panel` | `func show_panel(panel_name: String, data: Dictionary = {}) -> void` | void | Show named panel |
|
||||||
|
| `hide_panel` | `func hide_panel(panel_name: String) -> void` | void | Hide named panel |
|
||||||
|
| `toggle_panel` | `func toggle_panel(panel_name: String) -> void` | void | Toggle panel visibility |
|
||||||
|
| `show_hud` | `func show_hud() -> void` | void | Display HUD |
|
||||||
|
| `hide_hud` | `func hide_hud() -> void` | void | Hide HUD |
|
||||||
|
| `create_dynamic_ui` | `func create_dynamic_ui(scene_path: String) -> Node` | Node | Instantiate UI from tscn |
|
||||||
|
| `destroy_dynamic_ui` | `func destroy_dynamic_ui(ui_node: Node) -> void` | void | Remove dynamic UI |
|
||||||
|
| `focus_panel` | `func focus_panel(panel_name: String) -> void` | void | Bring panel to front |
|
||||||
|
| `get_active_panels` | `func get_active_panels() -> Array` | Array | Currently visible panels |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** main.gd, lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.2 SfxManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/sfx_manager.gd` (2,046 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** SfxManager
|
||||||
|
|
||||||
|
Sound effect playback pool. Manages one-shot SFX with positional audio support.
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `play` | `func play(sfx_name: String, position: Vector3 = Vector3.ZERO) -> void` | void | Play SFX by name, optionally 3D positioned |
|
||||||
|
| `stop` | `func stop(sfx_name: String) -> void` | void | Stop specific SFX |
|
||||||
|
| `stop_all` | `func stop_all() -> void` | void | Silence all SFX |
|
||||||
|
| `set_volume` | `func set_volume(db: float) -> void` | void | Set master SFX volume |
|
||||||
|
|
||||||
|
**Dependencies:** AudioStreamPlayer pool (scene).
|
||||||
|
**Depended by:** player.gd, StopNGoManager, UIManager, many gameplay managers.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.3 MusicManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/music_manager.gd` (4,082 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** MusicManager
|
||||||
|
|
||||||
|
Background music controller. Handles crossfade between tracks, playlist sequencing, and volume control.
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_music` | `func start_music(track_name: String = "") -> void` | void | Begin playing track or playlist |
|
||||||
|
| `stop_music` | `func stop_music(fade: float = 0.5) -> void` | void | Fade out and stop |
|
||||||
|
| `crossfade_to` | `func crossfade_to(track_name: String, fade_duration: float = 1.0) -> void` | void | Smooth transition |
|
||||||
|
| `set_volume` | `func set_volume(db: float) -> void` | void | Set master music volume |
|
||||||
|
| `set_paused` | `func set_paused(paused: bool) -> void` | void | Pause/resume |
|
||||||
|
| `get_current_track` | `func get_current_track() -> String` | String | Currently playing track name |
|
||||||
|
|
||||||
|
**Dependencies:** AudioStreamPlayer (scene).
|
||||||
|
**Depended by:** lobby.gd, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.4 NotificationManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/notification_manager.gd` (2,215 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** NotificationManager
|
||||||
|
|
||||||
|
On-screen message queue. Displays transient notification messages with type-based styling.
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| MessageType (enum) | {NORMAL, WARNING, POWERUP, ERROR, SYSTEM} | Message severity/style |
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `send_message` | `func send_message(sender: Node, message: String, msg_type: int = 0) -> void` | void | Queue message for display |
|
||||||
|
| `clear_messages` | `func clear_messages() -> void` | void | Clear all pending messages |
|
||||||
|
| `get_message_queue` | `func get_message_queue() -> Array` | Array | Current pending messages |
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** player.gd, main.gd (unstuck feedback), StopNGoManager, many gameplay managers.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.5 ScreenShake
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/screen_shake.gd` (1,839 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** ScreenShake
|
||||||
|
|
||||||
|
Camera screen shake effect manager. Applies noise-based displacement to Camera3D.
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `shake` | `func shake(intensity: float, duration: float = 0.3) -> void` | void | Trigger camera shake |
|
||||||
|
| `stop_shake` | `func stop_shake() -> void` | void | Stop ongoing shake |
|
||||||
|
|
||||||
|
**Dependencies:** Camera3D (scene).
|
||||||
|
**Depended by:** player.gd (heavy knock triggers shake), main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.6 CameraContextManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/camera_context_manager.gd` (2,543 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** CameraContextManager
|
||||||
|
|
||||||
|
Camera zoom level and context switching. Manages follow-camera behavior, zoom levels for different game phases, and camera transitions.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `zoom_changed` | level: float | Camera zoom level changed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set_zoom` | `func set_zoom(level: float) -> void` | void | Set camera zoom |
|
||||||
|
| `get_zoom` | `func get_zoom() -> float` | float | Current zoom |
|
||||||
|
| `focus_on_player` | `func focus_on_player(player_id: int) -> void` | void | Snap camera to player |
|
||||||
|
| `focus_on_position` | `func focus_on_position(world_pos: Vector3) -> void` | void | Center camera on position |
|
||||||
|
|
||||||
|
**Dependencies:** Camera3D (scene).
|
||||||
|
**Depended by:** main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.7 TouchControls
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/touch_controls.gd` (23,640 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** TouchControls
|
||||||
|
|
||||||
|
Mobile touch input overlay. Provides virtual joystick, action buttons, and gesture recognition for grid-based controls.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `touch_moved` | direction: Vector2i | Grid direction from swipe |
|
||||||
|
| `action_triggered` | action: String | Touch button pressed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set_joystick_enabled` | `func set_joystick_enabled(enabled: bool) -> void` | void | Toggle joystick |
|
||||||
|
| `get_joystick_direction` | `func get_joystick_direction() -> Vector2` | Vector2 | Normalized joystick |
|
||||||
|
| `_save_settings` | internal | void | Persist touch control settings |
|
||||||
|
|
||||||
|
**Dependencies:** InputManager (scene).
|
||||||
|
**Depended by:** PlayerInputManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 8.8 TutorialManager / TutorialOverlay
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/tutorial_manager.gd` (22,243 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** TutorialManager
|
||||||
|
|
||||||
|
Tutorial flow controller. Manages step-by-step tutorial sequences, triggers, and completion tracking.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `tutorial_started` | tutorial_id: String | Tutorial began |
|
||||||
|
| `step_completed` | step: int | Step finished |
|
||||||
|
| `tutorial_completed` | tutorial_id: String | Tutorial fully complete |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_tutorial` | `func start_tutorial(tutorial_id: String) -> void` | void | Begin tutorial sequence |
|
||||||
|
| `advance_step` | `func advance_step() -> void` | void | Move to next step |
|
||||||
|
| `skip_tutorial` | `func skip_tutorial() -> void` | void | Exit tutorial early |
|
||||||
|
| `is_tutorial_active` | `func is_tutorial_active() -> bool` | bool | Tutorial in progress |
|
||||||
|
| `get_current_step` | `func get_current_step() -> int` | int | Current step index |
|
||||||
|
| `get_total_steps` | `func get_total_steps() -> int` | int | Total steps in tutorial |
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/tutorial_overlay.gd` (11,077 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** TutorialOverlay
|
||||||
|
|
||||||
|
Tutorial UI overlay. Displays step instructions, highlights UI elements, and provides step navigation.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `overlay_closed` | none | Overlay dismissed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `show_step` | `func show_step(step_data: Dictionary) -> void` | void | Display step with text + highlight |
|
||||||
|
| `hide_overlay` | `func hide_overlay() -> void` | void | Dismiss overlay |
|
||||||
|
| `highlight_element` | `func highlight_element(node_path: NodePath) -> void` | void | Spotlight a UI element |
|
||||||
|
| `clear_highlights` | `func clear_highlights() -> void` | void | Remove spotlights |
|
||||||
|
|
||||||
|
**Dependencies:** TutorialManager, UIManager.
|
||||||
|
**Depended by:** TutorialManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 9. Social / Economy Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.1 UserProfileManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/user_profile_manager.gd` (20,044 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** UserProfileManager
|
||||||
|
|
||||||
|
User profile CRUD operations. Manages display name, avatar, bio, wallet balance, stats, and loadout configuration. Syncs with Nakama storage.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `profile_loaded` | profile: Dictionary | Profile fetched from server |
|
||||||
|
| `profile_updated` | none | Profile modified locally |
|
||||||
|
| `wallet_updated` | wallet: Dictionary | Balance changed |
|
||||||
|
| `stats_updated` | stats: Dictionary | Player stats changed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `load_profile` | `func load_profile() -> void` | void (async) | Fetch profile from Nakama storage |
|
||||||
|
| `save_profile` | `func save_profile() -> void` | void (async) | Persist profile to Nakama |
|
||||||
|
| `get_display_name` | `func get_display_name(fallback: String = "Player") -> String` | String | Display name with fallback |
|
||||||
|
| `set_display_name` | `func set_display_name(name: String) -> void` | void | Update display name |
|
||||||
|
| `get_avatar_url` | `func get_avatar_url() -> String` | String | Current avatar path |
|
||||||
|
| `set_avatar` | `func set_avatar(url: String) -> void` | void | Change avatar |
|
||||||
|
| `get_wallet_balance` | `func get_wallet_balance(currency: String) -> int` | int | Balance for gold/star |
|
||||||
|
| `get_stats` | `func get_stats() -> Dictionary` | Dictionary | Player stats snapshot |
|
||||||
|
| `update_stats` | `func update_stats(delta: Dictionary) -> void` | void | Increment stats |
|
||||||
|
| `get_loadout` | `func get_loadout() -> Dictionary` | Dictionary | Current cosmetics loadout |
|
||||||
|
| `set_loadout` | `func set_loadout(loadout: Dictionary) -> void` | void | Save cosmetics config |
|
||||||
|
| `get_loadout_character` | `func get_loadout_character() -> String` | String | Selected character name |
|
||||||
|
| `sync_wallet` | `func sync_wallet() -> void` | void (async) | Refresh wallet from server |
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager, EventBus, BackendService.
|
||||||
|
**Depended by:** LobbyMainMenu, lobby.gd, ShopManager, GachaManager, SkinManager, many UI panels.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.2 GachaManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/gacha_manager.gd` (5,117 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GachaManager
|
||||||
|
|
||||||
|
Gacha pull orchestration. Calls BackendService.perform_gacha_pull, processes results, updates inventory.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `gacha_result` | items: Array, fragments: Array | Pull results |
|
||||||
|
| `gacha_error` | error: String | Pull failed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `perform_pull` | `func perform_pull(gacha_id: String, count: int) -> void` | void (async) | Execute gacha pull RPC |
|
||||||
|
| `get_pity_count` | `func get_pity_count(banner_id: String) -> int` | int | Current pity counter |
|
||||||
|
|
||||||
|
**Dependencies:** BackendService, UserProfileManager, EventBus.
|
||||||
|
**Depended by:** gacha_panel.tscn (scene UI).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.3 SkinManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/skin_manager.gd` (13,909 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** SkinManager
|
||||||
|
|
||||||
|
Cosmetic skin system. Manages skin definitions, owned skins, equipped loadout, and applies cosmetics to character models.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `skin_equipped` | skin_id: String | Skin applied |
|
||||||
|
| `skin_unequipped` | skin_id: String | Skin removed |
|
||||||
|
| `inventory_updated` | owned_skins: Array | Inventory changed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `equip_skin` | `func equip_skin(skin_id: String, slot: String) -> void` | void | Equip skin to slot |
|
||||||
|
| `unequip_skin` | `func unequip_skin(slot: String) -> void` | void | Unequip from slot |
|
||||||
|
| `is_skin_owned` | `func is_skin_owned(skin_id: String) -> bool` | bool | Check ownership |
|
||||||
|
| `get_equipped_skins` | `func get_equipped_skins() -> Dictionary` | Dictionary | Current loadout |
|
||||||
|
| `apply_loadout` | `func apply_loadout(character_root: Node3D, loadout: Dictionary) -> void` | void | Apply cosmetics to 3D model |
|
||||||
|
| `get_skins_for_character` | `func get_skins_for_character(char_name: String) -> Array` | Array | Available skins |
|
||||||
|
| `get_all_skins` | `func get_all_skins() -> Array` | Array | All skin definitions |
|
||||||
|
|
||||||
|
**Dependencies:** UserProfileManager.
|
||||||
|
**Depended by:** lobby.gd (3D preview), SkinShop UI.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.4 ShopManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/shop_manager.gd` (484 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** ShopManager
|
||||||
|
|
||||||
|
Thin data layer for shop catalog. Currently a stub; full shop logic lives in scene scripts.
|
||||||
|
|
||||||
|
**Properties:** Minimal (shop catalog array).
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:** None (data container only).
|
||||||
|
|
||||||
|
**Dependencies:** BackendService.
|
||||||
|
**Depended by:** shop_panel.tscn (scene).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.5 JoinManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/join_manager.gd` (484 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** JoinManager
|
||||||
|
|
||||||
|
Thin manager for join code input and validation. Minimal stub.
|
||||||
|
|
||||||
|
**Properties:** Minimal.
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:** None (stub).
|
||||||
|
|
||||||
|
**Dependencies:** None.
|
||||||
|
**Depended by:** lobby.gd (join code UI).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.6 FriendManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/friend_manager.gd` (11,911 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** FriendManager
|
||||||
|
|
||||||
|
Friends list management. Handles friend requests, accept/reject, friend list sync, DM messaging, and lobby invitations.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `friend_list_updated` | friends: Array | Friend list refreshed |
|
||||||
|
| `friend_request_received` | from_user_id: String | Incoming request |
|
||||||
|
| `friend_added` | user_id: String | Friendship established |
|
||||||
|
| `friend_removed` | user_id: String | Friendship ended |
|
||||||
|
| `dm_message_received` | from_user_id: String, from_name: String, message: String | Direct message |
|
||||||
|
| `lobby_invite_received` | from_user_id: String, from_name: String, match_id: String | Lobby invitation |
|
||||||
|
| `friend_online_changed` | user_id: String, online: bool | Presence changed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `add_friend_by_id` | `func add_friend_by_id(nakama_id: String) -> bool` | bool (async) | Send friend request |
|
||||||
|
| `remove_friend` | `func remove_friend(user_id: String) -> void` | void (async) | Remove friendship |
|
||||||
|
| `accept_request` | `func accept_request(user_id: String) -> void` | void (async) | Accept friend request |
|
||||||
|
| `decline_request` | `func decline_request(user_id: String) -> void` | void (async) | Decline request |
|
||||||
|
| `get_friends` | `func get_friends() -> Array` | Array | Current friends list |
|
||||||
|
| `get_mutual_friends` | `func get_mutual_friends() -> Array` | Array | Friends also in room |
|
||||||
|
| `is_friend` | `func is_friend(nakama_id: String) -> bool` | bool | Check friendship |
|
||||||
|
| `send_dm` | `func send_dm(user_id: String, text: String) -> bool` | bool (async) | Send direct message |
|
||||||
|
| `get_dm_history` | `func get_dm_history(user_id: String) -> Array` | Array (async) | Fetch DM history |
|
||||||
|
| `send_lobby_invite` | `func send_lobby_invite(to_user_id: String, match_id: String) -> void` | void (async) | Send invitation |
|
||||||
|
|
||||||
|
**Dependencies:** BackendService, NakamaManager.
|
||||||
|
**Depended by:** LobbyRoom, LobbyChat, lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.7 MailManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/mail_manager.gd` (5,271 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** MailManager
|
||||||
|
|
||||||
|
Mail/inbox CRUD operations. Calls BackendService RPCs for get, claim, delete mail.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `mail_updated` | mails: Array | Mail list refreshed |
|
||||||
|
| `unread_count_changed` | count: int | Unread mail count |
|
||||||
|
| `mail_claimed` | mail_id: String, rewards: Dictionary | Reward collected |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `fetch_mail` | `func fetch_mail() -> void` | void (async) | Fetch mailbox |
|
||||||
|
| `claim_mail` | `func claim_mail(mail_id: String) -> void` | void (async) | Claim reward |
|
||||||
|
| `delete_mail` | `func delete_mail(mail_id: String) -> void` | void (async) | Delete mail |
|
||||||
|
| `get_unread_count` | `func get_unread_count() -> int` | int | Unread count |
|
||||||
|
|
||||||
|
**Dependencies:** BackendService.
|
||||||
|
**Depended by:** lobby.gd, mailbox_panel.tscn (scene).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.8 DailyRewardManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/daily_reward_manager.gd` (1,009 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** DailyRewardManager
|
||||||
|
|
||||||
|
Daily reward system. Handles claim state, reward config, and streak tracking.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `reward_claimed` | day: int, reward: Dictionary | Daily reward collected |
|
||||||
|
| `streak_updated` | streak: int | Consecutive days |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `claim_daily_reward` | `func claim_daily_reward() -> void` | void (async) | Claim today's reward |
|
||||||
|
| `get_reward_state` | `func get_reward_state() -> Dictionary` | Dictionary (async) | Current state + schedule |
|
||||||
|
| `can_claim_today` | `func can_claim_today() -> bool` | bool | Check if claimable |
|
||||||
|
|
||||||
|
**Dependencies:** BackendService.
|
||||||
|
**Depended by:** daily_reward_panel.tscn (scene).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 9.9 AdminManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/admin_manager.gd` (2,538 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** AdminManager
|
||||||
|
|
||||||
|
Admin panel state and permission checks. Determines if local player is admin or moderator.
|
||||||
|
|
||||||
|
**Signals:** None.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_check_admin_status` | `func _check_admin_status() -> bool` | bool (async) | Verify admin via Nakama storage |
|
||||||
|
| `kick_player` | `func kick_player(player_id: int) -> void` | void (async) | Kick player from match |
|
||||||
|
| `ban_player` | `func ban_player(player_id: int) -> void` | void (async) | Ban player |
|
||||||
|
| `give_currency` | `func give_currency(gold: int, star: int) -> void` | void (async) | Admin give currency |
|
||||||
|
|
||||||
|
**Dependencies:** BackendService, NakamaManager.
|
||||||
|
**Depended by:** admin_panel.tscn (scene), LobbyChat (/clear command).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 10. System Managers
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 10.1 SettingsManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/settings_manager.gd` (13,874 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** SettingsManager
|
||||||
|
|
||||||
|
User settings persistence. Reads/writes config to user://settings.cfg. Manages audio, video, gameplay, and control settings.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `setting_changed` | key: String, value: Variant | A setting was modified |
|
||||||
|
| `settings_reset` | none | All settings restored to defaults |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `get_setting` | `func get_setting(key: String, default: Variant = null) -> Variant` | Variant | Read setting value |
|
||||||
|
| `set_setting` | `func set_setting(key: String, value: Variant) -> void` | void | Write and persist setting |
|
||||||
|
| `reset_settings` | `func reset_settings() -> void` | void | Restore defaults |
|
||||||
|
| `load_settings` | `func load_settings() -> void` | void | Load from config file |
|
||||||
|
| `save_settings` | `func save_settings() -> void` | void | Write to config file |
|
||||||
|
| `get_all_settings` | `func get_all_settings() -> Dictionary` | Dictionary | Full settings snapshot |
|
||||||
|
|
||||||
|
**Dependencies:** ConfigFile.
|
||||||
|
**Depended by:** Audio buses, video settings, gameplay UI, settings_menu.tscn.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 10.2 SessionManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/session_manager.gd` (4,742 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** SessionManager
|
||||||
|
|
||||||
|
Nakama session refresh lifecycle. Monitors session expiry and auto-refreshes before expiration.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `session_refreshed` | none | Token refreshed |
|
||||||
|
| `session_expired` | none | Could not refresh |
|
||||||
|
| `session_warning` | seconds_remaining: int | About to expire |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `start_monitoring` | `func start_monitoring() -> void` | void | Begin session expiry timer |
|
||||||
|
| `stop_monitoring` | `func stop_monitoring() -> void` | void | Stop timer |
|
||||||
|
| `refresh_now` | `func refresh_now() -> void` | void (async) | Force refresh |
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager.
|
||||||
|
**Depended by:** AuthManager.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 10.3 GameUpdateManager
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scripts/managers/game_update_manager.gd` (14,405 chars)
|
||||||
|
**Extends:** Node
|
||||||
|
**Autoload name:** GameUpdateManager
|
||||||
|
|
||||||
|
Hot-reload update system. Checks for patch.pck on the Gitea patches branch and downloads/loads it at runtime.
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `update_available` | version: String, changelog: String | New patch detected |
|
||||||
|
| `update_downloading` | progress: float | Download progress |
|
||||||
|
| `update_ready` | path: String | Patch downloaded and verified |
|
||||||
|
| `update_failed` | error: String | Download error |
|
||||||
|
| `up_to_date` | none | No update needed |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `check_for_updates` | `func check_for_updates() -> void` | void (async) | Query Gitea for latest patch |
|
||||||
|
| `download_update` | `func download_update() -> void` | void (async) | Download patch.pck |
|
||||||
|
| `apply_update` | `func apply_update() -> void` | void | Load patch from ProjectSettings |
|
||||||
|
| `get_current_version` | `func get_current_version() -> String` | String | Current client version |
|
||||||
|
| `get_available_version` | `func get_available_version() -> String` | String | Latest available version |
|
||||||
|
|
||||||
|
**Dependencies:** HTTPRequest (scene).
|
||||||
|
**Depended by:** boot_screen.tscn, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 11. Core Scene Scripts
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 11.1 main.gd (Main game scene controller)
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/main.gd` (2956 lines)
|
||||||
|
**Extends:** Node
|
||||||
|
**Scene:** main.tscn
|
||||||
|
|
||||||
|
The core game scene controller. Handles game initialization, player spawn, grid setup, goal cycle start, leaderboard display, pause menu, unstuck system, match cleanup, and result screen flow.
|
||||||
|
|
||||||
|
**Key Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| enhanced_gridmap | Node | Reference to EnhancedGridMap child |
|
||||||
|
| player_scene | PackedScene | Player.tscn loaded |
|
||||||
|
| stop_n_go_winner_id | int | Winner's peer ID (-1 if none) |
|
||||||
|
| _unstuck_cooldown_remaining | float | Unstuck button cooldown |
|
||||||
|
| touch_controls | Node | TouchControls autoload ref |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
- (none declared; uses method-based event routing)
|
||||||
|
|
||||||
|
**Public Functions (selected key ones):**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Initializes ENet multiplayer, spawns players, starts goals cycle, sets up HUD |
|
||||||
|
| `_process` | `func _process(delta: float) -> void` | void | Unstuck cooldown tick |
|
||||||
|
| `_input` | `func _input(event: InputEvent) -> void` | void | ESC pause, F9 debug floor check |
|
||||||
|
| `initialize_game` | -- | void | Create EnhancedGridMap, spawn player scene instances |
|
||||||
|
| `spawn_player` | -- | Node | Instantiate player.tscn, position, set authority |
|
||||||
|
| `_spawn_local_player` | -- | void | Create local player node |
|
||||||
|
| `add_bot_players_if_needed` | -- | void | Fill remaining slots with bot players |
|
||||||
|
| `display_message` | `func display_message(message: String, type: int) -> void` | void (RPC) | Broadcast message to local player's UI |
|
||||||
|
| `request_leaderboard_sync` | `func request_leaderboard_sync() -> void` | void (RPC) | Client requests leaderboard from server |
|
||||||
|
| `sync_leaderboard_data` | `func sync_leaderboard_data(player_data: Array) -> void` | void (RPC authority) | Receive + render leaderboard |
|
||||||
|
| `_update_leaderboard_display` | internal | void | Local leaderboard refresh |
|
||||||
|
| `_render_leaderboard_entries` | internal | void | Populate leaderboard entries |
|
||||||
|
| `_get_ordinal` | `func _get_ordinal(n: int) -> String` | String | "1st", "2nd", "3rd", etc. |
|
||||||
|
| `can_rpc` | `func can_rpc() -> bool` | bool | Check multiplayer peer state |
|
||||||
|
| `check_multiplayer` | `func check_multiplayer() -> bool` | bool | Safety check for peer access |
|
||||||
|
| `_toggle_pause_menu` | internal | void | Show/hide pause overlay |
|
||||||
|
| `_on_resume_pressed` | -- | void | Close pause menu |
|
||||||
|
| `_on_how_to_play_pressed` | -- | void | Open help panel |
|
||||||
|
| `_on_settings_pressed` | -- | void | Open settings dynamically |
|
||||||
|
| `_on_quit_match_pressed` | -- | void | Leave match, return to lobby |
|
||||||
|
| `_on_unstuck_pressed` | -- | void | Teleport local player to safe position |
|
||||||
|
| `_find_safe_spawn_position` | internal | Vector2i | Scan grid for safe walkable cell |
|
||||||
|
| `_on_back_to_menu_pressed` | -- | void | Cleanup and transition to lobby |
|
||||||
|
| `_cleanup_multiplayer` | -- | void | NakamaManager.cleanup() wrapper |
|
||||||
|
| `_deferred_init_leaderboard` | internal | void | Delayed leaderboard init (1.5s) |
|
||||||
|
| `_on_rematch_pressed` | -- | void | Request rematch vote |
|
||||||
|
| `check_all_floors` | `func check_all_floors() -> void` | void | Debug F9: scan missing floor tiles |
|
||||||
|
| `update_visual_position` | on player | void | Snap player to grid-aligned world position |
|
||||||
|
| `grid_to_world` | on player | Vector3 | Convert grid Vector2i to world Vector3 |
|
||||||
|
|
||||||
|
**RPCs (network-synced functions):**
|
||||||
|
|
||||||
|
| Function | RPC Mode | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `request_leaderboard_sync` | any_peer | Client requests data from server |
|
||||||
|
| `sync_leaderboard_data` | authority, call_local | Server sends leaderboard to client |
|
||||||
|
| `display_message` | authority, call_local | Broadcast message to player UI |
|
||||||
|
| `sync_position` (on player) | any_peer, call_local | Sync grid position |
|
||||||
|
| `sync_grid_item` (on player) | any_peer, call_local | Sync grid cell item |
|
||||||
|
| `sync_goals` (on player) | any_peer, call_local | Sync active goals |
|
||||||
|
| `sync_rotation` (on player) | any_peer, call_local | Sync character rotation |
|
||||||
|
| `sync_bump` (on player) | any_peer, call_local, unreliable | Visual bump animation |
|
||||||
|
| `sync_knock_tekton` (on player) | any_peer, call_local, reliable | Knock tekton |
|
||||||
|
| `sync_grab_tekton` (on player) | any_peer, call_local, reliable | Grab roaming tekton |
|
||||||
|
| `sync_throw_tekton` (on player) | any_peer, call_local, reliable | Throw tekton |
|
||||||
|
| `sync_drop_tekton` (on player) | any_peer, call_local, reliable | Drop tekton |
|
||||||
|
| `set_spawn_position` (on player) | any_peer, call_local, reliable | Random spawn position |
|
||||||
|
| `complete_race` (on player) | any_peer, call_local, reliable | Player finished race |
|
||||||
|
| `force_action_state_none` (on player) | any_peer, call_local, reliable | Reset UI action state |
|
||||||
|
| `request_server_grab` (on player) | any_peer, reliable | Server-authoritative grab |
|
||||||
|
| `request_server_put` (on player) | any_peer, reliable | Server-authoritative put |
|
||||||
|
| `notify_spawn_selected` (on player) | any_peer, reliable | Occupancy sync for spawn |
|
||||||
|
| `trigger_screen_shake` | -- | Camera shake RPC |
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager, LobbyManager, GameStateManager, PlayerMovementManager, PlayerActionManager, GoalsCycleManager, StopNGoManager, GauntletManager, PortalModeManager, PlayerRaceManager, PlayerboardManager, UIManager, SfxManager, MusicManager, NotificationManager, ScreenShake, CameraContextManager, TouchControls.
|
||||||
|
**Depended by:** (this is the root game scene, depends on everything).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 11.2 player.gd
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/player.gd` (2751 lines)
|
||||||
|
**Extends:** CharacterBody3D (assumed from Node3D methods)
|
||||||
|
**Scene:** player.tscn
|
||||||
|
|
||||||
|
The player character controller. Handles movement, action execution (grab/put/arrange), tekton interaction (carry/snatch/throw/knock), grid positioning, bot AI, visual synchronization, and playerboard management delegation.
|
||||||
|
|
||||||
|
**Key Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Default | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| current_position | Vector2i | Vector2i(0, 0) | Grid-aligned position |
|
||||||
|
| cell_size | Vector3 | (1.0, 1.0, 1.0) | Grid cell dimensions |
|
||||||
|
| cell_offset | Vector3 | Vector3.ZERO | Visual position offset |
|
||||||
|
| is_player_moving | bool | false | Movement tween active |
|
||||||
|
| is_carrying_tekton | bool | false | Holding roaming tekton |
|
||||||
|
| carried_tekton | Node3D | null | Reference to carried tekton |
|
||||||
|
| is_charged_strike | bool | false | Charged attack mode |
|
||||||
|
| is_frozen | bool | false | Stun/freeze state |
|
||||||
|
| is_stop_frozen | bool | false | Stop n Go freeze |
|
||||||
|
| is_invisible | bool | false | Ghost mode |
|
||||||
|
| is_bot | bool | false | Bot AI flag |
|
||||||
|
| display_name | String | "" | Player display name |
|
||||||
|
| score | int | 0 | Match score |
|
||||||
|
| action_points | int | 1 | Actions per turn |
|
||||||
|
| playerboard | Array | [-1, -1, ...] | Item slot board |
|
||||||
|
| goals | Array | [] | Active goals |
|
||||||
|
| enhanced_gridmap | Node | null | Grid reference |
|
||||||
|
| anim_player | AnimationPlayer | null | Character animations |
|
||||||
|
| movement_manager | PlayerMovementManager | ref | Movement delegation |
|
||||||
|
| action_manager | PlayerActionManager | ref | Action delegation |
|
||||||
|
| playerboard_manager | PlayerboardManager | ref | Board delegation |
|
||||||
|
| race_manager | PlayerRaceManager | ref | Race delegate |
|
||||||
|
| powerup_manager | PowerupManager | ref | Boost/charge delegate |
|
||||||
|
|
||||||
|
**Signals:**
|
||||||
|
|
||||||
|
| Signal | Params | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `position_changed` | none | Player grid position changed |
|
||||||
|
|
||||||
|
**Public Functions (selected key ones):**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Init references, connect signals, set initial position |
|
||||||
|
| `_physics_process` | `func _physics_process(delta: float) -> void` | void | Movement smoothing, carry timer, unstuck timer |
|
||||||
|
| `_input` | `func _input(event: InputEvent) -> void` | void | Click-to-move on gridmap, slot clicks |
|
||||||
|
| `grid_to_world` | `func grid_to_world(pos: Vector2i) -> Vector3` | Vector3 | Convert grid to world coordinates |
|
||||||
|
| `move_to_grid_position` | `func move_to_grid_position(target: Vector2i) -> void` | void | Initiate grid movement |
|
||||||
|
| `grab_item` | `func grab_item(grid_pos: Vector2i = current_position) -> bool` | bool | Delegates to playerboard_manager.grab_item |
|
||||||
|
| `auto_put_item` | `func auto_put_item() -> bool` | bool | Delegates auto-put |
|
||||||
|
| `handle_playerboard_slot_selected` | `func handle_playerboard_slot_selected(slot_index: int) -> void` | void | Delegates to playerboard_manager |
|
||||||
|
| `handle_put_slot_selected` | `func handle_put_slot_selected(slot_index: int) -> void` | void | Delegates put slot |
|
||||||
|
| `arrange_playerboard_item` | `func arrange_playerboard_item(slot_index: int) -> void` | void | Delegates arrange |
|
||||||
|
| `_on_slot_clicked` | `func _on_slot_clicked(event: InputEvent, slot_index: int) -> void` | void | Delegates to playerboard_manager |
|
||||||
|
| `has_item_at_current_position` | `func has_item_at_current_position() -> bool` | bool | Check grid cell occupancy |
|
||||||
|
| `has_items_in_playerboard` | `func has_items_in_playerboard() -> bool` | bool | Any items in board |
|
||||||
|
| `playerboard_is_full` | `func playerboard_is_full() -> bool` | bool | All slots filled |
|
||||||
|
| `highlight_movement_range` | `func highlight_movement_range() -> void` | void | Delegates to movement_manager |
|
||||||
|
| `highlight_adjacent_cells` | `func highlight_adjacent_cells() -> void` | void | Delegates to movement_manager |
|
||||||
|
| `highlight_cells_if_authorized` | `func highlight_cells_if_authorized(cells: Array, item_id: int) -> void` | void | Delegates to action_manager |
|
||||||
|
| `clear_highlights` | `func clear_highlights() -> void` | void | Clear grid highlights |
|
||||||
|
| `rotate_towards_target` | `func rotate_towards_target(target_pos: Vector2i) -> void` | void | Delegates to movement_manager |
|
||||||
|
| `select_playerboard_slot` | `func select_playerboard_slot(slot_index: int) -> void` | void | Delegates to playerboard_manager |
|
||||||
|
| `deselect_playerboard_slot` | `func deselect_playerboard_slot() -> void` | void | Clear selection |
|
||||||
|
| `target_playerboard_slot` | `func target_playerboard_slot(slot_index: int) -> void` | void | Target slot for move |
|
||||||
|
| `untarget_playerboard_slot` | `func untarget_playerboard_slot() -> void` | void | Clear target |
|
||||||
|
| `can_move_to_target_playerboard_slot` | `func can_move_to_target_playerboard_slot() -> bool` | bool | Target slot validity |
|
||||||
|
| `update_visual_position` | `func update_visual_position() -> void` | void | Snap to grid |
|
||||||
|
| `grab_tekton` | `func grab_tekton() -> void` | void | Tekton interaction: snatch or grab |
|
||||||
|
| `snatch_tekton` | `func snatch_tekton(target_carrier: Node3D) -> void` | void | Steal tekton from carrier |
|
||||||
|
| `throw_tekton` | `func throw_tekton() -> void` | void | Throw tekton in facing direction |
|
||||||
|
| `drop_tekton` | `func drop_tekton() -> void` | void | Drop tekton at current position |
|
||||||
|
| `enter_charged_strike` | `func enter_charged_strike() -> void` | void | Activate charged attack mode |
|
||||||
|
| `knock_tekton` | `func knock_tekton() -> void` | void | Special attack on nearby tekton |
|
||||||
|
| `update_active_player_indicator` | `func update_active_player_indicator() -> void` | void | Refresh visual state |
|
||||||
|
| `is_finish_position` | `func is_finish_position(pos: Vector2i) -> bool` | bool | Check if pos is a finish line |
|
||||||
|
| `_after_action_completed` | internal | void | Post-action: cycle goals, check win |
|
||||||
|
| `consume_action_points` | `func consume_action_points(points: int) -> void` | void | Deduct AP |
|
||||||
|
| `display_message` | on player | void | Show notification to this player |
|
||||||
|
| `apply_stagger` | `func apply_stagger(duration: float) -> void` | void | Stun for duration |
|
||||||
|
|
||||||
|
**RPCs (network-synced functions on player.gd):**
|
||||||
|
|
||||||
|
| Function | RPC Mode | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `sync_position` | any_peer, call_local | Sync current grid position |
|
||||||
|
| `sync_rotation` | any_peer, call_local | Sync Y rotation |
|
||||||
|
| `sync_grid_item` | any_peer, call_local | Sync grid cell item change |
|
||||||
|
| `sync_goals` | any_peer, call_local | Sync active goal set |
|
||||||
|
| `sync_second_lap_goals` | any_peer, call_local | Sync lap 2 goals |
|
||||||
|
| `sync_grab_tekton` | any_peer, call_local, reliable | Grab tekton network sync |
|
||||||
|
| `sync_snatch_tekton` | any_peer, call_local, reliable | Tekton theft sync |
|
||||||
|
| `sync_throw_tekton` | any_peer, call_local, reliable | Throw tekton sync |
|
||||||
|
| `sync_drop_tekton` | any_peer, call_local, reliable | Drop tekton sync |
|
||||||
|
| `sync_bump` | any_peer, call_local, unreliable | Visual bump animation |
|
||||||
|
| `sync_knock_tekton` | any_peer, call_local, reliable | Knock attack sync |
|
||||||
|
| `set_spawn_position` | any_peer, call_local, reliable | Random spawn position |
|
||||||
|
| `complete_race` | any_peer, call_local, reliable | Race completion |
|
||||||
|
| `force_action_state_none` | any_peer, call_local, reliable | Reset UI action state |
|
||||||
|
| `request_server_grab` | any_peer, reliable | Server-auth grab request |
|
||||||
|
| `request_server_put` | any_peer, reliable | Server-auth put request |
|
||||||
|
| `notify_spawn_selected` | any_peer, reliable | Spawn occupancy sync |
|
||||||
|
| `trigger_screen_shake` | (authority) | Screen shake RPC |
|
||||||
|
| `bot_grab_item` | any_peer, call_local | Bot grab sync |
|
||||||
|
| `bot_put_item` | any_peer, call_local | Bot put sync |
|
||||||
|
| `bot_arrange_item` | any_peer, call_local | Bot arrange sync |
|
||||||
|
|
||||||
|
**Dependencies:** PlayerMovementManager, PlayerInputManager, PlayerActionManager, PlayerboardManager, PowerupManager, PlayerRaceManager, GoalsCycleManager, SfxManager, NotificationManager, EnhancedGridMap (scene node), LobbyManager.
|
||||||
|
**Depended by:** main.gd (spawned per player).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 11.3 lobby.gd
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/lobby.gd` (583 lines)
|
||||||
|
**Extends:** Control
|
||||||
|
**Scene:** lobby.tscn
|
||||||
|
|
||||||
|
The lobby/home screen controller. Manages main menu, room creation/joining, player slots, server selection, character selection, settings, mail, chat, social panel, and 3D character preview.
|
||||||
|
|
||||||
|
**Key Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| chat | LobbyChat | Chat helper instance |
|
||||||
|
| main_menu | LobbyMainMenu | Main menu helper |
|
||||||
|
| room_list_helper | LobbyRoomList | Room list helper |
|
||||||
|
| room_helper | LobbyRoom | Room/lobby helper |
|
||||||
|
| character_textures | Dictionary | {char_name: Texture2D} |
|
||||||
|
| profile_panel_instance | Control | Dynamic profile panel |
|
||||||
|
| shop_panel_instance | Control | Dynamic shop panel |
|
||||||
|
| daily_reward_panel_instance | Control | Daily reward panel |
|
||||||
|
| leaderboard_panel_instance | Control | Leaderboard panel |
|
||||||
|
| _mailbox_panel_instance | Control | Mail panel |
|
||||||
|
| social_panel_instance | Control | Social panel |
|
||||||
|
| _local_player_rank | int | Cached rank |
|
||||||
|
| _bot_names | Dictionary | Slot index -> bot name |
|
||||||
|
| _room_mode_filter | String | Room list filter |
|
||||||
|
| _is_hosting | bool | Re-entry guard |
|
||||||
|
|
||||||
|
**UI Node References (onready):**
|
||||||
|
|
||||||
|
| Variable | Node Path | Type |
|
||||||
|
|---|---|---|
|
||||||
|
| main_menu_panel | $MainMenuPanel | Control |
|
||||||
|
| main_title | %Title | Label |
|
||||||
|
| username_label | %Username | Label |
|
||||||
|
| create_room_btn | %CreateRoomBtn | Button |
|
||||||
|
| browse_rooms_btn | %BrowseRoomsBtn | Button |
|
||||||
|
| tutorial_btn | %TutorialBtn | Button |
|
||||||
|
| main_menu_profile_btn | %MainProfileBtn | Button |
|
||||||
|
| avatar_display | %AvatarDisplay | TextureRect |
|
||||||
|
| lobby_settings_btn | %SettingsBtn | Button |
|
||||||
|
| quit_btn | %QuitBtn | Button |
|
||||||
|
| character_root | %CharacterRoot | Node3D |
|
||||||
|
| anim_player | %AnimationPlayer | AnimationPlayer |
|
||||||
|
| gold_label | %GoldLabel | Label |
|
||||||
|
| star_label | %StarLabel | Label |
|
||||||
|
| server_option | %ServerOption | OptionButton |
|
||||||
|
| server_ip_input | %ServerIPInput | LineEdit |
|
||||||
|
| leaderboard_btn | %LeaderboardBtn | Button |
|
||||||
|
| shop_btn | %CartBtn | Button |
|
||||||
|
| top_right_profile_btn | %ProfileBtn | Button |
|
||||||
|
| mailbox_btn | %MailboxBtn | Button |
|
||||||
|
| mail_badge | %MailBadge | Label |
|
||||||
|
| banner1_btn | %Banner1 | Button |
|
||||||
|
| ticket_btn | %TicketBtn | Button |
|
||||||
|
| room_list_panel | %RoomListPanel | Control |
|
||||||
|
| room_list | %RoomList | ItemList |
|
||||||
|
| match_id_input | %MatchIdInput | LineEdit |
|
||||||
|
| refresh_btn | %RefreshBtn | Button |
|
||||||
|
| join_btn | %JoinBtn | Button |
|
||||||
|
| back_btn | %RoomListCloseBtn | Button |
|
||||||
|
| lobby_panel | $LobbyPanel | Control |
|
||||||
|
| host_banner | $LobbyPanel/HostBanner | Panel |
|
||||||
|
| match_id_display | $LobbyPanel/TopBar/... | Label |
|
||||||
|
| copy_id_btn | $LobbyPanel/TopBar/... | Button |
|
||||||
|
| duration_option | $LobbyPanel/TopBar/... | OptionButton |
|
||||||
|
| random_spawn_check | `" " ` | CheckButton |
|
||||||
|
| enable_timer_check | `" " ` | CheckButton |
|
||||||
|
| scarcity_option | `" " ` | OptionButton |
|
||||||
|
| game_mode_option | `" " ` | OptionButton |
|
||||||
|
| players_container | $LobbyPanel/PlayersContainer | Control |
|
||||||
|
| area_selector | $LobbyPanel/AreaSelector | Control |
|
||||||
|
| leave_btn | $LobbyPanel/BottomBar/LeaveBtn | Button |
|
||||||
|
| ready_btn | $LobbyPanel/BottomBar/ReadyBtn | Button |
|
||||||
|
| start_game_btn | $LobbyPanel/BottomBar/StartGameBtn | Button |
|
||||||
|
| connection_status | $StatusBar/ConnectionStatus | Label |
|
||||||
|
| chat_display | %RichTextLabel | RichTextLabel |
|
||||||
|
| chat_input | %ChatInput | LineEdit |
|
||||||
|
| chat_send_btn | %SendBtn | Button |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_ready` | auto-called | void | Initialize all helpers, load textures, setup UI, connect signals |
|
||||||
|
| `_setup_3d_preview` | `func _setup_3d_preview() -> void` | void | Swap character model in SubViewport |
|
||||||
|
| `_load_character_textures` | `func _load_character_textures() -> void` | void | Load preview textures |
|
||||||
|
| `_on_server_option_selected` | `func _on_server_option_selected(index: int) -> void` | void | Handle server type dropdown |
|
||||||
|
| `_on_server_ip_submitted` | `func _on_server_ip_submitted(new_text: String) -> void` | void | Handle IP input |
|
||||||
|
| `_setup_game_modes` | `func _setup_game_modes() -> void` | void | Populate game mode dropdown |
|
||||||
|
| `_setup_player_slots` | `func _setup_player_slots() -> void` | void | Collect player slot nodes |
|
||||||
|
| `_connect_slot_signals` | `func _connect_slot_signals(slot: Control, i: int)` | void | Wire character nav buttons |
|
||||||
|
| `_show_panel` | `func _show_panel(panel_name: String) -> void` | void | Toggle main_menu/room_list/lobby panels |
|
||||||
|
| `_update_settings_visibility` | `func _update_settings_visibility() -> void` | void | Show/hide settings by mode and host status |
|
||||||
|
| `_create_custom_settings_ui` | `func _create_custom_settings_ui() -> void` | void | Build SNG/Tekton Doors settings dynamically |
|
||||||
|
| `_sync_room_profile_card` | `func _sync_room_profile_card() -> void` | void | Refresh username, score, rank, avatar, currency |
|
||||||
|
| `_apply_loadout_character` | `func _apply_loadout_character() -> void` | void | Apply saved character to LobbyManager |
|
||||||
|
| `admin_wipe_chat` | `func admin_wipe_chat() -> void` | void (async) | Admin: clear global chat |
|
||||||
|
| `admin_purge_chat` | `func admin_purge_chat(max_age_days: int) -> int` | int (async) | Admin: purge old messages |
|
||||||
|
|
||||||
|
**Dependencies:** AuthManager, NakamaManager, LobbyManager, UserProfileManager, SkinManager, MusicManager, FriendManager, MailManager, BackendService.
|
||||||
|
**Depended by:** (root lobby scene; no dependents).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 11.4 animation.gd
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/animation.gd` (41 lines)
|
||||||
|
**Extends:** Control
|
||||||
|
**Scene:** (embedded in main.tscn for Stop n Go UI)
|
||||||
|
|
||||||
|
Stop n Go phase animation player. Controls ready-go countdown, stop phase overlay, safe zone, and go animation sequences.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `play_ready_go` | `func play_ready_go() -> void` | void | Play ready-set-go sequence |
|
||||||
|
| `play_stop_phase` | `func play_stop_phase() -> void` | void | Play STOP overlay |
|
||||||
|
| `play_safe_zone_appear` | `func play_safe_zone_appear() -> void` | void | Show safe zone indicator |
|
||||||
|
| `stop_phase_anim_play` | `func stop_phase_anim_play() -> void` | void | Play stop phase spritesheet |
|
||||||
|
| `stop_phase_anim_stop` | `func stop_phase_anim_stop() -> void` | void | Stop phase animation |
|
||||||
|
| `play_countdown_30s` | `func play_countdown_30s() -> void` | void | 30-second countdown |
|
||||||
|
| `play_countdown_15s` | `func play_countdown_15s() -> void` | void | 15-second countdown |
|
||||||
|
| `play_go_animation` | `func play_go_animation() -> void` | void | GO animation |
|
||||||
|
| `play_go_finish_animation` | `func play_go_finish_animation() -> void` | void | Finish line animation |
|
||||||
|
|
||||||
|
**Dependencies:** AnimatedSprite2D, AnimationPlayer (scene nodes).
|
||||||
|
**Depended by:** StopNGoManager, main.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 12. UI Helper Classes (RefCounted)
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
All UI helper classes are RefCounted objects instantiated by lobby.gd in _ready(). They do NOT extend Node -- they are lightweight event wiring and state management objects.
|
||||||
|
|
||||||
|
### 12.1 LobbyMainMenu
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LobbyMainMenu extends RefCounted
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/ui/lobby_main_menu.gd` (338 lines)
|
||||||
|
|
||||||
|
Event wiring for main menu buttons. Connects all lobby button signals to handler methods.
|
||||||
|
|
||||||
|
**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects 15+ button signals.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `on_tutorial_pressed` | `func on_tutorial_pressed() -> void` | void | Set name, apply loadout, call LobbyManager.start_tutorial |
|
||||||
|
| `on_create_room_pressed` | `func on_create_room_pressed() -> void` | void | Show room list panel, create tab |
|
||||||
|
| `host_room` | `func host_room(game_mode: String) -> void` | void | Guarded double-click, set name/mode, create Nakama or LAN room |
|
||||||
|
| `on_browse_rooms_pressed` | `func on_browse_rooms_pressed() -> void` | void | Show room list, browse tab, refresh |
|
||||||
|
| `on_profile_btn_pressed` | `func on_profile_btn_pressed() -> void` | void | Instantiate and show profile_panel.tscn |
|
||||||
|
| `on_mailbox_pressed` | `func on_mailbox_pressed() -> void` | void | Instantiate and show mailbox_panel.tscn |
|
||||||
|
| `on_settings_pressed` | `func on_settings_pressed() -> void` | void | Instantiate and show settings_menu.tscn |
|
||||||
|
| `restore_after_settings` | `func restore_after_settings() -> void` | void | Restore lobby/main_menu panel visibility |
|
||||||
|
| `on_shop_pressed` | `func on_shop_pressed() -> void` | void | Instantiate and show shop_panel.tscn |
|
||||||
|
| `on_banner1_pressed` | `func on_banner1_pressed() -> void` | void | Instantiate and show gacha_panel.tscn |
|
||||||
|
| `on_leaderboard_pressed` | `func on_leaderboard_pressed() -> void` | void | Show leaderboard_panel.tscn |
|
||||||
|
| `on_ticket_pressed` | `func on_ticket_pressed() -> void` | void | Show daily_reward_panel.tscn |
|
||||||
|
| `on_social_pressed` | `func on_social_pressed() -> void` | void | Show social_panel.tscn, hide main menu UI |
|
||||||
|
| `on_logout_pressed` | `func on_logout_pressed() -> void` | void | AuthManager.logout() -> login screen |
|
||||||
|
| `on_quit_pressed` | `func on_quit_pressed() -> void` | void | get_tree().quit() |
|
||||||
|
| `go_to_login` | `func go_to_login() -> void` | void | Change scene to login_screen.tscn |
|
||||||
|
|
||||||
|
**Dependencies:** AuthManager, LobbyManager, UserProfileManager, NakamaManager, BackendService.
|
||||||
|
**Depended by:** lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 12.2 LobbyRoom
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LobbyRoom extends RefCounted
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/ui/lobby_room.gd` (432 lines)
|
||||||
|
|
||||||
|
Room/lobby panel event wiring. Handles ready/start/leave buttons, player slot rendering, character navigation, game mode/duration/scarcity settings, friend invites, and lobby invitation popup.
|
||||||
|
|
||||||
|
**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects 20+ signals.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_on_ready_toggled` | `func _on_ready_toggled(is_ready: bool) -> void` | void | Toggle ready state |
|
||||||
|
| `_on_start_game_pressed` | `func _on_start_game_pressed() -> void` | void | Host starts game |
|
||||||
|
| `_on_leave_pressed` | `func _on_leave_pressed() -> void` | void | Leave room, release bot names |
|
||||||
|
| `_on_copy_id_pressed` | `func _on_copy_id_pressed() -> void` | void | Copy match ID to clipboard |
|
||||||
|
| `_on_duration_selected` | `func _on_duration_selected(index: int) -> void` | void | Host sets match duration |
|
||||||
|
| `_on_random_spawn_toggled` | `func _on_random_spawn_toggled(toggled_on: bool) -> void` | void | Toggle random spawn |
|
||||||
|
| `_on_enable_timer_toggled` | `func _on_enable_timer_toggled(toggled_on: bool) -> void` | void | Toggle cycle timer |
|
||||||
|
| `_on_scarcity_selected` | `func _on_scarcity_selected(index: int) -> void` | void | Host sets scarcity |
|
||||||
|
| `_on_scarcity_mode_changed` | `func _on_scarcity_mode_changed(mode: String) -> void` | void | UI update for scarcity |
|
||||||
|
| `_on_game_mode_selected` | `func _on_game_mode_selected(index: int) -> void` | void | Host sets game mode |
|
||||||
|
| `_on_game_mode_changed` | `func _on_game_mode_changed(mode: String) -> void` | void | UI update for game mode |
|
||||||
|
| `_on_sng_update` | `func _on_sng_update(_val: int = 0) -> void` | void | Sync SNG setting UI |
|
||||||
|
| `_on_doors_update` | `func _on_doors_update(_val: int = 0) -> void` | void | Sync Doors setting UI |
|
||||||
|
| `_on_room_joined` | `func _on_room_joined(room_data: Dictionary) -> void` | void | Switch to lobby panel, populate settings |
|
||||||
|
| `_on_room_left` | `func _on_room_left() -> void` | void | Return to main menu |
|
||||||
|
| `_on_host_disconnected` | `func _on_host_disconnected() -> void` | void | Show disconnect message |
|
||||||
|
| `_on_player_joined` | `func _on_player_joined(player_data: Dictionary) -> void` | void | Update slots + status |
|
||||||
|
| `_on_player_left` | `func _on_player_left(_player_id: int) -> void` | void | Update slots |
|
||||||
|
| `_on_ready_state_changed` | `func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void` | void | Update slot visuals |
|
||||||
|
| `_on_all_players_ready` | `func _on_all_players_ready() -> void` | void | Enable start button |
|
||||||
|
| `_on_game_starting` | `func _on_game_starting() -> void` | void | Transition to main.tscn |
|
||||||
|
| `_update_player_slots` | `func _update_player_slots() -> void` | void | Render all 8 player slots (players + bot slots) |
|
||||||
|
| `_update_status` | `func _update_status() -> void` | void | Show ready count |
|
||||||
|
| `_on_add_friend_pressed` | `func _on_add_friend_pressed(nakama_id: String) -> void` | void (async) | Add friend by Nakama ID |
|
||||||
|
| `on_invite_friends_pressed` | `func on_invite_friends_pressed() -> void` | void | Open invite dialog |
|
||||||
|
| `_on_lobby_invite_received` | `func _on_lobby_invite_received(from_user_id: String, from_name: String, match_id: String) -> void` | void | Show invite popup |
|
||||||
|
| `_on_invite_accepted` | `func _on_invite_accepted() -> void` | void | Join invited match |
|
||||||
|
|
||||||
|
**Dependencies:** LobbyManager, FriendManager, NakamaManager, NameGenerator, UserProfileManager.
|
||||||
|
**Depended by:** lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 12.3 LobbyRoomList
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LobbyRoomList extends RefCounted
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/ui/lobby_room_list.gd` (155 lines)
|
||||||
|
|
||||||
|
Room list panel event wiring. Handles room list refresh, selection, join, and back navigation.
|
||||||
|
|
||||||
|
**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects signals.
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `_on_refresh_pressed` | `func _on_refresh_pressed() -> void` | void | Clear + refresh room list |
|
||||||
|
| `_on_room_selected` | `func _on_room_selected(index: int) -> void` | void | Copy room match_id/IP to input |
|
||||||
|
| `_on_room_activated` | `func _on_room_activated(index: int) -> void` | void | Select + auto-join |
|
||||||
|
| `_on_join_pressed` | `func _on_join_pressed() -> void` | void | Validate input, set name, join room (LAN or Nakama) |
|
||||||
|
| `_on_back_pressed` | `func _on_back_pressed() -> void` | void | Return to main menu |
|
||||||
|
| `on_room_list_updated` | `func on_room_list_updated(rooms: Array) -> void` | void | Render room rows, apply mode filter |
|
||||||
|
|
||||||
|
**Dependencies:** LobbyManager, AuthManager, UserProfileManager.
|
||||||
|
**Depended by:** lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 12.4 LobbyChat
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LobbyChat extends RefCounted
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `/home/dev/tekton/scenes/ui/lobby_chat.gd` (373 lines)
|
||||||
|
|
||||||
|
Global and direct message chat system. Handles Nakama socket channel chat, DM tabs, friend suggestions, and admin chat commands.
|
||||||
|
|
||||||
|
**Constants:** `GLOBAL_CHAT_ROOM = "social_global"`
|
||||||
|
|
||||||
|
**Properties:**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| _chat_channel | NakamaChannel | Current chat channel |
|
||||||
|
| _chat_messages | Array | [{sender, content, ts, date}] |
|
||||||
|
| _active_chat_context | String | "global" or user_id |
|
||||||
|
| _dm_tabs | Dictionary | user_id -> HBoxContainer (tab UI) |
|
||||||
|
| _dm_messages | Dictionary | user_id -> Array of messages |
|
||||||
|
| _chat_config | Dictionary | {prefix, max_messages, max_age_days} |
|
||||||
|
|
||||||
|
**Public Functions:**
|
||||||
|
|
||||||
|
| Function | Signature | Return | Description |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `join_global_chat` | `func join_global_chat() -> void` | void (async) | Join social_global channel, fetch history, inject prefix |
|
||||||
|
| `switch_chat_tab` | `func switch_chat_tab(context_id: String) -> void` | void | Switch between global and DM tabs |
|
||||||
|
| `_on_chat_send_pressed` | `func _on_chat_send_pressed() -> void` | void (async) | Send message: @username for DM, /clear for admin |
|
||||||
|
| `on_lobby_dm_received` | `func on_lobby_dm_received(from_user_id: String, from_name: String, message: String) -> void` | void | Incoming DM handler |
|
||||||
|
| `leave_global_chat` | `func leave_global_chat() -> void` | void (async) | Disconnect and leave channel |
|
||||||
|
|
||||||
|
**Internal Functions:** `_add_chat_message`, `_send_dm_message`, `_open_dm_tab`, `_create_dm_tab`, `_close_dm_tab`, `_inject_local_message`, `_trim_old_messages`, `_refresh_chat_display`, `_format_nakama_time`, `_get_local_time`, `_on_chat_input_changed`, `_on_friend_suggest_activated`, `_setup_friend_suggest_ui`.
|
||||||
|
|
||||||
|
**Dependencies:** NakamaManager, BackendService, FriendManager, AdminManager, UserProfileManager.
|
||||||
|
**Depended by:** lobby.gd.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 13. Dependency Graph
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 13.1 Manager Autoload Dependencies
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
ASCII diagram showing which autoloads reference others:
|
||||||
|
|
||||||
|
```
|
||||||
|
NakamaManager (no deps on other managers -- pure Nakama SDK)
|
||||||
|
|
|
||||||
|
+-- BackendService
|
||||||
|
| +-- SteamworksManager (child node, not autoload)
|
||||||
|
|
|
||||||
|
+-- AuthManager
|
||||||
|
| +-- NakamaManager
|
||||||
|
| +-- BackendService
|
||||||
|
|
|
||||||
|
+-- SessionManager
|
||||||
|
| +-- NakamaManager
|
||||||
|
|
|
||||||
|
+-- LobbyManager
|
||||||
|
| +-- NakamaManager
|
||||||
|
| +-- GameStateManager
|
||||||
|
|
|
||||||
|
+-- GameStateManager (no deps)
|
||||||
|
|
|
||||||
|
+-- PlayerManager (no deps -- data only)
|
||||||
|
|
|
||||||
|
+-- EventBus (no deps -- pure observer)
|
||||||
|
|
|
||||||
|
+-- UserProfileManager
|
||||||
|
| +-- NakamaManager
|
||||||
|
| +-- BackendService
|
||||||
|
| +-- EventBus
|
||||||
|
|
|
||||||
|
+-- FriendManager
|
||||||
|
| +-- BackendService
|
||||||
|
| +-- NakamaManager
|
||||||
|
|
|
||||||
|
+-- MailManager
|
||||||
|
| +-- BackendService
|
||||||
|
|
|
||||||
|
+-- GachaManager
|
||||||
|
| +-- BackendService
|
||||||
|
| +-- UserProfileManager
|
||||||
|
| +-- EventBus
|
||||||
|
|
|
||||||
|
+-- DailyRewardManager
|
||||||
|
| +-- BackendService
|
||||||
|
|
|
||||||
|
+-- AdminManager
|
||||||
|
| +-- BackendService
|
||||||
|
| +-- NakamaManager
|
||||||
|
|
|
||||||
|
+-- SkinManager
|
||||||
|
| +-- UserProfileManager
|
||||||
|
|
|
||||||
|
+-- ShopManager
|
||||||
|
| +-- BackendService (thin)
|
||||||
|
|
|
||||||
|
+-- PlayerInputManager
|
||||||
|
| +-- TouchControls
|
||||||
|
|
|
||||||
|
+-- PlayerMovementManager
|
||||||
|
| +-- ObstacleManager
|
||||||
|
| +-- SpecialTilesManager
|
||||||
|
| +-- EnhancedGridMap (scene)
|
||||||
|
|
|
||||||
|
+-- PlayerActionManager
|
||||||
|
| +-- PlayerboardManager
|
||||||
|
| +-- PlayerInputManager
|
||||||
|
| +-- GoalsCycleManager
|
||||||
|
|
|
||||||
|
+-- PlayerboardManager
|
||||||
|
| +-- GoalsCycleManager
|
||||||
|
| +-- GoalManager
|
||||||
|
|
|
||||||
|
+-- PlayerRaceManager
|
||||||
|
| +-- GoalsCycleManager
|
||||||
|
|
|
||||||
|
+-- GoalsCycleManager
|
||||||
|
| +-- GoalManager
|
||||||
|
| +-- TurnManager
|
||||||
|
| +-- Timer (scene)
|
||||||
|
|
|
||||||
|
+-- StopNGoManager
|
||||||
|
| +-- TurnManager
|
||||||
|
| +-- GoalManager
|
||||||
|
| +-- GoalsCycleManager
|
||||||
|
| +-- animation.gd (scene)
|
||||||
|
|
|
||||||
|
+-- GauntletManager
|
||||||
|
| +-- TurnManager
|
||||||
|
| +-- EnhancedGridMap
|
||||||
|
|
|
||||||
|
+-- PortalModeManager
|
||||||
|
| +-- SpecialTilesManager
|
||||||
|
| +-- EnhancedGridMap
|
||||||
|
|
|
||||||
|
+-- SpecialTilesManager
|
||||||
|
| +-- ObstacleManager
|
||||||
|
| +-- EnhancedGridMap
|
||||||
|
|
|
||||||
|
+-- ObstacleManager
|
||||||
|
| +-- EnhancedGridMap
|
||||||
|
|
|
||||||
|
+-- StaticTektonManager
|
||||||
|
| +-- EnhancedGridMap
|
||||||
|
| +-- ObstacleManager
|
||||||
|
|
|
||||||
|
+-- PowerupManager (no deps)
|
||||||
|
|
|
||||||
|
+-- UIManager (no deps -- dynamic UI)
|
||||||
|
|
|
||||||
|
+-- SettingsManager (no deps -- ConfigFile)
|
||||||
|
|
|
||||||
|
+-- GameUpdateManager (HTTPRequest -- no manager deps)
|
||||||
|
|
|
||||||
|
+-- TutorialManager
|
||||||
|
| +-- TutorialOverlay
|
||||||
|
|
|
||||||
|
+-- TutorialOverlay
|
||||||
|
| +-- TutorialManager
|
||||||
|
| +-- UIManager
|
||||||
|
|
|
||||||
|
+-- MusicManager (no deps)
|
||||||
|
+-- SfxManager (no deps)
|
||||||
|
+-- ScreenShake (no deps)
|
||||||
|
+-- NotificationManager (no deps)
|
||||||
|
+-- CameraContextManager (no deps)
|
||||||
|
+-- TouchControls (no deps)
|
||||||
|
+-- JoinManager (no deps -- stub)
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 13.2 Cross-Manager Signal Wiring
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
Key signal connections between managers and scene scripts:
|
||||||
|
|
||||||
|
```
|
||||||
|
NakamaManager.match_joined -> LobbyManager._on_match_joined
|
||||||
|
NakamaManager.match_join_error -> lobby.gd (clears _is_hosting)
|
||||||
|
NakamaManager.connection_failed -> lobby.gd (clears _is_hosting)
|
||||||
|
|
||||||
|
LobbyManager.room_joined -> LobbyRoom._on_room_joined
|
||||||
|
LobbyManager.room_left -> LobbyRoom._on_room_left
|
||||||
|
LobbyManager.host_disconnected -> LobbyRoom._on_host_disconnected
|
||||||
|
LobbyManager.player_joined -> LobbyRoom._on_player_joined
|
||||||
|
LobbyManager.player_left -> LobbyRoom._on_player_left
|
||||||
|
LobbyManager.ready_state_changed -> LobbyRoom._on_ready_state_changed
|
||||||
|
LobbyManager.all_players_ready -> LobbyRoom._on_all_players_ready
|
||||||
|
LobbyManager.game_starting -> LobbyRoom._on_game_starting
|
||||||
|
LobbyManager.game_mode_changed -> LobbyRoom._on_game_mode_changed
|
||||||
|
LobbyManager.room_list_updated -> LobbyRoomList.on_room_list_updated
|
||||||
|
LobbyManager.character_changed -> LobbyRoom._on_character_changed
|
||||||
|
LobbyManager.rematch_votes_updated -> main.gd (update rematch button)
|
||||||
|
|
||||||
|
FriendManager.dm_message_received -> LobbyChat.on_lobby_dm_received
|
||||||
|
FriendManager.lobby_invite_received -> LobbyRoom._on_lobby_invite_received
|
||||||
|
|
||||||
|
MailManager.unread_count_changed -> lobby.gd (update badge)
|
||||||
|
|
||||||
|
UserProfileManager.profile_loaded -> lobby.gd (_sync_room_profile_card)
|
||||||
|
UserProfileManager.profile_updated -> lobby.gd (_sync_room_profile_card)
|
||||||
|
|
||||||
|
AuthManager.logged_out -> LobbyMainMenu.go_to_login
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## 14. Scene Node Trees
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 14.1 main.tscn
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```
|
||||||
|
Main (Node) -- attached: main.gd
|
||||||
|
+-- EnhancedGridMap (GridMap / custom EnhancedGridMap node)
|
||||||
|
+-- PlayerSpawnPoints (Node3D)
|
||||||
|
+-- HUD (CanvasLayer)
|
||||||
|
| +-- LeaderboardPanel (Panel)
|
||||||
|
| | +-- MarginContainer/VBox
|
||||||
|
| | +-- Entry1-8 (HBoxContainer with RankLabel/NameLabel/ScoreLabel)
|
||||||
|
| +-- NotificationOverlay (Control)
|
||||||
|
| +-- ActionButtons (Control)
|
||||||
|
+-- PauseMenu (Panel)
|
||||||
|
| +-- Panel/VBox/ResumeBtn, HowToPlayBtn, SettingsBtn, UnstuckBtn, QuitMatchBtn
|
||||||
|
+-- HowToPlayPanel (Panel)
|
||||||
|
+-- StopNGoUI (Control) -- attached: animation.gd
|
||||||
|
| +-- StopPhase/AnimatedSprite2D
|
||||||
|
| +-- AnimationPlayer
|
||||||
|
| +-- CountDown/CountDownAnimation (AnimatedSprite2D)
|
||||||
|
| +-- GoFinish/GoAnimation2D (AnimatedSprite2D)
|
||||||
|
+-- Camera3D
|
||||||
|
+-- WorldEnvironment
|
||||||
|
+-- Player instances (added dynamically by main.gd)
|
||||||
|
+-- Player (CharacterBody3D) -- attached: player.gd
|
||||||
|
+-- MeshInstance3D (visual)
|
||||||
|
+-- CollisionShape3D
|
||||||
|
+-- PlayerboardUI (Control overlay)
|
||||||
|
+-- AnimationPlayer
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 14.2 player.tscn
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```
|
||||||
|
Player (CharacterBody3D) -- attached: player.gd
|
||||||
|
+-- MeshInstance3D (character model)
|
||||||
|
+-- CollisionShape3D
|
||||||
|
+-- PlayerboardUI (Control)
|
||||||
|
| +-- Slot0-9 (Panel/TextureRect)
|
||||||
|
+-- AnimationPlayer
|
||||||
|
+-- (tektons picked up become children at runtime)
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
### 14.3 lobby.tscn
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
```
|
||||||
|
Lobby (Control) -- attached: lobby.gd
|
||||||
|
+-- StatusBar (HBoxContainer)
|
||||||
|
| +-- ConnectionStatus (Label)
|
||||||
|
+-- MainMenuPanel (Panel)
|
||||||
|
| +-- Title/Username/Subtitle/Buttons (CreateRoom, BrowseRooms, Tutorial, etc.)
|
||||||
|
| +-- CharacterRoot (SubViewportContainer > SubViewport > Node3D)
|
||||||
|
| | +-- Oldpop, Masbro, Gatot, Bob (character meshes, hidden by default)
|
||||||
|
| +-- %AnimationPlayer
|
||||||
|
| +-- CurrencyLabels (GoldLabel, StarLabel)
|
||||||
|
| +-- ServerOption / ServerIPInput
|
||||||
|
| +-- LeaderboardBtn, CartBtn, ProfileBtn, MailboxBtn, Banner1Btn, TicketBtn
|
||||||
|
+-- RoomListPanel (Control)
|
||||||
|
| +-- RoomListTabs (TabContainer)
|
||||||
|
| +-- RoomTab, PlayTab
|
||||||
|
| +-- MatchIdInput, RefreshBtn, JoinBtn, BackBtn
|
||||||
|
| +-- RoomList (ItemList)
|
||||||
|
| +-- ItemTemplate (hidden)
|
||||||
|
| +-- ProfileCard (PlayerUsername, PlayerScore, Rank, Avatar)
|
||||||
|
+-- LobbyPanel (Panel)
|
||||||
|
| +-- RoomNameHeader
|
||||||
|
| +-- HostBanner
|
||||||
|
| +-- TopBar/SettingsSection (Duration, Spawn, Timer, Scarcity, GameMode options)
|
||||||
|
| +-- AreaSelector
|
||||||
|
| +-- PlayersContainer (slots 1-4)
|
||||||
|
| +-- PlayersContainer2 (slots 5-8)
|
||||||
|
| +-- BottomBar (LeaveBtn, ReadyBtn, StartGameBtn, InviteBtn)
|
||||||
|
| +-- StatusLabel
|
||||||
|
+-- ChatPanel (Panel)
|
||||||
|
| +-- RichTextLabel
|
||||||
|
| +-- ChatInput (LineEdit)
|
||||||
|
| +-- SendBtn
|
||||||
|
| +-- ChatTabsContainer (GlobalChatTabBtn + DM tabs)
|
||||||
|
| +-- FriendSuggestPanel (hidden)
|
||||||
|
+-- (dynamic instances: MailboxPanel, ShopPanel, GachaPanel, ProfilePanel, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
# Tekton Armageddon - Server Architecture
|
||||||
|
|
||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
High-level architecture of the Nakama Lua backend. For detailed RPC reference (params, returns, errors), see [Nakama-Server-API](../Nakama-Server-API).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Overview
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Client[Godot Client<br/>player.tscn] -->|RPC calls| Nakama[Nakama Server<br/>port 7350/7351]
|
||||||
|
Nakama -->|wallet_update| Wallet[Wallet Engine]
|
||||||
|
Nakama -->|storage_read/write| Storage[Nakama Storage DB<br/>PostgreSQL]
|
||||||
|
Nakama -->|leaderboard_*| LB[Native Leaderboard]
|
||||||
|
Nakama -->|match_*| Match[Match Handler]
|
||||||
|
Nakama -->|channel_*| Chat[Lobby Chat]
|
||||||
|
|
||||||
|
Client -->|nk.match_join| Match
|
||||||
|
Client -->|Direct RPC| Lua[Lua RPC Handlers<br/>server/nakama/lua/]
|
||||||
|
```
|
||||||
|
|
||||||
|
The Lua backend runs **inside** the Nakama process. Lua modules hook into Nakama's lifecycle (after-authentication, RPC dispatch, match signals). All game transactions (wallet, inventory, gacha, shop) are **server-authoritative** — the Godot client sends RPC requests and the Lua code validates and executes.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
main["main.lua<br/>Entry point: loads all modules"] --> utils["utils.lua<br/>Auth guards, channel resolver"]
|
||||||
|
main --> core["core.lua<br/>Authentication hooks<br/>Wallet init, ban check"]
|
||||||
|
main --> economy["economy.lua<br/>Shop catalog, currency IAP<br/>Item purchases"]
|
||||||
|
main --> gacha["gacha.lua<br/>Gacha pulls, pity system<br/>RNG on server"]
|
||||||
|
main --> leaderboard["leaderboard.lua<br/>Score submission, sync<br/>Global rankings"]
|
||||||
|
main --> inbox["inbox.lua<br/>Global/personal mail<br/>Reward claiming"]
|
||||||
|
main --> daily["daily_rewards.lua<br/>Daily login rewards<br/>Monthly schedule"]
|
||||||
|
main --> user["user.lua<br/>Profile updates, identity<br/>Friend sync"]
|
||||||
|
main --> admin["admin.lua<br/>Kick/ban, stats, chat mgmt<br/>Role management"]
|
||||||
|
|
||||||
|
core --> utils
|
||||||
|
economy --> utils
|
||||||
|
gacha --> utils
|
||||||
|
leaderboard --> utils
|
||||||
|
inbox --> utils
|
||||||
|
daily --> utils
|
||||||
|
user --> utils
|
||||||
|
admin --> utils
|
||||||
|
|
||||||
|
economy -->|Wallet deduction| Wallet[(Wallet Engine)]
|
||||||
|
gacha -->|Wallet deduction| Wallet
|
||||||
|
inbox -->|Claim rewards| Wallet
|
||||||
|
daily -->|Daily claim| Wallet
|
||||||
|
user -->|Profile load| Wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency order (loading sequence)
|
||||||
|
|
||||||
|
1. `utils.lua` — no deps (loaded first)
|
||||||
|
2. `economy.lua` — depends on utils
|
||||||
|
3. `core.lua` — depends on utils
|
||||||
|
4. `admin.lua` — depends on utils
|
||||||
|
5. `daily_rewards.lua` — depends on utils
|
||||||
|
6. `user.lua` — depends on utils
|
||||||
|
7. `leaderboard.lua` — depends on utils
|
||||||
|
8. `inbox.lua` — depends on utils
|
||||||
|
9. `gacha.lua` — no deps beyond utils
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Godot Client
|
||||||
|
participant N as Nakama Core
|
||||||
|
participant A as after_hooks (core.lua)
|
||||||
|
participant DB as Nakama Storage
|
||||||
|
|
||||||
|
C->>N: AuthenticateDevice/Custom/Email
|
||||||
|
N->>A: after_authenticate(context)
|
||||||
|
A->>DB: storage_read(profiles/profile)
|
||||||
|
alt First login (no profile)
|
||||||
|
A->>DB: storage_write(initial profile)
|
||||||
|
A->>N: wallet_update(gold=100, star=500)
|
||||||
|
end
|
||||||
|
A->>DB: storage_read(profiles/profile)
|
||||||
|
alt metadata.banned == true
|
||||||
|
A-->>C: error("Account banned")
|
||||||
|
end
|
||||||
|
A->>C: session token returned
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boot sequence per player
|
||||||
|
|
||||||
|
1. Godot client calls `AuthenticateDevice` (or Email/Custom/Steam).
|
||||||
|
2. Nakama core calls `after_authenticate()` hook in `core.lua`.
|
||||||
|
3. Hook reads `profiles/profile` from storage.
|
||||||
|
4. **If first login:** initializes default profile and grants starting wallet (`gold: 100, star: 500`).
|
||||||
|
5. **Ban check:** If `metadata.banned == true`, raises error (player rejected).
|
||||||
|
6. Client receives session token and proceeds to lobby.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wallet & Economy Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Client Side
|
||||||
|
ShopUI[Shop Panel] -->|buy_currency RPC| Server
|
||||||
|
ShopUI -->|purchase_item RPC| Server
|
||||||
|
GachaUI[Gacha Panel] -->|perform_gacha_pull RPC| Server
|
||||||
|
MailUI[Inbox] -->|claim_mail_reward RPC| Server
|
||||||
|
DailyUI[Daily Rewards] -->|claim_daily_reward RPC| Server
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server Side
|
||||||
|
Server[Lua RPC Handler]
|
||||||
|
Server -->|nk.wallet_update| Wallet[(Wallet)]
|
||||||
|
Server -->|nk.storage_write| Inv[(Inventory Storage)]
|
||||||
|
Server -->|nk.storage_write| Rec[(Receipts Storage)]
|
||||||
|
Server -->|nk.storage_write| Frag[(Fragments Storage)]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Client Refresh
|
||||||
|
Wallet -->|Wallet updated| Client
|
||||||
|
Client -->|get_account RPC| Wallet
|
||||||
|
Client -->|emit profile_updated| UI[All UI Panels<br/>update labels]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Currency types
|
||||||
|
|
||||||
|
| Currency | Purpose | Earned by |
|
||||||
|
|---|---|---|
|
||||||
|
| `gold` | Shop purchases, star conversion | IAP (real money), admin topup |
|
||||||
|
| `star` | Gacha pulls | Gold conversion, daily rewards, mail rewards |
|
||||||
|
|
||||||
|
### All wallet changesets
|
||||||
|
|
||||||
|
| Operation | Changeset |
|
||||||
|
|---|---|
|
||||||
|
| First login grant | `{gold: 100, star: 500}` |
|
||||||
|
| Buy gold IAP | `{gold: +N}` (N=100/550/1150/2400/6250/13000) |
|
||||||
|
| Buy stars (gold convert) | `{gold: -N, star: +M}` |
|
||||||
|
| Buy shop item | `{gold: -price}` or `{star: -price}` |
|
||||||
|
| Gacha pull | `{star: -cost}` or `{gold: -cost}` |
|
||||||
|
| Claim mail reward | `{gold: +N}` and/or `{star: +N}` |
|
||||||
|
| Claim daily reward | `{star: +N}` or `{gold: +N}` |
|
||||||
|
| Admin topup | `{gold: 999999}` |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gacha Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Client: perform_gacha_pull] --> B{Check banner}
|
||||||
|
B -->|star/gold| C[Read wallet balance]
|
||||||
|
C --> D{Sufficient funds?}
|
||||||
|
D -->|No| E[error: Insufficient currency]
|
||||||
|
D -->|Yes| F{Check pity}
|
||||||
|
F -->|pity >= 90| G[Force real_prize rarity]
|
||||||
|
F -->|pity < 90| H[Roll rarity by drop rates]
|
||||||
|
|
||||||
|
G --> I[Pick from real_prize pool]
|
||||||
|
H --> J[Pick from rarity pool]
|
||||||
|
|
||||||
|
I --> K[Deduct wallet cost]
|
||||||
|
J --> K
|
||||||
|
|
||||||
|
K --> L{real_prize?}
|
||||||
|
L -->|Yes| M[Add to inventory storage]
|
||||||
|
L -->|No| N[Increment fragment count]
|
||||||
|
|
||||||
|
M --> O[Return results]
|
||||||
|
N --> O
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drop rates
|
||||||
|
|
||||||
|
| Rarity | Rate | Result |
|
||||||
|
|---|---|---|
|
||||||
|
| Common | 60% | Fragment (`frag_common`) |
|
||||||
|
| Uncommon | 25% | Fragment (`frag_uncommon`) |
|
||||||
|
| Rare | 14% | Fragment (`frag_rare`) |
|
||||||
|
| Real Prize | 1% | Skin from catalog |
|
||||||
|
|
||||||
|
**Pity:** Guaranteed Real Prize at 90 pulls. Pity counter resets on any Real Prize win.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mail/Inbox System
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Admin[Admin RPC] -->|admin_send_mail| Global[Global Mail<br/>config/global_mail<br/>system user]
|
||||||
|
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
|
||||||
|
|
||||||
|
Client -->|get_mail| Merge[Merge global + personal]
|
||||||
|
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>not expired]
|
||||||
|
Filter --> Response[Return to client]
|
||||||
|
|
||||||
|
Client -->|claim_mail_reward| CW{Check claimed_ids}
|
||||||
|
CW -->|Already claimed| Err[error: Reward already claimed]
|
||||||
|
CW -->|Not claimed| Grant[Grant rewards:<br/>gold/star -> wallet<br/>fragments -> fragment storage<br/>skins -> inventory storage]
|
||||||
|
Grant --> UpdateState[Update state:<br/>add mailId to claimed_ids]
|
||||||
|
```[Global Mail<br/>config/global_mail<br/>system user]
|
||||||
|
Admin -->|admin_send_mail<br/>with target_user_id| Personal[Personal Mail<br/>inbox/personal<br/>per user]
|
||||||
|
|
||||||
|
Client -->|get_mail| Merge[Merge global + personal]
|
||||||
|
Merge --> Filter[Filter by: not deleted,<br/>within date range,<br/>not expired]
|
||||||
|
Filter --> Response[Return to client]
|
||||||
|
|
||||||
|
Client -->|claim_mail_reward| CW[Check claimed_ids]
|
||||||
|
CW -->|Already claimed| Err[error: Reward already claimed]
|
||||||
|
CW -->|Not claimed| Grant[Grant rewards:<br/>gold/star -> wallet<br/>fragments -> fragment storage<br/>skins -> inventory storage]
|
||||||
|
Grant --> UpdateState[Update state:<br/>add mailId to claimed_ids]
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client-Server Data Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Godot Client
|
||||||
|
participant L as Lua RPC
|
||||||
|
participant S as Nakama Storage
|
||||||
|
participant W as Wallet
|
||||||
|
|
||||||
|
Note over C,W: Purchase Flow
|
||||||
|
C->>L: purchase_item(item_id, idempotency_key)
|
||||||
|
L->>L: Look up item in SHOP_CATALOG_DEFS
|
||||||
|
L->>W: wallet_update(-price)
|
||||||
|
alt Insufficient funds
|
||||||
|
W-->>L: error
|
||||||
|
L-->>C: error("NotEnoughFunds")
|
||||||
|
else Success
|
||||||
|
L->>S: storage_write(inventory/item_id)
|
||||||
|
L->>S: storage_write(receipts/idempotency_key)
|
||||||
|
L-->>C: {success: true, item: item_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over C,W: Wallet State Sync
|
||||||
|
C->>L: get_account (Nakama SDK call)
|
||||||
|
S->>C: wallet JSON string
|
||||||
|
C->>C: Parse wallet JSON
|
||||||
|
C->>C: emit profile_updated signal
|
||||||
|
C->>C: All UI panels update labels
|
||||||
|
```
|
||||||
|
|
||||||
|
All transactions are **idempotent** via `idempotency_key` — if the same key is used twice, the server returns the previous result instead of re-executing.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Hierarchy
|
||||||
|
|
||||||
|
| Role | Can | Guarded by |
|
||||||
|
|---|---|---|
|
||||||
|
| `player` (default) | Nothing special | — |
|
||||||
|
| `moderator` | Match-related admin: kick players, get server stats | `utils.require_admin_or_host` (also checks match host) |
|
||||||
|
| `admin` | All moderation: ban/unban, manage mail, manage chat, view users | `utils.require_admin(context)` |
|
||||||
|
| `owner` | Everything admin can + set user roles | Inline check: `callerMetadata.role == "owner"` |
|
||||||
|
|
||||||
|
### Guard functions (utils.lua)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
utils.require_admin(context) -- errors if role not admin or owner
|
||||||
|
utils.require_admin_or_host(context, match_id) -- errors if not admin/owner AND not match host
|
||||||
|
utils.is_banned(metadata) -- returns boolean
|
||||||
|
utils.resolve_channel_id(channelId) -- channel name → hashed ID
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Storage Collections
|
||||||
|
|
||||||
|
| Collection | Owner | Key | Public R | Public W | Purpose |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `profiles` | User | `"profile"` | 1 | 0 | User metadata, role, ban status, loadout |
|
||||||
|
| `profiles` | User | `"pity_counters"` | 1 | 0 | Per-banner gacha pity counts |
|
||||||
|
| `profiles` | User | `"fragments"` | 1 | 0 | Accumulated gacha fragments |
|
||||||
|
| `inventory` | User | Item ID | 1 | 0 | Owned cosmetic items |
|
||||||
|
| `inventory` | User | `"fragments"` | 1 | 0 | Fragment counts (legacy) |
|
||||||
|
| `stats` | User | `"game_stats"` | 1 | 0 | Player stats (wins, kills, score) |
|
||||||
|
| `receipts` | User | Idempotency key | 1 | 0 | Purchase receipts (IAP + shop) |
|
||||||
|
| `inbox` | User | `"personal"` | 1 | 0 | Personalized mail inbox |
|
||||||
|
| `inbox` | User | `"state"` | 1 | 0 | claimed_ids, deleted_ids, read_ids |
|
||||||
|
| `daily_rewards` | User | `"state"` | 1 | 0 | Daily reward claim tracking |
|
||||||
|
| `config` | SYSTEM | `"global_mail"` | 2 | 0 | Global mail sent to all players |
|
||||||
|
| `config` | SYSTEM | `"daily_rewards"` | 2 | 0 | Monthly reward schedule |
|
||||||
|
| `config` | SYSTEM | `"lobby_chat"` | 2 | 0 | Chat prefix/max_messages config |
|
||||||
|
| `shop_config` | SYSTEM | `"featured_banners"` | 2 | 0 | Featured shop banners (max 3) |
|
||||||
|
| `bans` | SYSTEM | User ID | 2 | 0 | Ban records (redundant with metadata) |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## vs Nakama-Server-API
|
||||||
|
|
||||||
|
| Aspect | Architecture-Server (this page) | Nakama-Server-API |
|
||||||
|
|---|---|---|
|
||||||
|
| Audience | Architects, new devs | Implementers, AI agents |
|
||||||
|
| Detail level | High-level flow, diagrams | Per-function: params, returns, errors |
|
||||||
|
| Diagrams | Mermaid flowcharts | None |
|
||||||
|
| RPC listing | Summary table with key flows | Full 48 RPC documentation |
|
||||||
|
| Storage | Conceptual collection overview | Exact schema per collection |
|
||||||
|
| Best for | Understanding the system | Calling RPCs without reading code |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference: All 48 Registered RPCs
|
||||||
|
|
||||||
|
| RPC Name | Module | Auth | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `update_display_name` | user | required | Change display name |
|
||||||
|
| `update_avatar` | user | required | Change avatar URL |
|
||||||
|
| `sync_profile` | user | required | Push profile to server |
|
||||||
|
| `change_identity` | user | required | Link new device/email |
|
||||||
|
| `set_password` | user | required | Set email password |
|
||||||
|
| `sync_friends` | user | required | Push friend list |
|
||||||
|
| `get_shop_catalog` | economy | required | Get catalog + featured |
|
||||||
|
| `buy_currency` | economy | required | IAP gold/star purchase |
|
||||||
|
| `purchase_item` | economy | required | Buy cosmetic item |
|
||||||
|
| `perform_gacha_pull` | gacha | required | Roll gacha |
|
||||||
|
| `get_leaderboard_stats` | leaderboard | no | Get top 50 scores |
|
||||||
|
| `submit_score` | leaderboard | required | Record match score |
|
||||||
|
| `sync_leaderboard` | leaderboard | required | Sync stats → leaderboard |
|
||||||
|
| `reset_stats` | leaderboard | required | Clear own stats |
|
||||||
|
| `get_mail` | inbox | required | Get available mail |
|
||||||
|
| `claim_mail_reward` | inbox | required | Claim mail rewards |
|
||||||
|
| `delete_mail` | inbox | required | Soft-delete mail |
|
||||||
|
| `save_mail_state` | inbox | required | Mark as read |
|
||||||
|
| `claim_daily_reward` | daily_rewards | required | Claim today's reward |
|
||||||
|
| `get_daily_reward_state` | daily_rewards | required | View monthly schedule |
|
||||||
|
| `set_daily_reward_config` | daily_rewards | admin | Set reward schedule |
|
||||||
|
| `get_daily_reward_config_admin` | daily_rewards | admin | Get reward config |
|
||||||
|
| `admin_*` (18 RPCs) | admin | admin/owner | Moderation tools |
|
||||||
|
|
||||||
|
For full params/returns/errors on any RPC above, see [Nakama-Server-API](../Nakama-Server-API).
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment Topology
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph VPS [VPS 52.74.133.55]
|
||||||
|
Gitea[Gitea Server<br/>port 3000]
|
||||||
|
Nakama[Nakama Server<br/>port 7350/7351]
|
||||||
|
PG[(PostgreSQL<br/>Nakama DB)]
|
||||||
|
Act[act_runner<br/>CI/CD Worker]
|
||||||
|
end
|
||||||
|
|
||||||
|
Client[Godot Player] -->|HTTPS| Gitea
|
||||||
|
Client -->|gRPC/WebSocket| Nakama
|
||||||
|
Nakama --> PG
|
||||||
|
|
||||||
|
GitHub_mirror[GitHub Mirror] -->|Push| Gitea
|
||||||
|
Gitea -->|Webhook/Manual| Act
|
||||||
|
Act -->|Build| Binary[Binary Releases]
|
||||||
|
Act -->|Build| Patch[patch.pck on patches branch]
|
||||||
|
Client -->|Check version| Gitea
|
||||||
|
Client -->|Download patch| Gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
# Game Modes
|
||||||
|
|
||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
[Back to Home](./Home)
|
||||||
|
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Architecture](#architecture)
|
||||||
|
- [GameMode Enum & ModeConfig](#gamemode-enum--modeconfig)
|
||||||
|
- [Session Flow (all modes)](#session-flow-all-modes)
|
||||||
|
- [Core Managers (shared)](#core-managers-shared)
|
||||||
|
- [Freemode](#freemode)
|
||||||
|
- [Stop n Go](#stop-n-go)
|
||||||
|
- [Tekton Doors (Portal)](#tekton-doors-portal)
|
||||||
|
- [Candy Pump Survival (Gauntlet)](#candy-pump-survival-gauntlet)
|
||||||
|
- [Scoring & Leaderboard](#scoring--leaderboard)
|
||||||
|
- [Glossary](#glossary)
|
||||||
|
- [File Index](#file-index)
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Four game modes, each implementing the same core loop — players navigate a grid, collect tiles (Heart / Diamond / Star / Coin), match them to goals on a virtual 5x5 playerboard, and compete for score before the match timer expires.
|
||||||
|
|
||||||
|
| Mode | Enum Value | Display Name | Play Area | Gimmick |
|
||||||
|
|------|-----------|-------------|-----------|---------|
|
||||||
|
| Freemode | `FREEMODE = 0` | "Freemode" | Variable | No restrictions, just goals + timer |
|
||||||
|
| Stop n Go | `STOP_N_GO = 1` | "Stop n Go" | 23x12 | GO/STOP phases, safe zones, scatter penalty |
|
||||||
|
| Tekton Doors | `TEKTON_DOORS = 2` | "Tekton Doors" | 14x14 | 4 rooms, portal doors swap connections every 15s |
|
||||||
|
| Candy Pump Survival | `GAUNTLET = 3` | "Candy Pump Survival" | 20x20 | Ground growth — candy slowly fills the arena |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
GameMode (enum/RefCounted)
|
||||||
|
├── .from_string() / .mode_to_string() / .is_restricted() / .get_all_modes()
|
||||||
|
│
|
||||||
|
├── ModeConfig (RefCounted)
|
||||||
|
│ └── SCHEMA with defaults, type-checking, min/max for each mode
|
||||||
|
│
|
||||||
|
├── GoalsCycleManager (Node, autoload?)
|
||||||
|
│ ├── 30s cycle timer
|
||||||
|
│ ├── Match timer (configurable 60-600s)
|
||||||
|
│ ├── Score tracking (player_scores, player_goal_counts)
|
||||||
|
│ ├── Goal completion: _process_goal_completion()
|
||||||
|
│ └── RPC sync: sync_player_score, sync_goal_count, sync_timer
|
||||||
|
│
|
||||||
|
├── GoalManager (Node)
|
||||||
|
│ ├── initialize_random_goals() — generates 9-slot goal patterns
|
||||||
|
│ ├── Speed tracking (completion_times, boost_multiplier)
|
||||||
|
│ └── generate_preset_goals() / get_goals_for_player()
|
||||||
|
│
|
||||||
|
├── PlayerRaceManager (per-player)
|
||||||
|
│ ├── goals: Array[int] (9 slots)
|
||||||
|
│ ├── playerboard: Array[int] (25 slots, 5x5)
|
||||||
|
│ ├── check_pattern_match() — core matching logic
|
||||||
|
│ └── DEPRECATED lap/finish-line stubs
|
||||||
|
│
|
||||||
|
├── PlayerboardManager (per-player)
|
||||||
|
│ ├── grab_item() / auto_put_item() / arrange operations
|
||||||
|
│ ├── _execute_grab() — server-authoritative grab validation
|
||||||
|
│ ├── HIDDEN_SLOTS (12 of 25 cells blocked)
|
||||||
|
│ ├── bot_try_grab_item() — AI grab logic
|
||||||
|
│ └── _check_and_refill_grid_if_needed() — scarcity refill
|
||||||
|
│
|
||||||
|
├── TurnManager (shared)
|
||||||
|
│ ├── next_turn() / end_current_turn()
|
||||||
|
│ └── turn_based_mode toggle
|
||||||
|
│
|
||||||
|
└── Mode-specific managers
|
||||||
|
├── StopNGoManager — phase transitions, safe zones, mission HUD
|
||||||
|
├── PortalModeManager — room partitions, portal doors, swap timer
|
||||||
|
└── GauntletManager — ground growth, phases, bubbles, smack
|
||||||
|
```
|
||||||
|
|
||||||
|
All mode managers live under `/root/Main` and connect to `GoalsCycleManager` signals for score / goal tracking.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## GameMode Enum & ModeConfig
|
||||||
|
|
||||||
|
**File:** `scripts/game_mode.gd` (41 lines)
|
||||||
|
|
||||||
|
```
|
||||||
|
enum Mode { FREEMODE = 0, STOP_N_GO = 1, TEKTON_DOORS = 2, GAUNTLET = 3 }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Static Method | Returns | Description |
|
||||||
|
|--------------|---------|-------------|
|
||||||
|
| `from_string(mode: String)` | `Mode` | Converts "Freemode"/"Stop n Go"/"Tekton Doors"/"Candy Pump Survival" to enum |
|
||||||
|
| `mode_to_string(mode: Mode)` | `String` | Reverse of from_string |
|
||||||
|
| `is_restricted(mode: Mode)` | `bool` | true for STOP_N_GO, TEKTON_DOORS, GAUNTLET |
|
||||||
|
| `get_all_modes()` | `Array[String]` | Returns display names |
|
||||||
|
|
||||||
|
**File:** `scripts/mode_config.gd` (109 lines)
|
||||||
|
|
||||||
|
SCHEMA defines per-mode settings with type, default, min, max, allowed values:
|
||||||
|
|
||||||
|
| Mode | Settings |
|
||||||
|
|------|---------|
|
||||||
|
| Freemode | match_duration (180s default), randomize_spawn (bool), enable_cycle_timer (bool), scarcity_mode (Normal/Aggressive/Chaos) |
|
||||||
|
| Stop n Go | match_duration, sng_go_duration (20s), sng_stop_duration (4s), sng_required_goals (8) |
|
||||||
|
| Tekton Doors | match_duration, doors_swap_time (15s), doors_refresh_time (25s), doors_required_goals (8) |
|
||||||
|
| Gauntlet | match_duration, gauntlet_growth_interval (3.0s), gauntlet_cells_per_tick (phase dict) |
|
||||||
|
|
||||||
|
| Method | Args | Returns |
|
||||||
|
|--------|------|---------|
|
||||||
|
| `get_defaults(mode)` | String | Dictionary of defaults for that mode |
|
||||||
|
| `validate_setting(mode, key, value)` | String, String, Variant | `{"valid": bool, "error": String}` |
|
||||||
|
| `validate_config(mode, config)` | String, Dictionary | `{"valid": bool, "errors": Array}` |
|
||||||
|
| `get_mode_settings(mode)` | String | Array of setting keys |
|
||||||
|
| `get_setting_schema(mode, key)` | String, String | Dictionary with type/default/min/max/allowed |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Session Flow (all modes)
|
||||||
|
|
||||||
|
1. **Lobby** — players join, select mode + settings
|
||||||
|
2. **Game start** — `main.gd` calls `_setup_host_game()` which:
|
||||||
|
- Creates arena (gridmap resize + clear)
|
||||||
|
- Spawns mission tiles on Layer 1
|
||||||
|
- Calls mode manager's `start_game_mode()`
|
||||||
|
3. **Countdown** — brief timer then match begins
|
||||||
|
4. **Match active** — `GoalsCycleManager.start_match()` runs match timer + cycle timer
|
||||||
|
5. **Per-cycle** (30s):
|
||||||
|
- Players grab tiles from grid → place on 5x5 playerboard
|
||||||
|
- Match 3x3 pattern against 9-slot goals
|
||||||
|
- Complete goals → B1000 score + new goals + tiles randomize around player
|
||||||
|
- Cycle ends → board cleared, unmatched tiles scored at 10/tile match
|
||||||
|
6. **Match ends** — final leaderboard sync
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Core Managers (shared)
|
||||||
|
|
||||||
|
### GoalManager (`scripts/managers/goal_manager.gd`, 108 lines)
|
||||||
|
|
||||||
|
Generates 9-slot goal arrays using tile IDs 7-10 (Heart=7, Diamond=8, Star=9, Coin=10) with -1 for no-goal slots (~3 nulls per set).
|
||||||
|
|
||||||
|
| Function | Returns | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `initialize_random_goals(size, min_value, max_value, null_count)` | `Array` | Random goals with controlled null distribution |
|
||||||
|
| `generate_preset_goals(count)` | `Array` | Pre-generates N goal sets for all players |
|
||||||
|
| `get_goals_for_player(player_index)` | `Array` | Returns goals for a specific player slot |
|
||||||
|
| `mark_goal_start(player_id)` | void | Records timestamp for speed tracking |
|
||||||
|
| `mark_goal_complete(player_id)` | void | Records completion duration |
|
||||||
|
| `get_player_average_time(player_id)` | `float` | Average completion speed |
|
||||||
|
| `get_global_average_time()` | `float` | Average across all players |
|
||||||
|
| `get_boost_multiplier(player_id)` | `float` | 0.8-1.5x fill rate based on speed vs average |
|
||||||
|
| `reset()` | void | Clears all state |
|
||||||
|
|
||||||
|
### GoalsCycleManager (`scripts/managers/goals_cycle_manager.gd`, 520 lines)
|
||||||
|
|
||||||
|
Central scoring and timer system. Emits signals consumed by mode managers, UI, and leaderboard.
|
||||||
|
|
||||||
|
| Signal | Payload |
|
||||||
|
|--------|---------|
|
||||||
|
| `cycle_started()` | — |
|
||||||
|
| `cycle_ended()` | — |
|
||||||
|
| `timer_updated(time_remaining)` | float |
|
||||||
|
| `score_updated(peer_id, new_score)` | int, int |
|
||||||
|
| `goal_count_updated(peer_id, count)` | int, int |
|
||||||
|
| `leaderboard_updated(sorted_scores)` | Array |
|
||||||
|
| `match_started()` | — |
|
||||||
|
| `match_ended()` | — |
|
||||||
|
| `global_timer_updated(time_remaining)` | float |
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `start_match(duration_seconds, start_cycles)` | Begins match timer, optionally starts first 30s cycle |
|
||||||
|
| `_on_match_end()` | Processes final scores, syncs to clients |
|
||||||
|
| `start_cycle()` | Begin 30s cycle, emit `cycle_started` |
|
||||||
|
| `on_goal_completed(player, time_remaining)` | Entry point — routes to server or optimistic local path |
|
||||||
|
| `_process_goal_completion(player, time_remaining)` | Server: award B1000 + time bonus, regen goals, randomize tiles |
|
||||||
|
| `regenerate_goals_for_player(player)` | Generate new 9-slot goal set, sync via RPC |
|
||||||
|
| `_randomize_tiles_around_player(player)` | Randomize 3x3 area around player on the grid |
|
||||||
|
| `_process_cycle_end_for_all_players()` | Clear all boards, convert matches to B10/tile |
|
||||||
|
| `add_score(peer_id, amount)` | Server: add arbitrary score points |
|
||||||
|
| `_update_leaderboard()` | Sort by score descending (or with SNG winner override) |
|
||||||
|
|
||||||
|
### PlayerRaceManager (`scripts/managers/player_race_manager.gd`, 133 lines)
|
||||||
|
|
||||||
|
State holder per player. Core logic is `check_pattern_match()`.
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `check_pattern_match()` | Returns true if any 3x3 sub-grid of 5x5 board matches 3x3 goals |
|
||||||
|
| `check_3x3_section(board, goals, start_row, start_col)` | Checks single 3x3 section |
|
||||||
|
| `_normalize_tile(tile)` | Converts holo tiles 11-14 → 7-10 for match comparison |
|
||||||
|
| Remaining functions | DEPRECATED lap/finish line stubs |
|
||||||
|
|
||||||
|
**Playerboard Layout:** 5x5 (indices 0-24). 13 cells are HIDDEN_SLOTS (cannot hold tiles). Only 12 usable slots arranged in an L-shape matching the HUD.
|
||||||
|
|
||||||
|
### PlayerboardManager (`scripts/managers/playerboard_manager.gd`, 793 lines)
|
||||||
|
|
||||||
|
Handles grab, put, arrange operations with optimistic local updates + server-authoritative validation.
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `grab_item(grid_position)` | Grab tile from grid → place on board (auto-arrange) or consume as power-up |
|
||||||
|
| `_execute_grab(grid_pos, cell, item_id, expected_slot)` | Server-side validation + state update + sync |
|
||||||
|
| `_force_sync_to_client(cell, server_item)` | Revert client when server rejects grab |
|
||||||
|
| `auto_put_item()` | AI/bot: find best tile to remove from board |
|
||||||
|
| `find_best_goal_slot_for_item(item)` | Auto-arrange into best matching slot |
|
||||||
|
| `bot_try_grab_item()` | AI grab logic |
|
||||||
|
| `_check_and_refill_grid_if_needed(gridmap)` | Refill floor 1 via ScarcityController when empty |
|
||||||
|
|
||||||
|
### TurnManager (`scripts/managers/turn_manager.gd`, 27 lines)
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `next_turn(players)` | Advance turn index, emit `turn_changed` |
|
||||||
|
| `end_current_turn()` | Emit `turn_ended` |
|
||||||
|
| `reset_turn()` / `reset()` | Clear state |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Freemode
|
||||||
|
|
||||||
|
**No dedicated manager.** Freemode relies entirely on the shared core managers (GoalsCycleManager, PlayerboardManager, etc.) with no mode-specific restrictions or gimmicks. Arena size is configurable via LobbyManager settings.
|
||||||
|
|
||||||
|
**Settings effects:**
|
||||||
|
- `enable_cycle_timer` false → cycle never expires (board never auto-clears)
|
||||||
|
- `scarcity_mode` → controls tile refill aggression
|
||||||
|
- `randomize_spawn` → players start at random positions
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Stop n Go
|
||||||
|
|
||||||
|
**Manager:** `StopNGoManager` (`scripts/managers/stop_n_go_manager.gd`, 1107 lines)
|
||||||
|
|
||||||
|
A 23x12 arena with two alternating phases:
|
||||||
|
|
||||||
|
### Phase System
|
||||||
|
|
||||||
|
| Phase | Duration (default) | Behaviour |
|
||||||
|
|-------|-------------------|-----------|
|
||||||
|
| GO | 20s | Players move freely, collect tiles, complete goals |
|
||||||
|
| STOP | 4s | Players frozen if outside safe zone → tiles scattered |
|
||||||
|
|
||||||
|
When STOP begins:
|
||||||
|
1. 3 dynamic safe zones spawn randomly (green tiles)
|
||||||
|
2. All players outside safe zone get `_scatter_player_tiles()` — board tiles scattered on grid
|
||||||
|
3. Power-up tiles (Speed=11, Ghost=14) spawn at 5 permanent locations
|
||||||
|
4. Mission requirement: complete 8 goals before reaching finish (x=22)
|
||||||
|
|
||||||
|
When GO begins:
|
||||||
|
1. Dynamic safe zones cleared
|
||||||
|
2. All STOP freeze effects removed via `sync_stop_freeze(false)`
|
||||||
|
|
||||||
|
### Tile IDs
|
||||||
|
|
||||||
|
| ID | Meaning |
|
||||||
|
|----|---------|
|
||||||
|
| 0 | Walkable floor |
|
||||||
|
| 2 | Safe zone (green) |
|
||||||
|
| 3 | Start/Finish line |
|
||||||
|
| 4 | Wall/obstacle |
|
||||||
|
| 15 | Lightning stone (decorative ancient rock) |
|
||||||
|
| 16 | Safe zone wall |
|
||||||
|
|
||||||
|
### Arena
|
||||||
|
|
||||||
|
22x10 walkable area with two interior rooms with entrances:
|
||||||
|
- Room 1: (7,6) to (11,9) — 5x4 area with 4 door entrances
|
||||||
|
- Room 2: (15,1) to (19,5) — 5x5 area with 4 door entrances
|
||||||
|
|
||||||
|
### HUD
|
||||||
|
|
||||||
|
- Center-bottom mission label: "GOALS (X/8)" or "ALL GOALS COMPLETE! REACH THE FINISH!"
|
||||||
|
- Traffic light stop timer (3 segments): all empty during GO, fills red during STOP phase
|
||||||
|
- Last 3 seconds of GO phase: segments light up one-by-one (countdown)
|
||||||
|
- VFX: `vfx_manager.play_go_animation()` / `play_stop_phase()`
|
||||||
|
|
||||||
|
### RPCs
|
||||||
|
|
||||||
|
| RPC | Direction | Description |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `sync_phase(phase_name, duration)` | Authority → all | Broadcast GO/STOP phase change |
|
||||||
|
| `sync_arena_setup()` | Authority → remote | Sync 23x12 grid dimensions + obstacles |
|
||||||
|
| `sync_all_safe_zones_vfx()` | Authority → all | Trigger safe zone visual effects |
|
||||||
|
|
||||||
|
### Key Functions (server-only unless noted)
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `start_game_mode()` | Server: setup arena, assign missions, start GO phase |
|
||||||
|
| `_start_phase(phase)` | Transition GO↔STOP, penalize players outside safe zone |
|
||||||
|
| `_setup_arena()` | Build 23x12 with obstacles + rooms |
|
||||||
|
| `_spawn_mission_tiles()` | Heart(7)/Diamond(8)/Star(9)/Coin(10) at 60% density |
|
||||||
|
| `_spawn_powerup_tiles()` | Speed(11)+Ghost(14) at 5 permanent locations |
|
||||||
|
| `_assign_missions()` | NO-OP (mission = achievement: collect 8 goals) |
|
||||||
|
| `activate_client_side()` | Client: show HUD, connect to GoalsCycleManager signals |
|
||||||
|
| `rotate_players_to_start()` | Force all players to face East (PI/2) |
|
||||||
|
| `can_rpc()` | Check multiplayer peer is connected |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Tekton Doors (Portal)
|
||||||
|
|
||||||
|
**Manager:** `PortalModeManager` (`scripts/managers/portal_mode_manager.gd`, 585 lines)
|
||||||
|
**Actor:** `PortalDoor` (`scripts/portal_door.gd`, 136 lines)
|
||||||
|
|
||||||
|
A 14x14 grid divided into 4 rooms (7x7 each) by cross-shaped wall partitions. Players move between rooms via portal doors that swap connections every 15 seconds.
|
||||||
|
|
||||||
|
### Room Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Room 0 (NW) | Room 1 (NE)
|
||||||
|
x: 0-6 | x: 7-13
|
||||||
|
z: 0-6 | z: 0-6
|
||||||
|
--------------+--------------
|
||||||
|
Room 2 (SW) | Room 3 (SE)
|
||||||
|
x: 0-6 | x: 7-13
|
||||||
|
z: 7-13 | z: 7-13
|
||||||
|
```
|
||||||
|
|
||||||
|
Central divider: columns 6,7 and rows 6,7 are walls (tile ID 4).
|
||||||
|
|
||||||
|
### Portal System
|
||||||
|
|
||||||
|
- 10 doors total (2 base per room + 2 randomly placed extras)
|
||||||
|
- Every 15s (`doors_swap_time`): `_randomize_connections()` shuffles pairings
|
||||||
|
- Each pair gets a color from `PORTAL_COLORS` (Cyan/Magenta/Red/Green/Orange)
|
||||||
|
- Validation ensures no pair connects doors in the same room
|
||||||
|
- 200ms anti-jitter cooldown per player in `handle_portal_interaction()`
|
||||||
|
|
||||||
|
### PortalDoor (actor)
|
||||||
|
|
||||||
|
| Property/Method | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `room_id` | Which room this door belongs to |
|
||||||
|
| `door_id` | Unique door index |
|
||||||
|
| `target_door_id` | Connected door (set by PortalModeManager) |
|
||||||
|
| `portal_color` | Color (set triggers `set_portal_color`) |
|
||||||
|
| `detection_area` | Area3D — body_entered triggers portal |
|
||||||
|
| `_on_body_entered(body)` | 200ms cooldown, emit `player_entered_portal` |
|
||||||
|
| `spawn_offset` | Vector2i meta — nudge spawn position into room |
|
||||||
|
| `_adjust_indicator_position()` | Move GroundIndicator toward room interior |
|
||||||
|
|
||||||
|
### Finish Room
|
||||||
|
|
||||||
|
- At 30s remaining on match timer, reveal `_spawn_finish_room()`
|
||||||
|
- Random 3x3 area converted to finish tiles (ID 3) in one room
|
||||||
|
- Player must be standing on finish tile AND have `doors_required_goals` complete
|
||||||
|
|
||||||
|
### Tile Refill
|
||||||
|
|
||||||
|
- Every 25s (`tile_refresh_time`): `_refresh_tiles()` refills Floor 1 at 60% density
|
||||||
|
- Uses `ScarcityModel.get_tile_weights()` for weighted random selection
|
||||||
|
- Avoids spawning under portal doors
|
||||||
|
|
||||||
|
### HUD
|
||||||
|
|
||||||
|
- Center-bottom: "GOALS (X/8)" or "ALL GOALS COMPLETE! FIND THE FINISH ROOM!"
|
||||||
|
- Message broadcasts: "PORTALS SWITCHED!", "TILES REPLENISHED!"
|
||||||
|
- Warning: "A 3x3 Finish Zone has appeared in Room N!"
|
||||||
|
|
||||||
|
### RPCs
|
||||||
|
|
||||||
|
| RPC | Direction | Description |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `sync_portal_data(data)` | Authority → local | Sync connections + colors to all clients |
|
||||||
|
| `sync_portal_configs(door_configs)` | Via main | Broadcast door positions/rotations |
|
||||||
|
|
||||||
|
### Key Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `initialize(p_main, p_gridmap)` | Create swap timer + tile refresh timer, connect signals |
|
||||||
|
| `start_game_mode()` | Setup arena, randomize connections, start timers |
|
||||||
|
| `setup_arena_locally()` | Resize to 14x14, build room walls, spawn portal doors |
|
||||||
|
| `_randomize_connections()` | Shuffle door pairings, assign colors, validate same-room rule |
|
||||||
|
| `handle_portal_interaction(player, door)` | Teleport player to connected door + offset |
|
||||||
|
| `_spawn_finish_room()` | Convert random 3x3 area to finish tiles |
|
||||||
|
| `check_win_condition(player_id, pos)` | Check finish tile + mission complete |
|
||||||
|
| `_refresh_tiles()` | Refill floor 1 items with scarcity weights |
|
||||||
|
| `sync_to_client(peer_id)` | Sync portal connections to late-joining client |
|
||||||
|
| `get_spawn_points()` | Returns 4 spawn positions (one per room quadrant) |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Candy Pump Survival (Gauntlet)
|
||||||
|
|
||||||
|
**Manager:** `GauntletManager` (`scripts/managers/gauntlet_manager.gd`, 1825 lines)
|
||||||
|
|
||||||
|
A 20x20 arena where sticky candy (pink) slowly grows from the edges inward over 3 phases. Players must navigate shrinking safe zones, avoid sticky tiles, and use the "Smack" ability to temporarily clear candy.
|
||||||
|
|
||||||
|
### Phase System
|
||||||
|
|
||||||
|
| Phase | Start | Duration | Cell Growth (per tick) | Description |
|
||||||
|
|-------|-------|----------|----------------------|-------------|
|
||||||
|
| OPEN_ARENA (0) | 0s | 60s | 4-6 | "Outer Pressure" — candy pushes from perimeter |
|
||||||
|
| ROUTE_PRESSURE (1) | 60s | 60s | 6-8 | "Middle Pressure" — corridors tighten |
|
||||||
|
| SURVIVAL_ENDGAME (2) | 120s | 60s | 8-10 | "Inner Survival" — center fills in |
|
||||||
|
|
||||||
|
Each phase transition shrinks the arena bounds by removing outer layers (via `_shrink_arena()`).
|
||||||
|
|
||||||
|
### Growth Algorithm
|
||||||
|
|
||||||
|
Each tick (every `growth_interval` = 3s):
|
||||||
|
|
||||||
|
1. **Detect movement buffers** — identify critical corridor cells (#083)
|
||||||
|
2. **Generate candidates** — all SAFE cells scored by formula:
|
||||||
|
```
|
||||||
|
CandidateScore = LayerPriority + StickyNeighbor + InwardPressure
|
||||||
|
+ PlayerPressure + ClusterGrowth + CampingPressure
|
||||||
|
+ RandomNoise(-20..+20) + MovementBuffer + PathSafety + Repetition
|
||||||
|
```
|
||||||
|
3. **Weighted selection** — pick `_cells_this_tick()` cells via roulette wheel
|
||||||
|
4. **Path safety check** — `_apply_path_safety()` ensures no player gets fully trapped
|
||||||
|
5. **Telegraph** — amber warning overlay appears for 1s (cells still passable)
|
||||||
|
6. **Apply** — cells convert to permanent STICKY (pink overlay on Layer 2)
|
||||||
|
|
||||||
|
### Scoring Components
|
||||||
|
|
||||||
|
| Score Component | Range | Description |
|
||||||
|
|----------------|-------|-------------|
|
||||||
|
| `_score_layer_priority` | -40..+60 | Phase weight by ring (outer/middle/inner) |
|
||||||
|
| `_score_sticky_neighbor` | 0..+64 | +8 per adjacent sticky cell (cap +64) |
|
||||||
|
| `_score_inward_pressure` | 0..+30 | Push inward, scales with phase |
|
||||||
|
| `_score_player_pressure` | -50..+20 | 2-4 cells away +20; under player -50 (+10 in final 30s) |
|
||||||
|
| `_score_cluster_growth` | 0..+25 | +15 expansion, +25 bridge between clusters |
|
||||||
|
| `_score_camping_pressure` | 0..+60 | Per-region: >5s +20, >8s +40, >10s +60 |
|
||||||
|
| `_score_movement_buffer` | -40..0 | Hidden corridor buffers + player proximity floor |
|
||||||
|
| `_score_path_safety` | -100..0 | Soft penalty if selection would strand a player |
|
||||||
|
| `_score_repetition` | -30..0 | Penalty for cells near last tick's selection |
|
||||||
|
|
||||||
|
### Cell States
|
||||||
|
|
||||||
|
| State | Meaning | Passable? |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| SAFE | Normal floor | Yes |
|
||||||
|
| TELEGRAPHED | Amber warning (1s) | Yes |
|
||||||
|
| STICKY | Permanent candy overlay | No (slows) |
|
||||||
|
| BUBBLE_GROWING | Candy bubble expanding | No |
|
||||||
|
| BLOCKED | NPC zone / permanent obstacle | No |
|
||||||
|
|
||||||
|
### Candy Bubble System (#082)
|
||||||
|
|
||||||
|
Anti-camping hazard: grows 1x1 → 3x3 sticky area.
|
||||||
|
|
||||||
|
| Phase | Max Bubbles |
|
||||||
|
|-------|-------------|
|
||||||
|
| OPEN_ARENA | 0 (disabled) |
|
||||||
|
| ROUTE_PRESSURE | 2 |
|
||||||
|
| SURVIVAL_ENDGAME | 3 |
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Grow duration | 2.75s |
|
||||||
|
| Explosion radius | 1 (3x3) |
|
||||||
|
| Recent memory | 4 positions |
|
||||||
|
| Anti-stack radius | 3 (no bubbles within 3 of recent) |
|
||||||
|
|
||||||
|
### Camping Detection (#073)
|
||||||
|
|
||||||
|
Players tracked in 4x4 regions. Time accumulates while player stays in same region, resets on region change. Drives camping pressure in candidate scoring.
|
||||||
|
|
||||||
|
### Movement Buffers (#083)
|
||||||
|
|
||||||
|
Hidden per-cell penalties on critical corridor cells (chokepoints where removing the cell would isolate part of the arena). Decay over time and phase transitions so arena can still close in.
|
||||||
|
|
||||||
|
### Smack Mechanic
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Cooldown | 8s |
|
||||||
|
| Charge window | 3s |
|
||||||
|
| Effect | Clears nearby sticky? (consumes charge) |
|
||||||
|
|
||||||
|
Per-player cooldown/charge tracked in `smack_cooldowns` / `smack_charged` Dictionaries. Pink modulate during charge window, white on cooldown.
|
||||||
|
|
||||||
|
### Arena NPC
|
||||||
|
|
||||||
|
Candy Pump NPC at center (9,9) in a 3x3 blocked zone. Visual-only in v2 (projectile logic removed). Scattered projectiles still spawned for visual effect during telegraph phase.
|
||||||
|
|
||||||
|
### Slow-Mo
|
||||||
|
|
||||||
|
- Triggered conditionally, duration 4s
|
||||||
|
- `Engine.time_scale = 0.25` (1/4 speed)
|
||||||
|
- Restored to 1.0 in `_exit_tree()`
|
||||||
|
|
||||||
|
### Spawn Points
|
||||||
|
|
||||||
|
| Player Count | Positions |
|
||||||
|
|-------------|-----------|
|
||||||
|
| 4 | 4 corners: (1,1), (18,1), (1,18), (18,18) |
|
||||||
|
| 5-6 | 4 corners + top-mid (10,1) + bottom-mid (10,18) |
|
||||||
|
| 7-8 | 4 corners + all 4 mid-edges |
|
||||||
|
|
||||||
|
### RPCs
|
||||||
|
|
||||||
|
| RPC | Direction | Description |
|
||||||
|
|-----|-----------|-------------|
|
||||||
|
| `sync_phase(phase_index, phase_name)` | Authority → local | Phase change broadcast |
|
||||||
|
| `sync_arena_setup()` | Authority → remote | Arena dimensions + layout |
|
||||||
|
| `sync_growth_telegraph(cells)` | Authority → local | Show amber warning on selected cells |
|
||||||
|
| `sync_growth_apply(cells)` | Authority → local | Convert telegraphed to sticky |
|
||||||
|
| `consume_smack(pid)` | Any peer → local | Smack consumption + animation |
|
||||||
|
| `sync_stop_freeze` | (inherited from player.gd) | Freeze/unfreeze player |
|
||||||
|
|
||||||
|
### Key Functions
|
||||||
|
|
||||||
|
| Function | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `initialize(main, grid)` | Connect to GoalsCycleManager |
|
||||||
|
| `start_game_mode()` | Activate client side, start OPEN_ARENA phase |
|
||||||
|
| `_setup_arena()` | Build 20x20, spawn Candy Pump NPC |
|
||||||
|
| `_process_growth_tick()` | One growth cycle: score → select → telegraph → apply |
|
||||||
|
| `_generate_candidates()` | Score all SAFE cells |
|
||||||
|
| `_calculate_candidate_score(pos, player_cells)` | Full formula with 10 components |
|
||||||
|
| `_select_cells_weighted(candidates, count)` | Roulette-wheel selection |
|
||||||
|
| `_apply_path_safety(selected)` | Filter: ensure no player stranded |
|
||||||
|
| `_try_spawn_bubble()` | Anti-camping bubble spawn attempt |
|
||||||
|
| `_update_camp_tracking(delta)` | Per-player region residency timer |
|
||||||
|
| `_detect_movement_buffers()` | Identify critical corridor chokepoints |
|
||||||
|
| `_shrink_arena()` | Remove outer arena layers on phase change |
|
||||||
|
| `_spawn_mission_tiles()` | Heart/Diamond/Star/Coin at 60% density |
|
||||||
|
| `get_spawn_points(player_count)` | Return spawn positions by player count |
|
||||||
|
| `has_smack_charged(pid)` / `consume_smack(pid)` | Smack mechanic |
|
||||||
|
| `_spawn_telegraph_highlight(pos)` | Amber glow visual (2-stage: build-up + flash) |
|
||||||
|
| `_spawn_impact_particles(targets)` | Candy splash particles on sticky impact |
|
||||||
|
| `_check_all_players_trapped()` | Re-evaluate sticky traps after growth apply |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Scoring & Leaderboard
|
||||||
|
|
||||||
|
| Action | Points |
|
||||||
|
|--------|--------|
|
||||||
|
| Complete goal pattern (match 3x3) | B1000 + time bonus |
|
||||||
|
| Tile match at cycle end (per tile) | B10 |
|
||||||
|
| Time bonus formula | `int(time_remaining * TIME_BONUS_MULTIPLIER)` — currently 0 (flat 1000) |
|
||||||
|
|
||||||
|
Leaderboard sorted descending. Stop n Go special case: winner (first to reach finish) placed at top regardless of score.
|
||||||
|
|
||||||
|
Leaderboard signal payload:
|
||||||
|
```
|
||||||
|
[{"peer_id": int, "score": int}, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|------|------------|
|
||||||
|
| Goal | 3x3 pattern (9 slots, some -1 for null) player must match on their playerboard |
|
||||||
|
| Playerboard | 5x5 virtual grid (12 usable slots, 13 hidden) per player |
|
||||||
|
| Tile | Grid item on Floor 1: Heart(7), Diamond(8), Star(9), Coin(10) |
|
||||||
|
| Holo Tile | Power-up tiles: Speed(11), Ghost(14) — consumed on pickup, not placed on board |
|
||||||
|
| Cycle | 30-second scoring round; ends with board clear + point conversion |
|
||||||
|
| Sticky | Permanent pink overlay on Gauntlet floor cells — blocks/slows movement |
|
||||||
|
| Telegraph | Amber 1-second warning before a cell becomes sticky |
|
||||||
|
| Safe Zone | Green tiles in Stop n Go STOP phase — only safe tile type |
|
||||||
|
| Portal | Colored door connecting rooms in Tekton Doors |
|
||||||
|
| Scarcity | Tile refill model controlling spawn weights based on mode config |
|
||||||
|
| Smack | Gauntlet ability: clear nearby sticky (8s cooldown, 3s charge window) |
|
||||||
|
| Camping | Player staying in same 4x4 region >5s, attracts growth pressure |
|
||||||
|
| Movement Buffer | Hidden chokepoint corridor that growth algorithm avoids sealing early |
|
||||||
|
| Chebyshev Distance | `max(|x1-x2|, |y1-y2|)` — used for all proximity calculations |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
|
||||||
|
## File Index
|
||||||
|
|
||||||
|
| File | Lines | Role |
|
||||||
|
|------|-------|------|
|
||||||
|
| `scripts/game_mode.gd` | 41 | Mode enum + helper functions |
|
||||||
|
| `scripts/mode_config.gd` | 109 | Schema-driven per-mode settings |
|
||||||
|
| `scripts/managers/goal_manager.gd` | 108 | Goal generation + speed tracking |
|
||||||
|
| `scripts/managers/goals_cycle_manager.gd` | 520 | Timer, scoring, cycle control |
|
||||||
|
| `scripts/managers/player_race_manager.gd` | 133 | Per-player state: goals, board, pattern matching |
|
||||||
|
| `scripts/managers/playerboard_manager.gd` | 793 | Grab/put/arrange operations |
|
||||||
|
| `scripts/managers/turn_manager.gd` | 27 | Turn-based flow |
|
||||||
|
| `scripts/managers/stop_n_go_manager.gd` | 1107 | Stop n Go phase system, safe zones, HUD |
|
||||||
|
| `scripts/managers/portal_mode_manager.gd` | 585 | Tekton Doors room layout, portals, tiles |
|
||||||
|
| `scripts/managers/gauntlet_manager.gd` | 1825 | Candy Pump Survival growth, phases, smack |
|
||||||
|
| `scripts/portal_door.gd` | 136 | PortalDoor actor — detection, teleport, visuals |
|
||||||
|
| `scripts/managers/goals_cycle_manager.gd` | (shared) | Also referenced by gauntlet signal connections |
|
||||||
|
| `scripts/managers/camera_context_manager.gd` | ... | Camera mode changes per game mode? |
|
||||||
|
| `scripts/managers/player_movement_manager.gd` | ... | Movement restrictions per mode |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Tekton Dash Armageddon
|
||||||
|
|
||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
- [Game Modes](./Game-Modes.-) — Full per-mode reference: Stop n Go, Tekton Doors, Candy Pump Survival, Freemode
|
||||||
|
- [Architecture - Client](./Architecture-Client) — Godot client code structure, managers, scenes, player controller
|
||||||
|
- [Architecture - Server](./Architecture-Server) — Nakama Lua backend topology, auth flow, wallet economy, admin roles
|
||||||
|
- [Nakama Server API](./Nakama-Server-API) — Full per-function RPC reference with params, returns, errors
|
||||||
|
- [Patch Release Workflow](./Patch-Release-Workflow.-) — Hot patch and binary release CI/CD pipelines
|
||||||
|
- [Skin Creation Workflow](./Skin-Creation-Workflow.-) — Skin material authoring, catalog registration, gacha prizes
|
||||||
|
- [Nakama Deployment](./Nakama-Deployment.-) — Push Lua updates to Nakama server
|
||||||
|
- [SSH Setup — Linux](./SSH-Setup-Linux)
|
||||||
|
- [SSH Setup — macOS](./SSH-Setup-macOS)
|
||||||
|
- [SSH Setup — Windows](./SSH-Setup-Windows)
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# Patch & Release Workflow
|
||||||
|
|
||||||
|
Complete guide for shipping updates to Tekton players — hot patches (`.pck`) for content changes and full binary releases for engine/platform changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Two automated CI pipelines handle all distribution:
|
||||||
|
|
||||||
|
| Pipeline | Trigger | Output | Delivery |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Deploy Patch** (`deploy_patch.yml`) | Manual workflow dispatch | `patch.pck` + `version.json` → `patches` branch | Gitea raw endpoint |
|
||||||
|
| **Release** (`ci.yml`) | Git tag `v*` push | Windows/Linux/macOS `.zip` → Gitea Release | git.klud.top releases |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
### Gitea instance
|
||||||
|
- **URL:** https://git.klud.top
|
||||||
|
- **API:** http://52.74.133.55:3000/api/v1
|
||||||
|
- **Runner:** Local Docker container (`gitea-runner`) via `docker-compose`
|
||||||
|
- **Cache volume:** `/home/dev/godot-cache` → `/cache` (rw) inside runner containers
|
||||||
|
- **Secret:** `TEKTON_RELEASE_TOKEN` — Token from user `adtpdn` with repo write access
|
||||||
|
|
||||||
|
### Patch serving
|
||||||
|
Patches served directly from Gitea's built-in raw file endpoint — no external CDN:
|
||||||
|
- Manifest: `https://git.klud.top/danchie/tekton/raw/branch/patches/version.json`
|
||||||
|
- PCK: `https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck`
|
||||||
|
|
||||||
|
Old `raw.klud.top` (gitea-pages container) retired — Gitea raw endpoint is faster, simpler, and always available.
|
||||||
|
|
||||||
|
### Release page
|
||||||
|
- **URL:** https://git.klud.top/danchie/tekton/releases
|
||||||
|
- Assets auto-uploaded by CI on tag push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Hot Patch (content-only updates)
|
||||||
|
|
||||||
|
Use when: script changes, UI tweaks, balance patches, asset replacements, config changes.
|
||||||
|
|
||||||
|
### Step-by-step
|
||||||
|
|
||||||
|
**1. Write changelog**
|
||||||
|
|
||||||
|
Edit `CHANGELOG_DRAFT.md` — add player-facing notes under `## [NEXT]`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [NEXT]
|
||||||
|
- Fixed playerboard desync in multiplayer.
|
||||||
|
- Adjusted Gauntlet difficulty scaling.
|
||||||
|
```
|
||||||
|
|
||||||
|
If `[NEXT]` is missing, add the header. Format is markdown list items without leading dash (the tool strips it). Each line becomes a bullet on the patch notes page.
|
||||||
|
|
||||||
|
**2. Commit to `experimental`**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CHANGELOG_DRAFT.md
|
||||||
|
git commit -m "docs: patch notes for next release"
|
||||||
|
git push origin experimental
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Trigger patch deploy workflow**
|
||||||
|
|
||||||
|
Navigate to the Actions tab:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.klud.top/danchie/tekton/actions
|
||||||
|
```
|
||||||
|
|
||||||
|
Click **Deploy Patch** → **Run workflow**:
|
||||||
|
|
||||||
|
| Field | Example |
|
||||||
|
|---|---|
|
||||||
|
| **Patch version** | `2.4.3` |
|
||||||
|
| **Release notes** | `fix: multiplayer desync, gauntlet balance` |
|
||||||
|
|
||||||
|
OR via API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/workflows/deploy_patch.yml/dispatches" \
|
||||||
|
-H "Authorization: token $TEKTON_RELEASE_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"ref":"experimental","inputs":{"version":"2.4.3","notes":"fix: multiplayer desync, gauntlet balance"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### What the CI does (deploy_patch.yml)
|
||||||
|
|
||||||
|
1. **Checkout** — `git clone --depth 1` from `experimental` branch (shallow = fast).
|
||||||
|
2. **Setup Godot** — Uses cached `/cache/godot_4.7` binary. Downloads only if missing (140MB, cached forever).
|
||||||
|
3. **Generate version.json** — Runs `tools/generate_version_json.py --skip-changelog`. Reads version from `project.godot`, bumps patch number, writes `assets/data/version.json` with the new release entry including `pck_url` pointing to Gitea raw endpoint.
|
||||||
|
4. **Export patch PCK** — `godot --headless --export-pack "Windows Desktop" build/patch.pck`. No export templates needed — `--export-pack` only packs resources, not binaries. Output ~10-15MB.
|
||||||
|
5. **Push to patches branch** — Force-pushes `patch.pck` + `version.json` to the `patches` branch of the repo.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check manifest
|
||||||
|
curl -s "https://git.klud.top/danchie/tekton/raw/branch/patches/version.json"
|
||||||
|
# Expected: latest_version matches your patch number
|
||||||
|
|
||||||
|
# Check pck exists
|
||||||
|
curl -s -o /dev/null -w "%{http_code} %{size_download}B" \
|
||||||
|
"https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck"
|
||||||
|
# Expected: HTTP 200, size ~10-15MB
|
||||||
|
```
|
||||||
|
|
||||||
|
### How players receive patches
|
||||||
|
|
||||||
|
1. Game boots → `GameUpdateManager` fetches `version.json` from Gitea raw endpoint.
|
||||||
|
2. Compares `latest_version` against local version.
|
||||||
|
3. If remote is newer → downloads `patch.pck` to `user://patch.pck`.
|
||||||
|
4. Mounts with `ProjectSettings.load_resource_pack("user://patch.pck")`.
|
||||||
|
5. All files in `patch.pck` override base `res://` files in memory.
|
||||||
|
6. No files are overwritten on disk (safe rollback by deleting `patch.pck`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Full Binary Release (platform updates)
|
||||||
|
|
||||||
|
Use when: engine upgrade, native plugin change, export template update, platform-specific build fix, or any change that needs a new `.exe`/`.app`.
|
||||||
|
|
||||||
|
### Step-by-step
|
||||||
|
|
||||||
|
**1. Ensure changelog is written**
|
||||||
|
|
||||||
|
Same as patch step 1 — `CHANGELOG_DRAFT.md` must have `## [NEXT]` entries. The CI auto-extracts them for the release body.
|
||||||
|
|
||||||
|
**2. Commit and tag**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Commit all changes
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: bump to v2.4.3"
|
||||||
|
|
||||||
|
# Push to experimental
|
||||||
|
git push origin experimental
|
||||||
|
|
||||||
|
# Create and push tag
|
||||||
|
git tag v2.4.3 experimental
|
||||||
|
git push origin v2.4.3
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT:** Tag must match `v` + version format (e.g. `v2.4.3`). The CI is triggered by `v*` tags.
|
||||||
|
|
||||||
|
### What the CI does (ci.yml)
|
||||||
|
|
||||||
|
1. **Install tools** — `apt-get install curl unzip zip` (zip was missing in early runs — make sure it's present).
|
||||||
|
2. **Checkout** — Full clone from tag (shallow not used — needs full history for changelog extraction, though `--depth 1` works too).
|
||||||
|
3. **Setup Godot with templates** — Caches both Godot binary (140MB) and export templates (1.3GB) in `/cache/`. Templates downloaded once per runner lifetime.
|
||||||
|
4. **Export 3 platforms:**
|
||||||
|
- **Windows** — `godot --headless --export-release "Windows Desktop"` → zipped with `zip`.
|
||||||
|
- **Linux/X11** — Same pattern.
|
||||||
|
- **macOS** — Export to `.zip` directly (Godot's macOS export produces a zip).
|
||||||
|
- **Note:** Steam DLLs copied into Windows build from `addons/godotsteam/`.
|
||||||
|
- **Note:** `|| true` on export commands masks Godot errors (e.g. GodotSteam plugin warnings). Real failures (missing `zip`) will surface.
|
||||||
|
5. **Extract changelog** — Parses `CHANGELOG_DRAFT.md` for the `## [version]` section matching the tag. Writes to `$CHANGELOG_BODY` env var.
|
||||||
|
6. **Create/Update Gitea Release** — Checks if release exists for tag. Creates new one with changelog as body if missing. Updates draft release if re-run.
|
||||||
|
7. **Upload assets** — Each `.zip` uploaded as release asset via multipart POST.
|
||||||
|
8. **Publish** — Sets `draft:false` to make release public.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
Check the release page:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.klud.top/danchie/tekton/releases/tag/v2.4.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 3 assets (Windows, Linux, macOS) with correct sizes, changelog body populated, release marked as published (not draft).
|
||||||
|
|
||||||
|
### Cleaning duplicate assets
|
||||||
|
|
||||||
|
If a tag was force-pushed, multiple CI runs may upload duplicate assets to the same release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List assets
|
||||||
|
curl "https://git.klud.top/api/v1/repos/danchie/tekton/releases/tags/v2.4.3" \
|
||||||
|
-H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.assets[] | "\(.id): \(.name) \(.size/1024/1024)MiB"'
|
||||||
|
|
||||||
|
# Delete old duplicates (keep latest 3: Windows, Linux, macOS)
|
||||||
|
RELEASE_ID=<id>
|
||||||
|
curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets/$ASSET_ID" \
|
||||||
|
-H "Authorization: token $TEKTON_RELEASE_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cancelling stuck or duplicate runs
|
||||||
|
|
||||||
|
Gitea API cannot cancel in-progress runs. Wait for completion, then delete:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List runs for a tag
|
||||||
|
curl "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs?page=1&limit=10" \
|
||||||
|
-H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.workflow_runs[] | "\(.id): \(.status) \(.conclusion) \(.display_title)"'
|
||||||
|
|
||||||
|
# Delete completed run
|
||||||
|
curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs/$RUN_ID" \
|
||||||
|
-H "Authorization: token $TEKTON_RELEASE_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Agent-Automated Release
|
||||||
|
|
||||||
|
Agent (Hermes) can execute the full release flow from a single user request:
|
||||||
|
|
||||||
|
### Scenario: "Ship v2.4.3"
|
||||||
|
|
||||||
|
Agent actions:
|
||||||
|
1. Read `CHANGELOG_DRAFT.md` — verify `[NEXT]` has entries.
|
||||||
|
2. Check `project.godot` current version.
|
||||||
|
3. Commit changelog to `experimental`.
|
||||||
|
4. Create tag `v2.4.3` → push to trigger `ci.yml`.
|
||||||
|
5. Wait for CI completion (poll every 30s, up to 30 min).
|
||||||
|
6. If CI fails:
|
||||||
|
- Read job logs for failure reason.
|
||||||
|
- Fix the workflow file, commit, force-push tag.
|
||||||
|
- Clean up duplicate assets after re-run.
|
||||||
|
7. Verify release page has 3 assets with correct sizes.
|
||||||
|
8. If patch deploy also needed:
|
||||||
|
- Trigger `deploy_patch.yml` dispatch.
|
||||||
|
- Verify `patch.pck` is served and version.json updated.
|
||||||
|
|
||||||
|
### Scenario: "Quick hot patch"
|
||||||
|
|
||||||
|
Agent actions:
|
||||||
|
1. Check if `[NEXT]` has entries in `CHANGELOG_DRAFT.md`.
|
||||||
|
2. If empty, ask user for changelog notes.
|
||||||
|
3. Commit `CHANGELOG_DRAFT.md` to `experimental`.
|
||||||
|
4. Dispatch `deploy_patch.yml` workflow.
|
||||||
|
5. Verify patch files on Gitea raw endpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `zip: command not found` in CI
|
||||||
|
|
||||||
|
Root cause: `ubuntu-latest` container doesn't have `zip` pre-installed. The install step must include `zip`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Install tools
|
||||||
|
run: apt-get update -qq && apt-get install -y -qq curl unzip zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### Godot export fails silently (`|| true`)
|
||||||
|
|
||||||
|
The `|| true` on export commands means a failed Godot export still shows step as success. Check:
|
||||||
|
- Is `godot_4.7` cached at `/cache/`?
|
||||||
|
- Does the export preset name match exactly? E.g. `"Windows Desktop"` must match `export_presets.cfg`.
|
||||||
|
- Is `addons/godotsteam/libgodotsteam*` present? Missing DLLs cause Godot to exit 1.
|
||||||
|
|
||||||
|
### Runner container can't clone repo
|
||||||
|
|
||||||
|
Runner uses HTTP auth with `god` username and `TEKTON_RELEASE_TOKEN` as password. If token is revoked:
|
||||||
|
1. Generate new token from Gitea → Settings → Applications.
|
||||||
|
2. Update secret `TEKTON_RELEASE_TOKEN` in repo Settings → Actions → Secrets.
|
||||||
|
3. Restart runner: `docker compose -f /home/dev/gitea/docker-compose.yml restart runner`.
|
||||||
|
|
||||||
|
### Runner shows "permission denied" for Docker socket
|
||||||
|
|
||||||
|
User `dev` doesn't have Docker socket access. Commands that touch Docker must be run via `sudo` or by the root user on the VPS. The local agent can only:
|
||||||
|
- Restart runner via systemd: `systemctl --user restart docker-runner` (if running as user service).
|
||||||
|
- No Docker CLI commands from agent terminal.
|
||||||
|
|
||||||
|
### Release has duplicate assets
|
||||||
|
|
||||||
|
Each CI run uploads assets as new entries. To clean:
|
||||||
|
- Get release ID from API.
|
||||||
|
- Delete old asset IDs keeping only latest (highest IDs) for each platform.
|
||||||
|
- Use jq or manual curl loop (see "Cleaning duplicate assets" above).
|
||||||
|
|
||||||
|
### Tag force-push creates redundant CI runs
|
||||||
|
|
||||||
|
Each push to a tag triggers `ci.yml`. Force-pushing a tag to a new commit creates another run:
|
||||||
|
- Previous runs keep running (can't cancel via API).
|
||||||
|
- Wait for all to finish, then delete stale ones.
|
||||||
|
- The last run to publish sets the release state.
|
||||||
|
|
||||||
|
Best practice: Delete old release before force-pushing tag, or at minimum delete stale completed runs after.
|
||||||
|
|
||||||
|
### Patch manifest not updating
|
||||||
|
|
||||||
|
`generate_version_json.py --skip-changelog` only bumps version and writes `version.json`. If the version didn't change (e.g. `--skip-changelog` with no `[NEXT]` entries), the script exits with code 0 but doesn't write anything. Verify `assets/data/version.json` has the new version after CI run.
|
||||||
|
|
||||||
|
### `gitea-pages` (raw.klud.top) returns 404
|
||||||
|
|
||||||
|
gitea-pages container uses a Gitea token to read files. If the token is dead:
|
||||||
|
- Switch to Gitea native raw endpoint: `https://git.klud.top/danchie/tekton/raw/branch/patches/...`
|
||||||
|
- Update `MANIFEST_URL` in `generate_version_json.py` and `VERSION_MANIFEST_URL` in `game_update_manager.gd`.
|
||||||
|
- Retire gitea-pages container entirely (not needed, Gitea has built-in raw serving).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Reference
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `.gitea/workflows/deploy_patch.yml` | Patch deploy CI — generates pck + pushes to patches branch |
|
||||||
|
| `.gitea/workflows/ci.yml` | Full binary release CI — exports 3 platforms + creates release |
|
||||||
|
| `tools/generate_version_json.py` | Version bumping + changelog → version.json conversion |
|
||||||
|
| `CHANGELOG_DRAFT.md` | Human-readable changelog draft (source of truth for release notes) |
|
||||||
|
| `assets/data/version.json` | Machine-readable manifest served to players (auto-generated) |
|
||||||
|
| `scripts/managers/game_update_manager.gd` | Client-side update checker (reads version.json → downloads patch.pck) |
|
||||||
|
| `project.godot` | Godot project file (config/version = source of truth for version number) |
|
||||||
|
| `export_presets.cfg` | Export configuration for all platforms |
|
||||||
|
| `/home/dev/gitea/docker-compose.yml` | Runner container composition (cache volume mount: `/home/dev/godot-cache:/cache`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Gotchas
|
||||||
|
|
||||||
|
- **`zip` must be in install step** — missing zip kills Windows/Linux export. Added in run 141 — do not remove.
|
||||||
|
- **Tag format is `vX.Y.Z`** — `ci.yml` trigger is `v*`. A tag without `v` prefix won't build.
|
||||||
|
- **Force-push tag = new CI run** — Always expect a new run on force-push. Old run keeps running.
|
||||||
|
- **Changelog extracted from tag version** — `## [X.Y.Z]` section in `CHANGELOG_DRAFT.md`. If section doesn't exist, release body is empty.
|
||||||
|
- **Patch deploy skips changelog clearing** — `--skip-changelog` means `version.json` is written but `CHANGELOG_DRAFT.md` is NOT modified. Only the full `ci.yml` pipeline clears it.
|
||||||
|
- **Cache is per-runner-host, not per-run** — Godot binary (140MB) and templates (1.3GB) download once on fresh runner container, then persist via `/cache` volume. Running `docker compose down` + `up` reuses cache if volume isn't deleted.
|
||||||
|
- **`|| true` masks Godot export errors** — If export fails silently, check the `2>&1 | tail -5` output in CI logs. Error messages like "Cannot call method 'queue_free' on a null value" from GodotSteam are non-fatal (cosmetic plugin warnings).
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
# Skin Creation Workflow
|
||||||
|
|
||||||
|
<a id="top"></a>
|
||||||
|
|
||||||
|
How to author a new character skin, register it in the game's shop/gacha catalog, and ship it to players.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Each skin is defined in two places that must stay in sync:
|
||||||
|
|
||||||
|
| Layer | File | What it stores |
|
||||||
|
|---|---|---|
|
||||||
|
| **Client (visual)** | `scripts/managers/skin_manager.gd` | Mesh slots, material paths, override/overlay mode |
|
||||||
|
| **Server (shop)** | `server/nakama/lua/economy.lua` | Item ID, name, category, price (gold/star) |
|
||||||
|
|
||||||
|
The item `id` in `economy.lua` must match the dictionary key in `skin_manager.gd` `SKIN_CATALOG` exactly -- the Godot client looks up the item ID from the wallet/inventory and applies the matching skin data.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Create the Skin Material
|
||||||
|
|
||||||
|
Open the **Skin Shader Generator** at `res://scenes/tools/skin_shader_generator.tscn`.
|
||||||
|
|
||||||
|
1. Run the scene (F6).
|
||||||
|
2. Import your base albedo and mask textures (PNG with color/alpha channels).
|
||||||
|
3. Use the UI to visualize UV overlays and adjust color channels (Red, Green, Blue, Alpha).
|
||||||
|
4. Export the configured material as a `.tres` file into `assets/materials/skins/` or a subfolder:
|
||||||
|
- `assets/characters/skins/hat/`
|
||||||
|
- `assets/characters/skins/clothing/`
|
||||||
|
- `assets/characters/skins/gloves/`
|
||||||
|
|
||||||
|
**Material path conventions (Oldpop character):**
|
||||||
|
|
||||||
|
| Category | Example path |
|
||||||
|
|---|---|
|
||||||
|
| hat | `res://assets/characters/skins/hat/oldpop_mat_hat_blue.tres` |
|
||||||
|
| costume/clothing | `res://assets/characters/skins/clothing/oldpop_mat_cloth_red_pant.tres` |
|
||||||
|
| glove | `res://assets/characters/skins/gloves/oldpop_mat_gloves_blue.tres` |
|
||||||
|
| accessory | `res://assets/characters/skins/accessory/` |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2: Register the Skin in SkinManager (Client)
|
||||||
|
|
||||||
|
Open `res://scripts/managers/skin_manager.gd` and add a new entry inside `SKIN_CATALOG` (between `[BEGIN_SKIN_CATALOG]` and `[END_SKIN_CATALOG]` markers).
|
||||||
|
|
||||||
|
### Entry format
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
"item_id": {
|
||||||
|
"category": "head", # head | costume | glove | accessory
|
||||||
|
"character": "Oldpop", # node name under CharacterRoot
|
||||||
|
"slots": [
|
||||||
|
{
|
||||||
|
"mesh": "oldpop-hat1", # MeshInstance3D child name
|
||||||
|
"mode": "override", # "override" | "overlay"
|
||||||
|
"material": "res://path/to/material.tres"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slot modes
|
||||||
|
|
||||||
|
- **`override`** -- `set_surface_override_material(0, mat)`. Replaces the base material entirely. Preserves the outline shader (`next_pass`) automatically.
|
||||||
|
- **`overlay`** -- `material_overlay = mat`. Transparent layer on top of the base material. Good for costume/pant patterns.
|
||||||
|
|
||||||
|
### Multi-slot skins (costume example)
|
||||||
|
|
||||||
|
Costumes typically touch 3 meshes:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
"oldpop-grey-pant": {
|
||||||
|
"category": "costume",
|
||||||
|
"character": "Oldpop",
|
||||||
|
"slots": [
|
||||||
|
{ "mesh": "oldpop-body", "mode": "overlay", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" },
|
||||||
|
{ "mesh": "oldpop-bottom1", "mode": "override", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" },
|
||||||
|
{ "mesh": "oldpop-bottom2", "mode": "override", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
- Leave `"material"` as `""` if the `.tres` file is not ready yet. The slot is skipped gracefully.
|
||||||
|
- Use the **Skin Catalog Editor** (`res://scenes/tools/skin_catalog_editor.tscn`) to avoid manual edits. Click **Save & Generate** to rewrite both `skin_manager.gd` and `economy.lua`.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3: Register the Skin in Economy (Server)
|
||||||
|
|
||||||
|
Open `server/nakama/lua/economy.lua` and add a new entry to `SHOP_CATALOG_DEFS`.
|
||||||
|
|
||||||
|
### Catalog entry format
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{ id = "oldpop-blue-hat", name = "Oldpop Blue Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | string | Must match the `SKIN_CATALOG` key in `skin_manager.gd` exactly |
|
||||||
|
| `name` | string | Display name shown in shop |
|
||||||
|
| `category` | string | `head` / `costume` / `glove` / `accessory` |
|
||||||
|
| `gold` | number | Gold coin price (0 = not sold for gold) |
|
||||||
|
| `star` | number | Star gem price (0 = not sold for stars) |
|
||||||
|
| `rarity` | string | `"Common"` / `"Uncommon"` / `"Rare"` -- cosmetic label only |
|
||||||
|
| `character` | string | Character this skin belongs to (e.g. `"Oldpop"`) |
|
||||||
|
|
||||||
|
### Existing catalog (12 items)
|
||||||
|
|
||||||
|
```
|
||||||
|
oldpop-blue-hat head 100 gold Common Oldpop
|
||||||
|
oldpop-green-hat head 100 gold Common Oldpop
|
||||||
|
oldpop-red-hat head 100 gold Common Oldpop
|
||||||
|
oldpop-yellow-hat head 100 gold Common Oldpop
|
||||||
|
oldpop-og-pant costume 0 gold Common Oldpop (free)
|
||||||
|
oldpop-grey-pant costume 150 gold Common Oldpop
|
||||||
|
oldpop-red-pant costume 150 gold Common Oldpop
|
||||||
|
oldpop-yellow-pant costume 150 gold Common Oldpop
|
||||||
|
oldpop-blue-gloves glove 75 gold Common Oldpop
|
||||||
|
oldpop-green-gloves glove 75 gold Common Oldpop
|
||||||
|
oldpop-red-gloves glove 75 gold Common Oldpop
|
||||||
|
oldpop-yellow-gloves glove 75 gold Common Oldpop
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 (Optional): Add Skin as Gacha Prize
|
||||||
|
|
||||||
|
Gacha-only skins are registered in `server/nakama/lua/gacha.lua` inside `GACHA_DATA.real_prize_catalog`.
|
||||||
|
|
||||||
|
### Existing gacha skins (4 items)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
skin_gacha_rainbow_suit = { name = "Rainbow Suit", category = "costume", rarity = "real_prize", character = "" }
|
||||||
|
skin_gacha_dragon_hat = { name = "Dragon Hat", category = "head", rarity = "real_prize", character = "" }
|
||||||
|
skin_gacha_phantom_gloves = { name = "Phantom Gloves", category = "glove", rarity = "real_prize", character = "" }
|
||||||
|
skin_gacha_neon_acc = { name = "Neon Accessory", category = "accessory", rarity = "real_prize", character = "" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Gacha skins also need a `skin_manager.gd` entry (same step 2 format) and a `skin_catalog_editor` entry so `SkinManager.apply_loadout()` can render them. The server catalog is optional -- gacha skins are not sold in the shop directly.
|
||||||
|
|
||||||
|
The gacha pulls these IDs from `GACHA_DATA.pools.real_prize`, so add your item_id there too.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
pools = {
|
||||||
|
common = {"frag_common"},
|
||||||
|
uncommon = {"frag_uncommon"},
|
||||||
|
rare = {"frag_rare"},
|
||||||
|
real_prize = {
|
||||||
|
"skin_gacha_rainbow_suit",
|
||||||
|
"skin_gacha_dragon_hat",
|
||||||
|
"skin_gacha_phantom_gloves",
|
||||||
|
"skin_gacha_neon_acc",
|
||||||
|
-- add new skin here
|
||||||
|
}
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5: Deploy
|
||||||
|
|
||||||
|
### Hot patch (content only) -- recommended for skins
|
||||||
|
|
||||||
|
1. Commit changes to `experimental` branch:
|
||||||
|
- `scripts/managers/skin_manager.gd`
|
||||||
|
- `server/nakama/lua/economy.lua` (if shop item)
|
||||||
|
- `server/nakama/lua/gacha.lua` (if gacha prize)
|
||||||
|
- Material `.tres` files
|
||||||
|
|
||||||
|
2. Push to `experimental`.
|
||||||
|
|
||||||
|
3. Trigger `deploy_patch.yml` via Gitea UI workflow dispatch.
|
||||||
|
- CI runs `--export-pack` to build `patch.pck`.
|
||||||
|
- CI force-pushes `patch.pck` + `version.json` to `patches` branch.
|
||||||
|
- Existing players auto-download on next boot via `GameUpdateManager`.
|
||||||
|
|
||||||
|
### Full binary release (if engine/templates changed)
|
||||||
|
|
||||||
|
Tag a version (e.g. `v2.5.0`) and push. CI builds all platform binaries and uploads to the release.
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Flow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ 1. Create Material │
|
||||||
|
│ skin_shader_generator │
|
||||||
|
│ ────────────────── │
|
||||||
|
│ Export .tres file │
|
||||||
|
└────────┬─────────────┘
|
||||||
|
v
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 2. Register in SkinManager │
|
||||||
|
│ skin_manager.gd │
|
||||||
|
│ ────────────────── │
|
||||||
|
│ Add SKIN_CATALOG entry │
|
||||||
|
│ (mesh slots + material) │
|
||||||
|
└────────┬─────────────────────┘
|
||||||
|
v
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 3. Register in Economy │
|
||||||
|
│ economy.lua │
|
||||||
|
│ ────────────────── │
|
||||||
|
│ Add SHOP_CATALOG_DEFS │
|
||||||
|
│ (price, name, category) │
|
||||||
|
└────────┬─────────────────────┘
|
||||||
|
v (optional)
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 4. Gacha Prize │
|
||||||
|
│ gacha.lua │
|
||||||
|
│ ────────────────── │
|
||||||
|
│ real_prize_catalog + pools │
|
||||||
|
└────────┬─────────────────────┘
|
||||||
|
v
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ 5. Git Push & CI │
|
||||||
|
│ deploy_patch.yml │
|
||||||
|
│ ────────────────── │
|
||||||
|
│ patch.pck → players │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| Skin visible in editor but not in-game | Material path wrong or `.tres` not exported | Verify `res://` path in SKIN_CATALOG, run `--export-pack` |
|
||||||
|
| Skin purchase fails: "NotEnoughFunds" | Wallet balance insufficient | Check gold/star prices in economy.lua |
|
||||||
|
| Skin visible on all characters | `character` field wrong | Set correct character node name |
|
||||||
|
| Skin purchase fails: "ItemNotFound" | Item ID not in SHOP_CATALOG_DEFS | Add entry matching SKIN_CATALOG key |
|
||||||
|
| Player downloads patch but skin missing | econmy.lua change didn't reach server | Nakama hot-reload: restart Nakama container or wait for next restart |
|
||||||
|
| Outline shader lost on skin | next_pass not preserved | SkinManager preserves it automatically -- verify with latest `skin_manager.gd` |
|
||||||
|
|
||||||
|
[Back to top](#top)
|
||||||
Reference in New Issue
Block a user