106 Commits

Author SHA1 Message Date
god 0ab00afd37 docs: add jump-to-top links below all section headings 2026-07-05 12:58:23 +08:00
god 878ad659db fix: replace Unicode with ASCII equivalents 2026-07-05 12:54:57 +08:00
god 644f2f0f95 chore: clean stale docs, add lua server reference, update steamworks guide 2026-07-05 12:47:47 +08:00
god dcb252f412 chore: remove obsolete docs (TASKS.md, GIT_CLONE_TUTORIAL.md) 2026-07-05 00:59:32 +08:00
god bd8b81bbc1 ci: install zip for binary packaging
Release / Build & Release (push) Successful in 7m32s
2026-07-04 23:18:26 +08:00
god a2d008a480 ci: include changelog in release body
Release / Build & Release (push) Successful in 18m31s
2026-07-04 22:59:49 +08:00
god 260a448a70 docs: changelog for v2.4.3
Release / Build & Release (push) Failing after 12m16s
2026-07-04 22:51:47 +08:00
god e73421c9b1 chore: remove stale test files
Release / Build & Release (push) Failing after 18m54s
2026-07-04 22:15:32 +08:00
god 2dd3020d0b feat: use Gitea raw endpoint instead of gitea-pages for patches 2026-07-04 22:15:24 +08:00
god fbca048aba ci: no templates needed for pck export, cache godot binary
Release / Build & Release (push) Successful in 16m34s
2026-07-04 22:07:07 +08:00
god 4c6c8e1587 ci: fix apt-get update before install, guard template extraction 2026-07-04 21:56:03 +08:00
god 939d2b0ac2 ci: shallow checkout for patch deploy 2026-07-04 21:47:58 +08:00
god e845c1be25 ci: use cached godot export templates for patch deployment 2026-07-04 20:21:15 +08:00
god 1f6bdf4d06 chore: bump version to 2.4.3 for patch release 2026-07-04 20:08:26 +08:00
god 36ae0d479d docs: mark multiplayer gauntlet bug as resolved 2026-07-04 20:01:55 +08:00
god c23c5f77bc fix: correct indentation of has_method check in GauntletManager to prevent multiplayer client crash 2026-07-04 20:01:39 +08:00
god 1691d8ebc9 docs: mark currency sync bug as resolved 2026-07-04 19:56:30 +08:00
god ab93f047ab fix: keep gacha panel wallet UI synced with global wallet updates 2026-07-04 19:56:19 +08:00
god b15e1153e0 fix: multiple punch SFX spam
Fixes #74

