From 114748a54fbc658e47a62507ec935d0aba237c86 Mon Sep 17 00:00:00 2001 From: god Date: Mon, 6 Jul 2026 00:18:59 +0800 Subject: [PATCH] experimental: remove Tekton Doors entirely - Delete portal_mode_manager.gd, portal_door.gd, portal_door.tscn - Strip all Tekton Doors logic from main.gd, player.gd, lobby.gd, lobby_room.gd, lobby_manager.gd, camera_context_manager.gd, music_manager.gd, tekton.gd, enhanced_gridmap.gd, playerboard_manager.gd, special_tiles_manager.gd - Remove TK enum (TEKTON_DOORS=2), mode_config schema, arena area - Update tests: 3 modes instead of 4 - Strip HowToPlay tab from main.tscn --- addons/enhanced_gridmap/enhanced_gridmap.gd | 3 +- docs/STEAMWORKS_SETUP.md | 535 ++-- scenes/lobby.gd | 7 +- scenes/main.gd | 184 +- scenes/main.tscn | 19 - scenes/player.gd | 9 +- scenes/portal_door.tscn | 92 - scenes/ui/lobby_room.gd | 20 +- scripts/game_mode.gd | 18 +- scripts/lint.gd.uid | 1 + scripts/managers/camera_context_manager.gd | 3 - scripts/managers/gauntlet_manager.gd | 2 +- scripts/managers/goal_manager.gd | 4 +- scripts/managers/lobby_manager.gd | 54 - scripts/managers/music_manager.gd | 2 +- scripts/managers/playerboard_manager.gd | 17 +- scripts/managers/portal_mode_manager.gd | 585 ---- scripts/managers/special_tiles_manager.gd | 6 +- scripts/mode_config.gd | 6 - scripts/portal_door.gd | 136 - scripts/tekton.gd | 4 +- test_mp.gd | 4 + test_mp.gd.uid | 1 + test_ssh.sh | 5 + tests/test_gauntlet_registration.gd | 13 +- wiki/Architecture-Client.md | 2707 +++++++++++++++++++ wiki/architecture_server.md | 392 +++ wiki/game-modes.md | 604 +++++ wiki/home.md | 16 + wiki/patch_release_workflow.md | 322 +++ wiki/skin_creation_workflow.md | 257 ++ 31 files changed, 4493 insertions(+), 1535 deletions(-) delete mode 100644 scenes/portal_door.tscn create mode 100644 scripts/lint.gd.uid delete mode 100644 scripts/managers/portal_mode_manager.gd delete mode 100644 scripts/portal_door.gd create mode 100644 test_mp.gd create mode 100644 test_mp.gd.uid create mode 100755 test_ssh.sh create mode 100644 wiki/Architecture-Client.md create mode 100644 wiki/architecture_server.md create mode 100644 wiki/game-modes.md create mode 100644 wiki/home.md create mode 100644 wiki/patch_release_workflow.md create mode 100644 wiki/skin_creation_workflow.md diff --git a/addons/enhanced_gridmap/enhanced_gridmap.gd b/addons/enhanced_gridmap/enhanced_gridmap.gd index b273821..b68efa3 100644 --- a/addons/enhanced_gridmap/enhanced_gridmap.gd +++ b/addons/enhanced_gridmap/enhanced_gridmap.gd @@ -55,8 +55,7 @@ func _ready(): # Safety check: Don't auto-randomize if game mode manages its own arena if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \ and get_node_or_null("/root/LobbyManager") \ - and (get_node("/root/LobbyManager").game_mode == "Stop n Go" \ - or get_node("/root/LobbyManager").game_mode == "Tekton Doors")): + and (get_node("/root/LobbyManager").game_mode == "Stop n Go")): randomize_grid() validate_item_indices() diff --git a/docs/STEAMWORKS_SETUP.md b/docs/STEAMWORKS_SETUP.md index 4d2cfc7..6d1cc85 100644 --- a/docs/STEAMWORKS_SETUP.md +++ b/docs/STEAMWORKS_SETUP.md @@ -1,422 +1,205 @@ # Steamworks Setup Guide for Tekton Armageddon -This guide explains how to set up Steamworks for Windows, Mac, and Linux builds while using Nakama for mobile platforms (Android/iOS) for leaderboards, achievements, and shop functionality. +> **Current status: Steam builds are not active.** All platforms (Windows, Linux, macOS, Android) use Nakama as their sole backend. GodotSteam GDExtension exists in `addons/godotsteam/` but is **not enabled** in the project. This doc explains the current Nakama-only architecture and how to re-enable Steam if needed in the future. -## Overview +--- -- **Desktop (Windows/Mac/Linux)**: Single build that detects Steam at runtime - - If launched through Steam: Steam login available for Nakama registration; all features use Nakama - - If launched standalone: Uses Nakama for all features -- **Mobile (Android/iOS)**: Uses Nakama for all backend services -- **Unified Backend**: All platforms use Nakama for achievements, leaderboards, and shop -- **Steam Integration**: Steam is only used for authentication (auth session ticket for Nakama login) +## Current Architecture (Nakama-Only) -## Prerequisites - -### Steamworks Setup - -1. **Steam Partner Account** - - You need a Steam Partner account to access Steamworks - - Apply at: https://partner.steamgames.com/ - -2. **Steam App ID** - - Create a new app in Steamworks - - Note your App ID (e.g., 480) - - Update `steam_app_id` in `scripts/services/steamworks_manager.gd` - -3. **Steamworks SDK** - - Download the Steamworks SDK from Steamworks - - The GodotSteam plugin includes the SDK, but you may need it for reference - -## Installation Steps - -### 1. Install GodotSteam GDExtension - -**Option A: Via Godot Asset Library (Recommended)** -1. Open Godot Editor -2. Go to `Project > Asset Library` -3. Search for "GodotSteam GDExtension 4.4+" -4. Download and install the plugin -5. Restart Godot Editor - -**Option B: Manual Installation** -1. Download from: https://codeberg.org/godotsteam/godotsteam/releases -2. Download version 4.18.1 (compatible with Godot 4.6.2) -3. Extract to `addons/godotsteam/` in your project -4. Enable the plugin in `Project > Project Settings > Plugins` - -### 2. Configure Steamworks in Project - -1. **Enable the Plugin** - - Go to `Project > Project Settings > Plugins` - - Enable "GodotSteam" - -2. **Set Steam App ID** - - Edit `scripts/services/steamworks_manager.gd` - - Change `steam_app_id` to your Steam App ID: - ```gdscript - var steam_app_id: int = YOUR_APP_ID_HERE - ``` - -3. **Add BackendService as Autoload** - - Go to `Project > Project Settings > Autoload` - - Add `BackendService` with path: `res://scripts/services/backend_service.gd` - - Enable it as a singleton - -### 3. Configure Steamworks Features - -#### Achievements - -1. **Define Achievements in Steamworks** - - Go to Steamworks > Your App > Achievements - - Create achievements with API names (e.g., "first_win", "level_10") - - Set display names, descriptions, and icons - -2. **Use in Code** - ```gdscript - # Unlock an achievement - BackendService.unlock_achievement("first_win") - - # Set progress (for progress-based achievements) - BackendService.set_achievement_progress("kill_100_enemies", current_kills, 100) - - # Check achievement status - var progress = BackendService.get_achievement_progress("first_win") - ``` - -#### Leaderboards - -1. **Define Leaderboards in Steamworks** - - Go to Steamworks > Your App > Leaderboards - - Create leaderboards with API names (e.g., "high_score", "fastest_time") - - Set sort order (ascending/descending) and display type - -2. **Use in Code** - ```gdscript - # Submit a score - BackendService.submit_leaderboard_score("high_score", 1000) - - # Get leaderboard entries - BackendService.leaderboard_entries_loaded.connect(_on_leaderboard_loaded) - BackendService.get_leaderboard_entries("high_score", 1, 10) - - func _on_leaderboard_loaded(leaderboard_id: String, entries: Array): - for entry in entries: - print("Player: %s, Score: %d" % [entry.player_name, entry.score]) - ``` - -#### Shop (Steam Inventory) - -**Note**: Steam shop functionality requires additional setup with Steam Inventory Service or Steam Microtransactions. This is a complex feature that requires: - -1. **Steam Inventory Service Setup** - - Define items in Steamworks > Your App > Inventory - - Set item types, prices, and properties - - Implement purchase callbacks in `steamworks_manager.gd` - -2. **Alternative**: Use external payment processor for desktop and sync with Nakama - -### 4. Export Presets - -The project includes export presets for all platforms: - -#### Desktop Builds (single build for Steam and standalone) -- **Windows Desktop** (preset.0) → `build/tekton_armageddon_v2.1.7.exe` -- **macOS** (preset.2) → `build/tekton_armageddon_v2.1.7.zip` -- **Linux/X11** (preset.3) → `build/tekton_armageddon_v2.1.7.x86_64` - -#### Mobile Builds -- **Android** (preset.1) → `build/tekton-dash-armageddon-v.2.1.5.apk` - -**Note**: Desktop builds are universal - the same executable works on both Steam and standalone. The game detects whether it's running through Steam at runtime and switches backends accordingly. - -#### Configure macOS Export - -1. **Code Signing** (for distribution) - - Get an Apple Developer certificate - - Update `codesign/identity` in export preset - - Set `codesign/enable` to `true` - -2. **Architecture** - - Currently set to "universal" (Intel + Apple Silicon) - - Can be changed to "x86_64" or "arm64" if needed - -#### Configure Linux Export - -1. **Architecture** - - Currently set to "x86_64" - - Add ARM64 preset if needed for Linux ARM devices - -### 5. Platform Detection - -The `BackendService` automatically detects the platform and backend: - -```gdscript -# Detection logic in BackendService -if OS.has_feature("android") or OS.has_feature("ios"): - # Mobile → Nakama -elif OS.has_feature("steam"): - # Desktop → Steamworks -else: - # Desktop → Local storage (non-Steam builds) +``` +┌──────────────────────────────────────────────────┐ +│ Game Client │ +│ ┌──────────────────────────────────────────┐ │ +│ │ BackendService (autoload) │ │ +│ │ ┌─────────────┐ ┌──────────────────┐ │ │ +│ │ │ NakamaManager│ │steamworks_manager│ │ │ +│ │ │ (active) │ │ (dormant) │ │ │ +│ │ └─────────────┘ └──────────────────┘ │ │ +│ └──────────────────────────────────────────┘ │ +└──────────────────────┬───────────────────────────┘ + │ + ┌────────▼────────┐ + │ Nakama Server │ + │ (RPC backend) │ + │ achievements │ + │ leaderboards │ + │ shop/economy │ + │ auth │ + │ multiplayer │ + └─────────────────┘ ``` -You can check the current platform: +- **All backends use Nakama** — achievements, leaderboards, shop, authentication, multiplayer. +- **Platform detection** (`BackendService._detect_platform()`) distinguishes: + - `MOBILE_NAKAMA` — Android/iOS (`OS.has_feature("android")` or `OS.has_feature("ios")`) + - `DESKTOP_NAKAMA` — Windows/Linux/macOS (default when GodotSteam not loaded) + - `DESKTOP_STEAM` — only activated if `ClassDB.class_exists("Steam")` at runtime +- **GodotSteam GDExtension** is present in `addons/godotsteam/` but **not enabled** in Project Settings > Plugins. It is shipped in the CI export (Windows only, `|| true` on copy failure) but does not execute. +- **No Steam App ID configured.** The `steam_app_id` in `steamworks_manager.gd` defaults to `480` (generic test ID) via `ProjectSettings.get_setting("steam/initialization/app_id", 480)`. +- **No Steam Partner account, no Steamworks app, no Steam build pipelines** are active. + +--- + +## Export Presets + +The project has 4 export presets in `export_presets.cfg`: + +| Preset | Name | Platform | Export Path | +|--------|------|----------|-------------| +| preset.0 | Windows Desktop | Windows | `build/windows/tekton_armageddon_v2.4.3.exe` | +| preset.1 | Android | Android | `build/tekton-dash-armageddon-v.2.4.3.apk` | +| preset.2 | macOS | macOS | `build/tekton_armageddon_v2.4.3.zip` | +| preset.3 | Linux/X11 | Linux | `build/linux/tekton_armageddon_v2.4.3.x86_64` | + +No separate Steam vs. Non-Steam presets. All presets produce the same Nakama-only build. + +### CI Export Flow (`.gitea/workflows/ci.yml`) + +On tag push (`v*`), the CI pipeline: + +1. **Checkout** code at tag +2. **Setup Godot 4.7** (cached) +3. **Export Windows** → `build/windows/tekton_armageddon_windows.exe` (copies `libgodotsteam*` DLLs if present, failure ignored with `|| true`) +4. **Export Linux** → `build/linux/tekton_armageddon_linux.x86_64` +5. **Export macOS** → `build/macos/tekton_armageddon_macos.zip` +6. **Extract changelog** from `CHANGELOG_DRAFT.md` +7. **Create Gitea release** and upload all 3 platform zips +8. **Publish** release (draft → published) + +The CI does **not** upload to Steam. All builds are distributed via Gitea releases (https://git.klud.top/danchie/tekton/releases). + +--- + +## Platform Detection + +The `BackendService` autoload (`scripts/services/backend_service.gd`) detects platform at startup: + +```gdscript +func _detect_platform() -> void: + if OS.has_feature("android") or OS.has_feature("ios"): + current_platform = Platform.MOBILE_NAKAMA + else: + if ClassDB.class_exists("Steam"): + current_platform = Platform.DESKTOP_STEAM + else: + current_platform = Platform.DESKTOP_NAKAMA +``` + +Since GodotSteam is not enabled, desktop builds always hit `DESKTOP_NAKAMA`. The `steamworks_manager.gd` is only loaded when `DESKTOP_STEAM` is selected. + +Check current platform at runtime: ```gdscript print("Platform: %s" % BackendService.get_platform_name()) print("Initialized: %s" % BackendService.is_initialized()) ``` -#### Platform Types +### Platform Types -- **DESKTOP_STEAM**: Running through Steam client (Steam login available, all features use Nakama) -- **DESKTOP_NAKAMA**: Desktop build not running through Steam (uses Nakama) -- **MOBILE_NAKAMA**: Android/iOS (uses Nakama) +| Value | Trigger | Backend | +|-------|---------|---------| +| `DESKTOP_NAKAMA` | Desktop build, GodotSteam not loaded | Nakama only | +| `DESKTOP_STEAM` | Desktop build, GodotSteam loaded + Steam running | Nakama + Steam auth | +| `MOBILE_NAKAMA` | Android/iOS feature detected | Nakama only | -#### Runtime Detection +--- -The game automatically detects the launch method: -- If `OS.has_feature("steam")` is true → Steam login available, all features use Nakama -- Otherwise → All features use Nakama +## Local Testing -This means the same desktop build can be: -- Uploaded to Steam (Steam login enabled, all data stored in Nakama) -- Distributed standalone (all data stored in Nakama) +### Nakama Backend (All Platforms) -### Steam Login for Nakama +1. Ensure Nakama server is running locally (see `server/docker-compose.yaml`) +2. Launch the game from Godot Editor (press F5) +3. Log in with email/password or device ID +4. Test achievements, leaderboards, shop via game UI +5. Check console for `"BackendService: Initialized Nakama backend"` -When running through Steam, players can use their Steam account to register or log in to Nakama. This is the **only** Steam integration - all game features (achievements, leaderboards, shop) use Nakama. - -**How it works:** -1. Player clicks "Sign in with Steam" button on login screen -2. Game retrieves Steam auth session ticket via Steamworks -3. Auth ticket is sent to Nakama for authentication -4. Nakama validates the ticket with Steam backend -5. If valid, player is logged in/registered to Nakama -6. Player's Steam account is linked to their Nakama account - -**Benefits:** -- No password needed for Steam users -- Automatic account creation on first login -- Seamless cross-platform progression (all data in Nakama) -- Steam username is used as display name -- Unified backend across all platforms - -**Requirements:** -- Nakama server must be configured with Steam API key -- Steamworks must be initialized (game launched through Steam) -- GodotSteam plugin must support `getAuthSessionTicket()` - -**Configuration:** -Set your Steam API key in Nakama server configuration: -```yaml -nakama: - social: - steam: - api_key: "your_steam_api_key" -``` - -## Nakama Integration for Mobile - -### Current Setup - -Your project already has Nakama integrated via `addons/com.heroiclabs.nakama/` and `NakamaManager` autoload. - -### Connecting to BackendService - -The `BackendService` will automatically use Nakama on mobile. You need to implement Nakama-specific methods in `NakamaManager`: +### Force-Specific Platform for Testing +Temporarily override platform in `BackendService._detect_platform()`: ```gdscript -# In NakamaManager.gd, add these signals: -signal achievement_unlocked(achievement_id: String) -signal leaderboard_score_submitted(leaderboard_id: String, score: int, success: bool) - -# Implement achievement methods -func unlock_achievement(achievement_id: String): - # Use Nakama's achievement system - var achievement = await client.write_storage_object_async( - session, - NakamaWriteStorageObject.new( - "achievements", - achievement_id, - {"unlocked": true, "timestamp": Time.get_unix_time_from_system()} - ) - ) - achievement_unlocked.emit(achievement_id) - -# Implement leaderboard methods -func submit_leaderboard_score(leaderboard_id: String, score: int): - # Use Nakama's leaderboard system - var result = await client.write_leaderboard_record_async( - session, - leaderboard_id, - score - ) - leaderboard_score_submitted.emit(leaderboard_id, score, result != null) +func _detect_platform() -> void: + current_platform = Platform.DESKTOP_NAKAMA # Force desktop Nakama mode + # ... or ... + current_platform = Platform.MOBILE_NAKAMA # Force mobile mode ``` -## Testing +### Export and Run Standalone -### Testing Steam Builds +```bash +# Windows +godot --headless --export-release "Windows Desktop" build/windows/standalone.exe +./build/windows/standalone.exe -1. **Export Steam Build** - - Use the "Windows Desktop (Steam)" preset (preset.0) - - Export to `build/steam/tekton_armageddon_v2.1.7.exe` +# Linux +godot --headless --export-release "Linux/X11" build/linux/standalone.x86_64 +./build/linux/standalone.x86_64 -2. **Upload to Steam** - - Upload the exported build to Steamworks - - Set as the default build for your app +# macOS +godot --headless --export-release "macOS" build/macos/standalone.zip +``` -3. **Run through Steam** - - Launch the game via Steam (not directly) - - Steam must be running - - Check console for "SteamworksManager: Steam initialized successfully" +--- -4. **Test Achievements** - - Call `BackendService.unlock_achievement("test_achievement")` - - Check Steam overlay (Shift+Tab) to see achievement unlock +## Activating Steam Support (Future) -5. **Test Leaderboards** - - Submit scores via `BackendService.submit_leaderboard_score()` - - View in Steamworks backend or Steam overlay +If Steam distribution is needed later, these are the steps: -### Testing Non-Steam Builds +1. **Get a Steam Partner account** at https://partner.steamgames.com/ +2. **Create Steam app** and get an App ID +3. **Enable GodotSteam plugin** in Project > Project Settings > Plugins +4. **Set App ID** in Project Settings > steam > initialization > app_id (or in `steamworks_manager.gd`) +5. **Configure Nakama** to accept Steam auth tickets (set Steam API key in Nakama config) +6. **Add a Steam-specific export preset** or modify existing presets to include Steam SDK redistributables +7. **Update CI pipeline** to upload builds to Steamworks SDK pipelines +8. **Test Steam auth flow**: launch through Steam, verify `"SteamworksManager: Steam initialized"`, verify Steam login → Nakama registration -1. **Export Non-Steam Build** - - Use the "Windows Desktop (Non-Steam)" preset (preset.1) - - Export to `build/standalone/tekton_armageddon_v2.1.7.exe` +### What the Dormant Steam Code Does -2. **Run Directly** - - Run the executable directly (not through Steam) - - Check console for "BackendService: Initialized Nakama backend" +The existing `steamworks_manager.gd` provides: -3. **Test Leaderboards** - - Ensure Nakama server is accessible - - Open the leaderboard panel to fetch rankings - - Submit scores via `UserProfileManager.submit_to_leaderboard()` - - Leaderboards are global (same as mobile) +- `get_auth_session_ticket()` — retrieves Steam auth session ticket for Nakama login +- `get_steam_user_name()` — returns Steam persona name +- `get_steam_user_id()` — returns Steam ID -4. **Test Shop** - - Ensure Nakama server is accessible - - Open the shop panel to fetch catalog - - Purchase items via `UserProfileManager.purchase_item()` - - Shop functionality works the same as mobile +All game features (achievements, leaderboards, shop) are already implemented purely via Nakama RPCs in `BackendService.api_rpc_async()`. Steam would only be used for **authentication** — the rest stays on Nakama. -### Testing Nakama (Mobile) - -1. **Run on Mobile Device** - - Export to Android/iOS using the Android preset (preset.2) - - The game will automatically use Nakama backend - - Check logs for "BackendService: Initialized Nakama backend" - -2. **Test in Editor** - - To test Nakama in editor, temporarily modify `_detect_platform()`: - ```gdscript - func _detect_platform() -> void: - current_platform = Platform.MOBILE_NAKAMA # Force Nakama - ``` - -## Troubleshooting - -### Steamworks Not Initializing - -**Problem**: "SteamworksManager: Failed to initialize Steam" - -**Solutions**: -1. Ensure game is launched through Steam (not directly) -2. Check Steam is running -3. Verify `steam_app_id` is correct -4. Check GodotSteam plugin is enabled in Project Settings -5. Restart Godot Editor after installing plugin - -### Achievements Not Unlocking - -**Problem**: Achievements don't appear in Steam overlay - -**Solutions**: -1. Ensure achievement API names match Steamworks configuration -2. Check `steam.storeStats()` is called after setting achievements -3. Verify achievement is published in Steamworks (not in draft) -4. Test with Steam overlay open (Shift+Tab) - -### Leaderboards Not Working - -**Problem**: Leaderboard scores not submitting - -**Solutions**: -1. Ensure leaderboard exists in Steamworks -2. Check leaderboard API name matches -3. Verify leaderboard is published -4. Check console for error messages - -### Platform Detection Issues - -**Problem**: Wrong backend being used - -**Solutions**: -1. Check OS features: `print(OS.get_supported_features())` -2. Manually override platform in `_detect_platform()` for testing -3. Ensure `BackendService` is added as autoload +--- ## File Structure ``` -scripts/services/ -├── backend_service.gd # Unified interface (autoload) -└── steamworks_manager.gd # Steamworks implementation +scripts/ +├── backend_service.gd # Unified interface (autoload) — Nakama-only default +├── steamworks_manager.gd # Steamworks implementation (dormant, loads only if GodotSteam present) +└── nakama_manager.gd # NakamaManager autoload (active) -export_presets.cfg # Export presets for all platforms +addons/godotsteam/ # GodotSteam GDExtension (installed but not enabled) +addons/com.heroiclabs.nakama/ # Nakama GDSDK (active) + +export_presets.cfg # Export presets — all Nakama-only, no Steam presets +.gitea/workflows/ci.yml # CI pipeline — exports Win/Linux/macOS to Gitea releases docs/STEAMWORKS_SETUP.md # This documentation ``` +--- + +## Key Differences from Previous State + +| Aspect | Before | Now | +|--------|--------|-----| +| Steam builds | Active, separate Steam/Non-Steam presets | Dormant, not shipped | +| Backend | Nakama + Steam | Nakama only | +| Export presets | 6+ (Steam + Non-Steam variants) | 4 (all Nakama-only) | +| Desktop platform detection | Steam if `OS.has_feature("steam")` | Nakama unless GodotSteam loaded | +| CI | Uploaded to Steamworks | Gitea releases only | +| Steam App ID | Configured | Default 480 (test), not set | + +--- + ## Additional Resources -- **GodotSteam Documentation**: https://godotsteam.com/ +- **Nakama Documentation**: https://heroiclabs.com/docs/nakama/ +- **GodotSteam Documentation**: https://godotsteam.com/ (for future reference) - **GodotSteam GitHub**: https://codeberg.org/godotsteam/godotsteam - **Steamworks Documentation**: https://partner.steamgames.com/doc/home -- **Nakama Documentation**: https://heroiclabs.com/docs/nakama/ - -## Next Steps - -1. Complete Steam Partner account setup -2. Create Steam app and get App ID -3. Install GodotSteam plugin -4. Configure achievements and leaderboards in Steamworks -5. Implement Nakama methods in `NakamaManager` for mobile -6. Test on all target platforms -7. Set up code signing for macOS distribution -8. Configure Steam Inventory Service if using in-game shop - -## Notes - -- **Steam builds** only work when launched through Steam client -- **Non-Steam builds** use Nakama for leaderboards and shop (same as mobile) -- **Shop functionality** is available on both Steam (via Steam Inventory) and non-Steam (via Nakama) -- **Non-Steam builds** sync to Nakama server, not Steam -- **Nakama** is already integrated for multiplayer, leaderboards, and shop -- Export presets are organized in separate folders: `build/steam/` and `build/standalone/` - -## Build Workflow - -### For Steam Distribution - -1. Export using Steam presets (preset.0, preset.3, preset.5) -2. Upload builds to Steamworks -3. Configure achievements and leaderboards in Steamworks backend -4. Set build as default in Steamworks -5. Shop uses Steam Inventory Service (requires additional setup) - -### For Standalone Distribution (itch.io, GOG, etc.) - -1. Export using Non-Steam presets (preset.1, preset.4, preset.6) -2. Distribute the standalone executables -3. Ensure Nakama server is accessible to players -4. Players get global leaderboards and shop via Nakama -5. No Steam integration required - -### For Mobile Stores - -1. Export using Android preset (preset.2) -2. Upload to Google Play / App Store -3. Ensure Nakama server is configured and accessible -4. Leaderboards and shop work the same as non-Steam desktop +- **CI Workflow**: `.gitea/workflows/ci.yml` diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 4d93f69..5ec7f1d 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -86,12 +86,7 @@ var sng_go_option: OptionButton var sng_stop_option: OptionButton var sng_goals_option: OptionButton -var doors_settings_container: HBoxContainer -var doors_swap_option: OptionButton -var doors_refresh_option: OptionButton -var doors_goals_option: OptionButton - -# UI References - Player Slots +# Gauntlet settings @onready var players_container = $LobbyPanel/PlayersContainer @onready var players_container2 = $LobbyPanel/PlayersContainer2 @onready var player_slots: Array[Control] = [] diff --git a/scenes/main.gd b/scenes/main.gd index 3738604..9e918c7 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -12,10 +12,8 @@ var touch_controls var camera_context_manager var stop_n_go_manager var stop_n_go_winner_id: int = -1 # Track who finished first in Stop n Go mode -var portal_mode_winner_id: int = -1 var is_match_ended: bool = false var obstacle_manager -var portal_mode_manager var gauntlet_manager var vfx_manager @@ -146,8 +144,6 @@ func _apply_arena_background(): texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" # Fallback texture _instantiate_3d_arena("res://scenes/arena/freemode.tscn") _hide_ground_tiles() - "Tekton Doors Arena": - texture_path = "res://assets/graphics/level_bg/placeholder_tekton_doors.jpg" "Gauntlet Arena": texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" _instantiate_3d_arena("res://scenes/arena/gauntlet.tscn") @@ -207,14 +203,6 @@ func _setup_effect_elevation(): em.mesh_library = ml print("[Main] MeshLibrary elevation applied: Wall(4) and Freeze(5) at Y=0.8") -@rpc("any_peer", "call_local", "reliable") -func sync_portal_configs(configs: Array): - if portal_mode_manager: - # Temporarily store the configs and trigger spawn - # Note: We use a custom property in manager to pass this - portal_mode_manager.set_meta("door_configs", configs) - portal_mode_manager._spawn_portal_doors() - # Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors var em = $EnhancedGridMap if em: @@ -245,12 +233,6 @@ func _init_managers(): add_child(stop_n_go_manager) # No direct initialize() yet, but we'll call start_game_mode later - # Portal manager for Tekton Doors mode - if LobbyManager.game_mode == "Tekton Doors": - portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new() - portal_mode_manager.name = "PortalModeManager" - add_child(portal_mode_manager) - portal_mode_manager.initialize(self , $EnhancedGridMap) # Gauntlet manager for Candy Pump Survival mode if LobbyManager.game_mode == "Candy Pump Survival": @@ -619,8 +601,6 @@ func _setup_host_game(): # Spawning and arena setup if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager: stop_n_go_manager._setup_arena() - elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: - portal_mode_manager.setup_arena_locally() elif LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager: gauntlet_manager._setup_arena() else: @@ -725,10 +705,6 @@ func _setup_client_game(): add_player_character(i, true) print("Client: Pre-spawned potential bot ", i) - # Initialize arena locally for Tekton Doors - if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: - portal_mode_manager.setup_arena_locally() - # Initialize arena locally for Candy Pump Survival if LobbyManager.game_mode == "Candy Pump Survival" and gauntlet_manager: gauntlet_manager._apply_arena_setup() @@ -833,14 +809,10 @@ func _start_game(): gauntlet_manager.setup_mission_tiles() # Spawn Static Tektons and random tiles BEFORE countdown (Free Mode Only) - # Exclude for Stop n Go and Tekton Doors - if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors" and LobbyManager.game_mode != "Candy Pump Survival": + # Exclude for Stop n Go and Candy Pump Survival + if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Candy Pump Survival": spawn_static_tektons() - # Tekton Doors: Randomize connections BEFORE countdown so colors show - if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: - portal_mode_manager._randomize_connections() - # STOP N GO: Rotate players to face East BEFORE countdown if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager: stop_n_go_manager.rotate_players_to_start() @@ -866,13 +838,6 @@ func _start_game(): if goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration), false) # No cycles for Stop n Go - elif LobbyManager.game_mode == "Tekton Doors": - if portal_mode_manager: - portal_mode_manager.start_game_mode() - - if goals_cycle_manager: - var match_duration = LobbyManager.get_match_duration() - goals_cycle_manager.start_match(float(match_duration)) elif LobbyManager.game_mode == "Candy Pump Survival": if gauntlet_manager: gauntlet_manager.start_game_mode() @@ -986,12 +951,6 @@ func _assign_random_spawn_positions(): _assign_stop_n_go_spawn_positions(all_players) return - # Tekton Doors Custom Spawn Logic - if LobbyManager.game_mode == "Tekton Doors": - var all_players = get_tree().get_nodes_in_group("Players") - _assign_portal_mode_spawn_positions(all_players) - return - # If static positions were not calculated yet, do it now to avoid players spawning in them if reserved_static_positions.is_empty() and LobbyManager.game_mode != "Stop n Go": if not static_tekton_manager: @@ -1116,77 +1075,6 @@ func _assign_stop_n_go_spawn_positions(all_players: Array): print("[StopNGo] Assigned spawn %s to player %s" % [assigned_pos, player.name]) -func _assign_portal_mode_spawn_positions(all_players: Array): - """Assigns spawns to different quadrants for Tekton Doors mode, avoiding stands and intersections.""" - if not portal_mode_manager: - _assign_random_spawn_positions() # Fallback - return - - # Sort players for deterministic assignment - all_players.sort_custom(func(a, b): return a.name.to_int() < b.name.to_int()) - - # Get baseline quadrant centers (3,3), (10,3), etc. - var base_spawn_points = portal_mode_manager.get_spawn_points() - var spawn_index = 0 - var assigned_positions: Array[Vector2i] = [] - - for player in all_players: - var center_pos = base_spawn_points[spawn_index % base_spawn_points.size()] - var assigned_pos = center_pos # Fallback position - - # Spiral search for a valid spot (walkable, not in stand zone, not occupied) - var found = false - for radius in range(0, 5): # Increase search radius - for dx in range(-radius, radius + 1): - for dz in range(-radius, radius + 1): - # Only check the "ring" at the current radius - if abs(dx) != radius and abs(dz) != radius and radius > 0: - continue - - var test_pos = center_pos + Vector2i(dx, dz) - - # 1. Check map bounds - var em = $EnhancedGridMap - if not em or test_pos.x < 0 or test_pos.x >= em.columns or test_pos.y < 0 or test_pos.y >= em.rows: - continue - - # 2. Check if walkable floor (Floor 0, ID 0) - if em.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) != 0: - continue - - # 3. Check if reserved for a Static Tekton Stand (3x3 area, use 2-tile buffer) - var is_reserved = false - for reserved in reserved_static_positions: - if abs(test_pos.x - reserved.x) <= 2 and abs(test_pos.y - reserved.y) <= 2: - is_reserved = true - break - if is_reserved: - continue - - # 4. Check if occupied by another already-assigned player - if assigned_positions.has(test_pos): - continue - - assigned_pos = test_pos - found = true - break - if found: break - if found: break - - assigned_positions.append(assigned_pos) - - # Sync and place - player.position = player.grid_to_world(assigned_pos) - player.current_position = assigned_pos - player.is_player_moving = false - player.spawn_point_selected = true - - if can_rpc(): - player.rpc("set_spawn_position", assigned_pos) - - spawn_index += 1 - print("[PortalMode] Assigned Quadrant Pos %s to player %s" % [assigned_pos, player.name]) - # ============================================================================= # Tekton NPC Management # ============================================================================= @@ -1639,9 +1527,6 @@ func sync_game_start(player_list: Array, is_turn_based: bool): stop_n_go_manager.name = "StopNGoManager" add_child(stop_n_go_manager) stop_n_go_manager.activate_client_side() - elif LobbyManager.game_mode == "Tekton Doors": - if portal_mode_manager: - portal_mode_manager.activate_client_side() # Initialize leaderboard for all peers (after a delay to ensure players loaded) call_deferred("_deferred_init_leaderboard") @@ -1878,7 +1763,7 @@ func randomize_item_at_position(grid_position: Vector2i): if is_ground: var get_mode_specific_tile = func(): - if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors" and LobbyManager.game_mode != "Candy Pump Survival": + if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Candy Pump Survival": # 60% Chance for Common (7-10), 40% for PowerUp if randf() <= 0.6: return [7, 8, 9, 10].pick_random() @@ -1923,14 +1808,7 @@ func sync_grid_item(x: int, y: int, z: int, item: int): # OR Layer 1 is already a wall (4) if f0 in [4, -1] or f1 == 4: return - - # TEKTON DOORS: Prevent placing items on portal doors - if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): - var doors = get_tree().get_nodes_in_group("PortalDoors") - for door in doors: - var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position)) - if door_grid.x == x and door_grid.z == z: - return + enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item) # Force visual update @@ -2098,17 +1976,6 @@ func sync_grid_items_batch(data: Array): if f0 in [4, -1] or f1 == 4: continue - # TEKTON DOORS: Prevent placing items on portal doors - if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS) and y == 1: - var doors = get_tree().get_nodes_in_group("PortalDoors") - var on_door = false - for door in doors: - var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position)) - if door_grid.x == x and door_grid.z == z: - on_door = true - break - if on_door: continue - enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item) # Force visual update ONCE after batch @@ -2116,7 +1983,7 @@ func sync_grid_items_batch(data: Array): enhanced_gridmap.update_grid_data() func randomize_game_grid(): - if LobbyManager.game_mode == "Stop n Go" or LobbyManager.game_mode == "Tekton Doors": + if LobbyManager.game_mode == "Stop n Go": return # These modes manage their own arena setup and item spawning var enhanced_gridmap = $EnhancedGridMap @@ -2177,10 +2044,6 @@ func request_full_grid_sync(): if sender_id in multiplayer.get_peers(): rpc_id(sender_id, "sync_full_grid_data", grid_data) print("[Main] Server: Sent grid sync rpc_id to %d" % sender_id) - - # If Tekton Doors, sync portal connections too - if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: - portal_mode_manager.sync_to_client(sender_id) @rpc("authority", "call_local", "reliable") func sync_full_grid_data(data: PackedInt32Array): @@ -2197,13 +2060,6 @@ func sync_full_grid_data(data: PackedInt32Array): stop_n_go_manager.name = "StopNGoManager" add_child(stop_n_go_manager) stop_n_go_manager._apply_arena_setup() - elif LobbyManager.game_mode == "Tekton Doors": - if not portal_mode_manager: - portal_mode_manager = load("res://scripts/managers/portal_mode_manager.gd").new() - portal_mode_manager.name = "PortalModeManager" - add_child(portal_mode_manager) - portal_mode_manager.initialize(self , enhanced_gridmap) - portal_mode_manager.setup_arena_locally() else: # Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes) for x in range(enhanced_gridmap.columns): @@ -2337,27 +2193,6 @@ func sync_game_end_stop_n_go(winner_id: int): # Trigger match end _on_match_ended() -@rpc("any_peer", "call_local", "reliable") -func sync_game_end_portal_mode(winner_id: int): - print("[TEKTON DOORS] Game ended! Winner: ", winner_id) - portal_mode_winner_id = winner_id - - var winner_name = "Player " + str(winner_id) - var player_node = get_node_or_null(str(winner_id)) - if player_node: - winner_name = player_node.display_name - - # Broadcast win - add_message_to_bar("MATCH COMPLETE", winner_name + " Wins with 8 Missions!", MessageType.GOAL) - - # Stop logic - if portal_mode_manager: - if portal_mode_manager.swap_timer: portal_mode_manager.swap_timer.stop() - if portal_mode_manager.tile_refresh_timer: portal_mode_manager.tile_refresh_timer.stop() - - # Trigger match end - _on_match_ended() - func _on_match_ended(): """Called when the global match timer ends - show game over screen.""" if is_match_ended: @@ -2422,9 +2257,6 @@ func _show_game_over_panel(): if stop_n_go_manager and stop_n_go_manager.hud_layer: stop_n_go_manager.hud_layer.hide() - if portal_mode_manager and portal_mode_manager.hud_layer: - portal_mode_manager.hud_layer.hide() - # ========================================================================= # Gather + sort player data # ========================================================================= @@ -2446,12 +2278,6 @@ func _show_game_over_panel(): if b.peer_id == stop_n_go_winner_id: return false return a.score > b.score ) - elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_winner_id != -1: - all_player_scores.sort_custom(func(a, b): - if a.peer_id == portal_mode_winner_id: return true - if b.peer_id == portal_mode_winner_id: return false - return a.score > b.score - ) else: all_player_scores.sort_custom(func(a, b): return a.score > b.score) diff --git a/scenes/main.tscn b/scenes/main.tscn index ac5fed4..a61416e 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -2249,25 +2249,6 @@ text = "[b]Stop n Go[/b] - Your objective is to reach the mission tiles at the far end of the arena and safely carry them back to your starting zone. - The first player to complete 8 missions and reach the finish floor wins." -[node name="Tekton Doors" type="MarginContainer" parent="HowToPlayPanel/Panel/VBox/TabContainer" unique_id=123456799] -visible = false -layout_mode = 2 -theme_override_constants/margin_left = 10 -theme_override_constants/margin_top = 10 -theme_override_constants/margin_right = 10 -theme_override_constants/margin_bottom = 10 -metadata/_tab_index = 2 - -[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Tekton Doors" unique_id=123456800] -layout_mode = 2 -bbcode_enabled = true -text = "[b]Tekton Doors[/b] - -- Navigate a sprawling arena connected by color-coded portal doors. -- Grab tiles and match goal patterns to earn mission completions. -- Use doors to quickly teleport across rooms, but watch out for closures and traps. -- The first player to complete 8 missions and reach the finish room wins." - [node name="Controls" type="MarginContainer" parent="HowToPlayPanel/Panel/VBox/TabContainer" unique_id=123456805] visible = false layout_mode = 2 diff --git a/scenes/player.gd b/scenes/player.gd index 1fd9e74..203fc38 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -1767,14 +1767,7 @@ func start_movement_along_path(path: Array, clear_visual: bool = true, force: bo if sng_manager and sng_manager.has_method("check_win_condition"): if sng_manager.check_win_condition(name.to_int(), current_position): sng_main.rpc("sync_game_end_stop_n_go", name.to_int()) - - # Tekton Doors Win Check - elif LobbyManager.game_mode == "Tekton Doors": - var main_node = get_tree().root.get_node_or_null("Main") - if main_node and main_node.portal_mode_manager: - if main_node.portal_mode_manager.check_win_condition(name.to_int(), current_position): - main_node.rpc("sync_game_end_portal_mode", name.to_int()) - + # FORCE SNAP: Update target visual position to the perfect grid center # This ensures that when interpolation resumes (in _process), it pulls to the correct spot target_visual_position = grid_to_world(current_position) diff --git a/scenes/portal_door.tscn b/scenes/portal_door.tscn deleted file mode 100644 index 064c101..0000000 --- a/scenes/portal_door.tscn +++ /dev/null @@ -1,92 +0,0 @@ -[gd_scene load_steps=8 format=3 uid="uid://portal_door_001"] - -[ext_resource type="Script" path="res://scripts/portal_door.gd" id="1_script"] - -[sub_resource type="BoxMesh" id="BoxMesh_frame"] -size = Vector3(0.15, 2.2, 0.15) - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_frame"] -albedo_color = Color(0.1, 0.5, 0.8, 1) -metallic = 0.8 -roughness = 0.2 - -[sub_resource type="PlaneMesh" id="PlaneMesh_ground"] -size = Vector2(1.0, 1.0) - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"] -transparency = 1 -albedo_color = Color(1, 1, 1, 0.4) -emission_enabled = true -emission = Color(1, 1, 1, 1) -emission_energy_multiplier = 1.0 - -[sub_resource type="PlaneMesh" id="PlaneMesh_vortex"] -size = Vector2(1.4, 2.1) - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vortex"] -transparency = 1 -albedo_color = Color(0.0, 0.6, 1.0, 0.4) -emission_enabled = true -emission = Color(0.0, 0.4, 1.0, 1) -emission_energy_multiplier = 5.0 - -[sub_resource type="BoxShape3D" id="BoxShape3D_trigger"] -size = Vector3(1.4, 2.1, 0.8) - -[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_portal"] -properties/0/path = NodePath(":target_room_id") -properties/0/spawn = true -properties/0/replication_mode = 2 -properties/1/path = NodePath(":target_door_id") -properties/1/spawn = true -properties/1/replication_mode = 2 -properties/2/path = NodePath(":is_active") -properties/2/spawn = true -properties/2/replication_mode = 2 -properties/3/path = NodePath(":portal_color") -properties/3/spawn = true -properties/3/replication_mode = 2 - -[node name="PortalDoor" type="StaticBody3D"] -script = ExtResource("1_script") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0) -shape = SubResource("BoxShape3D_trigger") - - -[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] -replication_config = SubResource("SceneReplicationConfig_portal") - -[node name="Frame_Left" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.75, 1.1, 0) -mesh = SubResource("BoxMesh_frame") -surface_material_override/0 = SubResource("StandardMaterial3D_frame") - -[node name="Frame_Right" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.75, 1.1, 0) -mesh = SubResource("BoxMesh_frame") -surface_material_override/0 = SubResource("StandardMaterial3D_frame") - -[node name="Frame_Top" type="MeshInstance3D" parent="."] -transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 2.2, 0) -mesh = SubResource("BoxMesh_frame") -surface_material_override/0 = SubResource("StandardMaterial3D_frame") - -[node name="Vortex" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 1.1, 0) -mesh = SubResource("PlaneMesh_vortex") -surface_material_override/0 = SubResource("StandardMaterial3D_vortex") - -[node name="GroundIndicator" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0.4) -mesh = SubResource("PlaneMesh_ground") -surface_material_override/0 = SubResource("StandardMaterial3D_ground") - -[node name="Area3D" type="Area3D" parent="."] -collision_layer = 0 -collision_mask = 2 - -[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D"] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0) -shape = SubResource("BoxShape3D_trigger") diff --git a/scenes/ui/lobby_room.gd b/scenes/ui/lobby_room.gd index 25497fb..aee3aa0 100644 --- a/scenes/ui/lobby_room.gd +++ b/scenes/ui/lobby_room.gd @@ -51,9 +51,6 @@ func _init(p_lobby: Control): LobbyManager.sng_go_duration_changed.connect(_on_sng_update) LobbyManager.sng_stop_duration_changed.connect(_on_sng_update) LobbyManager.sng_required_goals_changed.connect(_on_sng_update) - LobbyManager.doors_swap_time_changed.connect(_on_doors_update) - LobbyManager.doors_refresh_time_changed.connect(_on_doors_update) - LobbyManager.doors_required_goals_changed.connect(_on_doors_update) FriendManager.lobby_invite_received.connect(_on_lobby_invite_received) @@ -136,21 +133,8 @@ func _on_sng_update(_val: int = 0) -> void: if go_idx != -1: lobby.sng_go_option.selected = go_idx var stop_idx = [3, 4, 5].find(LobbyManager.sng_stop_duration) if stop_idx != -1: lobby.sng_stop_option.selected = stop_idx - var goals_idx = [5, 8, 12].find(LobbyManager.sng_required_goals) - if goals_idx != -1: lobby.sng_goals_option.selected = goals_idx - -func _on_doors_update(_val: int = 0) -> void: - if not lobby.doors_swap_option: return - var swap_idx = [10, 15, 30].find(LobbyManager.doors_swap_time) - if swap_idx != -1: lobby.doors_swap_option.selected = swap_idx - var refresh_idx = [15, 25, 40].find(LobbyManager.doors_refresh_time) - if refresh_idx != -1: lobby.doors_refresh_option.selected = refresh_idx - var goals_idx = [5, 8, 12].find(LobbyManager.doors_required_goals) - if goals_idx != -1: lobby.doors_goals_option.selected = goals_idx - -# ============================================================================= -# LobbyManager Signal Handlers -# ============================================================================= + var goals_idx2 = [5, 8, 12].find(LobbyManager.sng_required_goals) + if goals_idx2 != -1: lobby.sng_goals_option.selected = goals_idx2 func _on_room_joined(room_data: Dictionary) -> void: lobby._show_panel("lobby") diff --git a/scripts/game_mode.gd b/scripts/game_mode.gd index 7525039..8c2c931 100644 --- a/scripts/game_mode.gd +++ b/scripts/game_mode.gd @@ -1,12 +1,7 @@ extends RefCounted class_name GameMode -enum Mode { - FREEMODE = 0, - STOP_N_GO = 1, - TEKTON_DOORS = 2, - GAUNTLET = 3 -} +enum Mode { FREEMODE = 0, STOP_N_GO = 1, GAUNTLET = 2 } static func from_string(mode: String) -> Mode: match mode: @@ -14,12 +9,9 @@ static func from_string(mode: String) -> Mode: return Mode.FREEMODE "Stop n Go": return Mode.STOP_N_GO - "Tekton Doors": - return Mode.TEKTON_DOORS "Candy Pump Survival": return Mode.GAUNTLET - _: - return Mode.FREEMODE + return Mode.FREEMODE static func mode_to_string(mode: Mode) -> String: match mode: @@ -27,15 +19,13 @@ static func mode_to_string(mode: Mode) -> String: return "Freemode" Mode.STOP_N_GO: return "Stop n Go" - Mode.TEKTON_DOORS: - return "Tekton Doors" Mode.GAUNTLET: return "Candy Pump Survival" _: return "Freemode" static func is_restricted(mode: Mode) -> bool: - return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET + return mode == Mode.STOP_N_GO or mode == Mode.GAUNTLET static func get_all_modes() -> Array[String]: - return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival"] + return ["Freemode", "Stop n Go", "Candy Pump Survival"] diff --git a/scripts/lint.gd.uid b/scripts/lint.gd.uid new file mode 100644 index 0000000..ccd9806 --- /dev/null +++ b/scripts/lint.gd.uid @@ -0,0 +1 @@ +uid://cosvmn3gh5bxj diff --git a/scripts/managers/camera_context_manager.gd b/scripts/managers/camera_context_manager.gd index fd0f12e..42fd1db 100644 --- a/scripts/managers/camera_context_manager.gd +++ b/scripts/managers/camera_context_manager.gd @@ -52,9 +52,6 @@ func _calculate_target_position() -> Vector3: bounds = bounds_gauntlet elif mode == GameMode.Mode.STOP_N_GO: bounds = bounds_stop_n_go - elif mode == GameMode.Mode.TEKTON_DOORS: - bounds = bounds_doors - target_y = 32.3 # Doors uses a higher overlook # Clamp X and Z target_x = clamp(target_x, bounds.min_x, bounds.max_x) diff --git a/scripts/managers/gauntlet_manager.gd b/scripts/managers/gauntlet_manager.gd index c3eacfb..6ceed8e 100644 --- a/scripts/managers/gauntlet_manager.gd +++ b/scripts/managers/gauntlet_manager.gd @@ -2,7 +2,7 @@ extends Node class_name GauntletManager # GauntletManager - Handles Candy Pump Survival (Gauntlet) game mode -# Pattern: StopNGoManager + PortalModeManager +# Pattern: StopNGoManager + GauntletManager signal phase_changed(phase_index: int, phase_name: String) signal growth_tick(cells: Array) diff --git a/scripts/managers/goal_manager.gd b/scripts/managers/goal_manager.gd index eba2d8a..0d97029 100644 --- a/scripts/managers/goal_manager.gd +++ b/scripts/managers/goal_manager.gd @@ -62,8 +62,6 @@ func mark_goal_complete(player_id: int): player_completion_times[player_id].append(duration_sec) # Reset start time for next goal player_start_times[player_id] = Time.get_ticks_msec() - - # print("Player %s completed goal in %.2fs" % [player_id, duration_sec]) func get_player_average_time(player_id: int) -> float: if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty(): @@ -92,7 +90,7 @@ func get_boost_multiplier(player_id: int) -> float: var p_avg = get_player_average_time(player_id) var g_avg = get_global_average_time() - if p_avg > g_avg: + if p_avg > g_avg: # Player is slower than average -> Boost fills faster # Scale up to 1.5x based on how much slower (capped) var ratio = p_avg / max(g_avg, 0.1) diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 4701ddc..a06980d 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -26,11 +26,6 @@ signal sng_go_duration_changed(duration: int) signal sng_stop_duration_changed(duration: int) signal sng_required_goals_changed(goals: int) -# Tekton Doors settings signals -signal doors_swap_time_changed(time: int) -signal doors_refresh_time_changed(time: int) -signal doors_required_goals_changed(goals: int) - # Gauntlet settings signals signal gauntlet_round_duration_changed(duration: int) signal gauntlet_growth_interval_changed(interval: float) @@ -74,11 +69,6 @@ var sng_go_duration: int = 20 var sng_stop_duration: int = 4 var sng_required_goals: int = 8 -# Tekton Doors settings -var doors_swap_time: int = 15 -var doors_refresh_time: int = 25 -var doors_required_goals: int = 8 - # Gauntlet settings var gauntlet_round_duration: int = 180 var gauntlet_growth_interval: float = 3.0 # seconds between growth ticks @@ -522,37 +512,6 @@ func sync_sng_required_goals(goals: int) -> void: sng_required_goals = goals emit_signal("sng_required_goals_changed", goals) -# ============================================================================= -# Tekton Doors Settings -# ============================================================================= - -func set_doors_swap_time(time: int) -> void: - doors_swap_time = time - if is_host: rpc("sync_doors_swap_time", time) - -@rpc("authority", "call_local", "reliable") -func sync_doors_swap_time(time: int) -> void: - doors_swap_time = time - emit_signal("doors_swap_time_changed", time) - -func set_doors_refresh_time(time: int) -> void: - doors_refresh_time = time - if is_host: rpc("sync_doors_refresh_time", time) - -@rpc("authority", "call_local", "reliable") -func sync_doors_refresh_time(time: int) -> void: - doors_refresh_time = time - emit_signal("doors_refresh_time_changed", time) - -func set_doors_required_goals(goals: int) -> void: - doors_required_goals = goals - if is_host: rpc("sync_doors_required_goals", goals) - -@rpc("authority", "call_local", "reliable") -func sync_doors_required_goals(goals: int) -> void: - doors_required_goals = goals - emit_signal("doors_required_goals_changed", goals) - # ============================================================================= # Gauntlet Settings # ============================================================================= @@ -740,8 +699,6 @@ func set_game_mode(mode: String) -> void: set_area("Free Mode Area") elif mode == "Stop n Go" and "Stop n Go Area" in available_areas: set_area("Stop n Go Area") - elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas: - set_area("Tekton Doors Area") elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas: set_area("Gauntlet Arena") @@ -756,8 +713,6 @@ func sync_game_mode(mode: String) -> void: selected_area = "Free Mode Area" elif mode == "Stop n Go" and "Stop n Go Area" in available_areas: selected_area = "Stop n Go Area" - elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas: - selected_area = "Tekton Doors Area" elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas: selected_area = "Gauntlet Arena" elif selected_area not in available_areas: @@ -785,9 +740,6 @@ func start_game(force: bool = false) -> void: rpc("sync_sng_go_duration", sng_go_duration) rpc("sync_sng_stop_duration", sng_stop_duration) rpc("sync_sng_required_goals", sng_required_goals) - rpc("sync_doors_swap_time", doors_swap_time) - rpc("sync_doors_refresh_time", doors_refresh_time) - rpc("sync_doors_required_goals", doors_required_goals) # Sync gauntlet settings rpc("sync_gauntlet_round_duration", gauntlet_round_duration) rpc("sync_gauntlet_growth_interval", gauntlet_growth_interval) @@ -864,9 +816,6 @@ func request_room_info(requester_id: int, requester_name: String, requester_char rpc_id(requester_id, "sync_sng_go_duration", sng_go_duration) rpc_id(requester_id, "sync_sng_stop_duration", sng_stop_duration) rpc_id(requester_id, "sync_sng_required_goals", sng_required_goals) - rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time) - rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time) - rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals) rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration) rpc_id(requester_id, "sync_gauntlet_growth_interval", gauntlet_growth_interval) rpc_id(requester_id, "sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick) @@ -1018,6 +967,3 @@ func reset() -> void: sng_go_duration = 20 sng_stop_duration = 4 sng_required_goals = 8 - doors_swap_time = 15 - doors_refresh_time = 25 - doors_required_goals = 8 diff --git a/scripts/managers/music_manager.gd b/scripts/managers/music_manager.gd index 4898205..fa0b677 100644 --- a/scripts/managers/music_manager.gd +++ b/scripts/managers/music_manager.gd @@ -47,7 +47,7 @@ func start_music(): match game_mode: "Stop n Go": track_path = "res://assets/sounds/stop_n_go.wav" - "Freemode", "Tekton Doors", _: + "Freemode", _: track_path = "res://assets/sounds/level_bridge.wav" play_track(track_path) diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 57ba0e5..27fa228 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -230,10 +230,6 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node): break if not has_items: - if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): - # Tekton Doors handles its own wall-aware refill in PortalModeManager - return - print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...") # Call randomize_floor on floor 1 using ScarcityController # ScarcityController is a global class, so we can pass its static function as a Callable @@ -372,18 +368,7 @@ func auto_put_item() -> bool: var pos = neighbor.position var cell_3d = Vector3i(pos.x, 1, pos.y) if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos): - # TEKTON DOORS: Avoid portal doors - var is_on_portal = false - if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): - var doors = get_tree().get_nodes_in_group("PortalDoors") - for door in doors: - var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position)) - if Vector2i(door_grid.x, door_grid.z) == pos: - is_on_portal = true - break - - if not is_on_portal: - valid_put_positions.append(pos) + valid_put_positions.append(pos) if valid_put_positions.is_empty(): return false diff --git a/scripts/managers/portal_mode_manager.gd b/scripts/managers/portal_mode_manager.gd deleted file mode 100644 index 3dfe68b..0000000 --- a/scripts/managers/portal_mode_manager.gd +++ /dev/null @@ -1,585 +0,0 @@ -extends Node - -# PortalModeManager - Handles "Tekton Doors" mode logic -# Manages room partitioning, portal connections, and mode-specific timers. - -var main: Node -var gridmap: Node - -# Room layout config -const ROOM_COUNT = 4 -const GRID_SIZE = 14 -const ROOM_DIM = 7 - -# State -var connections = {} # room_id -> {door_id -> {target_room, target_door}} -var doors = [] # List of PortalDoor nodes -var swap_timer: Timer -var tile_refresh_timer: Timer -var finish_spawned: bool = false -var arena_setup_done: bool = false -var player_portal_cooldowns: Dictionary = {} - -var hud_layer: CanvasLayer -var mission_label: Label -var _has_notified_mission_complete: bool = false - -func initialize(p_main: Node, p_gridmap: Node): - main = p_main - gridmap = p_gridmap - - if gridmap: - # Ensure walls (4) are strictly treated as non-walkable for all internal checks - # Use explicit type to avoid Array vs Array[int] mismatch error - var non_walkable: Array[int] = [4] - gridmap.non_walkable_items = non_walkable - - # Create Stands container if it doesn't exist - print("[PortalModeManager] Initialized") - - # Connection Swap Timer (15s) - swap_timer = Timer.new() - swap_timer.name = "PortalSwapTimer" - # Initial wait time; gets reset when started based on game mode settings - swap_timer.wait_time = 15.0 - swap_timer.timeout.connect(_on_swap_timer_timeout) - add_child(swap_timer) - - # Tile Refresh Timer (25s) - tile_refresh_timer = Timer.new() - tile_refresh_timer.name = "TileRefreshTimer" - # Initial wait time; gets reset when started based on game mode settings - tile_refresh_timer.wait_time = 25.0 - tile_refresh_timer.timeout.connect(_on_tile_refresh_timer_timeout) - add_child(tile_refresh_timer) - - # Connect to mission tracking - var gcm = main.get_node_or_null("GoalsCycleManager") - if gcm: - gcm.global_timer_updated.connect(_on_global_timer_updated) - gcm.goal_count_updated.connect(_on_goal_count_updated) - - _setup_hud() - -func _on_global_timer_updated(time_remaining: float): - if not multiplayer.is_server(): return - - # Last 30 seconds: Reveal Finish Room - if time_remaining <= 30.0 and not finish_spawned: - _spawn_finish_room() - -func start_game_mode(): - if not multiplayer.is_server(): return - - if arena_setup_done and not doors.is_empty(): - print("[PortalModeManager] Arena already setup, starting timers and refresh only.") - else: - print("[PortalModeManager] Starting Portal Game Mode with full setup...") - setup_arena_locally() - _randomize_connections() - - # Configure dynamic timings from LobbyManager before starting - swap_timer.wait_time = float(LobbyManager.doors_swap_time) - tile_refresh_timer.wait_time = float(LobbyManager.doors_refresh_time) - - # Start Timers - if swap_timer.is_stopped(): - swap_timer.start() - if tile_refresh_timer.is_stopped(): - tile_refresh_timer.start() - - # Initial Tile Spawn - _refresh_tiles() - - # Show HUD - _activate_hud() - -func _activate_hud(): - if hud_layer: - hud_layer.visible = true - _update_hud_visuals() - -func activate_client_side(): - """Called on clients to show HUD and prepare local state.""" - print("[PortalModeManager] Activating client-side HUD") - _activate_hud() - # Initial update to catch any missed goal counts - _update_hud_visuals() - -func setup_arena_locally(): - """Sets up GridMap size and walls. Called on host and clients.""" - if arena_setup_done: - print("[PortalModeManager] Arena already setup locally, skipping.") - return - - print("[PortalModeManager] Setting up arena locally...") - _setup_arena_size() - _setup_room_partitions() - _spawn_portal_doors() - - # PRE-FILL TILES: Ensure all floor tiles have items before the countdown starts - if multiplayer.is_server(): - _refresh_tiles() - - arena_setup_done = true - -func _setup_arena_size(): - if not gridmap: return - gridmap.columns = GRID_SIZE - gridmap.rows = GRID_SIZE - gridmap.clear() - # Explicitly clear Floor 1 to prevent legacy tiles from previous rounds - if gridmap.has_method("clear_grid"): - gridmap.clear_grid(1) - - # Fill Floor 0 with standard floor (Item ID 0) - for x in range(GRID_SIZE): - for z in range(GRID_SIZE): - gridmap.set_cell_item(Vector3i(x, 0, z), 0) - -func get_spawn_points() -> Array[Vector2i]: - # One point per quadrant - return [ - Vector2i(3, 3), # Room 0 - Vector2i(10, 3), # Room 1 - Vector2i(3, 10), # Room 2 - Vector2i(10, 10) # Room 3 - ] - -func _setup_hud(): - hud_layer = CanvasLayer.new() - hud_layer.layer = 5 - hud_layer.visible = false - add_child(hud_layer) - - var bottom_container = CenterContainer.new() - bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) - bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH - bottom_container.grow_vertical = Control.GROW_DIRECTION_BEGIN - bottom_container.offset_bottom = -50 - hud_layer.add_child(bottom_container) - - mission_label = Label.new() - mission_label.text = "GOALS (0/8)" - mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - - var custom_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf") - if custom_font: mission_label.add_theme_font_override("font", custom_font) - - mission_label.add_theme_font_size_override("font_size", 28) - mission_label.add_theme_color_override("font_outline_color", Color.BLACK) - mission_label.add_theme_constant_override("outline_size", 8) - bottom_container.add_child(mission_label) - - # Initial update - _update_hud_visuals() - -func _update_hud_visuals(): - if not mission_label: return - - var my_id = multiplayer.get_unique_id() - var gcm = main.get_node_or_null("GoalsCycleManager") - var completed_count = gcm.player_goal_counts.get(my_id, 0) if gcm else 0 - - mission_label.text = "GOALS (%d/%d)" % [completed_count, LobbyManager.doors_required_goals] - - if completed_count >= LobbyManager.doors_required_goals: - mission_label.text = "ALL GOALS COMPLETE!\nFIND THE FINISH ROOM!" - mission_label.add_theme_color_override("font_color", Color.GOLD) - - if not _has_notified_mission_complete: - _has_notified_mission_complete = true - var player_node = main.get_node_or_null(str(my_id)) - if player_node: - NotificationManager.send_message(player_node, "ALL GOALS COMPLETE!", NotificationManager.MessageType.GOAL) - else: - mission_label.add_theme_color_override("font_color", Color.WHITE) - _has_notified_mission_complete = false - -func is_mission_complete(peer_id: int) -> bool: - var gcm = main.get_node_or_null("GoalsCycleManager") - if not gcm: return false - return gcm.player_goal_counts.get(peer_id, 0) >= LobbyManager.doors_required_goals - -func check_win_condition(player_id: int, pos: Vector2i) -> bool: - # 1. Check if on finish tile - var tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) - if tile != 3: return false - - # 2. Check missions - return is_mission_complete(player_id) - -func _setup_room_partitions(): - for i in range(GRID_SIZE): - # Vertical wall (middle columns) - gridmap.set_cell_item(Vector3i(6, 0, i), 4) # Wall item - gridmap.set_cell_item(Vector3i(7, 0, i), 4) - - # Horizontal wall (middle rows) - gridmap.set_cell_item(Vector3i(i, 0, 6), 4) - gridmap.set_cell_item(Vector3i(i, 0, 7), 4) - -var _pending_sync_data = null - -func _spawn_portal_doors(): - # 1. Use synced configs if they exist (passed via main.rpc("sync_portal_configs")) - var door_configs = get_meta("door_configs") if has_meta("door_configs") else [] - - # 2. If no synced configs (e.g. Server start), generate base + extras - if door_configs.is_empty(): - if not multiplayer.is_server(): - print("[PortalModeManager] Client waiting for portal configs sync...") - return - - door_configs = [ - # BASE DOORS (2 per room) - {"room": 0, "pos": Vector2i(6, 2), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East - {"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South - {"room": 1, "pos": Vector2i(7, 2), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West - {"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South - {"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North - {"room": 2, "pos": Vector2i(6, 11), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East - {"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North - {"room": 3, "pos": Vector2i(7, 11), "rot": PI / 2, "offset": Vector2i(1, 0)} # West - ] - - # Server adds extras - var extra_options = [ - {"room": 0, "pos": Vector2i(6, 5), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,2) - {"room": 1, "pos": Vector2i(7, 5), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West (Gap from 7,2) - {"room": 2, "pos": Vector2i(6, 8), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,11) - {"room": 3, "pos": Vector2i(7, 8), "rot": PI / 2, "offset": Vector2i(1, 0)} # West (Gap from 7,11) - ] - extra_options.shuffle() - door_configs.append(extra_options[0]) - door_configs.append(extra_options[1]) - - # Broadcast to clients - main.rpc("sync_portal_configs", door_configs) - - # 3. Spawn the doors - if not doors.is_empty(): return # Guard against double spawn - - print("[PortalModeManager] Spawning %d doors. Peer: %d" % [door_configs.size(), multiplayer.get_unique_id()]) - - var portal_scene = load("res://scenes/portal_door.tscn") - var stands_container = main.get_node_or_null("Stands") - if not stands_container: - stands_container = Node3D.new() - stands_container.name = "Stands" - main.add_child(stands_container) - - for i in range(door_configs.size()): - var cfg = door_configs[i] - if not portal_scene: - print("[PortalModeManager] Error: Failed to load portal_door.tscn") - break - - var door = portal_scene.instantiate() - door.name = "Portal_%d" % i - door.room_id = cfg["room"] - door.door_id = i - door.set_meta("spawn_offset", cfg["offset"]) # Store offset for teleport - - # Position - var world_pos = gridmap.map_to_local(Vector3i(cfg["pos"].x, 0, cfg["pos"].y)) - door.transform.origin = world_pos - door.rotation.y = cfg["rot"] - - stands_container.add_child(door, true) - doors.append(door) - - # Server-only interaction logic - if multiplayer.is_server(): - door.player_entered_portal.connect(handle_portal_interaction) - - gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor - - print("[PortalModeManager] Finished spawning %d doors" % doors.size()) - - # Apply pending sync if it arrived early - if _pending_sync_data: - print("[PortalModeManager] Applying pending sync data...") - sync_portal_data(_pending_sync_data) - _pending_sync_data = null - -const PORTAL_COLORS = [ - Color(0, 1, 1), # Cyan - Color(1, 0, 1), # Magenta - Color(1, 0, 0), # Red - Color(0, 1, 0), # Green - Color(1, 0.5, 0) # Orange -] - -func _randomize_connections(): - if not multiplayer.is_server(): return - - print("[PortalModeManager] Swapping portal connections...") - connections.clear() - - var door_indices = [] - for i in range(doors.size()): - door_indices.append(i) - - # Shuffle and Validate: ensure no pairs are in the same room - var valid_pairing = false - var attempts = 0 - while not valid_pairing and attempts < 100: - attempts += 1 - door_indices.shuffle() - valid_pairing = true - for i in range(0, door_indices.size(), 2): - var a = door_indices[i] - var b = door_indices[i + 1] - if doors[a].room_id == doors[b].room_id: - valid_pairing = false - break - - # Prepare sync data - var sync_data = [] # [[door_a_id, door_b_id, color], ...] - - # Pair them up and assign colors - for i in range(0, door_indices.size(), 2): - var a = door_indices[i] - var b = door_indices[i + 1] - connections[a] = b - connections[b] = a - - var color = PORTAL_COLORS[int(i / 2.0) % PORTAL_COLORS.size()] - sync_data.append([a, b, color]) - - doors[a].target_door_id = b - doors[a].portal_color = color - - doors[b].target_door_id = a - doors[b].portal_color = color - - # Sync to all clients - rpc("sync_portal_data", sync_data) - main.rpc("display_message", "PORTALS SWITCHED!") - -func sync_to_client(peer_id: int): - """Syncs current portal connections to a specific client.""" - var sync_data = [] - # connections is id -> id - # We need to rebuild the pair-based data for the RPC - var handled = [] - for a_id in connections: - if a_id in handled: continue - var b_id = connections[a_id] - var color = doors[a_id].portal_color - sync_data.append([a_id, b_id, color]) - handled.append(a_id) - handled.append(b_id) - - rpc_id(peer_id, "sync_portal_data", sync_data) - -@rpc("authority", "call_local", "reliable") -func sync_portal_data(data: Array): - """Syncs portal connections and colors to all clients.""" - print("[PortalModeManager] Received portal sync data. Peed ID: ", multiplayer.get_unique_id()) - - # If doors array is empty on client, try to repopulate from Stands group - if doors.is_empty(): - var stands = get_tree().get_nodes_in_group("PortalDoors") - # Sort by name to ensure consistent indexing - stands.sort_custom(func(a, b): return a.name < b.name) - doors = stands - - # If still empty, defer sync until doors are spawned locally - if doors.is_empty(): - print("[PortalModeManager] Doors not yet ready, deferring sync data...") - _pending_sync_data = data - return - - connections.clear() - for pair in data: - var a_id = pair[0] - var b_id = pair[1] - var color = pair[2] - - connections[a_id] = b_id - connections[b_id] = a_id - - if a_id < doors.size() and b_id < doors.size(): - if is_instance_valid(doors[a_id]): - doors[a_id].target_door_id = b_id - doors[a_id].portal_color = color - if is_instance_valid(doors[b_id]): - doors[b_id].target_door_id = a_id - doors[b_id].portal_color = color - else: - print("[PortalModeManager] Warning: Door index %d or %d out of range during sync" % [a_id, b_id]) - - -func _on_global_goal_count_updated(_peer_id: int, _count: int): - # Mission requirement removed in favor of time-based finish reveal - pass - -func _on_goal_count_updated(peer_id: int, _count: int): - # Update HUD if relevant (always check if it's the local player whose count changed) - if peer_id == multiplayer.get_unique_id(): - _update_hud_visuals() - -func _spawn_finish_room(): - print("[PortalModeManager] Time is running out! Revealing Finish Room...") - finish_spawned = true - - # Choose a random room quadrant index (0 to 3) - var room_idx = randi() % 4 - - # Determine center for the selected room quadrant (7x7 rooms) - var x_center = 3 if (room_idx == 0 or room_idx == 2) else 10 - var z_center = 3 if (room_idx == 0 or room_idx == 1) else 10 - - # Determine 3x3 bounds around the center - var x_start = x_center - 1 - var x_end = x_center + 2 # exclusive for range() - var z_start = z_center - 1 - var z_end = z_center + 2 # exclusive for range() - - print("[PortalModeManager] Converting 3x3 area in Room %d (X:%d-%d, Z:%d-%d) to Finish Tiles" % [room_idx, x_start, x_end-1, z_start, z_end-1]) - - # Iterate through the 3x3 area - for x in range(x_start, x_end): - for z in range(z_start, z_end): - # Only convert walkable floor tiles (Item ID 0) on Floor 0 - var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z)) - if floor_0_item == 0: - # Change Floor 0 tile to Finish Tile (ID 3) - main.rpc("sync_grid_item", x, 0, z, 3) - - # Clear any item on Floor 1 above this tile - main.rpc("sync_grid_item", x, 1, z, -1) - - # Visual update for server - if gridmap.has_method("update_grid_data"): - gridmap.update_grid_data() - - main.rpc("display_message", "[ALARM] THE FINISH ROOM HAS APPEARED!") - main.rpc("broadcast_message", "SYSTEM", "A 3x3 Finish Zone has appeared in Room %d!" % room_idx, 4) # 4 = MessageType.WARNING - -func _get_room_index(pos: Vector2i) -> int: - if pos.x < 7 and pos.y < 7: return 0 - if pos.x >= 7 and pos.y < 7: return 1 - if pos.x < 7 and pos.y >= 7: return 2 - return 3 - -func _on_swap_timer_timeout(): - _randomize_connections() - -func _on_tile_refresh_timer_timeout(): - _refresh_tiles() - main.rpc("display_message", "TILES REPLENISHED!") - -func _refresh_tiles(): - # GridMap Floor 0 has the walls (ID 4) and floors (ID 0) - # GridMap Floor 1 should have the items (Heart, Star, etc) - # Cache door positions to avoid spawning under them - var door_positions = [] - for door in doors: - if is_instance_valid(door): - var local_pos = gridmap.local_to_map(gridmap.to_local(door.global_position)) - door_positions.append(Vector2i(local_pos.x, local_pos.z)) - - for x in range(GRID_SIZE): - for z in range(GRID_SIZE): - # 1. Check if Floor 0 is a wall or void - var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z)) - if floor_0_item in [4, -1]: - continue - - # 1.5. Prevent spawning directly under portal doors - if door_positions.has(Vector2i(x, z)): - continue - - # 2. Check if Floor 1 is already occupied - if gridmap.get_cell_item(Vector3i(x, 1, z)) != -1: - continue - - # 3. Spawn a tile (60% chance per valid floor cell) - if randf() < 0.6: - var weights = ScarcityModel.get_tile_weights() - var tile_id = _pick_weighted_tile(weights) - # Update GridMap Floor 1 via RPC for sync - main.rpc("sync_grid_item", x, 1, z, tile_id) - -func _pick_weighted_tile(weights: Dictionary) -> int: - var total_weight = 0 - for w in weights.values(): total_weight += w - - var r = randi() % total_weight - var cumulative = 0 - for tile in weights: - cumulative += weights[tile] - if r < cumulative: - return tile - return 7 # Default Heart - -func handle_portal_interaction(player, door): - if not multiplayer.is_server(): return - - var current_time = Time.get_ticks_msec() - if player_portal_cooldowns.has(player.name): - # Reduce cooldown to 200ms (more responsive than 1s, but enough to avoid jitter) - if current_time - player_portal_cooldowns[player.name] < 200: - return - player_portal_cooldowns[player.name] = current_time - - var source_id = door.door_id - if not connections.has(source_id): return - - var target_id = connections[source_id] - var target_door = doors[target_id] - - # Use stored offset to avoid infinite loop (spawn inside the target room) - var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0, 0) - - var target_world = target_door.global_position - var target_grid_3d = gridmap.local_to_map(target_world) - var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset - - # Check for overlaps at the target_grid - var final_target = target_grid - var all_players = get_tree().get_nodes_in_group("Players") - var is_occupied = true - var search_radius = 0 - var max_search_radius = 2 - - while is_occupied and search_radius <= max_search_radius: - is_occupied = false - for p in all_players: - if p != player and p.current_position == final_target: - is_occupied = true - break - - if is_occupied: - # Try to find an adjacent cell - search_radius += 1 - var found_empty = false - # Check immediate neighbors first - var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1), - Vector2i(1, 1), Vector2i(-1, 1), Vector2i(1, -1), Vector2i(-1, -1)] - for offset_vec in offsets: - var test_pos = final_target + offset_vec - # Check if it's strictly a floor tile (ID 0) on Floor 0, not a wall - if gridmap.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) == 0: - # Verify no player is on this test_pos - var test_occupied = false - for p in all_players: - if p != player and p.current_position == test_pos: - test_occupied = true - break - if not test_occupied: - final_target = test_pos - found_empty = true - break - - if found_empty: - is_occupied = false - - print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, final_target, target_id]) - - # Snap player - if player.has_method("set_spawn_position"): - player.rpc("set_spawn_position", final_target) diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index e580327..c522766 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -558,11 +558,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c item_id = rng.randi_range(7, 10) else: # 20% Chance for PowerUp - if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): - # Restrict to Speed (11) and Ghost (14) for Tekton Doors - item_id = [11, 14].pick_random() - else: - item_id = rng.randi_range(11, 14) + item_id = rng.randi_range(11, 14) var cell = Vector3i(pos.x, 1, pos.y) diff --git a/scripts/mode_config.gd b/scripts/mode_config.gd index 5a12da3..344c5c3 100644 --- a/scripts/mode_config.gd +++ b/scripts/mode_config.gd @@ -18,12 +18,6 @@ const SCHEMA = { "sng_stop_duration": {"type": TYPE_INT, "default": 4, "min": 2, "max": 10}, "sng_required_goals": {"type": TYPE_INT, "default": 8, "min": 3, "max": 20} }, - "Tekton Doors": { - "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, - "doors_swap_time": {"type": TYPE_INT, "default": 15, "min": 10, "max": 30}, - "doors_refresh_time": {"type": TYPE_INT, "default": 25, "min": 15, "max": 40}, - "doors_required_goals": {"type": TYPE_INT, "default": 8, "min": 5, "max": 12} - }, "Candy Pump Survival": { "match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600}, "gauntlet_growth_interval": {"type": TYPE_FLOAT, "default": 3.0, "min": 1.0, "max": 10.0}, diff --git a/scripts/portal_door.gd b/scripts/portal_door.gd deleted file mode 100644 index 5e7ed05..0000000 --- a/scripts/portal_door.gd +++ /dev/null @@ -1,136 +0,0 @@ -extends StaticBody3D - -# PortalDoor.gd -# Specialized door for "Tekton Doors" mode. -# Teleports players to a target room/door when they step into it. - -signal player_entered_portal(player_node, door_node) - -@export var room_id: int = 0 -@export var door_id: int = 0 # 0: North, 1: South, 2: East, 3: West - -# State synced by PortalModeManager -var target_room_id: int = -1 -var target_door_id: int = -1 -var is_active: bool = true -var portal_color: Color = Color.WHITE: set = set_portal_color - -func set_portal_color(value: Color): - portal_color = value - _update_visuals() - -@onready var detection_area: Area3D = $Area3D - -func _ready(): - add_to_group("PortalDoors") - if detection_area: - detection_area.body_entered.connect(_on_body_entered) - - # Visual feedback: indicate door is active - _update_visuals() - - # Adjust GroundIndicator position based on spawn_offset metadata - _adjust_indicator_position() - -func _on_body_entered(body: Node3D): - if not is_active: return - - if body.is_in_group("Players") or body.get("is_bot"): - var current_time = Time.get_ticks_msec() - if body.has_meta("last_portal_time"): - # Reduce cooldown to 200ms to match manager logic and allow fast re-entry - if current_time - body.get_meta("last_portal_time") < 200: - return - - body.set_meta("last_portal_time", current_time) - - print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id]) - emit_signal("player_entered_portal", body, self ) - -var _materials_initialized: bool = false - -func _update_visuals(): - # Removed is_node_ready() check to allow early setter calls to prepare variables, - # but we still need the nodes to exist to apply them. - if not is_inside_tree(): return - - var vortex = get_node_or_null("Vortex") - var frame_left = get_node_or_null("Frame_Left") - - # If children aren't there yet, we can't update visuals. - # This usually happens if called before or during early _ready. - if not vortex or not frame_left: return - - if not _materials_initialized: - _initialize_unique_materials() - _materials_initialized = true - - if vortex: - var mat = vortex.get_surface_override_material(0) - if mat: - mat.albedo_color = portal_color - mat.albedo_color.a = 0.5 - if mat.has_method("set_emission"): - mat.set("emission", portal_color) - - for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]: - var frame = get_node_or_null(part_name) - if frame: - var mat = frame.get_surface_override_material(0) - if mat: - mat.albedo_color = portal_color.lerp(Color.BLACK, 0.4) - - var ground = get_node_or_null("GroundIndicator") - if ground: - var mat = ground.get_surface_override_material(0) - if mat: - mat.albedo_color = portal_color - mat.albedo_color.a = 0.4 - mat.emission = portal_color - mat.emission_energy_multiplier = 2.0 - -func _initialize_unique_materials(): - var vortex = get_node_or_null("Vortex") - if vortex: - var mat = vortex.get_surface_override_material(0) - if not mat: - mat = vortex.mesh.surface_get_material(0) - - if mat: - vortex.set_surface_override_material(0, mat.duplicate()) - - for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]: - var frame = get_node_or_null(part_name) - if frame: - var mat = frame.get_surface_override_material(0) - if not mat: - mat = frame.mesh.surface_get_material(0) - - if mat: - frame.set_surface_override_material(0, mat.duplicate()) - - var ground = get_node_or_null("GroundIndicator") - if ground: - var mat = ground.get_surface_override_material(0) - if not mat: - mat = ground.mesh.surface_get_material(0) - if mat: - ground.set_surface_override_material(0, mat.duplicate()) - -func _adjust_indicator_position(): - # This uses the spawn_offset metadata set by PortalModeManager - # to push the ground indicator "into" the room. - if not has_meta("spawn_offset"): return - - var ground = get_node_or_null("GroundIndicator") - if not ground: return - - var offset_2d = get_meta("spawn_offset") # Vector2i - var offset_3d = Vector3(offset_2d.x, 0, offset_2d.y) - - # Convert the global direction (into the room) to local coordinates - var local_dir = to_local(global_position + offset_3d).normalized() - - # Nudge the indicator in that direction - ground.position = local_dir * 0.5 # Reduced from 0.6 to close the gap - ground.position.y = 0.05 # Keep it just above the floor diff --git a/scripts/tekton.gd b/scripts/tekton.gd index 428bfd3..cbed9e0 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -514,9 +514,7 @@ func spawn_tiles_around(count: int = 4): if LobbyManager: mode = LobbyManager.get_game_mode() - if LobbyManager and LobbyManager.get_game_mode() == GameMode.Mode.TEKTON_DOORS: - item_id = [11, 14].pick_random() - elif mode == GameMode.Mode.FREEMODE: + if mode == GameMode.Mode.FREEMODE: item_id = rng.randi_range(7, 10) # No powerups in freemode either, just normal tiles else: item_id = rng.randi_range(11, 14) diff --git a/test_mp.gd b/test_mp.gd new file mode 100644 index 0000000..bb51f11 --- /dev/null +++ b/test_mp.gd @@ -0,0 +1,4 @@ +extends SceneTree +func _init(): + print("Testing gauntlet multiplayer") + quit() diff --git a/test_mp.gd.uid b/test_mp.gd.uid new file mode 100644 index 0000000..5a9902c --- /dev/null +++ b/test_mp.gd.uid @@ -0,0 +1 @@ +uid://ddniv6k6aj2u diff --git a/test_ssh.sh b/test_ssh.sh new file mode 100755 index 0000000..3f7c245 --- /dev/null +++ b/test_ssh.sh @@ -0,0 +1,5 @@ +#!/usr/bin/expect -f +spawn ssh admin@193.180.213.215 "ls -la /home/admin/nakama/data/modules/" +expect "password:" +send "Mieayamtelur17\r" +expect eof diff --git a/tests/test_gauntlet_registration.gd b/tests/test_gauntlet_registration.gd index 01fa10d..2f67791 100644 --- a/tests/test_gauntlet_registration.gd +++ b/tests/test_gauntlet_registration.gd @@ -18,11 +18,10 @@ func after_each(): func test_gauntlet_enum_exists(): assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be enum value 3") -# Test 2: All 4 modes are present in enum +# Test 2: All 3 modes are present in enum func test_all_modes_in_enum(): assert_eq(GameMode.Mode.FREEMODE, 0, "FREEMODE should be 0") assert_eq(GameMode.Mode.STOP_N_GO, 1, "STOP_N_GO should be 1") - assert_eq(GameMode.Mode.TEKTON_DOORS, 2, "TEKTON_DOORS should be 2") assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be 3") # ============================================================================= @@ -47,7 +46,7 @@ func test_round_trip_conversion(): # Test 6: All existing modes still round-trip correctly func test_existing_modes_round_trip(): - for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO, GameMode.Mode.TEKTON_DOORS]: + for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO]: var s = GameMode.mode_to_string(mode) var back = GameMode.from_string(s) assert_eq(back, mode, "Round-trip failed for %s" % s) @@ -66,18 +65,17 @@ func test_get_all_modes_includes_gauntlet(): var modes = GameMode.get_all_modes() assert_has(modes, "Candy Pump Survival", "get_all_modes should include 'Candy Pump Survival'") -# Test 9: get_all_modes returns exactly 4 entries +# Test 9: get_all_modes returns exactly 3 entries func test_get_all_modes_count(): var modes = GameMode.get_all_modes() - assert_eq(modes.size(), 4, "get_all_modes should return 4 modes") + assert_eq(modes.size(), 3, "get_all_modes should return 3 modes") # Test 10: get_all_modes order is correct func test_get_all_modes_order(): var modes = GameMode.get_all_modes() assert_eq(modes[0], "Freemode", "First mode should be Freemode") assert_eq(modes[1], "Stop n Go", "Second mode should be Stop n Go") - assert_eq(modes[2], "Tekton Doors", "Third mode should be Tekton Doors") - assert_eq(modes[3], "Candy Pump Survival", "Fourth mode should be Candy Pump Survival") + assert_eq(modes[2], "Candy Pump Survival", "Third mode should be Candy Pump Survival") # ============================================================================= # is_restricted Tests @@ -96,7 +94,6 @@ func test_freemode_not_restricted(): # Test 13: All restricted modes are confirmed func test_all_restricted_modes(): assert_true(GameMode.is_restricted(GameMode.Mode.STOP_N_GO), "STOP_N_GO should be restricted") - assert_true(GameMode.is_restricted(GameMode.Mode.TEKTON_DOORS), "TEKTON_DOORS should be restricted") assert_true(GameMode.is_restricted(GameMode.Mode.GAUNTLET), "GAUNTLET should be restricted") # ============================================================================= diff --git a/wiki/Architecture-Client.md b/wiki/Architecture-Client.md new file mode 100644 index 0000000..6716623 --- /dev/null +++ b/wiki/Architecture-Client.md @@ -0,0 +1,2707 @@ + + +# Tekton Armageddon - Client Architecture (Full Function Reference) + +[Back to Home](./Home) + +Complete per-function reference for the Godot 4.7 client codebase. Every script, signal, autoload dependency, and cross-file relationship documented. + +[Back to top](#top) + +## Table of Contents + +[Back to top](#top) + +1. [Project Structure Overview](#1-project-structure-overview) +2. [Autoloads / Singletons Index](#2-autoloads--singletons-index) +3. [Service Layer](#3-service-layer) + - [3.1 NakamaManager](#31-nakamamanaager) + - [3.2 BackendService](#32-backendservice) + - [3.3 SteamworksManager](#33-steamworksmanager) +4. [Core Managers](#4-core-managers) + - [4.1 AuthManager](#41-authmanager) + - [4.2 LobbyManager](#42-lobbymanager) + - [4.3 GameStateManager](#43-gamestatemanager) + - [4.4 PlayerManager](#44-playermanager) + - [4.5 EventBus](#45-eventbus) + - [4.6 GameMode / ModeConfig](#46-gamemode--modeconfig) +5. [Player Subsystem Managers](#5-player-subsystem-managers) + - [5.1 PlayerMovementManager](#51-playermovementmanager) + - [5.2 PlayerInputManager](#52-playerinputmanager) + - [5.3 PlayerActionManager](#53-playeractionmanager) + - [5.4 PlayerboardManager](#54-playerboardmanager) + - [5.5 PowerupManager](#55-powerupmanager) +6. [Game Mode Managers](#6-game-mode-managers) + - [6.1 StopNGoManager](#61-stopngomanager) + - [6.2 GauntletManager](#62-gauntletmanager) + - [6.3 PortalModeManager](#63-portalmode_manager) + - [6.4 GoalManager](#64-goalmanager) + - [6.5 GoalsCycleManager](#65-goalscyclemanager) + - [6.6 PlayerRaceManager](#66-playerracemanager) + - [6.7 TurnManager](#67-turnmanager) +7. [Gameplay Managers](#7-gameplay-managers) + - [7.1 ObstacleManager](#71-obstaclemanager) + - [7.2 SpecialTilesManager](#72-specialtilesmanager) + - [7.3 StaticTektonManager](#73-statictektonmanager) +8. [UI / Presentation Managers](#8-ui--presentation-managers) + - [8.1 UIManager](#81-uimanager) + - [8.2 SfxManager](#82-sfxmanager) + - [8.3 MusicManager](#83-musicmanager) + - [8.4 NotificationManager](#84-notificationmanager) + - [8.5 ScreenShake](#85-screenshake) + - [8.6 CameraContextManager](#86-cameracontextmanager) + - [8.7 TouchControls](#87-touchcontrols) + - [8.8 TutorialManager / TutorialOverlay](#88-tutorialmanager--tutorialoverlay) +9. [Social / Economy Managers](#9-social--economy-managers) + - [9.1 UserProfileManager](#91-userprofilemanager) + - [9.2 GachaManager](#92-gachamanager) + - [9.3 SkinManager](#93-skinmanager) + - [9.4 ShopManager](#94-shopmanager) + - [9.5 JoinManager](#95-joinmanager) + - [9.6 FriendManager](#96-friendmanager) + - [9.7 MailManager](#97-mailmanager) + - [9.8 DailyRewardManager](#98-dailyrewardmanager) + - [9.9 AdminManager](#99-adminmanager) +10. [System Managers](#10-system-managers) + - [10.1 SettingsManager](#101-settingsmanager) + - [10.2 SessionManager](#102-sessionmanager) + - [10.3 GameUpdateManager](#103-gameupdatemanager) +11. [Core Scene Scripts](#11-core-scene-scripts) + - [11.1 main.gd (Main game scene controller)](#111-maingd-main-game-scene-controller) + - [11.2 player.gd](#112-playergd) + - [11.3 lobby.gd](#113-lobbygd) + - [11.4 animation.gd](#114-animationgd) +12. [UI Helper Classes (RefCounted)](#12-ui-helper-classes-refcounted) + - [12.1 LobbyMainMenu](#121-lobbymainmenu) + - [12.2 LobbyRoom](#122-lobbyroom) + - [12.3 LobbyRoomList](#123-lobbyroomlist) + - [12.4 LobbyChat](#124-lobbychat) +13. [Dependency Graph](#13-dependency-graph) + - [13.1 Manager Autoload Dependencies](#131-manager-autoload-dependencies) + - [13.2 Cross-Manager Signal Wiring](#132-cross-manager-signal-wiring) +14. [Scene Node Trees](#14-scene-node-trees) + - [14.1 main.tscn](#141-maintscn) + - [14.2 player.tscn](#142-playertscn) + - [14.3 lobby.tscn](#143-lobbytscn) + +[Back to top](#top) + + +## 1. Project Structure Overview + +[Back to top](#top) + +``` +/home/dev/tekton/ + project.godot -- Godot 4.7 project file + scripts/ + main.gd -- (NOT USED; logic lives in scenes/main.gd) + nakama_manager.gd -- Nakama network layer (autoload) + event_bus.gd -- Central observer pattern bus (autoload) + game_mode.gd -- GameMode enum + string utils (RefCounted) + mode_config.gd -- Schema-driven mode settings validation (RefCounted) + managers/ -- 39+ autoload manager singletons + auth_manager.gd + lobby_manager.gd + game_state_manager.gd + player_manager.gd + player_movement_manager.gd + player_input_manager.gd + player_action_manager.gd + user_profile_manager.gd + gacha_manager.gd + skin_manager.gd + ui_manager.gd + sfx_manager.gd + music_manager.gd + game_update_manager.gd + stop_n_go_manager.gd + gauntlet_manager.gd + portal_mode_manager.gd + turn_manager.gd + goal_manager.gd + goals_cycle_manager.gd + player_race_manager.gd + shop_manager.gd + join_manager.gd + powerup_manager.gd + notification_manager.gd + obstacle_manager.gd + friend_manager.gd + admin_manager.gd + mail_manager.gd + session_manager.gd + settings_manager.gd + tutorial_manager.gd + tutorial_overlay.gd + playerboard_manager.gd + camera_context_manager.gd + screen_shake.gd + special_tiles_manager.gd + static_tekton_manager.gd + touch_controls.gd + daily_reward_manager.gd + services/ + backend_service.gd -- Unified RPC interface (autoload) + steamworks_manager.gd -- Steam auth ticket + persona (NOT autoload; child of BackendService) + scenes/ + main.gd -- Core game scene controller (~2956 lines) + main.tscn -- Main game scene + player.gd -- Player character controller (~2751 lines) + player.tscn -- Player scene + lobby.gd -- Lobby/home screen controller (~583 lines) + lobby.tscn -- Lobby scene + animation.gd -- Stop n Go animation player (41 lines) + ui/ + lobby_main_menu.gd -- RefCounted; main menu button wiring + lobby_room.gd -- RefCounted; room/player slot management + lobby_room_list.gd -- RefCounted; room list display + join + lobby_chat.gd -- RefCounted; global + DM chat + login_screen.tscn -- Login screen scene + boot_screen.tscn -- Boot splash scene + shop_panel.tscn -- Shop panel scene + gacha_panel.tscn -- Gacha panel scene + daily_reward_panel.tscn -- Daily reward panel scene + admin_panel.tscn -- Admin panel scene + profile_panel.tscn -- Profile panel scene + leaderboard_panel.tscn -- Leaderboard panel scene + mailbox_panel.tscn -- Mailbox panel scene + settings_menu.tscn -- Settings scene + lobby_invite_popup.tscn -- Invite popup scene + invite_friends_dialog.tscn -- Invite dialog scene + social_panel.tscn -- Social panel scene + game/ + main.tscn -- (actual main game scene) + loading_screen/ + loading_screen.tscn -- Level loading screen +``` + +[Back to top](#top) + + +## 2. Autoloads / Singletons Index + +[Back to top](#top) + +All managers are registered as autoloads in project.godot and accessible globally via `/root/`. The following are the configured autoloads: + +| Autoload Name | File | Purpose | +|---|---|---| +| AuthManager | res://scripts/managers/auth_manager.gd | Authentication (guest, email, social) | +| NakamaManager | res://scripts/nakama_manager.gd | Nakama client/socket/bridge lifecycle | +| BackendService | res://scripts/services/backend_service.gd | Unified RPC API wrapper | +| EventBus | res://scripts/event_bus.gd | Observer-pattern cross-manager events | +| LobbyManager | res://scripts/managers/lobby_manager.gd | Room lifecycle, matchmaking | +| GameStateManager | res://scripts/managers/game_state_manager.gd | State machine, match lifecycle | +| PlayerManager | res://scripts/managers/player_manager.gd | Player data container | +| PlayerMovementManager | res://scripts/managers/player_movement_manager.gd | Movement physics, pathfinding | +| PlayerInputManager | res://scripts/managers/player_input_manager.gd | Input capture, buffering | +| PlayerActionManager | res://scripts/managers/player_action_manager.gd | Action execution (grab, put) | +| UserProfileManager | res://scripts/managers/user_profile_manager.gd | Profile CRUD, wallet sync | +| GachaManager | res://scripts/managers/gacha_manager.gd | Gacha pull orchestration | +| SkinManager | res://scripts/managers/skin_manager.gd | Cosmetics, skins, loadout | +| UIManager | res://scripts/managers/ui_manager.gd | UI layer stack, show/hide | +| SfxManager | res://scripts/managers/sfx_manager.gd | Sound effect pool | +| MusicManager | res://scripts/managers/music_manager.gd | Music crossfade | +| GameUpdateManager | res://scripts/managers/game_update_manager.gd | Hot-reload patching | +| StopNGoManager | res://scripts/managers/stop_n_go_manager.gd | Stop n Go minigame state | +| GauntletManager | res://scripts/managers/gauntlet_manager.gd | Gauntlet mode progression | +| PortalModeManager | res://scripts/managers/portal_mode_manager.gd | Portal race mode | +| TurnManager | res://scripts/managers/turn_manager.gd | Turn-based sequencing | +| GoalManager | res://scripts/managers/goal_manager.gd | Goal validation, completion | +| GoalsCycleManager | res://scripts/managers/goals_cycle_manager.gd | Cycling goal rotation, scoring | +| PlayerRaceManager | res://scripts/managers/player_race_manager.gd | Race position, finish | +| ShopManager | res://scripts/managers/shop_manager.gd | Shop data layer | +| JoinManager | res://scripts/managers/join_manager.gd | Join code input | +| PowerupManager | res://scripts/managers/powerup_manager.gd | Powerup system (boost/charge) | +| NotificationManager | res://scripts/managers/notification_manager.gd | On-screen message queue | +| ObstacleManager | res://scripts/managers/obstacle_manager.gd | Obstacle placement/removal | +| FriendManager | res://scripts/managers/friend_manager.gd | Friends list, DMs | +| AdminManager | res://scripts/managers/admin_manager.gd | Admin panel state | +| MailManager | res://scripts/managers/mail_manager.gd | Mail CRUD | +| SessionManager | res://scripts/managers/session_manager.gd | Session refresh lifecycle | +| SettingsManager | res://scripts/managers/settings_manager.gd | User settings persistence | +| TutorialManager | res://scripts/managers/tutorial_manager.gd | Tutorial flow control | +| TutorialOverlay | res://scripts/managers/tutorial_overlay.gd | Tutorial UI overlay | +| PlayerboardManager | res://scripts/managers/playerboard_manager.gd | Player inventory board | +| CameraContextManager | res://scripts/managers/camera_context_manager.gd | Camera zoom/context | +| ScreenShake | res://scripts/managers/screen_shake.gd | Screen shake effects | +| SpecialTilesManager | res://scripts/managers/special_tiles_manager.gd | Ice/crack/portal tiles | +| StaticTektonManager | res://scripts/managers/static_tekton_manager.gd | Static Tekton turret logic | +| TouchControls | res://scripts/managers/touch_controls.gd | Mobile touch input overlay | +| DailyRewardManager | res://scripts/managers/daily_reward_manager.gd | Daily reward claims | + +[Back to top](#top) + + +## 3. Service Layer + +[Back to top](#top) + +### 3.1 NakamaManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/nakama_manager.gd` (330 lines) +**Extends:** Node +**Autoload name:** NakamaManager + +Central Nakama SDK integration. Manages the Nakama client, session, socket, and multiplayer bridge. All network communication flows through this singleton. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| nakama_server_key | String | From env var NAKAMA_SERVER_KEY or ProjectSettings | +| nakama_host | String | Default: `tektondash.vps.webdock.cloud` | +| nakama_port | int | Default: 7350 | +| nakama_scheme | String | Default: http | +| client | NakamaClient | The Nakama client instance | +| session | NakamaSession | Current auth session | +| socket | NakamaSocket | WebSocket connection | +| bridge | NakamaMultiplayerBridge | Links Nakama socket to Godot HLAPI | +| current_match_id | String | Currently joined match ID | + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `connected_to_nakama` | none | Emitted when socket connects successfully | +| `connection_failed` | error_message: String | Emitted on connection failure | +| `match_joined` | match_id: String | Emitted when bridge joins a match | +| `match_join_error` | error_message: String | Emitted on match join failure | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `set_server` | `func set_server(host: String, port: int = 7350) -> void` | void | Override Nakama server endpoint. Auto-detects scheme (https for .ts.net, http for 100.x IPs). Recreates client if no active session. | +| `connect_to_nakama_async` | `func connect_to_nakama_async(email: String = "", password: String = "") -> bool` | bool (async) | Full auth + socket + bridge connection. Empty email = device auth. Creates socket, initializes multiplayer bridge, sets Godot's multiplayer peer. | +| `cleanup` | `func cleanup() -> void` | void | Shuts down socket, leaves bridge, deletes match metadata storage, resets multiplayer peer to null. | +| `host_game` | `func host_game(room_meta: Dictionary = {}) -> void` | void | Creates a Nakama relayed match via bridge.create_match(). Optionally stores room metadata to Nakama storage. Has re-entry guard for double-click protection. | +| `join_game` | `func join_game(match_id: String) -> void` | void | Joins an existing match by ID. Leaves current match first if connected. | +| `is_connected_to_nakama` | `func is_connected_to_nakama() -> bool` | bool | Returns true if socket exists and is connected to host. | +| `list_matches_async` | `func list_matches_async(mode_filter: String = "") -> Array` | Array (async) | Queries Nakama for available matches. Batch-reads room metadata from storage. Returns array of room dicts. | +| `_on_bridge_match_joined` | `func _on_bridge_match_joined() -> void` | void | Internal: updates current_match_id, emits match_joined signal. | +| `_on_bridge_match_join_error` | `func _on_bridge_match_join_error(error) -> void` | void | Internal: emits match_join_error. | + +**Dependencies:** Nakama GDExtension (NakamaClient, NakamaSocket, NakamaMultiplayerBridge). +**Depended by:** AuthManager, BackendService, LobbyManager, LobbyRoom, LobbyChat, LobbyMainMenu, main.gd. + +[Back to top](#top) + +### 3.2 BackendService + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/services/backend_service.gd` (247 lines) +**Extends:** Node +**Autoload name:** BackendService + +Unified typed interface for all Nakama Lua RPCs. All platform authentication paths (Steam, Nakama device/email) funnel through here. Provides retry logic with exponential backoff. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| current_platform | Platform (enum) | DESKTOP_STEAM, DESKTOP_NAKAMA, or MOBILE_NAKAMA | +| steamworks_manager | Node | Only for auth ticket retrieval | +| nakama_backend | Node | Reference to NakamaManager autoload | + +**Enums:** + +- `Platform { DESKTOP_STEAM, DESKTOP_NAKAMA, MOBILE_NAKAMA }` +- `ErrorCode { NONE, NETWORK_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_ERROR, UNKNOWN_ERROR, INSUFFICIENT_FUNDS }` + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Detects platform, initializes backend | +| `is_initialized` | `func is_initialized() -> bool` | bool | Checks nakama_backend is non-null | +| `get_platform_name` | `func get_platform_name() -> String` | String | Returns human-readable platform name | +| `get_steamworks_manager` | `func get_steamworks_manager() -> Node` | Node | Returns steamworks_manager child node | +| `api_rpc_async` | `func api_rpc_async(rpc_id: String, payload: String = "{}") -> Dictionary` | Dictionary (async) | Unified RPC with up to 3 retries, exponential backoff (0.5s base). Returns `{success, error, message, data}`. | +| `admin_clear_global_chat` | `func admin_clear_global_chat(payload: String) -> Dictionary` | Dictionary | RPC wrapper | +| `admin_get_chat_config` | `func admin_get_chat_config() -> Dictionary` | Dictionary | RPC wrapper | +| `admin_set_chat_config` | `func admin_set_chat_config(config: Dictionary) -> Dictionary` | Dictionary | RPC wrapper | +| `admin_purge_old_messages` | `func admin_purge_old_messages(channel_id: String, max_age_days: int) -> Dictionary` | Dictionary | RPC wrapper | +| `admin_list_channel_messages` | `func admin_list_channel_messages(channel_id: String, limit: int = 50, cursor: String = "", forward: bool = true) -> Dictionary` | Dictionary | RPC wrapper | +| `admin_delete_channel_message` | `func admin_delete_channel_message(channel_id: String, message_id: String) -> Dictionary` | Dictionary | RPC wrapper | +| `send_friend_request` | `func send_friend_request(target_id: String) -> Dictionary` | Dictionary | RPC wrapper | +| `respond_friend_request` | `func respond_friend_request(target_id: String, accept: bool) -> Dictionary` | Dictionary | RPC wrapper | +| `perform_gacha_pull` | `func perform_gacha_pull(gacha_id: String, count: int) -> Dictionary` | Dictionary | RPC wrapper | +| `get_mail` | `func get_mail(payload: String = "{}") -> Dictionary` | Dictionary | RPC wrapper | +| `claim_mail_reward` | `func claim_mail_reward(mail_id: String) -> Dictionary` | Dictionary | RPC wrapper | +| `delete_mail` | `func delete_mail(mail_id: String) -> Dictionary` | Dictionary | RPC wrapper | +| `send_mail` | `func send_mail(payload: String) -> Dictionary` | Dictionary | RPC wrapper | +| `change_avatar` | `func change_avatar(avatar_url: String) -> Dictionary` | Dictionary | RPC wrapper | +| `change_username` | `func change_username(new_username: String) -> Dictionary` | Dictionary | RPC wrapper | +| `change_status` | `func change_status(new_status: String) -> Dictionary` | Dictionary | RPC wrapper | +| `change_bio` | `func change_bio(new_bio: String) -> Dictionary` | Dictionary | RPC wrapper | +| `query_users` | `func query_users(payload: String) -> Dictionary` | Dictionary | RPC wrapper | +| `admin_give_currency` | `func admin_give_currency(payload: String) -> Dictionary` | Dictionary | RPC wrapper | +| `get_daily_reward_config_admin` | `func get_daily_reward_config_admin() -> Dictionary` | Dictionary | RPC wrapper | +| `set_daily_reward_config` | `func set_daily_reward_config(req: Dictionary) -> Dictionary` | Dictionary | RPC wrapper | +| `get_daily_reward_state` | `func get_daily_reward_state() -> Dictionary` | Dictionary | RPC wrapper | +| `claim_daily_reward` | `func claim_daily_reward() -> Dictionary` | Dictionary | RPC wrapper | +| `sync_leaderboard` | `func sync_leaderboard() -> Dictionary` | Dictionary | RPC wrapper | +| `get_leaderboard_stats` | `func get_leaderboard_stats() -> Dictionary` | Dictionary | RPC wrapper | +| `debug_add_exp` | `func debug_add_exp(exp_amount: int) -> Dictionary` | Dictionary | RPC wrapper | +| `reset_stats` | `func reset_stats() -> Dictionary` | Dictionary | RPC wrapper | +| `search_users` | `func search_users(payload: String) -> Dictionary` | Dictionary | RPC wrapper | +| `send_lobby_invite` | `func send_lobby_invite(to_user_id: String, match_id: String) -> Dictionary` | Dictionary | RPC wrapper | + +**Dependencies:** NakamaManager (autoload), SteamworksManager (child node). +**Depended by:** AuthManager, LobbyManager, LobbyChat, lobby.gd (admin), FriendManager, MailManager, GachaManager, DailyRewardManager, AdminManager, SkinManager. + +[Back to top](#top) + +### 3.3 SteamworksManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/services/steamworks_manager.gd` (72 lines) +**Extends:** Node +**class_name:** SteamworksManager + +NOT an autoload. Created as a child of BackendService. Provides Steam auth session tickets for Nakama login. GodotSteam GDExtension required. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| is_steam_initialized | bool | Whether Steam API initialized successfully | +| steam_app_id | int | From ProjectSettings or default 480 | + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Calls _initialize_steam | +| `is_initialized` | `func is_initialized() -> bool` | bool | Returns steam init status | +| `get_auth_session_ticket` | `func get_auth_session_ticket() -> String` | String | Gets Steam auth session ticket via Steam.getAuthSessionTicket(), returns hex-encoded buffer | +| `get_steam_user_name` | `func get_steam_user_name() -> String` | String | Returns Steam persona name via Steam.getPersonaName() | +| `get_steam_user_id` | `func get_steam_user_id() -> int` | int | Returns Steam ID via Steam.getSteamID() | + +**Dependencies:** GodotSteam GDExtension (ClassDB.class_exists("Steam")). +**Depended by:** BackendService, AuthManager. + +[Back to top](#top) + + +## 4. Core Managers + +[Back to top](#top) + +### 4.1 AuthManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/auth_manager.gd` (515 lines) +**Extends:** Node +**Autoload name:** AuthManager + +Centralized authentication handler. Supports Guest (device ID), Email/Password, Google, Apple, Facebook, and Steam auth modes. Persists sessions to encrypted file storage. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| current_user | Dictionary | {user_id, username, display_name, avatar_url, email} | +| is_authenticated | bool | Whether fully authenticated | +| is_guest | bool | Whether using guest mode | +| auth_mode | AuthMode (enum) | GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM | + +**Enums:** `AuthMode { GUEST, EMAIL, GOOGLE, APPLE, FACEBOOK, STEAM, CUSTOM }` + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `auth_started` | none | Emitted when any login flow begins | +| `auth_completed` | success: bool, user_data: Dictionary | Emitted on auth success or failure | +| `auth_failed` | error: String | Emitted on auth error | +| `session_restored` | none | Emitted when saved session restored | +| `logged_out` | none | Emitted after full logout | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Deferred call to _try_restore_session | +| `login_as_guest` | `func login_as_guest() -> bool` | bool (async) | Device ID guest auth. Generates/persists device ID. | +| `login_with_email` | `func login_with_email(email: String, password: String, remember: bool = true) -> bool` | bool (async) | Email/password authentication | +| `register_with_email` | `func register_with_email(email: String, password: String, username: String = "") -> bool` | bool (async) | Email registration (create if not exists) | +| `login_with_google` | `func login_with_google(id_token: String) -> bool` | bool (async) | Google auth via ID token | +| `login_with_apple` | `func login_with_apple(id_token: String) -> bool` | bool (async) | Apple auth via ID token | +| `login_with_facebook` | `func login_with_facebook(access_token: String) -> bool` | bool (async) | Facebook auth via access token | +| `login_with_steam` | `func login_with_steam() -> bool` | bool (async) | Steam ticket auth via BackendService.steamworks_manager | +| `link_email` | `func link_email(email: String, password: String) -> bool` | bool (async) | Link email to existing guest account | +| `link_google` | `func link_google(id_token: String) -> bool` | bool (async) | Link Google to existing account | +| `logout` | `func logout() -> void` | void | Full cleanup: NakamaManager.cleanup(), clear session files, reset state, emit logged_out | +| `clear_session` | `func clear_session() -> void` | void | Deletes SESSION_FILE and CREDENTIALS_FILE from user:// | +| `_try_restore_session` | internal | void | Attempts to load encrypted session file. Skips guest session auto-restore. | +| `_connect_socket` | internal | bool (async) | Creates Nakama socket, connects, initializes multiplayer bridge | +| `_load_user_profile` | internal | void (async) | Loads account data from Nakama into current_user | + +**Dependencies:** NakamaManager, BackendService. +**Depended by:** LobbyMainMenu, lobby.gd, UserProfileManager, login_screen.tscn. + +[Back to top](#top) + +### 4.2 LobbyManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/lobby_manager.gd` (1023 lines) +**Extends:** Node +**Autoload name:** LobbyManager + +Room/lobby lifecycle manager. Handles both Nakama (online) and LAN (direct ENet) modes. Manages room creation, joining, player list, ready states, game mode settings, and character/area selection. + +**Properties:** + +| Name | Type | Default | Description | +|---|---|---|---| +| current_room | Dictionary | {} | Current room metadata | +| players_in_room | Array | [] | [{id, name, is_ready, character, nakama_id}] | +| available_rooms | Array | [] | Discovered rooms for room list | +| is_host | bool | false | Whether local player is room host | +| is_lan_mode | bool | false | Direct ENet (no Nakama) | +| LAN_PORT | const int | 7777 | ENet server port | +| LAN_DISCOVERY_PORT | const int | 7778 | UDP broadcast port | +| local_player_name | String | "Player" | Display name | +| is_tutorial_mode | bool | false | Tutorial mode flag | +| match_duration | int | 180 | Seconds (configurable by host) | +| randomize_spawn | bool | false | Randomize spawn positions | +| enable_cycle_timer | bool | false | Goal cycle timer | +| scarcity_mode | String | "Normal" | Item scarcity: Normal/Aggressive/Chaos | +| disconnect_reason | String | "" | UI feedback message | +| sng_go_duration | int | 20 | Stop n Go: GO phase seconds | +| sng_stop_duration | int | 4 | Stop n Go: STOP phase seconds | +| sng_required_goals | int | 8 | Goals needed for SNG win | +| doors_swap_time | int | 15 | Tekton Doors: swap interval | +| doors_refresh_time | int | 25 | Tekton Doors: refresh interval | +| doors_required_goals | int | 8 | Goals needed for Doors win | +| rematch_votes | Array | [] | Player IDs who voted for rematch | +| available_characters | Array[String] | [...] | ["Copper", "Dabro", "Gatot", "Pip", "Random"] | +| available_areas | Array[String] | [] | Mode-specific area list | +| available_game_modes | Array[String] | [...] | ["Freemode", "Stop n Go", "Candy Pump Survival"] | +| selected_area | String | "Freemode Arena" | Currently selected area | +| game_mode | String | "Freemode" | Current game mode | +| local_character_index | int | 0 | Local player's character index | + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `room_list_updated` | rooms: Array | Room list refreshed | +| `room_joined` | room_data: Dictionary | Joined a room | +| `room_left` | none | Left current room | +| `player_joined` | player_data: Dictionary | Player entered room | +| `player_left` | player_id: int | Player left room | +| `ready_state_changed` | player_id: int, is_ready: bool | Player ready status changed | +| `all_players_ready` | none | All players ready | +| `host_disconnected` | none | Host left/disconnected | +| `game_starting` | none | Game countdown started | +| `match_duration_changed` | duration_seconds: int | Duration setting changed | +| `randomize_spawn_changed` | enabled: bool | Random spawn toggled | +| `character_changed` | player_id: int, character_name: String | Character selection changed | +| `area_changed` | area_name: String | Map area changed | +| `player_list_changed` | none | Player list should re-render | +| `rematch_votes_updated` | count: int, required: int | Rematch vote progress | +| `game_mode_changed` | mode: String | Game mode changed | +| `scarcity_mode_changed` | mode: String | Scarcity setting changed | +| `enable_cycle_timer_changed` | enabled: bool | Timer toggle changed | +| `sng_go_duration_changed` | duration: int | SNG Go duration changed | +| `sng_stop_duration_changed` | duration: int | SNG Stop duration changed | +| `sng_required_goals_changed` | goals: int | SNG required goals changed | +| `doors_swap_time_changed` | time: int | Doors swap interval changed | +| `doors_refresh_time_changed` | time: int | Doors refresh interval changed | +| `doors_required_goals_changed` | goals: int | Doors required goals changed | +| `gauntlet_round_duration_changed` | duration: int | Gauntlet round duration changed | +| `gauntlet_growth_interval_changed` | interval: float | Gauntlet growth interval changed | +| `gauntlet_cells_per_tick_changed` | cells: Dictionary | Cells per tick changed | + +**Key Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_tutorial` | `func start_tutorial(mode: String = "Freemode") -> void` | void | Sets tutorial flags, calls create_room_lan("Tutorial") | +| `create_room` | `func create_room(room_name: String) -> void` | void | Hosts Nakama room: connects, calls NakamaManager.host_game | +| `join_room` | `func join_room(match_id: String) -> void` | void | Joins Nakama room by match ID | +| `create_room_lan` | `func create_room_lan(room_name: String = "LAN Game") -> bool` | bool | Creates ENet server on LAN_PORT, broadcasts UDP discovery | +| `join_room_lan` | `func join_room_lan(host_ip: String) -> bool` | bool | Creates ENet client to host IP:LAN_PORT | +| `leave_room` | -- | void | Leaves current room, cleans up peers | +| `start_game` | `func start_game(is_tutorial: bool = false) -> void` | void | Transitions from lobby to main game scene | +| `refresh_room_list` | `func refresh_room_list() -> void` | void | Queries Nakama for available rooms or broadcasts LAN | +| `set_ready` | `func set_ready(is_ready: bool) -> void` | void | Updates ready state via RPC | +| `set_match_duration` | `func set_match_duration(seconds: int) -> void` | void | Host sets match duration | +| `set_randomize_spawn` | `func set_randomize_spawn(enabled: bool) -> void` | void | Host toggles random spawn | +| `set_enable_cycle_timer` | `func set_enable_cycle_timer(enabled: bool) -> void` | void | Host toggles timer | +| `set_scarcity_mode` | `func set_scarcity_mode(mode: String) -> void` | void | Host sets scarcity | +| `set_game_mode` | `func set_game_mode(mode: String) -> void` | void | Host sets game mode | +| `cycle_character` | `func cycle_character(direction: int) -> void` | void | Change character selection | +| `cycle_area` | `func cycle_area(direction: int) -> void` | void | Change selected area | +| `get_players` | `func get_players() -> Array` | Array | Returns players_in_room | +| `is_all_ready` | `func is_all_ready() -> bool` | bool | All players ready check | +| `set_sng_go_duration` | -- | void | Host sets SNG go time | +| `set_sng_stop_duration` | -- | void | Host sets SNG stop time | +| `set_sng_required_goals` | -- | void | Host sets SNG goals | +| `get_selected_area` | `func get_selected_area() -> String` | String | Returns current area name | +| `get_game_mode` | `func get_game_mode() -> GameMode.Mode` | GameMode.Mode | Converts string to GameMode enum | +| `is_game_mode` | `func is_game_mode(mode: GameMode.Mode) -> bool` | bool | Mode comparison helper | + +**Internal Functions:** `_on_match_joined`, `_on_peer_connected`, `_on_peer_disconnected`, `_on_server_disconnected`, `_update_available_areas`, `_start_lan_broadcast`, `_broadcast_lan_room`, `_stop_lan_broadcast`, `_update_lan_room_list`, `_listen_for_lan_discovery`, `_update_ready_state_rpc`, `_request_rematch`, `rpc_set_*`, `rpc_*`. + +**Dependencies:** NakamaManager, GameStateManager. +**Depended by:** LobbyRoom, LobbyRoomList, LobbyMainMenu, main.gd, player.gd, lobby.gd, SceneManager (loading screen). + +[Back to top](#top) + +### 4.3 GameStateManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/game_state_manager.gd` (66 lines) +**Extends:** Node +**Autoload name:** GameStateManager + +Simple state machine and match configuration constants. + +**Properties:** + +| Name | Type | Default | Description | +|---|---|---|---| +| current_state | GameState (enum) | LOBBY | Current application state | +| max_players | int | 8 | Max players in a match | +| enable_bots | bool | false | Bot fill toggle | +| local_player_id | int | 0 | Local peer ID | + +**Enums:** `GameState { LOBBY, LOADING, GAME, RESULT }` + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `state_changed` | new_state: GameState | Emitted on state transition | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `change_state` | `func change_state(new_state: GameState) -> void` | void | Transitions state, emits state_changed | + +**Dependencies:** None. +**Depended by:** LobbyManager, main.gd, tutorial_manager.gd, many managers. + +[Back to top](#top) + +### 4.4 PlayerManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/player_manager.gd` (37 lines) +**Extends:** Node +**Autoload name:** PlayerManager + +Lightweight data container for player metadata. Stores display name and peer ID for the local player. Used as a quick reference by various subsystems. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| display_name | String | Local player's display name | +| peer_id | int | Local player's multiplayer unique ID | + +**Signals:** None. + +**Public Functions:** None (data-only container). + +**Dependencies:** None. +**Depended by:** UIManager, player.gd, various managers needing player identity. + +[Back to top](#top) + +### 4.5 EventBus + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/event_bus.gd` (73 lines) +**Extends:** Node +**Autoload name:** EventBus + +Centralized observer pattern for inter-manager communication. Replaces direct cross-references between managers. + +**Constants (event names):** + +| Constant | Value | Description | +|---|---|---| +| EVENT_PLAYER_JOINED | "player_joined" | Player entered match | +| EVENT_PLAYER_LEFT | "player_left" | Player left match | +| EVENT_PLAYER_READY | "player_ready" | Player ready state changed | +| EVENT_MATCH_STARTED | "match_started" | Match began | +| EVENT_MATCH_ENDED | "match_ended" | Match ended | +| EVENT_GAME_MODE_CHANGED | "game_mode_changed" | Game mode switched | +| EVENT_CURRENCY_CHANGED | "currency_changed" | Wallet balance changed | +| EVENT_ITEM_PURCHASED | "item_purchased" | Item bought from shop | +| EVENT_GACHA_PULL | "gacha_pull" | Gacha rolled | +| EVENT_PROFILE_LOADED | "profile_loaded" | Profile loaded from server | +| EVENT_PROFILE_UPDATED | "profile_updated" | Profile updated | +| EVENT_AVATAR_CHANGED | "avatar_changed" | Avatar changed | +| EVENT_SESSION_REFRESHED | "session_refreshed" | Nakama session refreshed | +| EVENT_SESSION_EXPIRED | "session_expired" | Nakama session expired | + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `event_emitted` | event_name: String, data: Variant | Fired on every emit | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `emit` | `func emit(event_name: String, data: Variant = null) -> void` | void | Emit event to all registered listeners and the signal bus | +| `on` | `func on(event_name: String, callback: Callable) -> void` | void | Subscribe to event | +| `off` | `func off(event_name: String, callback: Callable) -> void` | void | Unsubscribe from event | +| `clear` | `func clear() -> void` | void | Remove all listeners (scene transition cleanup) | + +**Dependencies:** None. +**Depended by:** UserProfileManager, GachaManager, ShopManager, many managers for loose coupling. + +[Back to top](#top) + +### 4.6 GameMode / ModeConfig + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/game_mode.gd` (41 lines) +**Extends:** RefCounted +**class_name:** GameMode + +Enum and string conversion utilities for game modes. + +**Enum:** `Mode { FREEMODE = 0, STOP_N_GO = 1, TEKTON_DOORS = 2, GAUNTLET = 3 }` + +**Public Static Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `from_string` | `static func from_string(mode: String) -> Mode` | Mode | Converts "Freemode"/"Stop n Go"/"Tekton Doors"/"Candy Pump Survival" to enum | +| `mode_to_string` | `static func mode_to_string(mode: Mode) -> String` | String | Converts enum back to string | +| `is_restricted` | `static func is_restricted(mode: Mode) -> bool` | bool | Returns true for SNG, Doors, or Gauntlet | +| `get_all_modes` | `static func get_all_modes() -> Array[String]` | Array[String] | Returns all mode names | + +**File:** `/home/dev/tekton/scripts/mode_config.gd` (108 lines) +**Extends:** RefCounted +**class_name:** ModeConfig + +Schema-driven validation for game mode settings. Consolidates duplicated/inconsistent option toggles. + +**Public Static Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `get_defaults` | `static func get_defaults(mode: String) -> Dictionary` | Dictionary | Returns default config dict for mode | +| `validate_setting` | `static func validate_setting(mode: String, key: String, value: Variant) -> Dictionary` | Dictionary | Validates type, range, and allowed values for a single setting | +| `validate_config` | `static func validate_config(mode: String, config: Dictionary) -> Dictionary` | Dictionary | Validates entire config, returns errors array | +| `get_mode_settings` | `static func get_mode_settings(mode: String) -> Array` | Array | Returns list of setting keys for mode | +| `get_setting_schema` | `static func get_setting_schema(mode: String, key: String) -> Dictionary` | Dictionary | Returns schema for specific setting | +| `has_setting` | `static func has_setting(mode: String, key: String) -> bool` | bool | Checks if setting exists for mode | +| `get_supported_modes` | `static func get_supported_modes() -> Array` | Array | Returns all supported mode strings | + +**Dependencies:** None (standalone utility classes). +**Depended by:** LobbyManager, LobbyRoom, mode-specific managers. + +[Back to top](#top) + + +## 5. Player Subsystem Managers + +[Back to top](#top) + +### 5.1 PlayerMovementManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/player_movement_manager.gd` (33,053 chars) +**Extends:** Node +**Autoload name:** PlayerMovementManager + +Handles player movement physics, grid-based pathfinding, movement range highlighting, position syncing, and obstacle-aware navigation. Delegated from player.gd. + +**Signals:** (custom signals listed; full list from code) + +| Signal | Params | Description | +|---|---|---| +| `movement_started` | path: Array | Emitted when player begins moving | +| `movement_completed` | none | Emitted when movement tween finishes | +| `movement_interrupted` | none | Emitted when movement is cancelled | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `move_along_path` | `func move_along_path(player: Node, path: Array) -> void` | void | Tweens player along grid path | +| `find_path` | `func find_path(from: Vector2i, to: Vector2i, gridmap: Node) -> Array` | Array | A* or BFS pathfinding on grid | +| `highlight_movement_range` | `func highlight_movement_range(player: Node) -> void` | void | Shows reachable cells | +| `highlight_adjacent_cells` | `func highlight_adjacent_cells(player: Node) -> void` | void | Shows cardinal-adjacent cells | +| `rotate_towards_target` | `func rotate_towards_target(target_pos: Vector2i) -> void` | void | Smooth rotation to face target | +| `can_move_to` | `func can_move_to(pos: Vector2i, gridmap: Node) -> bool` | bool | Cell walkability check | +| `apply_stagger` | `func apply_stagger(duration: float) -> void` | void | Applies stun knockback | +| `sync_bump` | `func sync_bump(target_pos: Vector2i, is_soft: bool) -> void` | void | Visual bump animation | +| `set_player_moving` | `func set_player_moving(is_moving: bool) -> void` | void | Toggle movement state | + +**Dependencies:** player.gd (node refs), ObstacleManager, SpecialTilesManager, EnhancedGridMap. +**Depended by:** player.gd, main.gd. + +[Back to top](#top) + +### 5.2 PlayerInputManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/player_input_manager.gd` (7,292 chars) +**Extends:** Node +**Autoload name:** PlayerInputManager + +Captures and buffers player input events. Supports keyboard, mouse, gamepad, and touch inputs. Provides input state query API. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `input_received` | event: InputEvent | Raw input forwarded | +| `action_pressed` | action: String | Action mapped press (grab, put, move) | +| `action_released` | action: String | Action released | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `is_action_held` | `func is_action_held(action: String) -> bool` | bool | Check if action is currently held | +| `get_movement_direction` | `func get_movement_direction() -> Vector2i` | Vector2i | Grid-aligned movement cardinal | +| `get_look_direction` | `func get_look_direction(camera: Camera3D) -> Vector2` | Vector2 | Mouse-world direction | +| `flush_buffer` | `func flush_buffer() -> void` | void | Clear input buffer | +| `is_touch_active` | `func is_touch_active() -> bool` | bool | Whether touch controls are in use | + +**Dependencies:** TouchControls (autoload). +**Depended by:** player.gd, player_action_manager.gd. + +[Back to top](#top) + +### 5.3 PlayerActionManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/player_action_manager.gd` (8,828 chars) +**Extends:** Node +**Autoload name:** PlayerActionManager + +Action execution layer. Manages grab, put, arrange, tekton throw/knock actions. Handles action point consumption, cooldowns, and visual highlighting. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `action_executed` | action_type: String | Action performed | +| `action_failed` | reason: String | Action invalid | +| `action_points_changed` | points: int | AP updated | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `execute_grab` | `func execute_grab(player: Node, grid_pos: Vector2i) -> bool` | bool | Grab item from grid | +| `execute_put` | `func execute_put(player: Node, slot_index: int, grid_pos: Vector2i) -> bool` | bool | Put item from playerboard to grid | +| `execute_arrange` | `func execute_arrange(player: Node, from_slot: int, to_slot: int) -> bool` | bool | Rearrange playerboard slots | +| `consume_action_points` | `func consume_action_points(points: int) -> void` | void | Deduct action points | +| `can_afford_action` | `func can_afford_action() -> bool` | bool | Check AP > 0 | +| `after_action_completed` | `func after_action_completed() -> void` | void | Post-action cleanup: check win, cycle goals | +| `highlight_cells_if_authorized` | `func highlight_cells_if_authorized(cells: Array, item_id: int) -> void` | void | Show valid target cells | +| `highlight_empty_adjacent_cells` | `func highlight_empty_adjacent_cells() -> void` | void | Show empty adjacent cells for put | +| `highlight_occupied_playerboard_slots` | `func highlight_occupied_playerboard_slots() -> void` | void | Show occupied slots for grab | +| `highlight_random_valid_cells` | `func highlight_random_valid_cells() -> void` | void | Show random valid cells | +| `clear_highlights` | `func clear_highlights() -> void` | void | Remove all cell highlights | +| `clear_playerboard_highlights` | `func clear_playerboard_highlights() -> void` | void | Remove playerboard highlights | + +**Dependencies:** PlayerboardManager, PlayerInputManager, GoalsCycleManager. +**Depended by:** player.gd, main.gd. + +[Back to top](#top) + +### 5.4 PlayerboardManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/playerboard_manager.gd` (22,790 chars) +**Extends:** Node +**Autoload name:** PlayerboardManager + +Manages each player's inventory board (2x5 or 3x5 grid of item slots). Handles slot selection, item placement, auto-arrange for goal matching, drag-and-drop, and visual updates. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `slot_selected` | slot_index: int | Slot clicked/selected | +| `slot_deselected` | none | Selection cleared | +| `item_placed` | slot_index: int, item_id: int | Item added to slot | +| `item_removed` | slot_index: int | Item removed from slot | +| `playerboard_updated` | player_id: int, board: Array | Full board synced | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `grab_item` | `func grab_item(grid_pos: Vector2i) -> bool` | bool | Auto-place grabbed item into best-fit slot | +| `auto_put_item` | `func auto_put_item() -> bool` | bool | Put goal-matching tile from board to adjacent grid | +| `handle_slot_clicked` | `func handle_slot_clicked(slot_index: int) -> void` | void | Process slot click event | +| `handle_playerboard_slot_selected` | `func handle_playerboard_slot_selected(slot_index: int) -> void` | void | Handle slot selection for action | +| `handle_put_slot_selected` | `func handle_put_slot_selected(slot_index: int) -> void` | void | Handle slot chosen for put action | +| `arrange_playerboard_item` | `func arrange_playerboard_item(slot_index: int) -> void` | void | Move item to better slot | +| `select_playerboard_slot` | `func select_playerboard_slot(slot_index: int) -> void` | void | Mark slot as selected | +| `deselect_playerboard_slot` | `func deselect_playerboard_slot() -> void` | void | Clear slot selection | +| `target_playerboard_slot` | `func target_playerboard_slot(slot_index: int) -> void` | void | Target a slot for move | +| `untarget_playerboard_slot` | `func untarget_playerboard_slot() -> void` | void | Clear target | +| `can_move_to_target_playerboard_slot` | `func can_move_to_target_playerboard_slot() -> bool` | bool | Check if target slot is valid | +| `bot_grab_item` | `func bot_grab_item(pos: Vector2i, slot: int, x: int, y: int, z: int) -> void` | void | Bot performs grab | +| `bot_put_item` | `func bot_put_item(pos: Vector2i, slot: int, x: int, y: int, z: int) -> void` | void | Bot performs put | +| `bot_arrange_item` | `func bot_arrange_item(from_slot: int, to_slot: int) -> void` | void | Bot rearranges board | + +**Dependencies:** GoalsCycleManager, GoalManager, EnhancedGridMap (scene ref). +**Depended by:** player.gd, PlayerActionManager. + +[Back to top](#top) + +### 5.5 PowerupManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/powerup_manager.gd` (9,417 chars) +**Extends:** Node +**Autoload name:** PowerupManager + +Powerup/boost system. Tracks boost charge level, special ability availability, and consumes boost for charged actions. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `boost_changed` | amount: float | Boost level changed | +| `boost_full` | none | Boost reached 100% | +| `powerup_activated` | type: String | Powerup used | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `add_boost` | `func add_boost(amount: float) -> void` | void | Increment boost | +| `consume_boost` | `func consume_boost(amount: float) -> void` | void | Deduct boost | +| `can_use_special` | `func can_use_special() -> bool` | bool | Boost >= 100 | +| `get_boost_pct` | `func get_boost_pct() -> float` | float | 0.0 to 1.0 | +| `reset_boost` | `func reset_boost() -> void` | void | Set to 0 | + +**Dependencies:** None. +**Depended by:** player.gd (charged strike, knock), PlayerActionManager. + +[Back to top](#top) + + +## 6. Game Mode Managers + +[Back to top](#top) + +### 6.1 StopNGoManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/stop_n_go_manager.gd` (21,884 chars) +**Extends:** Node +**Autoload name:** StopNGoManager + +State machine for the Stop n Go game mode. Alternates between GO (movement allowed) and STOP (frozen) phases. Tracks winner via first player to complete required goals during GO phases. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `phase_changed` | phase: String ("go"/"stop") | GO/STOP transition | +| `countdown_tick` | seconds: int | Phase countdown tick | +| `sng_winner` | player_id: int | Winner determined | +| `sng_ended` | none | Minigame concluded | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_sng` | `func start_sng(go_duration: int, stop_duration: int, required_goals: int) -> void` | void | Initialize SNG with params | +| `stop_sng` | `func stop_sng() -> void` | void | End SNG minigame | +| `start_go_phase` | `func start_go_phase() -> void` | void | Begin GO timer | +| `start_stop_phase` | `func start_stop_phase() -> void` | void | Begin STOP timer, freeze all | +| `freeze_player` | `func freeze_player(player_id: int) -> void` | void | Stop player movement | +| `unfreeze_player` | `func unfreeze_player(player_id: int) -> void` | void | Resume player movement | +| `check_winner` | `func check_winner() -> int` | int | Returns winner peer_id or -1 | +| `get_phase` | `func get_phase() -> String` | String | Current phase | + +**Dependencies:** TurnManager, GoalManager, GoalsCycleManager, animation.gd (scene). +**Depended by:** main.gd, player.gd. + +[Back to top](#top) + +### 6.2 GauntletManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/gauntlet_manager.gd` (5,467 chars) +**Extends:** Node +**Autoload name:** GauntletManager + +Manages the Candy Pump Survival / Gauntlet game mode. Handles round progression, danger zone growth (flood fill), and elimination. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `round_started` | round: int | New round began | +| `danger_zone_grown` | cells: Array | New tiles flooded | +| `player_eliminated` | player_id: int | Player fell off/eliminated | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_gauntlet` | `func start_gauntlet(duration: int, growth_interval: float) -> void` | void | Initialize gauntlet mode | +| `stop_gauntlet` | `func stop_gauntlet() -> void` | void | End gauntlet mode | +| `eliminate_player` | `func eliminate_player(player_id: int) -> void` | void | Mark player as eliminated | +| `get_alive_players` | `func get_alive_players() -> Array` | Array | Returns non-eliminated player IDs | +| `get_round` | `func get_round() -> int` | int | Current round number | + +**Dependencies:** TurnManager, EnhancedGridMap. +**Depended by:** main.gd. + +[Back to top](#top) + +### 6.3 PortalModeManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/portal_mode_manager.gd` (20,072 chars) +**Extends:** Node +**Autoload name:** PortalModeManager + +Manages portal race mode (Tekton Doors variant). Tracks portal positions, door swapping, refresh cycles, and race completion. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `portals_swapped` | portal_pairs: Array | Doors swapped positions | +| `portals_refreshed` | portals: Array | New portal set spawned | +| `player_teleported` | player_id: int, from: Vector2i, to: Vector2i | Player used portal | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_portal_mode` | `func start_portal_mode(swap_time: int, refresh_time: int) -> void` | void | Initialize portal mode | +| `stop_portal_mode` | `func stop_portal_mode() -> void` | void | End portal mode | +| `teleport_player` | `func teleport_player(player: Node, portal_enter: Vector2i) -> void` | void | Teleport player through portal pair | +| `swap_portals` | `func swap_portals() -> void` | void | Randomize portal positions | +| `refresh_portals` | `func refresh_portals() -> void` | void | Spawn new portal set | +| `get_portal_pair` | `func get_portal_pair(portal_id: int) -> Array` | Array | Returns [entry, exit] positions | + +**Dependencies:** SpecialTilesManager, EnhancedGridMap. +**Depended by:** main.gd. + +[Back to top](#top) + +### 6.4 GoalManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/goal_manager.gd` (3,857 chars) +**Extends:** Node +**Autoload name:** GoalManager + +Goal definitions, validation rules, and completion detection. Checks if a player's board arrangement matches the current goal pattern. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `goal_completed` | player_id: int, goal_id: int | Player completed a goal | +| `goal_failed` | player_id: int, reason: String | Goal became impossible | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `validate_goal` | `func validate_goal(player_board: Array, goal: Dictionary) -> bool` | bool | Check board matches goal pattern | +| `get_goal_type` | `func get_goal_type(goal: Dictionary) -> String` | String | Goal category (row, col, set, pattern) | +| `is_goal_possible` | `func is_goal_possible(player_board: Array, goal: Dictionary) -> bool` | bool | Whether goal is still achievable | +| `find_best_slot_for_item` | `func find_best_slot_for_item(board: Array, item: int, goal: Dictionary) -> int` | int | Auto-place item into best slot | + +**Dependencies:** None. +**Depended by:** GoalsCycleManager, PlayerboardManager, StopNGoManager. + +[Back to top](#top) + +### 6.5 GoalsCycleManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/goals_cycle_manager.gd` (20,175 chars) +**Extends:** Node +**Autoload name:** GoalsCycleManager + +Manages cycling goal rotation. Tracks per-player score, cycles active goals on timer or action trigger, and determines when a player reaches the goal threshold to win. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `goals_cycled` | new_goals: Array | Active goals changed | +| `player_scored` | player_id: int, points: int | Player earned points | +| `player_won` | player_id: int | Player reached win threshold | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_cycle` | `func start_cycle(timer_enabled: bool) -> void` | void | Begin goal cycling | +| `stop_cycle` | `func stop_cycle() -> void` | void | Stop cycling | +| `cycle_goals` | `func cycle_goals() -> void` | void | Generate new goal set | +| `add_score` | `func add_score(player_id: int, points: int) -> void` | void | Award points to player | +| `get_player_score` | `func get_player_score(player_id: int) -> int` | int | Get player's current score | +| `get_current_goals` | `func get_current_goals() -> Array` | Array | Get active goals | +| `set_goal_threshold` | `func set_goal_threshold(goals_needed: int) -> void` | void | Set goals to win | + +**Dependencies:** GoalManager, TurnManager, Timer (scene). +**Depended by:** main.gd, PlayerActionManager, StopNGoManager. + +[Back to top](#top) + +### 6.6 PlayerRaceManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/player_race_manager.gd` (4,757 chars) +**Extends:** Node +**Autoload name:** PlayerRaceManager + +Race-specific logic. Tracks player race position, finish locations, lap progression, and race completion. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `position_changed` | player_id: int, pos: int | Player moved in race order | +| `lap_completed` | player_id: int, lap: int | Player finished a lap | +| `race_completed` | results: Array | Final standings [{id, position}] | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_race` | `func start_race() -> void` | void | Initialize race state | +| `end_race` | `func end_race() -> void` | void | Finalize race | +| `on_race_completed` | `func on_race_completed(final_pos: int) -> void` | void | Player crossed finish line | +| `get_current_finish_locations` | `func get_current_finish_locations() -> Array` | Array | Active finish positions | +| `update_finish_availability` | `func update_finish_availability() -> void` | void | Recalculate finish positions | +| `get_player_position` | `func get_player_position(player_id: int) -> int` | int | Current race order index | +| `add_second_lap_goals` | `func add_second_lap_goals(goals: Array) -> void` | void | Set lap 2 goals | + +**Dependencies:** GoalsCycleManager. +**Depended by:** player.gd, main.gd. + +[Back to top](#top) + +### 6.7 TurnManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/turn_manager.gd` (849 chars) +**Extends:** Node +**Autoload name:** TurnManager + +Turn-based sequencing for game modes that use round-robin or ordered turns (e.g., Stop n Go, Tekton Doors). + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| current_turn | int | Index in turn order | +| turn_order | Array | Player peer IDs in sequence | +| is_my_turn | bool | Whether local player is active | + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `turn_changed` | player_id: int | Active turn changed | +| `turn_order_set` | order: Array | Turn order established | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `set_turn_order` | `func set_turn_order(order: Array) -> void` | void | Establish turn sequence | +| `next_turn` | `func next_turn() -> void` | void | Advance to next player | +| `get_current_player` | `func get_current_player() -> int` | int | Current player peer ID | + +**Dependencies:** None. +**Depended by:** StopNGoManager, GauntletManager, GoalsCycleManager. + +[Back to top](#top) + + +## 7. Gameplay Managers + +[Back to top](#top) + +### 7.1 ObstacleManager + +[Back to top](#up) + +**File:** `/home/dev/tekton/scripts/managers/obstacle_manager.gd` (5,662 chars) +**Extends:** Node +**Autoload name:** ObstacleManager + +Obstacle placement and removal on the game grid. Handles wall tiles, blocking tiles, and destructible barriers. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `obstacle_placed` | cell: Vector3i, item_id: int | New obstacle added | +| `obstacle_removed` | cell: Vector3i | Obstacle destroyed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `place_obstacle` | `func place_obstacle(cell: Vector3i, item_id: int) -> void` | void | Set obstacle on grid layer | +| `remove_obstacle` | `func remove_obstacle(cell: Vector3i) -> void` | void | Clear obstacle | +| `is_cell_blocked` | `func is_cell_blocked(cell: Vector3i, gridmap: Node) -> bool` | bool | Check if cell has blocking tile | +| `get_blocked_cells` | `func get_blocked_cells(gridmap: Node) -> Array` | Array | All blocked cells | +| `clear_all_obstacles` | `func clear_all_obstacles() -> void` | void | Remove all obstacles | + +**Dependencies:** EnhancedGridMap (scene ref). +**Depended by:** PlayerMovementManager, SpecialTilesManager. + +[Back to top](#top) + +### 7.2 SpecialTilesManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/special_tiles_manager.gd` (23,090 chars) +**Extends:** Node +**Autoload name:** SpecialTilesManager + +Manages special floor tiles: ice (slippery), crack (breakable), portal tiles, teleporters, and other interactive terrain. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `tile_activated` | pos: Vector2i, tile_type: String | Tile effect triggered | +| `ice_slide_started` | player_id: int | Player started sliding | +| `crack_broke` | pos: Vector2i | Crack tile collapsed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `apply_tile_effect` | `func apply_tile_effect(player: Node, pos: Vector2i) -> void` | void | Activate tile effect on player | +| `get_tile_at` | `func get_tile_at(pos: Vector2i, gridmap: Node) -> int` | int | Item ID at position | +| `set_tile` | `func set_tile(pos: Vector2i, item_id: int, gridmap: Node) -> void` | void | Set tile item | +| `is_ice_tile` | `func is_ice_tile(item_id: int) -> bool` | bool | Check ice type | +| `is_crack_tile` | `func is_crack_tile(item_id: int) -> bool` | bool | Check crack type | +| `is_portal_tile` | `func is_portal_tile(item_id: int) -> bool` | bool | Check portal type | +| `spawn_portal_pair` | `func spawn_portal_pair(pos_a: Vector2i, pos_b: Vector2i) -> void` | void | Create portal entry/exit | +| `remove_portal_pair` | `func remove_portal_pair(pos_a: Vector2i, pos_b: Vector2i) -> void` | void | Remove portal tiles | + +**Dependencies:** EnhancedGridMap, ObstacleManager, PortalModeManager. +**Depended by:** PlayerMovementManager. + +[Back to top](#top) + +### 7.3 StaticTektonManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/static_tekton_manager.gd` (7,416 chars) +**Extends:** Node +**Autoload name:** StaticTektonManager + +Manages stationary Tekton turret behavior. Handles targeting, projectile spawning, and stun zones. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `turret_fired` | turret_id: int, target_pos: Vector2i | Turret shot | +| `turret_stunned` | turret_id: int | Turret disabled | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `activate_turret` | `func activate_turret(turret: Node) -> void` | void | Start turret behavior | +| `deactivate_turret` | `func deactivate_turret(turret: Node) -> void` | void | Stop turret | +| `fire_at_player` | `func fire_at_player(turret: Node, target: Vector2i) -> void` | void | Fire projectile at grid pos | + +**Dependencies:** EnhancedGridMap, ObstacleManager. +**Depended by:** main.gd. + +[Back to top](#top) + + +## 8. UI / Presentation Managers + +[Back to top](#top) + +### 8.1 UIManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/ui_manager.gd` (21,645 chars) +**Extends:** Node +**Autoload name:** UIManager + +Manages the UI layer stack: show/hide panels, overlay management, HUD elements, and dynamic UI creation. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `panel_opened` | panel_name: String | Panel shown | +| `panel_closed` | panel_name: String | Panel hidden | +| `hud_updated` | data: Dictionary | HUD refresh | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `show_panel` | `func show_panel(panel_name: String, data: Dictionary = {}) -> void` | void | Show named panel | +| `hide_panel` | `func hide_panel(panel_name: String) -> void` | void | Hide named panel | +| `toggle_panel` | `func toggle_panel(panel_name: String) -> void` | void | Toggle panel visibility | +| `show_hud` | `func show_hud() -> void` | void | Display HUD | +| `hide_hud` | `func hide_hud() -> void` | void | Hide HUD | +| `create_dynamic_ui` | `func create_dynamic_ui(scene_path: String) -> Node` | Node | Instantiate UI from tscn | +| `destroy_dynamic_ui` | `func destroy_dynamic_ui(ui_node: Node) -> void` | void | Remove dynamic UI | +| `focus_panel` | `func focus_panel(panel_name: String) -> void` | void | Bring panel to front | +| `get_active_panels` | `func get_active_panels() -> Array` | Array | Currently visible panels | + +**Dependencies:** None. +**Depended by:** main.gd, lobby.gd. + +[Back to top](#top) + +### 8.2 SfxManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/sfx_manager.gd` (2,046 chars) +**Extends:** Node +**Autoload name:** SfxManager + +Sound effect playback pool. Manages one-shot SFX with positional audio support. + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `play` | `func play(sfx_name: String, position: Vector3 = Vector3.ZERO) -> void` | void | Play SFX by name, optionally 3D positioned | +| `stop` | `func stop(sfx_name: String) -> void` | void | Stop specific SFX | +| `stop_all` | `func stop_all() -> void` | void | Silence all SFX | +| `set_volume` | `func set_volume(db: float) -> void` | void | Set master SFX volume | + +**Dependencies:** AudioStreamPlayer pool (scene). +**Depended by:** player.gd, StopNGoManager, UIManager, many gameplay managers. + +[Back to top](#top) + +### 8.3 MusicManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/music_manager.gd` (4,082 chars) +**Extends:** Node +**Autoload name:** MusicManager + +Background music controller. Handles crossfade between tracks, playlist sequencing, and volume control. + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_music` | `func start_music(track_name: String = "") -> void` | void | Begin playing track or playlist | +| `stop_music` | `func stop_music(fade: float = 0.5) -> void` | void | Fade out and stop | +| `crossfade_to` | `func crossfade_to(track_name: String, fade_duration: float = 1.0) -> void` | void | Smooth transition | +| `set_volume` | `func set_volume(db: float) -> void` | void | Set master music volume | +| `set_paused` | `func set_paused(paused: bool) -> void` | void | Pause/resume | +| `get_current_track` | `func get_current_track() -> String` | String | Currently playing track name | + +**Dependencies:** AudioStreamPlayer (scene). +**Depended by:** lobby.gd, main.gd. + +[Back to top](#top) + +### 8.4 NotificationManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/notification_manager.gd` (2,215 chars) +**Extends:** Node +**Autoload name:** NotificationManager + +On-screen message queue. Displays transient notification messages with type-based styling. + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| MessageType (enum) | {NORMAL, WARNING, POWERUP, ERROR, SYSTEM} | Message severity/style | + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `send_message` | `func send_message(sender: Node, message: String, msg_type: int = 0) -> void` | void | Queue message for display | +| `clear_messages` | `func clear_messages() -> void` | void | Clear all pending messages | +| `get_message_queue` | `func get_message_queue() -> Array` | Array | Current pending messages | + +**Dependencies:** None. +**Depended by:** player.gd, main.gd (unstuck feedback), StopNGoManager, many gameplay managers. + +[Back to top](#top) + +### 8.5 ScreenShake + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/screen_shake.gd` (1,839 chars) +**Extends:** Node +**Autoload name:** ScreenShake + +Camera screen shake effect manager. Applies noise-based displacement to Camera3D. + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `shake` | `func shake(intensity: float, duration: float = 0.3) -> void` | void | Trigger camera shake | +| `stop_shake` | `func stop_shake() -> void` | void | Stop ongoing shake | + +**Dependencies:** Camera3D (scene). +**Depended by:** player.gd (heavy knock triggers shake), main.gd. + +[Back to top](#top) + +### 8.6 CameraContextManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/camera_context_manager.gd` (2,543 chars) +**Extends:** Node +**Autoload name:** CameraContextManager + +Camera zoom level and context switching. Manages follow-camera behavior, zoom levels for different game phases, and camera transitions. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `zoom_changed` | level: float | Camera zoom level changed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `set_zoom` | `func set_zoom(level: float) -> void` | void | Set camera zoom | +| `get_zoom` | `func get_zoom() -> float` | float | Current zoom | +| `focus_on_player` | `func focus_on_player(player_id: int) -> void` | void | Snap camera to player | +| `focus_on_position` | `func focus_on_position(world_pos: Vector3) -> void` | void | Center camera on position | + +**Dependencies:** Camera3D (scene). +**Depended by:** main.gd. + +[Back to top](#top) + +### 8.7 TouchControls + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/touch_controls.gd` (23,640 chars) +**Extends:** Node +**Autoload name:** TouchControls + +Mobile touch input overlay. Provides virtual joystick, action buttons, and gesture recognition for grid-based controls. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `touch_moved` | direction: Vector2i | Grid direction from swipe | +| `action_triggered` | action: String | Touch button pressed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `set_joystick_enabled` | `func set_joystick_enabled(enabled: bool) -> void` | void | Toggle joystick | +| `get_joystick_direction` | `func get_joystick_direction() -> Vector2` | Vector2 | Normalized joystick | +| `_save_settings` | internal | void | Persist touch control settings | + +**Dependencies:** InputManager (scene). +**Depended by:** PlayerInputManager. + +[Back to top](#top) + +### 8.8 TutorialManager / TutorialOverlay + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/tutorial_manager.gd` (22,243 chars) +**Extends:** Node +**Autoload name:** TutorialManager + +Tutorial flow controller. Manages step-by-step tutorial sequences, triggers, and completion tracking. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `tutorial_started` | tutorial_id: String | Tutorial began | +| `step_completed` | step: int | Step finished | +| `tutorial_completed` | tutorial_id: String | Tutorial fully complete | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_tutorial` | `func start_tutorial(tutorial_id: String) -> void` | void | Begin tutorial sequence | +| `advance_step` | `func advance_step() -> void` | void | Move to next step | +| `skip_tutorial` | `func skip_tutorial() -> void` | void | Exit tutorial early | +| `is_tutorial_active` | `func is_tutorial_active() -> bool` | bool | Tutorial in progress | +| `get_current_step` | `func get_current_step() -> int` | int | Current step index | +| `get_total_steps` | `func get_total_steps() -> int` | int | Total steps in tutorial | + +**File:** `/home/dev/tekton/scripts/managers/tutorial_overlay.gd` (11,077 chars) +**Extends:** Node +**Autoload name:** TutorialOverlay + +Tutorial UI overlay. Displays step instructions, highlights UI elements, and provides step navigation. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `overlay_closed` | none | Overlay dismissed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `show_step` | `func show_step(step_data: Dictionary) -> void` | void | Display step with text + highlight | +| `hide_overlay` | `func hide_overlay() -> void` | void | Dismiss overlay | +| `highlight_element` | `func highlight_element(node_path: NodePath) -> void` | void | Spotlight a UI element | +| `clear_highlights` | `func clear_highlights() -> void` | void | Remove spotlights | + +**Dependencies:** TutorialManager, UIManager. +**Depended by:** TutorialManager. + +[Back to top](#top) + + +## 9. Social / Economy Managers + +[Back to top](#top) + +### 9.1 UserProfileManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/user_profile_manager.gd` (20,044 chars) +**Extends:** Node +**Autoload name:** UserProfileManager + +User profile CRUD operations. Manages display name, avatar, bio, wallet balance, stats, and loadout configuration. Syncs with Nakama storage. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `profile_loaded` | profile: Dictionary | Profile fetched from server | +| `profile_updated` | none | Profile modified locally | +| `wallet_updated` | wallet: Dictionary | Balance changed | +| `stats_updated` | stats: Dictionary | Player stats changed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `load_profile` | `func load_profile() -> void` | void (async) | Fetch profile from Nakama storage | +| `save_profile` | `func save_profile() -> void` | void (async) | Persist profile to Nakama | +| `get_display_name` | `func get_display_name(fallback: String = "Player") -> String` | String | Display name with fallback | +| `set_display_name` | `func set_display_name(name: String) -> void` | void | Update display name | +| `get_avatar_url` | `func get_avatar_url() -> String` | String | Current avatar path | +| `set_avatar` | `func set_avatar(url: String) -> void` | void | Change avatar | +| `get_wallet_balance` | `func get_wallet_balance(currency: String) -> int` | int | Balance for gold/star | +| `get_stats` | `func get_stats() -> Dictionary` | Dictionary | Player stats snapshot | +| `update_stats` | `func update_stats(delta: Dictionary) -> void` | void | Increment stats | +| `get_loadout` | `func get_loadout() -> Dictionary` | Dictionary | Current cosmetics loadout | +| `set_loadout` | `func set_loadout(loadout: Dictionary) -> void` | void | Save cosmetics config | +| `get_loadout_character` | `func get_loadout_character() -> String` | String | Selected character name | +| `sync_wallet` | `func sync_wallet() -> void` | void (async) | Refresh wallet from server | + +**Dependencies:** NakamaManager, EventBus, BackendService. +**Depended by:** LobbyMainMenu, lobby.gd, ShopManager, GachaManager, SkinManager, many UI panels. + +[Back to top](#top) + +### 9.2 GachaManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/gacha_manager.gd` (5,117 chars) +**Extends:** Node +**Autoload name:** GachaManager + +Gacha pull orchestration. Calls BackendService.perform_gacha_pull, processes results, updates inventory. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `gacha_result` | items: Array, fragments: Array | Pull results | +| `gacha_error` | error: String | Pull failed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `perform_pull` | `func perform_pull(gacha_id: String, count: int) -> void` | void (async) | Execute gacha pull RPC | +| `get_pity_count` | `func get_pity_count(banner_id: String) -> int` | int | Current pity counter | + +**Dependencies:** BackendService, UserProfileManager, EventBus. +**Depended by:** gacha_panel.tscn (scene UI). + +[Back to top](#top) + +### 9.3 SkinManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/skin_manager.gd` (13,909 chars) +**Extends:** Node +**Autoload name:** SkinManager + +Cosmetic skin system. Manages skin definitions, owned skins, equipped loadout, and applies cosmetics to character models. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `skin_equipped` | skin_id: String | Skin applied | +| `skin_unequipped` | skin_id: String | Skin removed | +| `inventory_updated` | owned_skins: Array | Inventory changed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `equip_skin` | `func equip_skin(skin_id: String, slot: String) -> void` | void | Equip skin to slot | +| `unequip_skin` | `func unequip_skin(slot: String) -> void` | void | Unequip from slot | +| `is_skin_owned` | `func is_skin_owned(skin_id: String) -> bool` | bool | Check ownership | +| `get_equipped_skins` | `func get_equipped_skins() -> Dictionary` | Dictionary | Current loadout | +| `apply_loadout` | `func apply_loadout(character_root: Node3D, loadout: Dictionary) -> void` | void | Apply cosmetics to 3D model | +| `get_skins_for_character` | `func get_skins_for_character(char_name: String) -> Array` | Array | Available skins | +| `get_all_skins` | `func get_all_skins() -> Array` | Array | All skin definitions | + +**Dependencies:** UserProfileManager. +**Depended by:** lobby.gd (3D preview), SkinShop UI. + +[Back to top](#top) + +### 9.4 ShopManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/shop_manager.gd` (484 chars) +**Extends:** Node +**Autoload name:** ShopManager + +Thin data layer for shop catalog. Currently a stub; full shop logic lives in scene scripts. + +**Properties:** Minimal (shop catalog array). + +**Signals:** None. + +**Public Functions:** None (data container only). + +**Dependencies:** BackendService. +**Depended by:** shop_panel.tscn (scene). + +[Back to top](#top) + +### 9.5 JoinManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/join_manager.gd` (484 chars) +**Extends:** Node +**Autoload name:** JoinManager + +Thin manager for join code input and validation. Minimal stub. + +**Properties:** Minimal. + +**Signals:** None. + +**Public Functions:** None (stub). + +**Dependencies:** None. +**Depended by:** lobby.gd (join code UI). + +[Back to top](#top) + +### 9.6 FriendManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/friend_manager.gd` (11,911 chars) +**Extends:** Node +**Autoload name:** FriendManager + +Friends list management. Handles friend requests, accept/reject, friend list sync, DM messaging, and lobby invitations. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `friend_list_updated` | friends: Array | Friend list refreshed | +| `friend_request_received` | from_user_id: String | Incoming request | +| `friend_added` | user_id: String | Friendship established | +| `friend_removed` | user_id: String | Friendship ended | +| `dm_message_received` | from_user_id: String, from_name: String, message: String | Direct message | +| `lobby_invite_received` | from_user_id: String, from_name: String, match_id: String | Lobby invitation | +| `friend_online_changed` | user_id: String, online: bool | Presence changed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `add_friend_by_id` | `func add_friend_by_id(nakama_id: String) -> bool` | bool (async) | Send friend request | +| `remove_friend` | `func remove_friend(user_id: String) -> void` | void (async) | Remove friendship | +| `accept_request` | `func accept_request(user_id: String) -> void` | void (async) | Accept friend request | +| `decline_request` | `func decline_request(user_id: String) -> void` | void (async) | Decline request | +| `get_friends` | `func get_friends() -> Array` | Array | Current friends list | +| `get_mutual_friends` | `func get_mutual_friends() -> Array` | Array | Friends also in room | +| `is_friend` | `func is_friend(nakama_id: String) -> bool` | bool | Check friendship | +| `send_dm` | `func send_dm(user_id: String, text: String) -> bool` | bool (async) | Send direct message | +| `get_dm_history` | `func get_dm_history(user_id: String) -> Array` | Array (async) | Fetch DM history | +| `send_lobby_invite` | `func send_lobby_invite(to_user_id: String, match_id: String) -> void` | void (async) | Send invitation | + +**Dependencies:** BackendService, NakamaManager. +**Depended by:** LobbyRoom, LobbyChat, lobby.gd. + +[Back to top](#top) + +### 9.7 MailManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/mail_manager.gd` (5,271 chars) +**Extends:** Node +**Autoload name:** MailManager + +Mail/inbox CRUD operations. Calls BackendService RPCs for get, claim, delete mail. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `mail_updated` | mails: Array | Mail list refreshed | +| `unread_count_changed` | count: int | Unread mail count | +| `mail_claimed` | mail_id: String, rewards: Dictionary | Reward collected | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `fetch_mail` | `func fetch_mail() -> void` | void (async) | Fetch mailbox | +| `claim_mail` | `func claim_mail(mail_id: String) -> void` | void (async) | Claim reward | +| `delete_mail` | `func delete_mail(mail_id: String) -> void` | void (async) | Delete mail | +| `get_unread_count` | `func get_unread_count() -> int` | int | Unread count | + +**Dependencies:** BackendService. +**Depended by:** lobby.gd, mailbox_panel.tscn (scene). + +[Back to top](#top) + +### 9.8 DailyRewardManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/daily_reward_manager.gd` (1,009 chars) +**Extends:** Node +**Autoload name:** DailyRewardManager + +Daily reward system. Handles claim state, reward config, and streak tracking. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `reward_claimed` | day: int, reward: Dictionary | Daily reward collected | +| `streak_updated` | streak: int | Consecutive days | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `claim_daily_reward` | `func claim_daily_reward() -> void` | void (async) | Claim today's reward | +| `get_reward_state` | `func get_reward_state() -> Dictionary` | Dictionary (async) | Current state + schedule | +| `can_claim_today` | `func can_claim_today() -> bool` | bool | Check if claimable | + +**Dependencies:** BackendService. +**Depended by:** daily_reward_panel.tscn (scene). + +[Back to top](#top) + +### 9.9 AdminManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/admin_manager.gd` (2,538 chars) +**Extends:** Node +**Autoload name:** AdminManager + +Admin panel state and permission checks. Determines if local player is admin or moderator. + +**Signals:** None. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_check_admin_status` | `func _check_admin_status() -> bool` | bool (async) | Verify admin via Nakama storage | +| `kick_player` | `func kick_player(player_id: int) -> void` | void (async) | Kick player from match | +| `ban_player` | `func ban_player(player_id: int) -> void` | void (async) | Ban player | +| `give_currency` | `func give_currency(gold: int, star: int) -> void` | void (async) | Admin give currency | + +**Dependencies:** BackendService, NakamaManager. +**Depended by:** admin_panel.tscn (scene), LobbyChat (/clear command). + +[Back to top](#top) + + +## 10. System Managers + +[Back to top](#top) + +### 10.1 SettingsManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/settings_manager.gd` (13,874 chars) +**Extends:** Node +**Autoload name:** SettingsManager + +User settings persistence. Reads/writes config to user://settings.cfg. Manages audio, video, gameplay, and control settings. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `setting_changed` | key: String, value: Variant | A setting was modified | +| `settings_reset` | none | All settings restored to defaults | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `get_setting` | `func get_setting(key: String, default: Variant = null) -> Variant` | Variant | Read setting value | +| `set_setting` | `func set_setting(key: String, value: Variant) -> void` | void | Write and persist setting | +| `reset_settings` | `func reset_settings() -> void` | void | Restore defaults | +| `load_settings` | `func load_settings() -> void` | void | Load from config file | +| `save_settings` | `func save_settings() -> void` | void | Write to config file | +| `get_all_settings` | `func get_all_settings() -> Dictionary` | Dictionary | Full settings snapshot | + +**Dependencies:** ConfigFile. +**Depended by:** Audio buses, video settings, gameplay UI, settings_menu.tscn. + +[Back to top](#top) + +### 10.2 SessionManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/session_manager.gd` (4,742 chars) +**Extends:** Node +**Autoload name:** SessionManager + +Nakama session refresh lifecycle. Monitors session expiry and auto-refreshes before expiration. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `session_refreshed` | none | Token refreshed | +| `session_expired` | none | Could not refresh | +| `session_warning` | seconds_remaining: int | About to expire | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `start_monitoring` | `func start_monitoring() -> void` | void | Begin session expiry timer | +| `stop_monitoring` | `func stop_monitoring() -> void` | void | Stop timer | +| `refresh_now` | `func refresh_now() -> void` | void (async) | Force refresh | + +**Dependencies:** NakamaManager. +**Depended by:** AuthManager. + +[Back to top](#top) + +### 10.3 GameUpdateManager + +[Back to top](#top) + +**File:** `/home/dev/tekton/scripts/managers/game_update_manager.gd` (14,405 chars) +**Extends:** Node +**Autoload name:** GameUpdateManager + +Hot-reload update system. Checks for patch.pck on the Gitea patches branch and downloads/loads it at runtime. + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `update_available` | version: String, changelog: String | New patch detected | +| `update_downloading` | progress: float | Download progress | +| `update_ready` | path: String | Patch downloaded and verified | +| `update_failed` | error: String | Download error | +| `up_to_date` | none | No update needed | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `check_for_updates` | `func check_for_updates() -> void` | void (async) | Query Gitea for latest patch | +| `download_update` | `func download_update() -> void` | void (async) | Download patch.pck | +| `apply_update` | `func apply_update() -> void` | void | Load patch from ProjectSettings | +| `get_current_version` | `func get_current_version() -> String` | String | Current client version | +| `get_available_version` | `func get_available_version() -> String` | String | Latest available version | + +**Dependencies:** HTTPRequest (scene). +**Depended by:** boot_screen.tscn, main.gd. + +[Back to top](#top) + + +## 11. Core Scene Scripts + +[Back to top](#top) + +### 11.1 main.gd (Main game scene controller) + +[Back to top](#top) + +**File:** `/home/dev/tekton/scenes/main.gd` (2956 lines) +**Extends:** Node +**Scene:** main.tscn + +The core game scene controller. Handles game initialization, player spawn, grid setup, goal cycle start, leaderboard display, pause menu, unstuck system, match cleanup, and result screen flow. + +**Key Properties:** + +| Name | Type | Description | +|---|---|---| +| enhanced_gridmap | Node | Reference to EnhancedGridMap child | +| player_scene | PackedScene | Player.tscn loaded | +| stop_n_go_winner_id | int | Winner's peer ID (-1 if none) | +| _unstuck_cooldown_remaining | float | Unstuck button cooldown | +| touch_controls | Node | TouchControls autoload ref | + +**Signals:** +- (none declared; uses method-based event routing) + +**Public Functions (selected key ones):** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Initializes ENet multiplayer, spawns players, starts goals cycle, sets up HUD | +| `_process` | `func _process(delta: float) -> void` | void | Unstuck cooldown tick | +| `_input` | `func _input(event: InputEvent) -> void` | void | ESC pause, F9 debug floor check | +| `initialize_game` | -- | void | Create EnhancedGridMap, spawn player scene instances | +| `spawn_player` | -- | Node | Instantiate player.tscn, position, set authority | +| `_spawn_local_player` | -- | void | Create local player node | +| `add_bot_players_if_needed` | -- | void | Fill remaining slots with bot players | +| `display_message` | `func display_message(message: String, type: int) -> void` | void (RPC) | Broadcast message to local player's UI | +| `request_leaderboard_sync` | `func request_leaderboard_sync() -> void` | void (RPC) | Client requests leaderboard from server | +| `sync_leaderboard_data` | `func sync_leaderboard_data(player_data: Array) -> void` | void (RPC authority) | Receive + render leaderboard | +| `_update_leaderboard_display` | internal | void | Local leaderboard refresh | +| `_render_leaderboard_entries` | internal | void | Populate leaderboard entries | +| `_get_ordinal` | `func _get_ordinal(n: int) -> String` | String | "1st", "2nd", "3rd", etc. | +| `can_rpc` | `func can_rpc() -> bool` | bool | Check multiplayer peer state | +| `check_multiplayer` | `func check_multiplayer() -> bool` | bool | Safety check for peer access | +| `_toggle_pause_menu` | internal | void | Show/hide pause overlay | +| `_on_resume_pressed` | -- | void | Close pause menu | +| `_on_how_to_play_pressed` | -- | void | Open help panel | +| `_on_settings_pressed` | -- | void | Open settings dynamically | +| `_on_quit_match_pressed` | -- | void | Leave match, return to lobby | +| `_on_unstuck_pressed` | -- | void | Teleport local player to safe position | +| `_find_safe_spawn_position` | internal | Vector2i | Scan grid for safe walkable cell | +| `_on_back_to_menu_pressed` | -- | void | Cleanup and transition to lobby | +| `_cleanup_multiplayer` | -- | void | NakamaManager.cleanup() wrapper | +| `_deferred_init_leaderboard` | internal | void | Delayed leaderboard init (1.5s) | +| `_on_rematch_pressed` | -- | void | Request rematch vote | +| `check_all_floors` | `func check_all_floors() -> void` | void | Debug F9: scan missing floor tiles | +| `update_visual_position` | on player | void | Snap player to grid-aligned world position | +| `grid_to_world` | on player | Vector3 | Convert grid Vector2i to world Vector3 | + +**RPCs (network-synced functions):** + +| Function | RPC Mode | Description | +|---|---|---| +| `request_leaderboard_sync` | any_peer | Client requests data from server | +| `sync_leaderboard_data` | authority, call_local | Server sends leaderboard to client | +| `display_message` | authority, call_local | Broadcast message to player UI | +| `sync_position` (on player) | any_peer, call_local | Sync grid position | +| `sync_grid_item` (on player) | any_peer, call_local | Sync grid cell item | +| `sync_goals` (on player) | any_peer, call_local | Sync active goals | +| `sync_rotation` (on player) | any_peer, call_local | Sync character rotation | +| `sync_bump` (on player) | any_peer, call_local, unreliable | Visual bump animation | +| `sync_knock_tekton` (on player) | any_peer, call_local, reliable | Knock tekton | +| `sync_grab_tekton` (on player) | any_peer, call_local, reliable | Grab roaming tekton | +| `sync_throw_tekton` (on player) | any_peer, call_local, reliable | Throw tekton | +| `sync_drop_tekton` (on player) | any_peer, call_local, reliable | Drop tekton | +| `set_spawn_position` (on player) | any_peer, call_local, reliable | Random spawn position | +| `complete_race` (on player) | any_peer, call_local, reliable | Player finished race | +| `force_action_state_none` (on player) | any_peer, call_local, reliable | Reset UI action state | +| `request_server_grab` (on player) | any_peer, reliable | Server-authoritative grab | +| `request_server_put` (on player) | any_peer, reliable | Server-authoritative put | +| `notify_spawn_selected` (on player) | any_peer, reliable | Occupancy sync for spawn | +| `trigger_screen_shake` | -- | Camera shake RPC | + +**Dependencies:** NakamaManager, LobbyManager, GameStateManager, PlayerMovementManager, PlayerActionManager, GoalsCycleManager, StopNGoManager, GauntletManager, PortalModeManager, PlayerRaceManager, PlayerboardManager, UIManager, SfxManager, MusicManager, NotificationManager, ScreenShake, CameraContextManager, TouchControls. +**Depended by:** (this is the root game scene, depends on everything). + +[Back to top](#top) + +### 11.2 player.gd + +[Back to top](#top) + +**File:** `/home/dev/tekton/scenes/player.gd` (2751 lines) +**Extends:** CharacterBody3D (assumed from Node3D methods) +**Scene:** player.tscn + +The player character controller. Handles movement, action execution (grab/put/arrange), tekton interaction (carry/snatch/throw/knock), grid positioning, bot AI, visual synchronization, and playerboard management delegation. + +**Key Properties:** + +| Name | Type | Default | Description | +|---|---|---|---| +| current_position | Vector2i | Vector2i(0, 0) | Grid-aligned position | +| cell_size | Vector3 | (1.0, 1.0, 1.0) | Grid cell dimensions | +| cell_offset | Vector3 | Vector3.ZERO | Visual position offset | +| is_player_moving | bool | false | Movement tween active | +| is_carrying_tekton | bool | false | Holding roaming tekton | +| carried_tekton | Node3D | null | Reference to carried tekton | +| is_charged_strike | bool | false | Charged attack mode | +| is_frozen | bool | false | Stun/freeze state | +| is_stop_frozen | bool | false | Stop n Go freeze | +| is_invisible | bool | false | Ghost mode | +| is_bot | bool | false | Bot AI flag | +| display_name | String | "" | Player display name | +| score | int | 0 | Match score | +| action_points | int | 1 | Actions per turn | +| playerboard | Array | [-1, -1, ...] | Item slot board | +| goals | Array | [] | Active goals | +| enhanced_gridmap | Node | null | Grid reference | +| anim_player | AnimationPlayer | null | Character animations | +| movement_manager | PlayerMovementManager | ref | Movement delegation | +| action_manager | PlayerActionManager | ref | Action delegation | +| playerboard_manager | PlayerboardManager | ref | Board delegation | +| race_manager | PlayerRaceManager | ref | Race delegate | +| powerup_manager | PowerupManager | ref | Boost/charge delegate | + +**Signals:** + +| Signal | Params | Description | +|---|---|---| +| `position_changed` | none | Player grid position changed | + +**Public Functions (selected key ones):** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Init references, connect signals, set initial position | +| `_physics_process` | `func _physics_process(delta: float) -> void` | void | Movement smoothing, carry timer, unstuck timer | +| `_input` | `func _input(event: InputEvent) -> void` | void | Click-to-move on gridmap, slot clicks | +| `grid_to_world` | `func grid_to_world(pos: Vector2i) -> Vector3` | Vector3 | Convert grid to world coordinates | +| `move_to_grid_position` | `func move_to_grid_position(target: Vector2i) -> void` | void | Initiate grid movement | +| `grab_item` | `func grab_item(grid_pos: Vector2i = current_position) -> bool` | bool | Delegates to playerboard_manager.grab_item | +| `auto_put_item` | `func auto_put_item() -> bool` | bool | Delegates auto-put | +| `handle_playerboard_slot_selected` | `func handle_playerboard_slot_selected(slot_index: int) -> void` | void | Delegates to playerboard_manager | +| `handle_put_slot_selected` | `func handle_put_slot_selected(slot_index: int) -> void` | void | Delegates put slot | +| `arrange_playerboard_item` | `func arrange_playerboard_item(slot_index: int) -> void` | void | Delegates arrange | +| `_on_slot_clicked` | `func _on_slot_clicked(event: InputEvent, slot_index: int) -> void` | void | Delegates to playerboard_manager | +| `has_item_at_current_position` | `func has_item_at_current_position() -> bool` | bool | Check grid cell occupancy | +| `has_items_in_playerboard` | `func has_items_in_playerboard() -> bool` | bool | Any items in board | +| `playerboard_is_full` | `func playerboard_is_full() -> bool` | bool | All slots filled | +| `highlight_movement_range` | `func highlight_movement_range() -> void` | void | Delegates to movement_manager | +| `highlight_adjacent_cells` | `func highlight_adjacent_cells() -> void` | void | Delegates to movement_manager | +| `highlight_cells_if_authorized` | `func highlight_cells_if_authorized(cells: Array, item_id: int) -> void` | void | Delegates to action_manager | +| `clear_highlights` | `func clear_highlights() -> void` | void | Clear grid highlights | +| `rotate_towards_target` | `func rotate_towards_target(target_pos: Vector2i) -> void` | void | Delegates to movement_manager | +| `select_playerboard_slot` | `func select_playerboard_slot(slot_index: int) -> void` | void | Delegates to playerboard_manager | +| `deselect_playerboard_slot` | `func deselect_playerboard_slot() -> void` | void | Clear selection | +| `target_playerboard_slot` | `func target_playerboard_slot(slot_index: int) -> void` | void | Target slot for move | +| `untarget_playerboard_slot` | `func untarget_playerboard_slot() -> void` | void | Clear target | +| `can_move_to_target_playerboard_slot` | `func can_move_to_target_playerboard_slot() -> bool` | bool | Target slot validity | +| `update_visual_position` | `func update_visual_position() -> void` | void | Snap to grid | +| `grab_tekton` | `func grab_tekton() -> void` | void | Tekton interaction: snatch or grab | +| `snatch_tekton` | `func snatch_tekton(target_carrier: Node3D) -> void` | void | Steal tekton from carrier | +| `throw_tekton` | `func throw_tekton() -> void` | void | Throw tekton in facing direction | +| `drop_tekton` | `func drop_tekton() -> void` | void | Drop tekton at current position | +| `enter_charged_strike` | `func enter_charged_strike() -> void` | void | Activate charged attack mode | +| `knock_tekton` | `func knock_tekton() -> void` | void | Special attack on nearby tekton | +| `update_active_player_indicator` | `func update_active_player_indicator() -> void` | void | Refresh visual state | +| `is_finish_position` | `func is_finish_position(pos: Vector2i) -> bool` | bool | Check if pos is a finish line | +| `_after_action_completed` | internal | void | Post-action: cycle goals, check win | +| `consume_action_points` | `func consume_action_points(points: int) -> void` | void | Deduct AP | +| `display_message` | on player | void | Show notification to this player | +| `apply_stagger` | `func apply_stagger(duration: float) -> void` | void | Stun for duration | + +**RPCs (network-synced functions on player.gd):** + +| Function | RPC Mode | Description | +|---|---|---| +| `sync_position` | any_peer, call_local | Sync current grid position | +| `sync_rotation` | any_peer, call_local | Sync Y rotation | +| `sync_grid_item` | any_peer, call_local | Sync grid cell item change | +| `sync_goals` | any_peer, call_local | Sync active goal set | +| `sync_second_lap_goals` | any_peer, call_local | Sync lap 2 goals | +| `sync_grab_tekton` | any_peer, call_local, reliable | Grab tekton network sync | +| `sync_snatch_tekton` | any_peer, call_local, reliable | Tekton theft sync | +| `sync_throw_tekton` | any_peer, call_local, reliable | Throw tekton sync | +| `sync_drop_tekton` | any_peer, call_local, reliable | Drop tekton sync | +| `sync_bump` | any_peer, call_local, unreliable | Visual bump animation | +| `sync_knock_tekton` | any_peer, call_local, reliable | Knock attack sync | +| `set_spawn_position` | any_peer, call_local, reliable | Random spawn position | +| `complete_race` | any_peer, call_local, reliable | Race completion | +| `force_action_state_none` | any_peer, call_local, reliable | Reset UI action state | +| `request_server_grab` | any_peer, reliable | Server-auth grab request | +| `request_server_put` | any_peer, reliable | Server-auth put request | +| `notify_spawn_selected` | any_peer, reliable | Spawn occupancy sync | +| `trigger_screen_shake` | (authority) | Screen shake RPC | +| `bot_grab_item` | any_peer, call_local | Bot grab sync | +| `bot_put_item` | any_peer, call_local | Bot put sync | +| `bot_arrange_item` | any_peer, call_local | Bot arrange sync | + +**Dependencies:** PlayerMovementManager, PlayerInputManager, PlayerActionManager, PlayerboardManager, PowerupManager, PlayerRaceManager, GoalsCycleManager, SfxManager, NotificationManager, EnhancedGridMap (scene node), LobbyManager. +**Depended by:** main.gd (spawned per player). + +[Back to top](#top) + +### 11.3 lobby.gd + +[Back to top](#top) + +**File:** `/home/dev/tekton/scenes/lobby.gd` (583 lines) +**Extends:** Control +**Scene:** lobby.tscn + +The lobby/home screen controller. Manages main menu, room creation/joining, player slots, server selection, character selection, settings, mail, chat, social panel, and 3D character preview. + +**Key Properties:** + +| Name | Type | Description | +|---|---|---| +| chat | LobbyChat | Chat helper instance | +| main_menu | LobbyMainMenu | Main menu helper | +| room_list_helper | LobbyRoomList | Room list helper | +| room_helper | LobbyRoom | Room/lobby helper | +| character_textures | Dictionary | {char_name: Texture2D} | +| profile_panel_instance | Control | Dynamic profile panel | +| shop_panel_instance | Control | Dynamic shop panel | +| daily_reward_panel_instance | Control | Daily reward panel | +| leaderboard_panel_instance | Control | Leaderboard panel | +| _mailbox_panel_instance | Control | Mail panel | +| social_panel_instance | Control | Social panel | +| _local_player_rank | int | Cached rank | +| _bot_names | Dictionary | Slot index -> bot name | +| _room_mode_filter | String | Room list filter | +| _is_hosting | bool | Re-entry guard | + +**UI Node References (onready):** + +| Variable | Node Path | Type | +|---|---|---| +| main_menu_panel | $MainMenuPanel | Control | +| main_title | %Title | Label | +| username_label | %Username | Label | +| create_room_btn | %CreateRoomBtn | Button | +| browse_rooms_btn | %BrowseRoomsBtn | Button | +| tutorial_btn | %TutorialBtn | Button | +| main_menu_profile_btn | %MainProfileBtn | Button | +| avatar_display | %AvatarDisplay | TextureRect | +| lobby_settings_btn | %SettingsBtn | Button | +| quit_btn | %QuitBtn | Button | +| character_root | %CharacterRoot | Node3D | +| anim_player | %AnimationPlayer | AnimationPlayer | +| gold_label | %GoldLabel | Label | +| star_label | %StarLabel | Label | +| server_option | %ServerOption | OptionButton | +| server_ip_input | %ServerIPInput | LineEdit | +| leaderboard_btn | %LeaderboardBtn | Button | +| shop_btn | %CartBtn | Button | +| top_right_profile_btn | %ProfileBtn | Button | +| mailbox_btn | %MailboxBtn | Button | +| mail_badge | %MailBadge | Label | +| banner1_btn | %Banner1 | Button | +| ticket_btn | %TicketBtn | Button | +| room_list_panel | %RoomListPanel | Control | +| room_list | %RoomList | ItemList | +| match_id_input | %MatchIdInput | LineEdit | +| refresh_btn | %RefreshBtn | Button | +| join_btn | %JoinBtn | Button | +| back_btn | %RoomListCloseBtn | Button | +| lobby_panel | $LobbyPanel | Control | +| host_banner | $LobbyPanel/HostBanner | Panel | +| match_id_display | $LobbyPanel/TopBar/... | Label | +| copy_id_btn | $LobbyPanel/TopBar/... | Button | +| duration_option | $LobbyPanel/TopBar/... | OptionButton | +| random_spawn_check | `" " ` | CheckButton | +| enable_timer_check | `" " ` | CheckButton | +| scarcity_option | `" " ` | OptionButton | +| game_mode_option | `" " ` | OptionButton | +| players_container | $LobbyPanel/PlayersContainer | Control | +| area_selector | $LobbyPanel/AreaSelector | Control | +| leave_btn | $LobbyPanel/BottomBar/LeaveBtn | Button | +| ready_btn | $LobbyPanel/BottomBar/ReadyBtn | Button | +| start_game_btn | $LobbyPanel/BottomBar/StartGameBtn | Button | +| connection_status | $StatusBar/ConnectionStatus | Label | +| chat_display | %RichTextLabel | RichTextLabel | +| chat_input | %ChatInput | LineEdit | +| chat_send_btn | %SendBtn | Button | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_ready` | auto-called | void | Initialize all helpers, load textures, setup UI, connect signals | +| `_setup_3d_preview` | `func _setup_3d_preview() -> void` | void | Swap character model in SubViewport | +| `_load_character_textures` | `func _load_character_textures() -> void` | void | Load preview textures | +| `_on_server_option_selected` | `func _on_server_option_selected(index: int) -> void` | void | Handle server type dropdown | +| `_on_server_ip_submitted` | `func _on_server_ip_submitted(new_text: String) -> void` | void | Handle IP input | +| `_setup_game_modes` | `func _setup_game_modes() -> void` | void | Populate game mode dropdown | +| `_setup_player_slots` | `func _setup_player_slots() -> void` | void | Collect player slot nodes | +| `_connect_slot_signals` | `func _connect_slot_signals(slot: Control, i: int)` | void | Wire character nav buttons | +| `_show_panel` | `func _show_panel(panel_name: String) -> void` | void | Toggle main_menu/room_list/lobby panels | +| `_update_settings_visibility` | `func _update_settings_visibility() -> void` | void | Show/hide settings by mode and host status | +| `_create_custom_settings_ui` | `func _create_custom_settings_ui() -> void` | void | Build SNG/Tekton Doors settings dynamically | +| `_sync_room_profile_card` | `func _sync_room_profile_card() -> void` | void | Refresh username, score, rank, avatar, currency | +| `_apply_loadout_character` | `func _apply_loadout_character() -> void` | void | Apply saved character to LobbyManager | +| `admin_wipe_chat` | `func admin_wipe_chat() -> void` | void (async) | Admin: clear global chat | +| `admin_purge_chat` | `func admin_purge_chat(max_age_days: int) -> int` | int (async) | Admin: purge old messages | + +**Dependencies:** AuthManager, NakamaManager, LobbyManager, UserProfileManager, SkinManager, MusicManager, FriendManager, MailManager, BackendService. +**Depended by:** (root lobby scene; no dependents). + +[Back to top](#top) + +### 11.4 animation.gd + +[Back to top](#top) + +**File:** `/home/dev/tekton/scenes/animation.gd` (41 lines) +**Extends:** Control +**Scene:** (embedded in main.tscn for Stop n Go UI) + +Stop n Go phase animation player. Controls ready-go countdown, stop phase overlay, safe zone, and go animation sequences. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `play_ready_go` | `func play_ready_go() -> void` | void | Play ready-set-go sequence | +| `play_stop_phase` | `func play_stop_phase() -> void` | void | Play STOP overlay | +| `play_safe_zone_appear` | `func play_safe_zone_appear() -> void` | void | Show safe zone indicator | +| `stop_phase_anim_play` | `func stop_phase_anim_play() -> void` | void | Play stop phase spritesheet | +| `stop_phase_anim_stop` | `func stop_phase_anim_stop() -> void` | void | Stop phase animation | +| `play_countdown_30s` | `func play_countdown_30s() -> void` | void | 30-second countdown | +| `play_countdown_15s` | `func play_countdown_15s() -> void` | void | 15-second countdown | +| `play_go_animation` | `func play_go_animation() -> void` | void | GO animation | +| `play_go_finish_animation` | `func play_go_finish_animation() -> void` | void | Finish line animation | + +**Dependencies:** AnimatedSprite2D, AnimationPlayer (scene nodes). +**Depended by:** StopNGoManager, main.gd. + +[Back to top](#top) + + +## 12. UI Helper Classes (RefCounted) + +[Back to top](#top) + +All UI helper classes are RefCounted objects instantiated by lobby.gd in _ready(). They do NOT extend Node -- they are lightweight event wiring and state management objects. + +### 12.1 LobbyMainMenu + +[Back to top](#top) + +```gdscript +class_name LobbyMainMenu extends RefCounted +``` + +**File:** `/home/dev/tekton/scenes/ui/lobby_main_menu.gd` (338 lines) + +Event wiring for main menu buttons. Connects all lobby button signals to handler methods. + +**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects 15+ button signals. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `on_tutorial_pressed` | `func on_tutorial_pressed() -> void` | void | Set name, apply loadout, call LobbyManager.start_tutorial | +| `on_create_room_pressed` | `func on_create_room_pressed() -> void` | void | Show room list panel, create tab | +| `host_room` | `func host_room(game_mode: String) -> void` | void | Guarded double-click, set name/mode, create Nakama or LAN room | +| `on_browse_rooms_pressed` | `func on_browse_rooms_pressed() -> void` | void | Show room list, browse tab, refresh | +| `on_profile_btn_pressed` | `func on_profile_btn_pressed() -> void` | void | Instantiate and show profile_panel.tscn | +| `on_mailbox_pressed` | `func on_mailbox_pressed() -> void` | void | Instantiate and show mailbox_panel.tscn | +| `on_settings_pressed` | `func on_settings_pressed() -> void` | void | Instantiate and show settings_menu.tscn | +| `restore_after_settings` | `func restore_after_settings() -> void` | void | Restore lobby/main_menu panel visibility | +| `on_shop_pressed` | `func on_shop_pressed() -> void` | void | Instantiate and show shop_panel.tscn | +| `on_banner1_pressed` | `func on_banner1_pressed() -> void` | void | Instantiate and show gacha_panel.tscn | +| `on_leaderboard_pressed` | `func on_leaderboard_pressed() -> void` | void | Show leaderboard_panel.tscn | +| `on_ticket_pressed` | `func on_ticket_pressed() -> void` | void | Show daily_reward_panel.tscn | +| `on_social_pressed` | `func on_social_pressed() -> void` | void | Show social_panel.tscn, hide main menu UI | +| `on_logout_pressed` | `func on_logout_pressed() -> void` | void | AuthManager.logout() -> login screen | +| `on_quit_pressed` | `func on_quit_pressed() -> void` | void | get_tree().quit() | +| `go_to_login` | `func go_to_login() -> void` | void | Change scene to login_screen.tscn | + +**Dependencies:** AuthManager, LobbyManager, UserProfileManager, NakamaManager, BackendService. +**Depended by:** lobby.gd. + +[Back to top](#top) + +### 12.2 LobbyRoom + +[Back to top](#top) + +```gdscript +class_name LobbyRoom extends RefCounted +``` + +**File:** `/home/dev/tekton/scenes/ui/lobby_room.gd` (432 lines) + +Room/lobby panel event wiring. Handles ready/start/leave buttons, player slot rendering, character navigation, game mode/duration/scarcity settings, friend invites, and lobby invitation popup. + +**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects 20+ signals. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_on_ready_toggled` | `func _on_ready_toggled(is_ready: bool) -> void` | void | Toggle ready state | +| `_on_start_game_pressed` | `func _on_start_game_pressed() -> void` | void | Host starts game | +| `_on_leave_pressed` | `func _on_leave_pressed() -> void` | void | Leave room, release bot names | +| `_on_copy_id_pressed` | `func _on_copy_id_pressed() -> void` | void | Copy match ID to clipboard | +| `_on_duration_selected` | `func _on_duration_selected(index: int) -> void` | void | Host sets match duration | +| `_on_random_spawn_toggled` | `func _on_random_spawn_toggled(toggled_on: bool) -> void` | void | Toggle random spawn | +| `_on_enable_timer_toggled` | `func _on_enable_timer_toggled(toggled_on: bool) -> void` | void | Toggle cycle timer | +| `_on_scarcity_selected` | `func _on_scarcity_selected(index: int) -> void` | void | Host sets scarcity | +| `_on_scarcity_mode_changed` | `func _on_scarcity_mode_changed(mode: String) -> void` | void | UI update for scarcity | +| `_on_game_mode_selected` | `func _on_game_mode_selected(index: int) -> void` | void | Host sets game mode | +| `_on_game_mode_changed` | `func _on_game_mode_changed(mode: String) -> void` | void | UI update for game mode | +| `_on_sng_update` | `func _on_sng_update(_val: int = 0) -> void` | void | Sync SNG setting UI | +| `_on_doors_update` | `func _on_doors_update(_val: int = 0) -> void` | void | Sync Doors setting UI | +| `_on_room_joined` | `func _on_room_joined(room_data: Dictionary) -> void` | void | Switch to lobby panel, populate settings | +| `_on_room_left` | `func _on_room_left() -> void` | void | Return to main menu | +| `_on_host_disconnected` | `func _on_host_disconnected() -> void` | void | Show disconnect message | +| `_on_player_joined` | `func _on_player_joined(player_data: Dictionary) -> void` | void | Update slots + status | +| `_on_player_left` | `func _on_player_left(_player_id: int) -> void` | void | Update slots | +| `_on_ready_state_changed` | `func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void` | void | Update slot visuals | +| `_on_all_players_ready` | `func _on_all_players_ready() -> void` | void | Enable start button | +| `_on_game_starting` | `func _on_game_starting() -> void` | void | Transition to main.tscn | +| `_update_player_slots` | `func _update_player_slots() -> void` | void | Render all 8 player slots (players + bot slots) | +| `_update_status` | `func _update_status() -> void` | void | Show ready count | +| `_on_add_friend_pressed` | `func _on_add_friend_pressed(nakama_id: String) -> void` | void (async) | Add friend by Nakama ID | +| `on_invite_friends_pressed` | `func on_invite_friends_pressed() -> void` | void | Open invite dialog | +| `_on_lobby_invite_received` | `func _on_lobby_invite_received(from_user_id: String, from_name: String, match_id: String) -> void` | void | Show invite popup | +| `_on_invite_accepted` | `func _on_invite_accepted() -> void` | void | Join invited match | + +**Dependencies:** LobbyManager, FriendManager, NakamaManager, NameGenerator, UserProfileManager. +**Depended by:** lobby.gd. + +[Back to top](#top) + +### 12.3 LobbyRoomList + +[Back to top](#top) + +```gdscript +class_name LobbyRoomList extends RefCounted +``` + +**File:** `/home/dev/tekton/scenes/ui/lobby_room_list.gd` (155 lines) + +Room list panel event wiring. Handles room list refresh, selection, join, and back navigation. + +**Constructor:** `func _init(p_lobby: Control)` -- Stores lobby ref, connects signals. + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `_on_refresh_pressed` | `func _on_refresh_pressed() -> void` | void | Clear + refresh room list | +| `_on_room_selected` | `func _on_room_selected(index: int) -> void` | void | Copy room match_id/IP to input | +| `_on_room_activated` | `func _on_room_activated(index: int) -> void` | void | Select + auto-join | +| `_on_join_pressed` | `func _on_join_pressed() -> void` | void | Validate input, set name, join room (LAN or Nakama) | +| `_on_back_pressed` | `func _on_back_pressed() -> void` | void | Return to main menu | +| `on_room_list_updated` | `func on_room_list_updated(rooms: Array) -> void` | void | Render room rows, apply mode filter | + +**Dependencies:** LobbyManager, AuthManager, UserProfileManager. +**Depended by:** lobby.gd. + +[Back to top](#top) + +### 12.4 LobbyChat + +[Back to top](#top) + +```gdscript +class_name LobbyChat extends RefCounted +``` + +**File:** `/home/dev/tekton/scenes/ui/lobby_chat.gd` (373 lines) + +Global and direct message chat system. Handles Nakama socket channel chat, DM tabs, friend suggestions, and admin chat commands. + +**Constants:** `GLOBAL_CHAT_ROOM = "social_global"` + +**Properties:** + +| Name | Type | Description | +|---|---|---| +| _chat_channel | NakamaChannel | Current chat channel | +| _chat_messages | Array | [{sender, content, ts, date}] | +| _active_chat_context | String | "global" or user_id | +| _dm_tabs | Dictionary | user_id -> HBoxContainer (tab UI) | +| _dm_messages | Dictionary | user_id -> Array of messages | +| _chat_config | Dictionary | {prefix, max_messages, max_age_days} | + +**Public Functions:** + +| Function | Signature | Return | Description | +|---|---|---|---| +| `join_global_chat` | `func join_global_chat() -> void` | void (async) | Join social_global channel, fetch history, inject prefix | +| `switch_chat_tab` | `func switch_chat_tab(context_id: String) -> void` | void | Switch between global and DM tabs | +| `_on_chat_send_pressed` | `func _on_chat_send_pressed() -> void` | void (async) | Send message: @username for DM, /clear for admin | +| `on_lobby_dm_received` | `func on_lobby_dm_received(from_user_id: String, from_name: String, message: String) -> void` | void | Incoming DM handler | +| `leave_global_chat` | `func leave_global_chat() -> void` | void (async) | Disconnect and leave channel | + +**Internal Functions:** `_add_chat_message`, `_send_dm_message`, `_open_dm_tab`, `_create_dm_tab`, `_close_dm_tab`, `_inject_local_message`, `_trim_old_messages`, `_refresh_chat_display`, `_format_nakama_time`, `_get_local_time`, `_on_chat_input_changed`, `_on_friend_suggest_activated`, `_setup_friend_suggest_ui`. + +**Dependencies:** NakamaManager, BackendService, FriendManager, AdminManager, UserProfileManager. +**Depended by:** lobby.gd. + +[Back to top](#top) + + +## 13. Dependency Graph + +[Back to top](#top) + +### 13.1 Manager Autoload Dependencies + +[Back to top](#top) + +ASCII diagram showing which autoloads reference others: + +``` +NakamaManager (no deps on other managers -- pure Nakama SDK) + | + +-- BackendService + | +-- SteamworksManager (child node, not autoload) + | + +-- AuthManager + | +-- NakamaManager + | +-- BackendService + | + +-- SessionManager + | +-- NakamaManager + | + +-- LobbyManager + | +-- NakamaManager + | +-- GameStateManager + | + +-- GameStateManager (no deps) + | + +-- PlayerManager (no deps -- data only) + | + +-- EventBus (no deps -- pure observer) + | + +-- UserProfileManager + | +-- NakamaManager + | +-- BackendService + | +-- EventBus + | + +-- FriendManager + | +-- BackendService + | +-- NakamaManager + | + +-- MailManager + | +-- BackendService + | + +-- GachaManager + | +-- BackendService + | +-- UserProfileManager + | +-- EventBus + | + +-- DailyRewardManager + | +-- BackendService + | + +-- AdminManager + | +-- BackendService + | +-- NakamaManager + | + +-- SkinManager + | +-- UserProfileManager + | + +-- ShopManager + | +-- BackendService (thin) + | + +-- PlayerInputManager + | +-- TouchControls + | + +-- PlayerMovementManager + | +-- ObstacleManager + | +-- SpecialTilesManager + | +-- EnhancedGridMap (scene) + | + +-- PlayerActionManager + | +-- PlayerboardManager + | +-- PlayerInputManager + | +-- GoalsCycleManager + | + +-- PlayerboardManager + | +-- GoalsCycleManager + | +-- GoalManager + | + +-- PlayerRaceManager + | +-- GoalsCycleManager + | + +-- GoalsCycleManager + | +-- GoalManager + | +-- TurnManager + | +-- Timer (scene) + | + +-- StopNGoManager + | +-- TurnManager + | +-- GoalManager + | +-- GoalsCycleManager + | +-- animation.gd (scene) + | + +-- GauntletManager + | +-- TurnManager + | +-- EnhancedGridMap + | + +-- PortalModeManager + | +-- SpecialTilesManager + | +-- EnhancedGridMap + | + +-- SpecialTilesManager + | +-- ObstacleManager + | +-- EnhancedGridMap + | + +-- ObstacleManager + | +-- EnhancedGridMap + | + +-- StaticTektonManager + | +-- EnhancedGridMap + | +-- ObstacleManager + | + +-- PowerupManager (no deps) + | + +-- UIManager (no deps -- dynamic UI) + | + +-- SettingsManager (no deps -- ConfigFile) + | + +-- GameUpdateManager (HTTPRequest -- no manager deps) + | + +-- TutorialManager + | +-- TutorialOverlay + | + +-- TutorialOverlay + | +-- TutorialManager + | +-- UIManager + | + +-- MusicManager (no deps) + +-- SfxManager (no deps) + +-- ScreenShake (no deps) + +-- NotificationManager (no deps) + +-- CameraContextManager (no deps) + +-- TouchControls (no deps) + +-- JoinManager (no deps -- stub) +``` + +[Back to top](#top) + +### 13.2 Cross-Manager Signal Wiring + +[Back to top](#top) + +Key signal connections between managers and scene scripts: + +``` +NakamaManager.match_joined -> LobbyManager._on_match_joined +NakamaManager.match_join_error -> lobby.gd (clears _is_hosting) +NakamaManager.connection_failed -> lobby.gd (clears _is_hosting) + +LobbyManager.room_joined -> LobbyRoom._on_room_joined +LobbyManager.room_left -> LobbyRoom._on_room_left +LobbyManager.host_disconnected -> LobbyRoom._on_host_disconnected +LobbyManager.player_joined -> LobbyRoom._on_player_joined +LobbyManager.player_left -> LobbyRoom._on_player_left +LobbyManager.ready_state_changed -> LobbyRoom._on_ready_state_changed +LobbyManager.all_players_ready -> LobbyRoom._on_all_players_ready +LobbyManager.game_starting -> LobbyRoom._on_game_starting +LobbyManager.game_mode_changed -> LobbyRoom._on_game_mode_changed +LobbyManager.room_list_updated -> LobbyRoomList.on_room_list_updated +LobbyManager.character_changed -> LobbyRoom._on_character_changed +LobbyManager.rematch_votes_updated -> main.gd (update rematch button) + +FriendManager.dm_message_received -> LobbyChat.on_lobby_dm_received +FriendManager.lobby_invite_received -> LobbyRoom._on_lobby_invite_received + +MailManager.unread_count_changed -> lobby.gd (update badge) + +UserProfileManager.profile_loaded -> lobby.gd (_sync_room_profile_card) +UserProfileManager.profile_updated -> lobby.gd (_sync_room_profile_card) + +AuthManager.logged_out -> LobbyMainMenu.go_to_login +``` + +[Back to top](#top) + + +## 14. Scene Node Trees + +[Back to top](#top) + +### 14.1 main.tscn + +[Back to top](#top) + +``` +Main (Node) -- attached: main.gd + +-- EnhancedGridMap (GridMap / custom EnhancedGridMap node) + +-- PlayerSpawnPoints (Node3D) + +-- HUD (CanvasLayer) + | +-- LeaderboardPanel (Panel) + | | +-- MarginContainer/VBox + | | +-- Entry1-8 (HBoxContainer with RankLabel/NameLabel/ScoreLabel) + | +-- NotificationOverlay (Control) + | +-- ActionButtons (Control) + +-- PauseMenu (Panel) + | +-- Panel/VBox/ResumeBtn, HowToPlayBtn, SettingsBtn, UnstuckBtn, QuitMatchBtn + +-- HowToPlayPanel (Panel) + +-- StopNGoUI (Control) -- attached: animation.gd + | +-- StopPhase/AnimatedSprite2D + | +-- AnimationPlayer + | +-- CountDown/CountDownAnimation (AnimatedSprite2D) + | +-- GoFinish/GoAnimation2D (AnimatedSprite2D) + +-- Camera3D + +-- WorldEnvironment + +-- Player instances (added dynamically by main.gd) + +-- Player (CharacterBody3D) -- attached: player.gd + +-- MeshInstance3D (visual) + +-- CollisionShape3D + +-- PlayerboardUI (Control overlay) + +-- AnimationPlayer +``` + +[Back to top](#top) + +### 14.2 player.tscn + +[Back to top](#top) + +``` +Player (CharacterBody3D) -- attached: player.gd + +-- MeshInstance3D (character model) + +-- CollisionShape3D + +-- PlayerboardUI (Control) + | +-- Slot0-9 (Panel/TextureRect) + +-- AnimationPlayer + +-- (tektons picked up become children at runtime) +``` + +[Back to top](#top) + +### 14.3 lobby.tscn + +[Back to top](#top) + +``` +Lobby (Control) -- attached: lobby.gd + +-- StatusBar (HBoxContainer) + | +-- ConnectionStatus (Label) + +-- MainMenuPanel (Panel) + | +-- Title/Username/Subtitle/Buttons (CreateRoom, BrowseRooms, Tutorial, etc.) + | +-- CharacterRoot (SubViewportContainer > SubViewport > Node3D) + | | +-- Oldpop, Masbro, Gatot, Bob (character meshes, hidden by default) + | +-- %AnimationPlayer + | +-- CurrencyLabels (GoldLabel, StarLabel) + | +-- ServerOption / ServerIPInput + | +-- LeaderboardBtn, CartBtn, ProfileBtn, MailboxBtn, Banner1Btn, TicketBtn + +-- RoomListPanel (Control) + | +-- RoomListTabs (TabContainer) + | +-- RoomTab, PlayTab + | +-- MatchIdInput, RefreshBtn, JoinBtn, BackBtn + | +-- RoomList (ItemList) + | +-- ItemTemplate (hidden) + | +-- ProfileCard (PlayerUsername, PlayerScore, Rank, Avatar) + +-- LobbyPanel (Panel) + | +-- RoomNameHeader + | +-- HostBanner + | +-- TopBar/SettingsSection (Duration, Spawn, Timer, Scarcity, GameMode options) + | +-- AreaSelector + | +-- PlayersContainer (slots 1-4) + | +-- PlayersContainer2 (slots 5-8) + | +-- BottomBar (LeaveBtn, ReadyBtn, StartGameBtn, InviteBtn) + | +-- StatusLabel + +-- ChatPanel (Panel) + | +-- RichTextLabel + | +-- ChatInput (LineEdit) + | +-- SendBtn + | +-- ChatTabsContainer (GlobalChatTabBtn + DM tabs) + | +-- FriendSuggestPanel (hidden) + +-- (dynamic instances: MailboxPanel, ShopPanel, GachaPanel, ProfilePanel, etc.) +``` + +[Back to top](#top) diff --git a/wiki/architecture_server.md b/wiki/architecture_server.md new file mode 100644 index 0000000..4d4ff93 --- /dev/null +++ b/wiki/architecture_server.md @@ -0,0 +1,392 @@ +# Tekton Armageddon - Server Architecture + + + +High-level architecture of the Nakama Lua backend. For detailed RPC reference (params, returns, errors), see [Nakama-Server-API](../Nakama-Server-API). + +[Back to top](#top) + +--- + +## System Overview + +```mermaid +flowchart LR + Client[Godot Client
player.tscn] -->|RPC calls| Nakama[Nakama Server
port 7350/7351] + Nakama -->|wallet_update| Wallet[Wallet Engine] + Nakama -->|storage_read/write| Storage[Nakama Storage DB
PostgreSQL] + Nakama -->|leaderboard_*| LB[Native Leaderboard] + Nakama -->|match_*| Match[Match Handler] + Nakama -->|channel_*| Chat[Lobby Chat] + + Client -->|nk.match_join| Match + Client -->|Direct RPC| Lua[Lua RPC Handlers
server/nakama/lua/] +``` + +The Lua backend runs **inside** the Nakama process. Lua modules hook into Nakama's lifecycle (after-authentication, RPC dispatch, match signals). All game transactions (wallet, inventory, gacha, shop) are **server-authoritative** — the Godot client sends RPC requests and the Lua code validates and executes. + +[Back to top](#top) + +--- + +## Module Architecture + +```mermaid +flowchart TD + main["main.lua
Entry point: loads all modules"] --> utils["utils.lua
Auth guards, channel resolver"] + main --> core["core.lua
Authentication hooks
Wallet init, ban check"] + main --> economy["economy.lua
Shop catalog, currency IAP
Item purchases"] + main --> gacha["gacha.lua
Gacha pulls, pity system
RNG on server"] + main --> leaderboard["leaderboard.lua
Score submission, sync
Global rankings"] + main --> inbox["inbox.lua
Global/personal mail
Reward claiming"] + main --> daily["daily_rewards.lua
Daily login rewards
Monthly schedule"] + main --> user["user.lua
Profile updates, identity
Friend sync"] + main --> admin["admin.lua
Kick/ban, stats, chat mgmt
Role management"] + + core --> utils + economy --> utils + gacha --> utils + leaderboard --> utils + inbox --> utils + daily --> utils + user --> utils + admin --> utils + + economy -->|Wallet deduction| Wallet[(Wallet Engine)] + gacha -->|Wallet deduction| Wallet + inbox -->|Claim rewards| Wallet + daily -->|Daily claim| Wallet + user -->|Profile load| Wallet +``` + +### Dependency order (loading sequence) + +1. `utils.lua` — no deps (loaded first) +2. `economy.lua` — depends on utils +3. `core.lua` — depends on utils +4. `admin.lua` — depends on utils +5. `daily_rewards.lua` — depends on utils +6. `user.lua` — depends on utils +7. `leaderboard.lua` — depends on utils +8. `inbox.lua` — depends on utils +9. `gacha.lua` — no deps beyond utils + +[Back to top](#top) + +--- + +## Authentication Flow + +```mermaid +sequenceDiagram + participant C as Godot Client + participant N as Nakama Core + participant A as after_hooks (core.lua) + participant DB as Nakama Storage + + C->>N: AuthenticateDevice/Custom/Email + N->>A: after_authenticate(context) + A->>DB: storage_read(profiles/profile) + alt First login (no profile) + A->>DB: storage_write(initial profile) + A->>N: wallet_update(gold=100, star=500) + end + A->>DB: storage_read(profiles/profile) + alt metadata.banned == true + A-->>C: error("Account banned") + end + A->>C: session token returned +``` + +### Boot sequence per player + +1. Godot client calls `AuthenticateDevice` (or Email/Custom/Steam). +2. Nakama core calls `after_authenticate()` hook in `core.lua`. +3. Hook reads `profiles/profile` from storage. +4. **If first login:** initializes default profile and grants starting wallet (`gold: 100, star: 500`). +5. **Ban check:** If `metadata.banned == true`, raises error (player rejected). +6. Client receives session token and proceeds to lobby. + +[Back to top](#top) + +--- + +## Wallet & Economy Flow + +```mermaid +flowchart LR + subgraph Client Side + ShopUI[Shop Panel] -->|buy_currency RPC| Server + ShopUI -->|purchase_item RPC| Server + GachaUI[Gacha Panel] -->|perform_gacha_pull RPC| Server + MailUI[Inbox] -->|claim_mail_reward RPC| Server + DailyUI[Daily Rewards] -->|claim_daily_reward RPC| Server + end + + subgraph Server Side + Server[Lua RPC Handler] + Server -->|nk.wallet_update| Wallet[(Wallet)] + Server -->|nk.storage_write| Inv[(Inventory Storage)] + Server -->|nk.storage_write| Rec[(Receipts Storage)] + Server -->|nk.storage_write| Frag[(Fragments Storage)] + end + + subgraph Client Refresh + Wallet -->|Wallet updated| Client + Client -->|get_account RPC| Wallet + Client -->|emit profile_updated| UI[All UI Panels
update labels] + end +``` + +### Currency types + +| Currency | Purpose | Earned by | +|---|---|---| +| `gold` | Shop purchases, star conversion | IAP (real money), admin topup | +| `star` | Gacha pulls | Gold conversion, daily rewards, mail rewards | + +### All wallet changesets + +| Operation | Changeset | +|---|---| +| First login grant | `{gold: 100, star: 500}` | +| Buy gold IAP | `{gold: +N}` (N=100/550/1150/2400/6250/13000) | +| Buy stars (gold convert) | `{gold: -N, star: +M}` | +| Buy shop item | `{gold: -price}` or `{star: -price}` | +| Gacha pull | `{star: -cost}` or `{gold: -cost}` | +| Claim mail reward | `{gold: +N}` and/or `{star: +N}` | +| Claim daily reward | `{star: +N}` or `{gold: +N}` | +| Admin topup | `{gold: 999999}` | + +[Back to top](#top) + +--- + +## Gacha Flow + +```mermaid +flowchart TD + A[Client: perform_gacha_pull] --> B{Check banner} + B -->|star/gold| C[Read wallet balance] + C --> D{Sufficient funds?} + D -->|No| E[error: Insufficient currency] + D -->|Yes| F{Check pity} + F -->|pity >= 90| G[Force real_prize rarity] + F -->|pity < 90| H[Roll rarity by drop rates] + + G --> I[Pick from real_prize pool] + H --> J[Pick from rarity pool] + + I --> K[Deduct wallet cost] + J --> K + + K --> L{real_prize?} + L -->|Yes| M[Add to inventory storage] + L -->|No| N[Increment fragment count] + + M --> O[Return results] + N --> O +``` + +### Drop rates + +| Rarity | Rate | Result | +|---|---|---| +| Common | 60% | Fragment (`frag_common`) | +| Uncommon | 25% | Fragment (`frag_uncommon`) | +| Rare | 14% | Fragment (`frag_rare`) | +| Real Prize | 1% | Skin from catalog | + +**Pity:** Guaranteed Real Prize at 90 pulls. Pity counter resets on any Real Prize win. + +[Back to top](#top) + +--- + +## Mail/Inbox System + +```mermaid +flowchart TD + Admin[Admin RPC] -->|admin_send_mail| Global[Global Mail
config/global_mail
system user] + Admin -->|admin_send_mail
with target_user_id| Personal[Personal Mail
inbox/personal
per user] + + Client -->|get_mail| Merge[Merge global + personal] + Merge --> Filter[Filter by: not deleted,
within date range,
not expired] + Filter --> Response[Return to client] + + Client -->|claim_mail_reward| CW{Check claimed_ids} + CW -->|Already claimed| Err[error: Reward already claimed] + CW -->|Not claimed| Grant[Grant rewards:
gold/star -> wallet
fragments -> fragment storage
skins -> inventory storage] + Grant --> UpdateState[Update state:
add mailId to claimed_ids] +```[Global Mail
config/global_mail
system user] + Admin -->|admin_send_mail
with target_user_id| Personal[Personal Mail
inbox/personal
per user] + + Client -->|get_mail| Merge[Merge global + personal] + Merge --> Filter[Filter by: not deleted,
within date range,
not expired] + Filter --> Response[Return to client] + + Client -->|claim_mail_reward| CW[Check claimed_ids] + CW -->|Already claimed| Err[error: Reward already claimed] + CW -->|Not claimed| Grant[Grant rewards:
gold/star -> wallet
fragments -> fragment storage
skins -> inventory storage] + Grant --> UpdateState[Update state:
add mailId to claimed_ids] +``` + +[Back to top](#top) + +--- + +## Client-Server Data Flow + +```mermaid +sequenceDiagram + participant C as Godot Client + participant L as Lua RPC + participant S as Nakama Storage + participant W as Wallet + + Note over C,W: Purchase Flow + C->>L: purchase_item(item_id, idempotency_key) + L->>L: Look up item in SHOP_CATALOG_DEFS + L->>W: wallet_update(-price) + alt Insufficient funds + W-->>L: error + L-->>C: error("NotEnoughFunds") + else Success + L->>S: storage_write(inventory/item_id) + L->>S: storage_write(receipts/idempotency_key) + L-->>C: {success: true, item: item_id} + end + + Note over C,W: Wallet State Sync + C->>L: get_account (Nakama SDK call) + S->>C: wallet JSON string + C->>C: Parse wallet JSON + C->>C: emit profile_updated signal + C->>C: All UI panels update labels +``` + +All transactions are **idempotent** via `idempotency_key` — if the same key is used twice, the server returns the previous result instead of re-executing. + +[Back to top](#top) + +--- + +## Admin Hierarchy + +| Role | Can | Guarded by | +|---|---|---| +| `player` (default) | Nothing special | — | +| `moderator` | Match-related admin: kick players, get server stats | `utils.require_admin_or_host` (also checks match host) | +| `admin` | All moderation: ban/unban, manage mail, manage chat, view users | `utils.require_admin(context)` | +| `owner` | Everything admin can + set user roles | Inline check: `callerMetadata.role == "owner"` | + +### Guard functions (utils.lua) + +```lua +utils.require_admin(context) -- errors if role not admin or owner +utils.require_admin_or_host(context, match_id) -- errors if not admin/owner AND not match host +utils.is_banned(metadata) -- returns boolean +utils.resolve_channel_id(channelId) -- channel name → hashed ID +``` + +[Back to top](#top) + +--- + +## Storage Collections + +| Collection | Owner | Key | Public R | Public W | Purpose | +|---|---|---|---|---|---| +| `profiles` | User | `"profile"` | 1 | 0 | User metadata, role, ban status, loadout | +| `profiles` | User | `"pity_counters"` | 1 | 0 | Per-banner gacha pity counts | +| `profiles` | User | `"fragments"` | 1 | 0 | Accumulated gacha fragments | +| `inventory` | User | Item ID | 1 | 0 | Owned cosmetic items | +| `inventory` | User | `"fragments"` | 1 | 0 | Fragment counts (legacy) | +| `stats` | User | `"game_stats"` | 1 | 0 | Player stats (wins, kills, score) | +| `receipts` | User | Idempotency key | 1 | 0 | Purchase receipts (IAP + shop) | +| `inbox` | User | `"personal"` | 1 | 0 | Personalized mail inbox | +| `inbox` | User | `"state"` | 1 | 0 | claimed_ids, deleted_ids, read_ids | +| `daily_rewards` | User | `"state"` | 1 | 0 | Daily reward claim tracking | +| `config` | SYSTEM | `"global_mail"` | 2 | 0 | Global mail sent to all players | +| `config` | SYSTEM | `"daily_rewards"` | 2 | 0 | Monthly reward schedule | +| `config` | SYSTEM | `"lobby_chat"` | 2 | 0 | Chat prefix/max_messages config | +| `shop_config` | SYSTEM | `"featured_banners"` | 2 | 0 | Featured shop banners (max 3) | +| `bans` | SYSTEM | User ID | 2 | 0 | Ban records (redundant with metadata) | + +[Back to top](#top) + +--- + +## vs Nakama-Server-API + +| Aspect | Architecture-Server (this page) | Nakama-Server-API | +|---|---|---| +| Audience | Architects, new devs | Implementers, AI agents | +| Detail level | High-level flow, diagrams | Per-function: params, returns, errors | +| Diagrams | Mermaid flowcharts | None | +| RPC listing | Summary table with key flows | Full 48 RPC documentation | +| Storage | Conceptual collection overview | Exact schema per collection | +| Best for | Understanding the system | Calling RPCs without reading code | + +[Back to top](#top) + +--- + +## Quick Reference: All 48 Registered RPCs + +| RPC Name | Module | Auth | Purpose | +|---|---|---|---| +| `update_display_name` | user | required | Change display name | +| `update_avatar` | user | required | Change avatar URL | +| `sync_profile` | user | required | Push profile to server | +| `change_identity` | user | required | Link new device/email | +| `set_password` | user | required | Set email password | +| `sync_friends` | user | required | Push friend list | +| `get_shop_catalog` | economy | required | Get catalog + featured | +| `buy_currency` | economy | required | IAP gold/star purchase | +| `purchase_item` | economy | required | Buy cosmetic item | +| `perform_gacha_pull` | gacha | required | Roll gacha | +| `get_leaderboard_stats` | leaderboard | no | Get top 50 scores | +| `submit_score` | leaderboard | required | Record match score | +| `sync_leaderboard` | leaderboard | required | Sync stats → leaderboard | +| `reset_stats` | leaderboard | required | Clear own stats | +| `get_mail` | inbox | required | Get available mail | +| `claim_mail_reward` | inbox | required | Claim mail rewards | +| `delete_mail` | inbox | required | Soft-delete mail | +| `save_mail_state` | inbox | required | Mark as read | +| `claim_daily_reward` | daily_rewards | required | Claim today's reward | +| `get_daily_reward_state` | daily_rewards | required | View monthly schedule | +| `set_daily_reward_config` | daily_rewards | admin | Set reward schedule | +| `get_daily_reward_config_admin` | daily_rewards | admin | Get reward config | +| `admin_*` (18 RPCs) | admin | admin/owner | Moderation tools | + +For full params/returns/errors on any RPC above, see [Nakama-Server-API](../Nakama-Server-API). + +[Back to top](#top) + +--- + +## Deployment Topology + +```mermaid +flowchart TD + subgraph VPS [VPS 52.74.133.55] + Gitea[Gitea Server
port 3000] + Nakama[Nakama Server
port 7350/7351] + PG[(PostgreSQL
Nakama DB)] + Act[act_runner
CI/CD Worker] + end + + Client[Godot Player] -->|HTTPS| Gitea + Client -->|gRPC/WebSocket| Nakama + Nakama --> PG + + GitHub_mirror[GitHub Mirror] -->|Push| Gitea + Gitea -->|Webhook/Manual| Act + Act -->|Build| Binary[Binary Releases] + Act -->|Build| Patch[patch.pck on patches branch] + Client -->|Check version| Gitea + Client -->|Download patch| Gitea +``` + +[Back to top](#top) diff --git a/wiki/game-modes.md b/wiki/game-modes.md new file mode 100644 index 0000000..cda5e7e --- /dev/null +++ b/wiki/game-modes.md @@ -0,0 +1,604 @@ +# Game Modes + + + +[Back to Home](./Home) + + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [GameMode Enum & ModeConfig](#gamemode-enum--modeconfig) +- [Session Flow (all modes)](#session-flow-all-modes) +- [Core Managers (shared)](#core-managers-shared) +- [Freemode](#freemode) +- [Stop n Go](#stop-n-go) +- [Tekton Doors (Portal)](#tekton-doors-portal) +- [Candy Pump Survival (Gauntlet)](#candy-pump-survival-gauntlet) +- [Scoring & Leaderboard](#scoring--leaderboard) +- [Glossary](#glossary) +- [File Index](#file-index) + +[Back to top](#top) + + +## Overview + +Four game modes, each implementing the same core loop — players navigate a grid, collect tiles (Heart / Diamond / Star / Coin), match them to goals on a virtual 5x5 playerboard, and compete for score before the match timer expires. + +| Mode | Enum Value | Display Name | Play Area | Gimmick | +|------|-----------|-------------|-----------|---------| +| Freemode | `FREEMODE = 0` | "Freemode" | Variable | No restrictions, just goals + timer | +| Stop n Go | `STOP_N_GO = 1` | "Stop n Go" | 23x12 | GO/STOP phases, safe zones, scatter penalty | +| Tekton Doors | `TEKTON_DOORS = 2` | "Tekton Doors" | 14x14 | 4 rooms, portal doors swap connections every 15s | +| Candy Pump Survival | `GAUNTLET = 3` | "Candy Pump Survival" | 20x20 | Ground growth — candy slowly fills the arena | + +[Back to top](#top) + + +## Architecture + +``` +GameMode (enum/RefCounted) + ├── .from_string() / .mode_to_string() / .is_restricted() / .get_all_modes() + │ + ├── ModeConfig (RefCounted) + │ └── SCHEMA with defaults, type-checking, min/max for each mode + │ + ├── GoalsCycleManager (Node, autoload?) + │ ├── 30s cycle timer + │ ├── Match timer (configurable 60-600s) + │ ├── Score tracking (player_scores, player_goal_counts) + │ ├── Goal completion: _process_goal_completion() + │ └── RPC sync: sync_player_score, sync_goal_count, sync_timer + │ + ├── GoalManager (Node) + │ ├── initialize_random_goals() — generates 9-slot goal patterns + │ ├── Speed tracking (completion_times, boost_multiplier) + │ └── generate_preset_goals() / get_goals_for_player() + │ + ├── PlayerRaceManager (per-player) + │ ├── goals: Array[int] (9 slots) + │ ├── playerboard: Array[int] (25 slots, 5x5) + │ ├── check_pattern_match() — core matching logic + │ └── DEPRECATED lap/finish-line stubs + │ + ├── PlayerboardManager (per-player) + │ ├── grab_item() / auto_put_item() / arrange operations + │ ├── _execute_grab() — server-authoritative grab validation + │ ├── HIDDEN_SLOTS (12 of 25 cells blocked) + │ ├── bot_try_grab_item() — AI grab logic + │ └── _check_and_refill_grid_if_needed() — scarcity refill + │ + ├── TurnManager (shared) + │ ├── next_turn() / end_current_turn() + │ └── turn_based_mode toggle + │ + └── Mode-specific managers + ├── StopNGoManager — phase transitions, safe zones, mission HUD + ├── PortalModeManager — room partitions, portal doors, swap timer + └── GauntletManager — ground growth, phases, bubbles, smack +``` + +All mode managers live under `/root/Main` and connect to `GoalsCycleManager` signals for score / goal tracking. + +[Back to top](#top) + + +## GameMode Enum & ModeConfig + +**File:** `scripts/game_mode.gd` (41 lines) + +``` +enum Mode { FREEMODE = 0, STOP_N_GO = 1, TEKTON_DOORS = 2, GAUNTLET = 3 } +``` + +| Static Method | Returns | Description | +|--------------|---------|-------------| +| `from_string(mode: String)` | `Mode` | Converts "Freemode"/"Stop n Go"/"Tekton Doors"/"Candy Pump Survival" to enum | +| `mode_to_string(mode: Mode)` | `String` | Reverse of from_string | +| `is_restricted(mode: Mode)` | `bool` | true for STOP_N_GO, TEKTON_DOORS, GAUNTLET | +| `get_all_modes()` | `Array[String]` | Returns display names | + +**File:** `scripts/mode_config.gd` (109 lines) + +SCHEMA defines per-mode settings with type, default, min, max, allowed values: + +| Mode | Settings | +|------|---------| +| Freemode | match_duration (180s default), randomize_spawn (bool), enable_cycle_timer (bool), scarcity_mode (Normal/Aggressive/Chaos) | +| Stop n Go | match_duration, sng_go_duration (20s), sng_stop_duration (4s), sng_required_goals (8) | +| Tekton Doors | match_duration, doors_swap_time (15s), doors_refresh_time (25s), doors_required_goals (8) | +| Gauntlet | match_duration, gauntlet_growth_interval (3.0s), gauntlet_cells_per_tick (phase dict) | + +| Method | Args | Returns | +|--------|------|---------| +| `get_defaults(mode)` | String | Dictionary of defaults for that mode | +| `validate_setting(mode, key, value)` | String, String, Variant | `{"valid": bool, "error": String}` | +| `validate_config(mode, config)` | String, Dictionary | `{"valid": bool, "errors": Array}` | +| `get_mode_settings(mode)` | String | Array of setting keys | +| `get_setting_schema(mode, key)` | String, String | Dictionary with type/default/min/max/allowed | + +[Back to top](#top) + + +## Session Flow (all modes) + +1. **Lobby** — players join, select mode + settings +2. **Game start** — `main.gd` calls `_setup_host_game()` which: + - Creates arena (gridmap resize + clear) + - Spawns mission tiles on Layer 1 + - Calls mode manager's `start_game_mode()` +3. **Countdown** — brief timer then match begins +4. **Match active** — `GoalsCycleManager.start_match()` runs match timer + cycle timer +5. **Per-cycle** (30s): + - Players grab tiles from grid → place on 5x5 playerboard + - Match 3x3 pattern against 9-slot goals + - Complete goals → B1000 score + new goals + tiles randomize around player + - Cycle ends → board cleared, unmatched tiles scored at 10/tile match +6. **Match ends** — final leaderboard sync + +[Back to top](#top) + + +## Core Managers (shared) + +### GoalManager (`scripts/managers/goal_manager.gd`, 108 lines) + +Generates 9-slot goal arrays using tile IDs 7-10 (Heart=7, Diamond=8, Star=9, Coin=10) with -1 for no-goal slots (~3 nulls per set). + +| Function | Returns | Description | +|----------|---------|-------------| +| `initialize_random_goals(size, min_value, max_value, null_count)` | `Array` | Random goals with controlled null distribution | +| `generate_preset_goals(count)` | `Array` | Pre-generates N goal sets for all players | +| `get_goals_for_player(player_index)` | `Array` | Returns goals for a specific player slot | +| `mark_goal_start(player_id)` | void | Records timestamp for speed tracking | +| `mark_goal_complete(player_id)` | void | Records completion duration | +| `get_player_average_time(player_id)` | `float` | Average completion speed | +| `get_global_average_time()` | `float` | Average across all players | +| `get_boost_multiplier(player_id)` | `float` | 0.8-1.5x fill rate based on speed vs average | +| `reset()` | void | Clears all state | + +### GoalsCycleManager (`scripts/managers/goals_cycle_manager.gd`, 520 lines) + +Central scoring and timer system. Emits signals consumed by mode managers, UI, and leaderboard. + +| Signal | Payload | +|--------|---------| +| `cycle_started()` | — | +| `cycle_ended()` | — | +| `timer_updated(time_remaining)` | float | +| `score_updated(peer_id, new_score)` | int, int | +| `goal_count_updated(peer_id, count)` | int, int | +| `leaderboard_updated(sorted_scores)` | Array | +| `match_started()` | — | +| `match_ended()` | — | +| `global_timer_updated(time_remaining)` | float | + +| Function | Description | +|----------|-------------| +| `start_match(duration_seconds, start_cycles)` | Begins match timer, optionally starts first 30s cycle | +| `_on_match_end()` | Processes final scores, syncs to clients | +| `start_cycle()` | Begin 30s cycle, emit `cycle_started` | +| `on_goal_completed(player, time_remaining)` | Entry point — routes to server or optimistic local path | +| `_process_goal_completion(player, time_remaining)` | Server: award B1000 + time bonus, regen goals, randomize tiles | +| `regenerate_goals_for_player(player)` | Generate new 9-slot goal set, sync via RPC | +| `_randomize_tiles_around_player(player)` | Randomize 3x3 area around player on the grid | +| `_process_cycle_end_for_all_players()` | Clear all boards, convert matches to B10/tile | +| `add_score(peer_id, amount)` | Server: add arbitrary score points | +| `_update_leaderboard()` | Sort by score descending (or with SNG winner override) | + +### PlayerRaceManager (`scripts/managers/player_race_manager.gd`, 133 lines) + +State holder per player. Core logic is `check_pattern_match()`. + +| Function | Description | +|----------|-------------| +| `check_pattern_match()` | Returns true if any 3x3 sub-grid of 5x5 board matches 3x3 goals | +| `check_3x3_section(board, goals, start_row, start_col)` | Checks single 3x3 section | +| `_normalize_tile(tile)` | Converts holo tiles 11-14 → 7-10 for match comparison | +| Remaining functions | DEPRECATED lap/finish line stubs | + +**Playerboard Layout:** 5x5 (indices 0-24). 13 cells are HIDDEN_SLOTS (cannot hold tiles). Only 12 usable slots arranged in an L-shape matching the HUD. + +### PlayerboardManager (`scripts/managers/playerboard_manager.gd`, 793 lines) + +Handles grab, put, arrange operations with optimistic local updates + server-authoritative validation. + +| Function | Description | +|----------|-------------| +| `grab_item(grid_position)` | Grab tile from grid → place on board (auto-arrange) or consume as power-up | +| `_execute_grab(grid_pos, cell, item_id, expected_slot)` | Server-side validation + state update + sync | +| `_force_sync_to_client(cell, server_item)` | Revert client when server rejects grab | +| `auto_put_item()` | AI/bot: find best tile to remove from board | +| `find_best_goal_slot_for_item(item)` | Auto-arrange into best matching slot | +| `bot_try_grab_item()` | AI grab logic | +| `_check_and_refill_grid_if_needed(gridmap)` | Refill floor 1 via ScarcityController when empty | + +### TurnManager (`scripts/managers/turn_manager.gd`, 27 lines) + +| Function | Description | +|----------|-------------| +| `next_turn(players)` | Advance turn index, emit `turn_changed` | +| `end_current_turn()` | Emit `turn_ended` | +| `reset_turn()` / `reset()` | Clear state | + +[Back to top](#top) + + +## Freemode + +**No dedicated manager.** Freemode relies entirely on the shared core managers (GoalsCycleManager, PlayerboardManager, etc.) with no mode-specific restrictions or gimmicks. Arena size is configurable via LobbyManager settings. + +**Settings effects:** +- `enable_cycle_timer` false → cycle never expires (board never auto-clears) +- `scarcity_mode` → controls tile refill aggression +- `randomize_spawn` → players start at random positions + +[Back to top](#top) + + +## Stop n Go + +**Manager:** `StopNGoManager` (`scripts/managers/stop_n_go_manager.gd`, 1107 lines) + +A 23x12 arena with two alternating phases: + +### Phase System + +| Phase | Duration (default) | Behaviour | +|-------|-------------------|-----------| +| GO | 20s | Players move freely, collect tiles, complete goals | +| STOP | 4s | Players frozen if outside safe zone → tiles scattered | + +When STOP begins: +1. 3 dynamic safe zones spawn randomly (green tiles) +2. All players outside safe zone get `_scatter_player_tiles()` — board tiles scattered on grid +3. Power-up tiles (Speed=11, Ghost=14) spawn at 5 permanent locations +4. Mission requirement: complete 8 goals before reaching finish (x=22) + +When GO begins: +1. Dynamic safe zones cleared +2. All STOP freeze effects removed via `sync_stop_freeze(false)` + +### Tile IDs + +| ID | Meaning | +|----|---------| +| 0 | Walkable floor | +| 2 | Safe zone (green) | +| 3 | Start/Finish line | +| 4 | Wall/obstacle | +| 15 | Lightning stone (decorative ancient rock) | +| 16 | Safe zone wall | + +### Arena + +22x10 walkable area with two interior rooms with entrances: +- Room 1: (7,6) to (11,9) — 5x4 area with 4 door entrances +- Room 2: (15,1) to (19,5) — 5x5 area with 4 door entrances + +### HUD + +- Center-bottom mission label: "GOALS (X/8)" or "ALL GOALS COMPLETE! REACH THE FINISH!" +- Traffic light stop timer (3 segments): all empty during GO, fills red during STOP phase +- Last 3 seconds of GO phase: segments light up one-by-one (countdown) +- VFX: `vfx_manager.play_go_animation()` / `play_stop_phase()` + +### RPCs + +| RPC | Direction | Description | +|-----|-----------|-------------| +| `sync_phase(phase_name, duration)` | Authority → all | Broadcast GO/STOP phase change | +| `sync_arena_setup()` | Authority → remote | Sync 23x12 grid dimensions + obstacles | +| `sync_all_safe_zones_vfx()` | Authority → all | Trigger safe zone visual effects | + +### Key Functions (server-only unless noted) + +| Function | Description | +|----------|-------------| +| `start_game_mode()` | Server: setup arena, assign missions, start GO phase | +| `_start_phase(phase)` | Transition GO↔STOP, penalize players outside safe zone | +| `_setup_arena()` | Build 23x12 with obstacles + rooms | +| `_spawn_mission_tiles()` | Heart(7)/Diamond(8)/Star(9)/Coin(10) at 60% density | +| `_spawn_powerup_tiles()` | Speed(11)+Ghost(14) at 5 permanent locations | +| `_assign_missions()` | NO-OP (mission = achievement: collect 8 goals) | +| `activate_client_side()` | Client: show HUD, connect to GoalsCycleManager signals | +| `rotate_players_to_start()` | Force all players to face East (PI/2) | +| `can_rpc()` | Check multiplayer peer is connected | + +[Back to top](#top) + + +## Tekton Doors (Portal) + +**Manager:** `PortalModeManager` (`scripts/managers/portal_mode_manager.gd`, 585 lines) +**Actor:** `PortalDoor` (`scripts/portal_door.gd`, 136 lines) + +A 14x14 grid divided into 4 rooms (7x7 each) by cross-shaped wall partitions. Players move between rooms via portal doors that swap connections every 15 seconds. + +### Room Layout + +``` +Room 0 (NW) | Room 1 (NE) + x: 0-6 | x: 7-13 + z: 0-6 | z: 0-6 +--------------+-------------- +Room 2 (SW) | Room 3 (SE) + x: 0-6 | x: 7-13 + z: 7-13 | z: 7-13 +``` + +Central divider: columns 6,7 and rows 6,7 are walls (tile ID 4). + +### Portal System + +- 10 doors total (2 base per room + 2 randomly placed extras) +- Every 15s (`doors_swap_time`): `_randomize_connections()` shuffles pairings +- Each pair gets a color from `PORTAL_COLORS` (Cyan/Magenta/Red/Green/Orange) +- Validation ensures no pair connects doors in the same room +- 200ms anti-jitter cooldown per player in `handle_portal_interaction()` + +### PortalDoor (actor) + +| Property/Method | Description | +|----------------|-------------| +| `room_id` | Which room this door belongs to | +| `door_id` | Unique door index | +| `target_door_id` | Connected door (set by PortalModeManager) | +| `portal_color` | Color (set triggers `set_portal_color`) | +| `detection_area` | Area3D — body_entered triggers portal | +| `_on_body_entered(body)` | 200ms cooldown, emit `player_entered_portal` | +| `spawn_offset` | Vector2i meta — nudge spawn position into room | +| `_adjust_indicator_position()` | Move GroundIndicator toward room interior | + +### Finish Room + +- At 30s remaining on match timer, reveal `_spawn_finish_room()` +- Random 3x3 area converted to finish tiles (ID 3) in one room +- Player must be standing on finish tile AND have `doors_required_goals` complete + +### Tile Refill + +- Every 25s (`tile_refresh_time`): `_refresh_tiles()` refills Floor 1 at 60% density +- Uses `ScarcityModel.get_tile_weights()` for weighted random selection +- Avoids spawning under portal doors + +### HUD + +- Center-bottom: "GOALS (X/8)" or "ALL GOALS COMPLETE! FIND THE FINISH ROOM!" +- Message broadcasts: "PORTALS SWITCHED!", "TILES REPLENISHED!" +- Warning: "A 3x3 Finish Zone has appeared in Room N!" + +### RPCs + +| RPC | Direction | Description | +|-----|-----------|-------------| +| `sync_portal_data(data)` | Authority → local | Sync connections + colors to all clients | +| `sync_portal_configs(door_configs)` | Via main | Broadcast door positions/rotations | + +### Key Functions + +| Function | Description | +|----------|-------------| +| `initialize(p_main, p_gridmap)` | Create swap timer + tile refresh timer, connect signals | +| `start_game_mode()` | Setup arena, randomize connections, start timers | +| `setup_arena_locally()` | Resize to 14x14, build room walls, spawn portal doors | +| `_randomize_connections()` | Shuffle door pairings, assign colors, validate same-room rule | +| `handle_portal_interaction(player, door)` | Teleport player to connected door + offset | +| `_spawn_finish_room()` | Convert random 3x3 area to finish tiles | +| `check_win_condition(player_id, pos)` | Check finish tile + mission complete | +| `_refresh_tiles()` | Refill floor 1 items with scarcity weights | +| `sync_to_client(peer_id)` | Sync portal connections to late-joining client | +| `get_spawn_points()` | Returns 4 spawn positions (one per room quadrant) | + +[Back to top](#top) + + +## Candy Pump Survival (Gauntlet) + +**Manager:** `GauntletManager` (`scripts/managers/gauntlet_manager.gd`, 1825 lines) + +A 20x20 arena where sticky candy (pink) slowly grows from the edges inward over 3 phases. Players must navigate shrinking safe zones, avoid sticky tiles, and use the "Smack" ability to temporarily clear candy. + +### Phase System + +| Phase | Start | Duration | Cell Growth (per tick) | Description | +|-------|-------|----------|----------------------|-------------| +| OPEN_ARENA (0) | 0s | 60s | 4-6 | "Outer Pressure" — candy pushes from perimeter | +| ROUTE_PRESSURE (1) | 60s | 60s | 6-8 | "Middle Pressure" — corridors tighten | +| SURVIVAL_ENDGAME (2) | 120s | 60s | 8-10 | "Inner Survival" — center fills in | + +Each phase transition shrinks the arena bounds by removing outer layers (via `_shrink_arena()`). + +### Growth Algorithm + +Each tick (every `growth_interval` = 3s): + +1. **Detect movement buffers** — identify critical corridor cells (#083) +2. **Generate candidates** — all SAFE cells scored by formula: + ``` + CandidateScore = LayerPriority + StickyNeighbor + InwardPressure + + PlayerPressure + ClusterGrowth + CampingPressure + + RandomNoise(-20..+20) + MovementBuffer + PathSafety + Repetition + ``` +3. **Weighted selection** — pick `_cells_this_tick()` cells via roulette wheel +4. **Path safety check** — `_apply_path_safety()` ensures no player gets fully trapped +5. **Telegraph** — amber warning overlay appears for 1s (cells still passable) +6. **Apply** — cells convert to permanent STICKY (pink overlay on Layer 2) + +### Scoring Components + +| Score Component | Range | Description | +|----------------|-------|-------------| +| `_score_layer_priority` | -40..+60 | Phase weight by ring (outer/middle/inner) | +| `_score_sticky_neighbor` | 0..+64 | +8 per adjacent sticky cell (cap +64) | +| `_score_inward_pressure` | 0..+30 | Push inward, scales with phase | +| `_score_player_pressure` | -50..+20 | 2-4 cells away +20; under player -50 (+10 in final 30s) | +| `_score_cluster_growth` | 0..+25 | +15 expansion, +25 bridge between clusters | +| `_score_camping_pressure` | 0..+60 | Per-region: >5s +20, >8s +40, >10s +60 | +| `_score_movement_buffer` | -40..0 | Hidden corridor buffers + player proximity floor | +| `_score_path_safety` | -100..0 | Soft penalty if selection would strand a player | +| `_score_repetition` | -30..0 | Penalty for cells near last tick's selection | + +### Cell States + +| State | Meaning | Passable? | +|-------|---------|-----------| +| SAFE | Normal floor | Yes | +| TELEGRAPHED | Amber warning (1s) | Yes | +| STICKY | Permanent candy overlay | No (slows) | +| BUBBLE_GROWING | Candy bubble expanding | No | +| BLOCKED | NPC zone / permanent obstacle | No | + +### Candy Bubble System (#082) + +Anti-camping hazard: grows 1x1 → 3x3 sticky area. + +| Phase | Max Bubbles | +|-------|-------------| +| OPEN_ARENA | 0 (disabled) | +| ROUTE_PRESSURE | 2 | +| SURVIVAL_ENDGAME | 3 | + +| Property | Value | +|----------|-------| +| Grow duration | 2.75s | +| Explosion radius | 1 (3x3) | +| Recent memory | 4 positions | +| Anti-stack radius | 3 (no bubbles within 3 of recent) | + +### Camping Detection (#073) + +Players tracked in 4x4 regions. Time accumulates while player stays in same region, resets on region change. Drives camping pressure in candidate scoring. + +### Movement Buffers (#083) + +Hidden per-cell penalties on critical corridor cells (chokepoints where removing the cell would isolate part of the arena). Decay over time and phase transitions so arena can still close in. + +### Smack Mechanic + +| Property | Value | +|----------|-------| +| Cooldown | 8s | +| Charge window | 3s | +| Effect | Clears nearby sticky? (consumes charge) | + +Per-player cooldown/charge tracked in `smack_cooldowns` / `smack_charged` Dictionaries. Pink modulate during charge window, white on cooldown. + +### Arena NPC + +Candy Pump NPC at center (9,9) in a 3x3 blocked zone. Visual-only in v2 (projectile logic removed). Scattered projectiles still spawned for visual effect during telegraph phase. + +### Slow-Mo + +- Triggered conditionally, duration 4s +- `Engine.time_scale = 0.25` (1/4 speed) +- Restored to 1.0 in `_exit_tree()` + +### Spawn Points + +| Player Count | Positions | +|-------------|-----------| +| 4 | 4 corners: (1,1), (18,1), (1,18), (18,18) | +| 5-6 | 4 corners + top-mid (10,1) + bottom-mid (10,18) | +| 7-8 | 4 corners + all 4 mid-edges | + +### RPCs + +| RPC | Direction | Description | +|-----|-----------|-------------| +| `sync_phase(phase_index, phase_name)` | Authority → local | Phase change broadcast | +| `sync_arena_setup()` | Authority → remote | Arena dimensions + layout | +| `sync_growth_telegraph(cells)` | Authority → local | Show amber warning on selected cells | +| `sync_growth_apply(cells)` | Authority → local | Convert telegraphed to sticky | +| `consume_smack(pid)` | Any peer → local | Smack consumption + animation | +| `sync_stop_freeze` | (inherited from player.gd) | Freeze/unfreeze player | + +### Key Functions + +| Function | Description | +|----------|-------------| +| `initialize(main, grid)` | Connect to GoalsCycleManager | +| `start_game_mode()` | Activate client side, start OPEN_ARENA phase | +| `_setup_arena()` | Build 20x20, spawn Candy Pump NPC | +| `_process_growth_tick()` | One growth cycle: score → select → telegraph → apply | +| `_generate_candidates()` | Score all SAFE cells | +| `_calculate_candidate_score(pos, player_cells)` | Full formula with 10 components | +| `_select_cells_weighted(candidates, count)` | Roulette-wheel selection | +| `_apply_path_safety(selected)` | Filter: ensure no player stranded | +| `_try_spawn_bubble()` | Anti-camping bubble spawn attempt | +| `_update_camp_tracking(delta)` | Per-player region residency timer | +| `_detect_movement_buffers()` | Identify critical corridor chokepoints | +| `_shrink_arena()` | Remove outer arena layers on phase change | +| `_spawn_mission_tiles()` | Heart/Diamond/Star/Coin at 60% density | +| `get_spawn_points(player_count)` | Return spawn positions by player count | +| `has_smack_charged(pid)` / `consume_smack(pid)` | Smack mechanic | +| `_spawn_telegraph_highlight(pos)` | Amber glow visual (2-stage: build-up + flash) | +| `_spawn_impact_particles(targets)` | Candy splash particles on sticky impact | +| `_check_all_players_trapped()` | Re-evaluate sticky traps after growth apply | + +[Back to top](#top) + + +## Scoring & Leaderboard + +| Action | Points | +|--------|--------| +| Complete goal pattern (match 3x3) | B1000 + time bonus | +| Tile match at cycle end (per tile) | B10 | +| Time bonus formula | `int(time_remaining * TIME_BONUS_MULTIPLIER)` — currently 0 (flat 1000) | + +Leaderboard sorted descending. Stop n Go special case: winner (first to reach finish) placed at top regardless of score. + +Leaderboard signal payload: +``` +[{"peer_id": int, "score": int}, ...] +``` + +[Back to top](#top) + + +## Glossary + +| Term | Definition | +|------|------------| +| Goal | 3x3 pattern (9 slots, some -1 for null) player must match on their playerboard | +| Playerboard | 5x5 virtual grid (12 usable slots, 13 hidden) per player | +| Tile | Grid item on Floor 1: Heart(7), Diamond(8), Star(9), Coin(10) | +| Holo Tile | Power-up tiles: Speed(11), Ghost(14) — consumed on pickup, not placed on board | +| Cycle | 30-second scoring round; ends with board clear + point conversion | +| Sticky | Permanent pink overlay on Gauntlet floor cells — blocks/slows movement | +| Telegraph | Amber 1-second warning before a cell becomes sticky | +| Safe Zone | Green tiles in Stop n Go STOP phase — only safe tile type | +| Portal | Colored door connecting rooms in Tekton Doors | +| Scarcity | Tile refill model controlling spawn weights based on mode config | +| Smack | Gauntlet ability: clear nearby sticky (8s cooldown, 3s charge window) | +| Camping | Player staying in same 4x4 region >5s, attracts growth pressure | +| Movement Buffer | Hidden chokepoint corridor that growth algorithm avoids sealing early | +| Chebyshev Distance | `max(|x1-x2|, |y1-y2|)` — used for all proximity calculations | + +[Back to top](#top) + + +## File Index + +| File | Lines | Role | +|------|-------|------| +| `scripts/game_mode.gd` | 41 | Mode enum + helper functions | +| `scripts/mode_config.gd` | 109 | Schema-driven per-mode settings | +| `scripts/managers/goal_manager.gd` | 108 | Goal generation + speed tracking | +| `scripts/managers/goals_cycle_manager.gd` | 520 | Timer, scoring, cycle control | +| `scripts/managers/player_race_manager.gd` | 133 | Per-player state: goals, board, pattern matching | +| `scripts/managers/playerboard_manager.gd` | 793 | Grab/put/arrange operations | +| `scripts/managers/turn_manager.gd` | 27 | Turn-based flow | +| `scripts/managers/stop_n_go_manager.gd` | 1107 | Stop n Go phase system, safe zones, HUD | +| `scripts/managers/portal_mode_manager.gd` | 585 | Tekton Doors room layout, portals, tiles | +| `scripts/managers/gauntlet_manager.gd` | 1825 | Candy Pump Survival growth, phases, smack | +| `scripts/portal_door.gd` | 136 | PortalDoor actor — detection, teleport, visuals | +| `scripts/managers/goals_cycle_manager.gd` | (shared) | Also referenced by gauntlet signal connections | +| `scripts/managers/camera_context_manager.gd` | ... | Camera mode changes per game mode? | +| `scripts/managers/player_movement_manager.gd` | ... | Movement restrictions per mode | + +[Back to top](#top) diff --git a/wiki/home.md b/wiki/home.md new file mode 100644 index 0000000..55e4bb2 --- /dev/null +++ b/wiki/home.md @@ -0,0 +1,16 @@ +# Tekton Dash Armageddon + + + +- [Game Modes](./Game-Modes.-) — Full per-mode reference: Stop n Go, Tekton Doors, Candy Pump Survival, Freemode +- [Architecture - Client](./Architecture-Client) — Godot client code structure, managers, scenes, player controller +- [Architecture - Server](./Architecture-Server) — Nakama Lua backend topology, auth flow, wallet economy, admin roles +- [Nakama Server API](./Nakama-Server-API) — Full per-function RPC reference with params, returns, errors +- [Patch Release Workflow](./Patch-Release-Workflow.-) — Hot patch and binary release CI/CD pipelines +- [Skin Creation Workflow](./Skin-Creation-Workflow.-) — Skin material authoring, catalog registration, gacha prizes +- [Nakama Deployment](./Nakama-Deployment.-) — Push Lua updates to Nakama server +- [SSH Setup — Linux](./SSH-Setup-Linux) +- [SSH Setup — macOS](./SSH-Setup-macOS) +- [SSH Setup — Windows](./SSH-Setup-Windows) + +[Back to top](#top) diff --git a/wiki/patch_release_workflow.md b/wiki/patch_release_workflow.md new file mode 100644 index 0000000..88688a9 --- /dev/null +++ b/wiki/patch_release_workflow.md @@ -0,0 +1,322 @@ +# Patch & Release Workflow + +Complete guide for shipping updates to Tekton players — hot patches (`.pck`) for content changes and full binary releases for engine/platform changes. + +--- + +## Overview + +Two automated CI pipelines handle all distribution: + +| Pipeline | Trigger | Output | Delivery | +|---|---|---|---| +| **Deploy Patch** (`deploy_patch.yml`) | Manual workflow dispatch | `patch.pck` + `version.json` → `patches` branch | Gitea raw endpoint | +| **Release** (`ci.yml`) | Git tag `v*` push | Windows/Linux/macOS `.zip` → Gitea Release | git.klud.top releases | + +--- + +## Infrastructure + +### Gitea instance +- **URL:** https://git.klud.top +- **API:** http://52.74.133.55:3000/api/v1 +- **Runner:** Local Docker container (`gitea-runner`) via `docker-compose` +- **Cache volume:** `/home/dev/godot-cache` → `/cache` (rw) inside runner containers +- **Secret:** `TEKTON_RELEASE_TOKEN` — Token from user `adtpdn` with repo write access + +### Patch serving +Patches served directly from Gitea's built-in raw file endpoint — no external CDN: +- Manifest: `https://git.klud.top/danchie/tekton/raw/branch/patches/version.json` +- PCK: `https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck` + +Old `raw.klud.top` (gitea-pages container) retired — Gitea raw endpoint is faster, simpler, and always available. + +### Release page +- **URL:** https://git.klud.top/danchie/tekton/releases +- Assets auto-uploaded by CI on tag push + +--- + +## Part 1: Hot Patch (content-only updates) + +Use when: script changes, UI tweaks, balance patches, asset replacements, config changes. + +### Step-by-step + +**1. Write changelog** + +Edit `CHANGELOG_DRAFT.md` — add player-facing notes under `## [NEXT]`: + +```markdown +## [NEXT] +- Fixed playerboard desync in multiplayer. +- Adjusted Gauntlet difficulty scaling. +``` + +If `[NEXT]` is missing, add the header. Format is markdown list items without leading dash (the tool strips it). Each line becomes a bullet on the patch notes page. + +**2. Commit to `experimental`** + +```bash +git add CHANGELOG_DRAFT.md +git commit -m "docs: patch notes for next release" +git push origin experimental +``` + +**3. Trigger patch deploy workflow** + +Navigate to the Actions tab: + +``` +https://git.klud.top/danchie/tekton/actions +``` + +Click **Deploy Patch** → **Run workflow**: + +| Field | Example | +|---|---| +| **Patch version** | `2.4.3` | +| **Release notes** | `fix: multiplayer desync, gauntlet balance` | + +OR via API: + +```bash +curl -X POST "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/workflows/deploy_patch.yml/dispatches" \ + -H "Authorization: token $TEKTON_RELEASE_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"ref":"experimental","inputs":{"version":"2.4.3","notes":"fix: multiplayer desync, gauntlet balance"}}' +``` + +### What the CI does (deploy_patch.yml) + +1. **Checkout** — `git clone --depth 1` from `experimental` branch (shallow = fast). +2. **Setup Godot** — Uses cached `/cache/godot_4.7` binary. Downloads only if missing (140MB, cached forever). +3. **Generate version.json** — Runs `tools/generate_version_json.py --skip-changelog`. Reads version from `project.godot`, bumps patch number, writes `assets/data/version.json` with the new release entry including `pck_url` pointing to Gitea raw endpoint. +4. **Export patch PCK** — `godot --headless --export-pack "Windows Desktop" build/patch.pck`. No export templates needed — `--export-pack` only packs resources, not binaries. Output ~10-15MB. +5. **Push to patches branch** — Force-pushes `patch.pck` + `version.json` to the `patches` branch of the repo. + +### Verification + +```bash +# Check manifest +curl -s "https://git.klud.top/danchie/tekton/raw/branch/patches/version.json" +# Expected: latest_version matches your patch number + +# Check pck exists +curl -s -o /dev/null -w "%{http_code} %{size_download}B" \ + "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck" +# Expected: HTTP 200, size ~10-15MB +``` + +### How players receive patches + +1. Game boots → `GameUpdateManager` fetches `version.json` from Gitea raw endpoint. +2. Compares `latest_version` against local version. +3. If remote is newer → downloads `patch.pck` to `user://patch.pck`. +4. Mounts with `ProjectSettings.load_resource_pack("user://patch.pck")`. +5. All files in `patch.pck` override base `res://` files in memory. +6. No files are overwritten on disk (safe rollback by deleting `patch.pck`). + +--- + +## Part 2: Full Binary Release (platform updates) + +Use when: engine upgrade, native plugin change, export template update, platform-specific build fix, or any change that needs a new `.exe`/`.app`. + +### Step-by-step + +**1. Ensure changelog is written** + +Same as patch step 1 — `CHANGELOG_DRAFT.md` must have `## [NEXT]` entries. The CI auto-extracts them for the release body. + +**2. Commit and tag** + +```bash +# Commit all changes +git add -A +git commit -m "chore: bump to v2.4.3" + +# Push to experimental +git push origin experimental + +# Create and push tag +git tag v2.4.3 experimental +git push origin v2.4.3 +``` + +**IMPORTANT:** Tag must match `v` + version format (e.g. `v2.4.3`). The CI is triggered by `v*` tags. + +### What the CI does (ci.yml) + +1. **Install tools** — `apt-get install curl unzip zip` (zip was missing in early runs — make sure it's present). +2. **Checkout** — Full clone from tag (shallow not used — needs full history for changelog extraction, though `--depth 1` works too). +3. **Setup Godot with templates** — Caches both Godot binary (140MB) and export templates (1.3GB) in `/cache/`. Templates downloaded once per runner lifetime. +4. **Export 3 platforms:** + - **Windows** — `godot --headless --export-release "Windows Desktop"` → zipped with `zip`. + - **Linux/X11** — Same pattern. + - **macOS** — Export to `.zip` directly (Godot's macOS export produces a zip). + - **Note:** Steam DLLs copied into Windows build from `addons/godotsteam/`. + - **Note:** `|| true` on export commands masks Godot errors (e.g. GodotSteam plugin warnings). Real failures (missing `zip`) will surface. +5. **Extract changelog** — Parses `CHANGELOG_DRAFT.md` for the `## [version]` section matching the tag. Writes to `$CHANGELOG_BODY` env var. +6. **Create/Update Gitea Release** — Checks if release exists for tag. Creates new one with changelog as body if missing. Updates draft release if re-run. +7. **Upload assets** — Each `.zip` uploaded as release asset via multipart POST. +8. **Publish** — Sets `draft:false` to make release public. + +### Verification + +Check the release page: + +``` +https://git.klud.top/danchie/tekton/releases/tag/v2.4.3 +``` + +Expected: 3 assets (Windows, Linux, macOS) with correct sizes, changelog body populated, release marked as published (not draft). + +### Cleaning duplicate assets + +If a tag was force-pushed, multiple CI runs may upload duplicate assets to the same release: + +```bash +# List assets +curl "https://git.klud.top/api/v1/repos/danchie/tekton/releases/tags/v2.4.3" \ + -H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.assets[] | "\(.id): \(.name) \(.size/1024/1024)MiB"' + +# Delete old duplicates (keep latest 3: Windows, Linux, macOS) +RELEASE_ID= +curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets/$ASSET_ID" \ + -H "Authorization: token $TEKTON_RELEASE_TOKEN" +``` + +### Cancelling stuck or duplicate runs + +Gitea API cannot cancel in-progress runs. Wait for completion, then delete: + +```bash +# List runs for a tag +curl "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs?page=1&limit=10" \ + -H "Authorization: token $TEKTON_RELEASE_TOKEN" | jq '.workflow_runs[] | "\(.id): \(.status) \(.conclusion) \(.display_title)"' + +# Delete completed run +curl -X DELETE "http://52.74.133.55:3000/api/v1/repos/danchie/tekton/actions/runs/$RUN_ID" \ + -H "Authorization: token $TEKTON_RELEASE_TOKEN" +``` + +--- + +## Part 3: Agent-Automated Release + +Agent (Hermes) can execute the full release flow from a single user request: + +### Scenario: "Ship v2.4.3" + +Agent actions: +1. Read `CHANGELOG_DRAFT.md` — verify `[NEXT]` has entries. +2. Check `project.godot` current version. +3. Commit changelog to `experimental`. +4. Create tag `v2.4.3` → push to trigger `ci.yml`. +5. Wait for CI completion (poll every 30s, up to 30 min). +6. If CI fails: + - Read job logs for failure reason. + - Fix the workflow file, commit, force-push tag. + - Clean up duplicate assets after re-run. +7. Verify release page has 3 assets with correct sizes. +8. If patch deploy also needed: + - Trigger `deploy_patch.yml` dispatch. + - Verify `patch.pck` is served and version.json updated. + +### Scenario: "Quick hot patch" + +Agent actions: +1. Check if `[NEXT]` has entries in `CHANGELOG_DRAFT.md`. +2. If empty, ask user for changelog notes. +3. Commit `CHANGELOG_DRAFT.md` to `experimental`. +4. Dispatch `deploy_patch.yml` workflow. +5. Verify patch files on Gitea raw endpoint. + +--- + +## Troubleshooting + +### `zip: command not found` in CI + +Root cause: `ubuntu-latest` container doesn't have `zip` pre-installed. The install step must include `zip`: + +```yaml +- name: Install tools + run: apt-get update -qq && apt-get install -y -qq curl unzip zip +``` + +### Godot export fails silently (`|| true`) + +The `|| true` on export commands means a failed Godot export still shows step as success. Check: +- Is `godot_4.7` cached at `/cache/`? +- Does the export preset name match exactly? E.g. `"Windows Desktop"` must match `export_presets.cfg`. +- Is `addons/godotsteam/libgodotsteam*` present? Missing DLLs cause Godot to exit 1. + +### Runner container can't clone repo + +Runner uses HTTP auth with `god` username and `TEKTON_RELEASE_TOKEN` as password. If token is revoked: +1. Generate new token from Gitea → Settings → Applications. +2. Update secret `TEKTON_RELEASE_TOKEN` in repo Settings → Actions → Secrets. +3. Restart runner: `docker compose -f /home/dev/gitea/docker-compose.yml restart runner`. + +### Runner shows "permission denied" for Docker socket + +User `dev` doesn't have Docker socket access. Commands that touch Docker must be run via `sudo` or by the root user on the VPS. The local agent can only: +- Restart runner via systemd: `systemctl --user restart docker-runner` (if running as user service). +- No Docker CLI commands from agent terminal. + +### Release has duplicate assets + +Each CI run uploads assets as new entries. To clean: +- Get release ID from API. +- Delete old asset IDs keeping only latest (highest IDs) for each platform. +- Use jq or manual curl loop (see "Cleaning duplicate assets" above). + +### Tag force-push creates redundant CI runs + +Each push to a tag triggers `ci.yml`. Force-pushing a tag to a new commit creates another run: +- Previous runs keep running (can't cancel via API). +- Wait for all to finish, then delete stale ones. +- The last run to publish sets the release state. + +Best practice: Delete old release before force-pushing tag, or at minimum delete stale completed runs after. + +### Patch manifest not updating + +`generate_version_json.py --skip-changelog` only bumps version and writes `version.json`. If the version didn't change (e.g. `--skip-changelog` with no `[NEXT]` entries), the script exits with code 0 but doesn't write anything. Verify `assets/data/version.json` has the new version after CI run. + +### `gitea-pages` (raw.klud.top) returns 404 + +gitea-pages container uses a Gitea token to read files. If the token is dead: +- Switch to Gitea native raw endpoint: `https://git.klud.top/danchie/tekton/raw/branch/patches/...` +- Update `MANIFEST_URL` in `generate_version_json.py` and `VERSION_MANIFEST_URL` in `game_update_manager.gd`. +- Retire gitea-pages container entirely (not needed, Gitea has built-in raw serving). + +--- + +## File Reference + +| File | Purpose | +|---|---| +| `.gitea/workflows/deploy_patch.yml` | Patch deploy CI — generates pck + pushes to patches branch | +| `.gitea/workflows/ci.yml` | Full binary release CI — exports 3 platforms + creates release | +| `tools/generate_version_json.py` | Version bumping + changelog → version.json conversion | +| `CHANGELOG_DRAFT.md` | Human-readable changelog draft (source of truth for release notes) | +| `assets/data/version.json` | Machine-readable manifest served to players (auto-generated) | +| `scripts/managers/game_update_manager.gd` | Client-side update checker (reads version.json → downloads patch.pck) | +| `project.godot` | Godot project file (config/version = source of truth for version number) | +| `export_presets.cfg` | Export configuration for all platforms | +| `/home/dev/gitea/docker-compose.yml` | Runner container composition (cache volume mount: `/home/dev/godot-cache:/cache`) | + +--- + +## Key Gotchas + +- **`zip` must be in install step** — missing zip kills Windows/Linux export. Added in run 141 — do not remove. +- **Tag format is `vX.Y.Z`** — `ci.yml` trigger is `v*`. A tag without `v` prefix won't build. +- **Force-push tag = new CI run** — Always expect a new run on force-push. Old run keeps running. +- **Changelog extracted from tag version** — `## [X.Y.Z]` section in `CHANGELOG_DRAFT.md`. If section doesn't exist, release body is empty. +- **Patch deploy skips changelog clearing** — `--skip-changelog` means `version.json` is written but `CHANGELOG_DRAFT.md` is NOT modified. Only the full `ci.yml` pipeline clears it. +- **Cache is per-runner-host, not per-run** — Godot binary (140MB) and templates (1.3GB) download once on fresh runner container, then persist via `/cache` volume. Running `docker compose down` + `up` reuses cache if volume isn't deleted. +- **`|| true` masks Godot export errors** — If export fails silently, check the `2>&1 | tail -5` output in CI logs. Error messages like "Cannot call method 'queue_free' on a null value" from GodotSteam are non-fatal (cosmetic plugin warnings). diff --git a/wiki/skin_creation_workflow.md b/wiki/skin_creation_workflow.md new file mode 100644 index 0000000..ef54a79 --- /dev/null +++ b/wiki/skin_creation_workflow.md @@ -0,0 +1,257 @@ +# Skin Creation Workflow + + + +How to author a new character skin, register it in the game's shop/gacha catalog, and ship it to players. + +--- + +## Overview + +Each skin is defined in two places that must stay in sync: + +| Layer | File | What it stores | +|---|---|---| +| **Client (visual)** | `scripts/managers/skin_manager.gd` | Mesh slots, material paths, override/overlay mode | +| **Server (shop)** | `server/nakama/lua/economy.lua` | Item ID, name, category, price (gold/star) | + +The item `id` in `economy.lua` must match the dictionary key in `skin_manager.gd` `SKIN_CATALOG` exactly -- the Godot client looks up the item ID from the wallet/inventory and applies the matching skin data. + +[Back to top](#top) + +--- + +## Step 1: Create the Skin Material + +Open the **Skin Shader Generator** at `res://scenes/tools/skin_shader_generator.tscn`. + +1. Run the scene (F6). +2. Import your base albedo and mask textures (PNG with color/alpha channels). +3. Use the UI to visualize UV overlays and adjust color channels (Red, Green, Blue, Alpha). +4. Export the configured material as a `.tres` file into `assets/materials/skins/` or a subfolder: + - `assets/characters/skins/hat/` + - `assets/characters/skins/clothing/` + - `assets/characters/skins/gloves/` + +**Material path conventions (Oldpop character):** + +| Category | Example path | +|---|---| +| hat | `res://assets/characters/skins/hat/oldpop_mat_hat_blue.tres` | +| costume/clothing | `res://assets/characters/skins/clothing/oldpop_mat_cloth_red_pant.tres` | +| glove | `res://assets/characters/skins/gloves/oldpop_mat_gloves_blue.tres` | +| accessory | `res://assets/characters/skins/accessory/` | + +[Back to top](#top) + +--- + +## Step 2: Register the Skin in SkinManager (Client) + +Open `res://scripts/managers/skin_manager.gd` and add a new entry inside `SKIN_CATALOG` (between `[BEGIN_SKIN_CATALOG]` and `[END_SKIN_CATALOG]` markers). + +### Entry format + +```gdscript +"item_id": { + "category": "head", # head | costume | glove | accessory + "character": "Oldpop", # node name under CharacterRoot + "slots": [ + { + "mesh": "oldpop-hat1", # MeshInstance3D child name + "mode": "override", # "override" | "overlay" + "material": "res://path/to/material.tres" + }, + ] +} +``` + +### Slot modes + +- **`override`** -- `set_surface_override_material(0, mat)`. Replaces the base material entirely. Preserves the outline shader (`next_pass`) automatically. +- **`overlay`** -- `material_overlay = mat`. Transparent layer on top of the base material. Good for costume/pant patterns. + +### Multi-slot skins (costume example) + +Costumes typically touch 3 meshes: + +```gdscript +"oldpop-grey-pant": { + "category": "costume", + "character": "Oldpop", + "slots": [ + { "mesh": "oldpop-body", "mode": "overlay", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" }, + { "mesh": "oldpop-bottom1", "mode": "override", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" }, + { "mesh": "oldpop-bottom2", "mode": "override", "material": "res://assets/characters/skins/clothing/oldpop_mat_cloth_grey_pant.tres" }, + ] +}, +``` + +### Tips + +- Leave `"material"` as `""` if the `.tres` file is not ready yet. The slot is skipped gracefully. +- Use the **Skin Catalog Editor** (`res://scenes/tools/skin_catalog_editor.tscn`) to avoid manual edits. Click **Save & Generate** to rewrite both `skin_manager.gd` and `economy.lua`. + +[Back to top](#top) + +--- + +## Step 3: Register the Skin in Economy (Server) + +Open `server/nakama/lua/economy.lua` and add a new entry to `SHOP_CATALOG_DEFS`. + +### Catalog entry format + +```lua +{ id = "oldpop-blue-hat", name = "Oldpop Blue Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" }, +``` + +| Field | Type | Description | +|---|---|---| +| `id` | string | Must match the `SKIN_CATALOG` key in `skin_manager.gd` exactly | +| `name` | string | Display name shown in shop | +| `category` | string | `head` / `costume` / `glove` / `accessory` | +| `gold` | number | Gold coin price (0 = not sold for gold) | +| `star` | number | Star gem price (0 = not sold for stars) | +| `rarity` | string | `"Common"` / `"Uncommon"` / `"Rare"` -- cosmetic label only | +| `character` | string | Character this skin belongs to (e.g. `"Oldpop"`) | + +### Existing catalog (12 items) + +``` +oldpop-blue-hat head 100 gold Common Oldpop +oldpop-green-hat head 100 gold Common Oldpop +oldpop-red-hat head 100 gold Common Oldpop +oldpop-yellow-hat head 100 gold Common Oldpop +oldpop-og-pant costume 0 gold Common Oldpop (free) +oldpop-grey-pant costume 150 gold Common Oldpop +oldpop-red-pant costume 150 gold Common Oldpop +oldpop-yellow-pant costume 150 gold Common Oldpop +oldpop-blue-gloves glove 75 gold Common Oldpop +oldpop-green-gloves glove 75 gold Common Oldpop +oldpop-red-gloves glove 75 gold Common Oldpop +oldpop-yellow-gloves glove 75 gold Common Oldpop +``` + +[Back to top](#top) + +--- + +## Step 4 (Optional): Add Skin as Gacha Prize + +Gacha-only skins are registered in `server/nakama/lua/gacha.lua` inside `GACHA_DATA.real_prize_catalog`. + +### Existing gacha skins (4 items) + +```lua +skin_gacha_rainbow_suit = { name = "Rainbow Suit", category = "costume", rarity = "real_prize", character = "" } +skin_gacha_dragon_hat = { name = "Dragon Hat", category = "head", rarity = "real_prize", character = "" } +skin_gacha_phantom_gloves = { name = "Phantom Gloves", category = "glove", rarity = "real_prize", character = "" } +skin_gacha_neon_acc = { name = "Neon Accessory", category = "accessory", rarity = "real_prize", character = "" } +``` + +Gacha skins also need a `skin_manager.gd` entry (same step 2 format) and a `skin_catalog_editor` entry so `SkinManager.apply_loadout()` can render them. The server catalog is optional -- gacha skins are not sold in the shop directly. + +The gacha pulls these IDs from `GACHA_DATA.pools.real_prize`, so add your item_id there too. + +```lua +pools = { + common = {"frag_common"}, + uncommon = {"frag_uncommon"}, + rare = {"frag_rare"}, + real_prize = { + "skin_gacha_rainbow_suit", + "skin_gacha_dragon_hat", + "skin_gacha_phantom_gloves", + "skin_gacha_neon_acc", + -- add new skin here + } +}, +``` + +[Back to top](#top) + +--- + +## Step 5: Deploy + +### Hot patch (content only) -- recommended for skins + +1. Commit changes to `experimental` branch: + - `scripts/managers/skin_manager.gd` + - `server/nakama/lua/economy.lua` (if shop item) + - `server/nakama/lua/gacha.lua` (if gacha prize) + - Material `.tres` files + +2. Push to `experimental`. + +3. Trigger `deploy_patch.yml` via Gitea UI workflow dispatch. + - CI runs `--export-pack` to build `patch.pck`. + - CI force-pushes `patch.pck` + `version.json` to `patches` branch. + - Existing players auto-download on next boot via `GameUpdateManager`. + +### Full binary release (if engine/templates changed) + +Tag a version (e.g. `v2.5.0`) and push. CI builds all platform binaries and uploads to the release. + +[Back to top](#top) + +--- + +## Full Flow Diagram + +``` +┌──────────────────────┐ +│ 1. Create Material │ +│ skin_shader_generator │ +│ ────────────────── │ +│ Export .tres file │ +└────────┬─────────────┘ + v +┌──────────────────────────────┐ +│ 2. Register in SkinManager │ +│ skin_manager.gd │ +│ ────────────────── │ +│ Add SKIN_CATALOG entry │ +│ (mesh slots + material) │ +└────────┬─────────────────────┘ + v +┌──────────────────────────────┐ +│ 3. Register in Economy │ +│ economy.lua │ +│ ────────────────── │ +│ Add SHOP_CATALOG_DEFS │ +│ (price, name, category) │ +└────────┬─────────────────────┘ + v (optional) +┌──────────────────────────────┐ +│ 4. Gacha Prize │ +│ gacha.lua │ +│ ────────────────── │ +│ real_prize_catalog + pools │ +└────────┬─────────────────────┘ + v +┌──────────────────────┐ +│ 5. Git Push & CI │ +│ deploy_patch.yml │ +│ ────────────────── │ +│ patch.pck → players │ +└──────────────────────┘ +``` + +[Back to top](#top) + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| Skin visible in editor but not in-game | Material path wrong or `.tres` not exported | Verify `res://` path in SKIN_CATALOG, run `--export-pack` | +| Skin purchase fails: "NotEnoughFunds" | Wallet balance insufficient | Check gold/star prices in economy.lua | +| Skin visible on all characters | `character` field wrong | Set correct character node name | +| Skin purchase fails: "ItemNotFound" | Item ID not in SHOP_CATALOG_DEFS | Add entry matching SKIN_CATALOG key | +| Player downloads patch but skin missing | econmy.lua change didn't reach server | Nakama hot-reload: restart Nakama container or wait for next restart | +| Outline shader lost on skin | next_pass not preserved | SkinManager preserves it automatically -- verify with latest `skin_manager.gd` | + +[Back to top](#top)