The multiple punch SFX bug has been fixed.
Cause: The is_charged_strike state was never cleared after pushing a player, causing the push logic to re-trigger and play the sound every frame while the movement key was held.
Fix: is_charged_strike is now immediately cleared upon a successful push.
2026-07-04 19:33:35 +08:00
god e6e4c7b6f6 docs: mark playerboard desync and punch SFX bugs as resolved 2026-07-04 19:31:37 +08:00
god d524f23104 fix: prevent multiple attack/smash SFX spam by clearing charged strike state immediately 2026-07-04 19:31:26 +08:00
god 7762a82d18 fix: wait for playerboard before applying multiplayer sync 2026-07-04 19:17:27 +08:00
god 0786638a98 docs: update clone instructions for Gitea SSH and HTTPS 2026-07-04 11:38:58 +08:00
god 08a527195f patch system: use HTTPS for raw.klud.top 2026-07-04 10:48:21 +08:00
god 4a3637482c patch system: use raw.klud.top, remove GitHub raw URLs 2026-07-04 10:11:45 +08:00
god 8414f48106 patch system: gitea-pages serving, MANIFEST_URL updated 2026-07-04 10:00:57 +08:00
god 7c6f66e821 chore: upgrade Godot 4.6 -> 4.7 stable in CI and local 2026-07-04 09:20:08 +08:00
god 2643ef24fa chore: test god commit 2026-07-04 09:11:47 +08:00
adtpdn c47fadf152 ci: use god account for git checkout, update release token 2026-07-04 09:08:59 +08:00
adtpdn 4486e4d3fd fix: set macOS bundle identifier for export
Release / Build & Release (push) Successful in 6m40s
2026-07-04 08:52:19 +08:00
adtpdn 6971c27d77 ci: use GITEA_TOKEN env var, skip macOS upload if missing
Release / Build & Release (push) Successful in 24m57s
2026-07-04 01:59:48 +08:00
adtpdn 7e6e0f7bef ci: guard macOS mv if export fails
Release / Build & Release (push) Failing after 5m45s
2026-07-04 01:48:36 +08:00
adtpdn 869f670605 ci: macOS export can fail, allow continue
Release / Build & Release (push) Failing after 5m12s
2026-07-04 01:42:33 +08:00
adtpdn db061a7946 ci: fix second ref to .official dir
Release / Build & Release (push) Failing after 5m14s
2026-07-04 01:34:28 +08:00
adtpdn 20cd2d08b8 ci: fix export templates path 4.6.stable not .official
Release / Build & Release (push) Failing after 2m52s
2026-07-04 01:28:24 +08:00
adtpdn 1652630153 ci: update URLs from local to VPS
Release / Build & Release (push) Failing after 4m3s
2026-07-04 01:13:17 +08:00
adtpdn 34039db92c chore(ci): use HTTP manual clone with Gitea internal Docker network IP, ditch ssh
Release / Build & Release (push) Failing after 2m20s
2026-07-03 18:26:11 +08:00
adtpdn bf9ae51702 chore(ci): use explicit ip for gitea in ssh config
Release / Build & Release (push) Failing after 2m26s
2026-07-03 18:21:13 +08:00
adtpdn 7a02eee277 chore(ci): use manual clone because checkout action overrides ssh settings
Release / Build & Release (push) Failing after 33s
2026-07-03 18:17:21 +08:00
adtpdn d6daed62b8 chore(ci): use proper gitea act_runner network config to resolve gitea internally
Release / Build & Release (push) Failing after 1m14s
2026-07-03 18:14:05 +08:00
adtpdn 0548f54168 chore(ci): rewrite thunderobot without ts suffix down to port 22 as well
Release / Build & Release (push) Has been cancelled
2026-07-03 18:04:37 +08:00
adtpdn 3fe8de2e32 chore(ci): alias magicdns thunderobot.tail5d6e8e.ts.net and strip port 222
Release / Build & Release (push) Failing after 14m23s
2026-07-03 18:04:13 +08:00
adtpdn f40dae5a03 chore(ci): remap port 222 to port 22 via git config
Release / Build & Release (push) Failing after 7m30s
2026-07-03 18:02:08 +08:00
adtpdn ab3ffbbec8 chore(ci): alias both gitea and thunderobot directly to IP
Release / Build & Release (push) Failing after 7m27s
2026-07-03 18:01:36 +08:00
adtpdn da5c319a5b chore(ci): use internal gitea docker networking alias for checkout
Release / Build & Release (push) Failing after 1m5s
2026-07-03 17:56:10 +08:00
adtpdn 5e5d0c8ecf chore(ci): move network connect step before checkout
Release / Build & Release (push) Failing after 1m3s
2026-07-03 17:41:56 +08:00
adtpdn b5e22f3ca5 chore(ci): export to exe/x86_64 then zip, ignore gdscript warning
Release / Build & Release (push) Failing after 7s
2026-07-03 17:21:25 +08:00
adtpdn 8abf07a0d4 chore(ci): route SSH clone via docker bridge gateway instead of tailscale IP
Release / Build & Release (push) Failing after 5m1s
2026-07-03 17:13:30 +08:00
adtpdn 350ae269f2 chore(ci): bypass alias entirely and use explicit IP for clone
Release / Build & Release (push) Failing after 4m8s
2026-07-03 17:12:31 +08:00
adtpdn e8604e2c02 chore(ci): use correct tailscale IP and ssh format
Release / Build & Release (push) Failing after 4m52s
2026-07-03 17:10:39 +08:00
adtpdn c9995f8578 chore(ci): use manual git clone via ssh instead of checkout action
Release / Build & Release (push) Failing after 3s
2026-07-03 17:07:16 +08:00
adtpdn 4aa765c502 chore(ci): use printf for ssh config, drop ssh-keyscan
Release / Build & Release (push) Failing after 40s
2026-07-03 17:05:33 +08:00
adtpdn 1d653bb7d0 chore(ci): use ssh config alias for thunderobot host
Release / Build & Release (push) Failing after 9s
2026-07-03 17:02:28 +08:00
adtpdn 4fe0378d1c chore(ci): fix ssh-keyscan using direct IP
Release / Build & Release (push) Failing after 3s
2026-07-03 17:01:16 +08:00
adtpdn fb58e62fd9 chore(ci): fix ssh-keyscan host resolution
Release / Build & Release (push) Failing after 8s
2026-07-03 17:00:48 +08:00
adtpdn aa45bb0afd chore(ci): use cached templates, build to zip, export windows/linux/macos with prefix
Release / Build & Release (push) Failing after 28s
2026-07-03 16:50:02 +08:00
adtpdn d05ebdff05 chore(ci): docker network connect gitea_default for API access
Release / Build & Release (push) Failing after 54s
2026-07-03 16:00:35 +08:00
adtpdn e984c1f8b5 chore(ci): use git.klud.top API (internet-reachable)
Release / Build & Release (push) Failing after 13m49s
2026-07-03 15:44:30 +08:00
adtpdn 99d38134b8 chore(ci): host network, localhost API
Release / Build & Release (push) Failing after 4m10s
2026-07-03 15:37:43 +08:00
adtpdn 66bc1658a4 chore(ci): use Tailscale IP for API, not gitea hostname
Release / Build & Release (push) Failing after 4m11s
2026-07-03 15:21:56 +08:00
adtpdn 3cb8a606b5 chore(ci): use gitea:3000 internal API, fix upload URLs
Release / Build & Release (push) Failing after 4m17s
2026-07-03 15:07:22 +08:00
adtpdn 19e7f619ab feat(gauntlet): replace Cleanser with Ghost powerup sticky bypass (v2.4.2)
- Remove entire Cleanser system (signal, vars, HUD, input, RPCs, bot AI)
- Ghost (Invisible Mode) now bypasses sticky tiles in Gauntlet
- Grant Ghost powerup every 2 missions instead of Cleanser charges
- Ghost tiles spawn naturally on Gauntlet arena (15% chance)
- Bots use Ghost powerup when boxed in by sticky tiles
- Players pushed into sticky while Ghost are not slowed
- Remove use_cleanser input action from project.godot
- Remove CleanserHBox UI from gauntlet_hud.tscn
- Bump version to 2.4.2
2026-07-03 14:55:03 +08:00
adtpdn 5ba7de3fd6 chore(ci): single job, no upload-artifact (not supported)
Release / Build & Release (push) Failing after 4m14s
2026-07-03 14:51:57 +08:00
adtpdn a89e54783f chore(ci): cp from /cache with wget fallback, fix release tokens
Release / Export Linux (push) Failing after 7m20s
Release / Export Windows (push) Failing after 7m25s
Release / Create Release (push) Has been skipped
2026-07-03 14:38:26 +08:00
adtpdn 376be28366 fix(ci): switch autoload refs from uid:// to res:// paths
Nakama, Satori, and SettingsManager used uid:// references in
project.godot which fail in CI headless export (UID cache not
built). Switch to res:// file paths to fix infinite reimport loop.
2026-07-03 13:52:04 +08:00
adtpdn 0415875128 chore: remove unused workflows
Release / Export Windows (push) Failing after 40s
Release / Export Linux (push) Failing after 42s
Release / Create Release (push) Has been skipped
2026-07-03 13:31:19 +08:00
adtpdn 03028413ca chore(ci): only tag release + patch, no testing, cp from /cache
Test Suite / Unit Tests (GUT) (push) Successful in 46s
Release / Export Windows (push) Failing after 45s
Release / Export Linux (push) Failing after 16s
Release / Create Release (push) Has been skipped
Upload PCK to Gitea Release / upload (push) Failing after 47s
Test Suite / Security Scan (push) Failing after 13m23s
Test Suite / Code Style Check (push) Failing after 13m41s
Test Suite / Integration Tests (push) Failing after 14m26s
Build and Upload Binaries / build (push) Failing after 17m44s
2026-07-03 13:25:27 +08:00
adtpdn 340ba13a48 chore(ci): prefer shared host cache /home/dev/godot-cache, fallback to download
CI / Export Linux (push) Failing after 8m9s
CI / Export Windows (push) Failing after 8m11s
Test Suite / Unit Tests (GUT) (push) Successful in 59s
Test Suite / Code Style Check (push) Failing after 39s
Test Suite / Integration Tests (push) Failing after 4m18s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 5m35s
2026-07-03 12:34:08 +08:00
adtpdn 04f7fdd2dd chore(ci): use LAN mirror 192.168.0.114:9009 instead of docker bridge 172.17.0.1
CI / Create Release (push) Has been cancelled
CI / Export Windows (push) Has been cancelled
CI / Export Linux (push) Has been cancelled
Test Suite / Code Style Check (push) Has been cancelled
Test Suite / Security Scan (push) Has been cancelled
Test Suite / Unit Tests (GUT) (push) Has been cancelled
Test Suite / Integration Tests (push) Has been cancelled
2026-07-03 12:19:45 +08:00
adtpdn edce1fa1fa chore(ci): use local godot mirror on 172.17.0.1:9009
CI / Create Release (push) Has been cancelled
CI / Export Windows (push) Has been cancelled
CI / Export Linux (push) Has been cancelled
Test Suite / Integration Tests (push) Has been cancelled
Test Suite / Code Style Check (push) Has been cancelled
Test Suite / Security Scan (push) Has been cancelled
Test Suite / Unit Tests (GUT) (push) Has been cancelled
2026-07-03 12:03:35 +08:00
adtpdn 6ac9acea35 chore(ci): fix templates path nesting and replace failing android setup action with sdkmanager
CI / Export Linux (push) Failing after 6m37s
CI / Export Windows (push) Failing after 6m45s
Test Suite / Unit Tests (GUT) (push) Successful in 44s
CI / Export Android (push) Failing after 5m11s
Test Suite / Code Style Check (push) Failing after 24s
Test Suite / Integration Tests (push) Failing after 5m34s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 5m40s
2026-07-03 11:18:03 +08:00
adtpdn 9ddb1dd199 chore(ci): fix export templates install and android cmdline version
CI / Export Windows (push) Failing after 5m23s
CI / Export Linux (push) Failing after 5m3s
Test Suite / Unit Tests (GUT) (push) Successful in 51s
CI / Export Android (push) Failing after 3m20s
Test Suite / Code Style Check (push) Failing after 34s
Test Suite / Integration Tests (push) Failing after 4m1s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 5m37s
2026-07-03 11:03:14 +08:00
adtpdn 74de7f15ef chore: trigger CI verify SSH secret
CI / Export Linux (push) Failing after 4s
CI / Export Android (push) Failing after 5m29s
Test Suite / Unit Tests (GUT) (push) Successful in 41s
CI / Export Windows (push) Failing after 6m47s
Test Suite / Code Style Check (push) Failing after 40s
Test Suite / Integration Tests (push) Failing after 4m5s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 5m33s
2026-07-03 10:52:12 +08:00
adtpdn d40a242cbc feat(gauntlet): shrink arena per phase 20x20 -> 18x18 -> 7x7; sticky cells block movement
CI / Export Linux (push) Failing after 4s
CI / Export Windows (push) Failing after 36s
CI / Export Android (push) Failing after 38s
Test Suite / Unit Tests (GUT) (push) Failing after 26s
Test Suite / Integration Tests (push) Failing after 29s
Test Suite / Code Style Check (push) Failing after 38s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 51s
2026-07-02 18:11:14 +08:00
GMaysa 419771c50b ci: add diagnostic step for git remote connectivity in CI workflow
CI / Export Linux (push) Failing after 4s
CI / Export Android (push) Failing after 31s
CI / Export Windows (push) Failing after 43s
Test Suite / Unit Tests (GUT) (push) Failing after 33s
Test Suite / Integration Tests (push) Failing after 30s
Test Suite / Security Scan (push) Failing after 36s
Test Suite / Code Style Check (push) Failing after 41s
CI / Create Release (push) Has been skipped
2026-07-02 18:10:48 +08:00
GMaysa e7a0717aff ci: update SSH configuration for Gitea checkout step to improve security and fetch full history
CI / Export Windows (push) Failing after 38s
CI / Export Linux (push) Failing after 38s
CI / Export Android (push) Failing after 39s
Test Suite / Unit Tests (GUT) (push) Failing after 40s
Test Suite / Code Style Check (push) Failing after 32s
Test Suite / Integration Tests (push) Failing after 36s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 37s
2026-07-02 18:07:34 +08:00
GMaysa 754fb0d0a3 ci: enable step debugging for Windows export job
CI / Export Linux (push) Failing after 33s
CI / Export Windows (push) Failing after 41s
Test Suite / Unit Tests (GUT) (push) Failing after 33s
CI / Export Android (push) Failing after 41s
Test Suite / Code Style Check (push) Failing after 27s
Test Suite / Integration Tests (push) Failing after 32s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 39s
2026-07-02 17:58:39 +08:00
GMaysa 2fd8130743 ci: fix formatting of checkout step in CI workflow
CI / Export Linux (push) Failing after 39s
CI / Export Windows (push) Failing after 47s
Test Suite / Unit Tests (GUT) (push) Failing after 33s
CI / Export Android (push) Failing after 42s
Test Suite / Integration Tests (push) Failing after 31s
Test Suite / Code Style Check (push) Failing after 34s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 39s
2026-07-02 17:55:24 +08:00
GMaysa 5ad9952750 ci: update checkout step to fetch full history in CI workflow
Test Suite / Integration Tests (push) Failing after 28s
Test Suite / Unit Tests (GUT) (push) Failing after 43s
Test Suite / Code Style Check (push) Failing after 27s
Test Suite / Security Scan (push) Failing after 36s
2026-07-02 17:53:27 +08:00
GMaysa 720882f0dc ci: add network connectivity tests for Gitea in deploy_patch.yml
CI / Export Linux (push) Failing after 38s
CI / Export Windows (push) Failing after 39s
Test Suite / Unit Tests (GUT) (push) Failing after 30s
CI / Export Android (push) Failing after 32s
Test Suite / Integration Tests (push) Failing after 32s
Test Suite / Code Style Check (push) Failing after 37s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 41s
2026-07-02 17:42:51 +08:00
GMaysa 456ca94786 ci: update SSH configuration in deploy_patch.yml to use user-specific config and improve security
CI / Export Linux (push) Failing after 30s
CI / Export Windows (push) Failing after 36s
Test Suite / Unit Tests (GUT) (push) Failing after 33s
CI / Export Android (push) Failing after 37s
Test Suite / Code Style Check (push) Failing after 37s
Test Suite / Integration Tests (push) Failing after 40s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 40s
2026-07-02 17:39:38 +08:00
GMaysa 78f8c5bf5e ci: update SSH config in deploy_patch.yml to use correct host for Gitea
CI / Export Linux (push) Failing after 32s
CI / Export Windows (push) Failing after 43s
CI / Export Android (push) Failing after 37s
Test Suite / Unit Tests (GUT) (push) Failing after 30s
Test Suite / Integration Tests (push) Failing after 29s
Test Suite / Code Style Check (push) Failing after 38s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 39s
2026-07-02 17:31:16 +08:00
GMaysa 58b66bcb8f ci: update SSH config setup to append instead of using sudo tee for Gitea
CI / Export Windows (push) Failing after 36s
CI / Export Linux (push) Failing after 36s
CI / Export Android (push) Failing after 30s
Test Suite / Unit Tests (GUT) (push) Failing after 32s
Test Suite / Integration Tests (push) Failing after 29s
Test Suite / Code Style Check (push) Failing after 29s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 35s
2026-07-02 17:23:04 +08:00
GMaysa 477c7ca7ec ci: update SSH configuration setup for Gitea to use system-wide ssh_config
CI / Export Windows (push) Failing after 4s
CI / Export Linux (push) Failing after 3s
CI / Export Android (push) Failing after 5s
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 13s
2026-07-02 17:19:58 +08:00
GMaysa ecb080e943 ci: add SSH configuration setup for Gitea in workflows
CI / Export Linux (push) Failing after 28s
CI / Export Windows (push) Failing after 38s
CI / Export Android (push) Failing after 36s
Test Suite / Unit Tests (GUT) (push) Failing after 32s
Test Suite / Integration Tests (push) Failing after 33s
Test Suite / Code Style Check (push) Failing after 32s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 13m25s
2026-07-02 16:53:31 +08:00
GMaysa e539a862d5 Clean the yml script
CI / Export Windows (push) Failing after 43s
CI / Export Linux (push) Failing after 1m4s
CI / Export Android (push) Failing after 55s
Test Suite / Unit Tests (GUT) (push) Failing after 39s
Test Suite / Integration Tests (push) Failing after 46s
Test Suite / Code Style Check (push) Failing after 46s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 1m1s
2026-07-02 16:40:52 +08:00
adtpdn 82763e4d5a ci: fix ssh clone url in build_binaries and upload_pck
CI / Run Tests (push) Failing after 35s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 30s
Test Suite / Integration Tests (push) Failing after 32s
CI / Create Release (push) Has been skipped
Test Suite / Security Scan (push) Failing after 11m17s
Test Suite / Code Style Check (push) Failing after 11m50s
2026-07-02 16:12:46 +08:00
adtpdn c30888b74c ci: enforce specific IP and Port 222 for gitea ssh route
CI / Run Tests (push) Has been cancelled
CI / Export Windows (push) Has been cancelled
CI / Export Linux (push) Has been cancelled
CI / Export Android (push) Has been cancelled
CI / Create Release (push) Has been cancelled
Test Suite / Unit Tests (GUT) (push) Has been cancelled
Test Suite / Integration Tests (push) Has been cancelled
Test Suite / Code Style Check (push) Has been cancelled
Test Suite / Security Scan (push) Has been cancelled
2026-07-02 16:08:36 +08:00
adtpdn 858bf08212 ci: map internal gitea hostname to external ip before checkout
CI / Export Windows (push) Has been cancelled
CI / Export Linux (push) Has been cancelled
CI / Export Android (push) Has been cancelled
CI / Create Release (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
Test Suite / Unit Tests (GUT) (push) Has been cancelled
Test Suite / Integration Tests (push) Has been cancelled
Test Suite / Code Style Check (push) Has been cancelled
Test Suite / Security Scan (push) Has been cancelled
2026-07-02 16:01:13 +08:00
adtpdn 64630662ac ci: replace missing godot action with manual wget download
CI / Run Tests (push) Failing after 45s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 47s
Test Suite / Integration Tests (push) Failing after 37s
Test Suite / Code Style Check (push) Failing after 40s
Test Suite / Security Scan (push) Failing after 54s
CI / Create Release (push) Has been skipped
2026-07-02 15:47:55 +08:00
adtpdn 187b530cbf ci: enforce ssh checkout on all gitea workflows
Adds ssh-key to actions/checkout configurations and fixes yaml indentation in upload_pck.yml
2026-07-02 15:40:37 +08:00
adtpdn 6da55003e2 Fix checkout to use SSH
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 3s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 3s
Test Suite / Security Scan (push) Failing after 1m51s
CI / Create Release (push) Has been skipped
2026-07-02 15:04:07 +08:00
adtpdn 598065f255 feat: test workflows
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 43s
CI / Create Release (push) Has been skipped
Upload PCK to Gitea Release / upload (push) Failing after 1s
2026-07-02 10:33:50 +08:00
adtpdn c471afaee4 Merge branch 'experimental' of ssh://100.79.174.108:222/danchie/tekton into experimental
CI / Run Tests (push) Failing after 2s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 58s
CI / Create Release (push) Has been skipped
2026-07-02 09:20:29 +08:00
adtpdn 0228fcfd92 update .gitignore 2026-07-02 09:15:45 +08:00
adtpdn 8fc3c1b915 Fix README breadcrumb/wiki links to absolute URLs
CI / Run Tests (push) Failing after 2s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 52s
CI / Create Release (push) Has been skipped
2026-07-02 01:58:18 +08:00
adtpdn 74c1e86c32 Use absolute wiki URLs in README
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 51s
CI / Create Release (push) Has been skipped
2026-07-02 01:56:46 +08:00
adtpdn 19f10a4486 Remove wiki folder from main repo; wiki pages live in tekton.wiki.git
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 53s
CI / Create Release (push) Has been skipped
2026-07-02 01:54:18 +08:00
adtpdn 8255b1f465 Fix README wiki links to relative paths
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 1m0s
CI / Create Release (push) Has been skipped
2026-07-02 01:50:54 +08:00
adtpdn 625ff5ec69 Replace SSH tutorial in README with wiki pages by OS
CI / Run Tests (push) Failing after 2s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 3s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 1m1s
CI / Create Release (push) Has been skipped
Linux, macOS, Windows SSH setup guides moved to wiki. README becomes index with wiki links and breadcrumb to existing docs.
2026-07-02 01:35:51 +08:00
adtpdn ccd759da45 Add SSH clone tutorial for tailnet users via README
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 2s
Test Suite / Security Scan (push) Failing after 58s
CI / Create Release (push) Has been skipped
2026-07-02 01:18:54 +08:00
adtpdn a708da8836 feat: update CI/CD for .gitea
CI / Run Tests (push) Failing after 3s
CI / Export Windows (push) Has been skipped
CI / Export Linux (push) Has been skipped
CI / Export Android (push) Has been skipped
Test Suite / Unit Tests (GUT) (push) Failing after 2s
Test Suite / Integration Tests (push) Failing after 2s
Test Suite / Code Style Check (push) Failing after 3s
Test Suite / Security Scan (push) Failing after 12m56s
CI / Create Release (push) Has been skipped
2026-07-01 18:33:27 +08:00
adtpdn d2156c6d1a feat: edit CI/CD 2026-07-01 18:33:08 +08:00
adtpdn 5b34f8b96f docs: move developer docs to wiki; README is now a pointer 2026-07-01 15:47:02 +08:00
adtpdn b4ce7453c3 feat: cleanup mess 2026-07-01 15:07:00 +08:00
adtpdn 594a0ce84d Fix gauntlet_manager indentation and scope 2026-07-01 11:28:07 +08:00
77 changed files with 2659 additions and 7284 deletions
+154
View File
@@ -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"
+68
View File
@@ -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
-128
View File
@@ -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"
-177
View File
@@ -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'
-130
View File
@@ -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'
-59
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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.
+25 -129
View File
@@ -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 ```bash
Once your material is ready, you need to update the game's catalog and deploy the changes to the Nakama server. git clone git@ssh.git.klud.top:danchie/tekton.git
#### 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
nano ~/tekton_admin.js
# Or use micro (recommended): micro ~/tekton_admin.js
```
Paste the copied contents and save the file.
- **Find your Nakama Container ID:**
It is highly recommended to use **lazydocker** to manage containers.
*(To install on Ubuntu: `curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash`)*
Open lazydocker or run `docker ps` to find the Container ID for your Nakama server.
- **Copy the file into the Nakama container:**
```bash
# Replace ed21ac5d442a with your actual Container ID
docker cp ~/tekton_admin.js ed21ac5d442a:/nakama/data/modules/tekton_admin.js
```
- **Restart the container** (via lazydocker or `docker restart <Container ID>`) for Nakama to load the new modules. Live game clients will fetch this new catalog automatically upon booting.
#### 🎨 Skin Creation & Deployment Flow
```mermaid
flowchart TD
%% Creation Phase
subgraph phase1 [Skin Material Creation]
A[Albedo & Mask Textures] --> B{Skin Shader Generator}
B -->|Export| C[material.tres]
end
%% Catalog Phase
subgraph phase2 [Catalog Definition]
C --> D{Skin Catalog Editor}
D -->|Save & Generate| E[tekton_admin.js]
D -->|Save & Generate| F[skin_manager.gd]
end
%% Deployment Phase (Dual Path)
subgraph phase3 ["Shop Backend (VPS)"]
E -->|SSH & nano| G[VPS: ~/tekton_admin.js]
G -->|docker cp| H[Nakama Container]
H -->|Restart| I[Live Shop Logic Sync]
end
subgraph phase4 ["Asset Delivery (CI/CD)"]
C --> J[Git Push]
F --> J
J -->|GitHub Actions| K[patch.pck]
K -->|Automatic Download| L[Player Client Assets Sync]
end
``` ```
### Pushing a New Version (Automated Patching) HTTPS also works:
When you're ready to deploy new features or assets to players:
- Document your changes in `CHANGELOG_DRAFT.md` using player-friendly language.
- Run the version generation script from the terminal:
```bash
python generate_version_json.py --bump patch
```
*(Use `--bump minor` or `--bump major` for larger updates.)*
- Commit and push your changes to the `main` branch on GitHub:
```bash
git add .
git commit -m "Release version X.Y.Z"
git push origin main
```
- The **GitHub Actions Workflow** (`deploy_patch.yml`) will automatically detect the push, build the patch manifest (`version.json`), and deploy it to the public `gh-pages` branch.
- Live game clients will detect the new version on boot, download the updated files, and apply the patch seamlessly.
### Local Testing & Understanding the Patch System ```bash
When a player (or you) downloads an in-game patch, Godot downloads a `patch.pck` file to the system's `user://` directory. git clone https://git.klud.top/danchie/tekton.git
- **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
``` ```
--- First-time SSH setup:
## 🚀 Ongoing Features (Incoming) - [Linux](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Linux)
- [macOS](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-macOS)
- [Windows](https://git.klud.top/danchie/tekton/wiki/SSH-Setup-Windows)
### 🎰 Gacha System Backend Editor SSH test:
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. ```bash
- **Dynamic Banners:** Will support updating specific slots on the gacha banner dynamically. ssh -T git@ssh.git.klud.top
- **Seasonal Rotations:** Will introduce automated scheduling so banners rotate based on active seasons and automatically remove themselves when the season ends. ```
Expected output:
```text
Hi there, <yourname>! You've successfully authenticated with the key named <key-title>, but Gitea does not provide shell access.
```
-174
View File
@@ -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)
+20
View File
@@ -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)
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
+28 -1
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
-157
View File
@@ -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
-1366
View File
@@ -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> (060s), <strong>Route Pressure</strong> (60120s), <strong>Survival Endgame</strong> (120180s). 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 1718 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>
-793
View File
@@ -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 68 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 |
-1209
View File
@@ -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>
-89
View File
@@ -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**
-2191
View File
@@ -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 (&ge; 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 &times; (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
View File
@@ -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
-6
View File
@@ -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)
-6
View File
@@ -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)
-68
View File
@@ -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
-48
View File
@@ -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()
-24
View File
@@ -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)
-8
View File
@@ -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)
-25
View File
@@ -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)
-37
View File
@@ -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
View File
@@ -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)
-19
View File
@@ -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
+23 -5
View File
@@ -1686,11 +1686,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 +1713,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:
+19 -22
View File
@@ -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 %sburning Cleanser" % [actor.name, here]) print("[BotController] %s stepped onto sticky at %sactivating Ghost" % [actor.name, here])
return _try_activate_cleanser() return _try_activate_ghost()
return false return false
# ============================================================================= # =============================================================================
+16 -23
View File
@@ -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
+36
View File
@@ -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()
+1 -1
View File
@@ -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"
+93 -279
View File
@@ -7,7 +7,7 @@ class_name 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
# ============================================================================= # =============================================================================
+14 -28
View File
@@ -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:
+4 -2
View File
@@ -547,9 +547,11 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request) # 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:
+4
View File
@@ -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")
+1963
View File
@@ -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).
+1 -1
View File
@@ -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")