Author SHA1 Message Date
adtpdn 8573f8a85d Fix missing player_goals declaration in GoalsCycleManager
Release / Build & Release (push) Successful in 8m19s
2026-07-09 16:19:33 +08:00
adtpdn 6506e9f416 Release 2.4.4: Candy Survival Knock and backfire fixes
Release / Build & Release (push) Successful in 7m54s
2026-07-09 15:05:15 +08:00
adtpdn 85cdd5e9d4 feat update 2026-07-08 18:02:49 +08:00
adtpdn 90e320ffac fix(candy-survival): initialize local client starting charges and support action_knock_tekton action in is_knock check 2026-07-08 17:47:04 +08:00
adtpdn f060a2f4b8 fix(candy-survival): implement stagger (knockdown), lunge visual bump, and target stealing/backfire animations for Knock (Q) 2026-07-08 17:40:01 +08:00
adtpdn 5cf08f249b fix(candy-survival): disable Ghost special tile spawns when dropping the Tekton in Candy Survival 2026-07-08 17:34:10 +08:00
adtpdn f407551d60 fix(candy-survival): restrict scarcity weights to prevent spawning any special tiles in Candy Survival 2026-07-08 17:24:04 +08:00
adtpdn 682c5925d3 fix(candy-survival): disable special tiles spawning in arena, reward ghost charge every 2 completed missions 2026-07-08 17:20:44 +08:00
adtpdn 5797197097 fix(candy-survival): restore ability to lift/carry roaming Tekton character 2026-07-08 16:49:22 +08:00
adtpdn 37e5d96b55 fix(candy-survival): initialize local HUD badge text splits and synchronize Knock/Ghost charge counts reliably 2026-07-08 16:42:59 +08:00
adtpdn 8410679890 fix(candy-survival): ensure Ghost mode button is always visible on HUD and syncs correctly on player load 2026-07-08 16:32:30 +08:00
adtpdn c5f691b861 fix(candy-survival): connect Ghost charges to SpecialTilesManager powerup inventory, spawn Ghost tiles in arena, and support powerup button activations 2026-07-08 16:14:23 +08:00
adtpdn f39726a7db fix(candy-survival): disable Space and G, rebind Q to Knock and F to Ghost mode 2026-07-08 16:08:30 +08:00
adtpdn 0836f4b8f0 fix(candy-survival): support automatic delivery upon walking adjacent to Mekton NPC 2026-07-08 16:03:09 +08:00
adtpdn 8e15018e23 fix(candy-survival): implement Mekton delivery in grab_item and Ghost mode in grab_tekton for mobile touch controls and bot support 2026-07-08 15:52:30 +08:00
adtpdn 47ba6e2456 fix(candy-survival): map interact/delivery input to action_grab instead of nonexistent action_interact 2026-07-08 15:42:24 +08:00
adtpdn 38d09a50e7 fix(candy-survival): resolve persistent candy color bugs, sync HUD scores and counts, bypass Mekton face delivery requirement, and correct STAR/COIN color switch 2026-07-08 15:37:23 +08:00
adtpdn 950b1969d8 feat: update 2026-07-07 18:15:47 +08:00
adtpdn 286d0ce069 feat: update candy logic 2026-07-07 18:08:49 +08:00
adtpdn 04e40f5b76 feat: edit tiles placement 2026-07-06 19:43:12 +08:00
adtpdn 90ada5ace4 docs: add completed tasks for July 6 to TODO.md 2026-07-06 19:11:31 +08:00
adtpdn ffbc3956ee chore: reset EditorTileReference scale to 1:1 global size for perfectly accurate tile footprint visualization in editor 2026-07-06 19:06:37 +08:00
adtpdn 6dae67fce4 fix: normalize arena root transforms to zero and restructure so EditorTileReference flawlessly aligns with world grid tiles 2026-07-06 18:59:35 +08:00
adtpdn baeb2f6cfc fix: counter-scale EditorTileReference in freemode so it renders correctly despite root 100x scale 2026-07-06 18:31:10 +08:00
adtpdn 7c3e55e904 chore: re-save Stop N Go scene to confirm shadow render priorities 2026-07-06 18:14:29 +08:00
adtpdn 4fd46af3c0 feat: embed tile height placeholder inside arena .tscn files and bake terrain transform shifts natively instead of using code hacks 2026-07-06 18:08:06 +08:00
adtpdn c114dd5175 chore: remove leftover test_mp.gd.uid file 2026-07-06 17:54:58 +08:00
adtpdn b4ac0afda7 chore: clean up temporary test scripts 2026-07-06 17:53:59 +08:00
adtpdn 48b748849b fix: set stop n go arena shadow planes render priority to -1 to prevent z-fighting with tiles 2026-07-06 17:51:27 +08:00
adtpdn f1ef43cb38 fix: adjust safe zone elevation and transparency sorting priority to prevent it covering tiles or clipping with terrain 2026-07-06 17:42:26 +08:00
adtpdn b879c7b684 fix: restrict 0.08 tile elevation exclusively to Stop n Go and Freemode 2026-07-06 17:35:19 +08:00
adtpdn ce515365a7 fix: elevate all gridmap meshes by 0.08 to prevent clipping into terrain shifted at -0.22 2026-07-06 17:31:02 +08:00
adtpdn b86011a6c6 fix: make non-walkable red blocks (Tile 4) invisible in 3D arenas 2026-07-06 16:42:54 +08:00
adtpdn 4bf9f7aee1 fix: adjust arena shift offset to -0.22 based on visual check for freemode and stopngo 2026-07-06 16:34:49 +08:00
adtpdn d3322f14ad fix: lower Stop N Go arena by 0.28 to fix buried tiles 2026-07-06 16:29:05 +08:00
adtpdn adad53e94d fix: resolve static cyclic references and unresolvable script dependencies in editor by retrieving Nakama session via get() and loading Autoloads via get_node_or_null 2026-07-06 16:17:16 +08:00
adtpdn 14d46b8823 fix: shift Freemode 3D arena down to Y=0 to prevent buried tiles, remove deprecated mesh elevation hacks 2026-07-06 16:01:58 +08:00
adtpdn acd927eef6 fix: correct JSON payload key for gacha pull RPC and update gacha panel UI labels 2026-07-06 14:37:22 +08:00
119 changed files with 6052 additions and 3925 deletions
-164
View File
@@ -1,164 +0,0 @@
---
description: This document tells AI agents how to work on Tekton Dash tasks end-to-end.
---
# AI Agent Workflow Guide for Tekton Dash
This document tells AI agents how to work on Tekton Dash tasks end-to-end.
---
## 1. Task Source: Notion MCP
All tasks live on the **"TektonDash - Armageddon PR Tasks"** Notion board.
https://www.notion.so/danchiego-game/36433be43b29800c8422ed5bdd65671b?v=36433be43b2980de8635000c0a910a0d
### Finding Tasks
Should always start with this to find tasks, find the highest priority task that is not done, second is In Progress, then To Do.
example, query for "Gauntlet"
```
Use: mcp_notion-mcp-server_API-post-search
query: "[Gauntlet]" or task name
filter: {"property": "object", "value": "page"}
```
### Reading a Task
Each task page has these properties:
| Property | Type | Purpose |
|---|---|---|
| **Name** | title | Task title, e.g. `[Gauntlet] #1 Game Mode Registration` |
| **Status** | select | `To Do``In Progress``Done` |
| **Priority** | select | `P0` (critical) / `P1` / `P2` / `P3` |
| **Effort** | select | `S - Small` / `M - Medium` / `L - Large` / `XL - Epic` |
| **Sprint** | select | `Alpha` / `Beta` / `Release` |
| **ProjectType** | select | `CORE` / `CLIENT` / `SERVER` / `INFRA` |
| **Description** | rich_text | Full task description — **read this to understand what to do** |
| **Acceptance** | checkbox | Check when task is verified complete |
| **DueDate** | date | Optional deadline |
| **UnitTest** | date | Optional test completion date |
### Task Lifecycle
```
To Do → In Progress → Done
```
1. **Pick up task**: Set `Status``In Progress`
2. **Do the work**: Read `Description`, implement the changes
3. **Write unit tests**: Follow pattern in `tests/` directory
4. **Mark complete**: Set `Status``Done`, check `Acceptance`
5. **Update changelog**: Add entry to `CHANGELOG_DRAFT.md` (consumer language)
6. **Bump version**: Update `project.godot` + `export_presets.cfg`
```
Use: mcp_notion-mcp-server_API-patch-page
page_id: "<task_page_id>"
properties: {"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}}
```
---
## 2. Code Structure
| Path | Purpose |
|---|---|
| `scripts/game_mode.gd` | GameMode enum + helpers (add new modes here) |
| `scripts/managers/` | All game mode managers (lobby, stop_n_go, portal, gauntlet) |
| `scenes/main.gd` | Central orchestrator — init, setup, game start routing |
| `tests/` | GUT unit tests — one file per task/feature |
### Adding a New Game Mode
1. Add enum to `scripts/game_mode.gd` → update `from_string()`, `mode_to_string()`, `get_all_modes()`, `is_restricted()`
2. Add mode name to `LobbyManager.available_game_modes` in `lobby_manager.gd`
3. Add arena name to `_update_available_areas()` in `lobby_manager.gd`
4. Add manager var + init branch in `main.gd` `_init_managers()`
5. Add setup branch in `_setup_host_game()` and `_setup_client_game()`
6. Add start branch in `_start_game()`
7. Add background in `_apply_arena_background()`
---
## 3. Unit Testing
### Pattern
All tests extend `GutTest` and live in `tests/`. Naming: `test_<feature>.gd`
```gdscript
extends GutTest
func before_all():
gut.p("=== Feature Tests [Task ID] ===")
func test_something():
assert_eq(actual, expected, "Description")
func after_all():
gut.p("=== Feature Tests Complete ===")
```
### Running Tests
```cmd
run_tests.cmd # all tests
run_tests.cmd test_gauntlet_registration # specific test
```
Reports saved to `test_reports/` with timestamps.
---
## 4. Version Bumping
**Before bumping, check git for existing uncommitted version changes:**
```cmd
git diff --cached -- project.godot CHANGELOG_DRAFT.md
git diff -- project.godot CHANGELOG_DRAFT.md
```
### If version changes already exist (staged or unstaged):
**APPEND** your changelog bullet to the existing version block in `CHANGELOG_DRAFT.md`
**DO NOT** bump `project.godot` or `export_presets.cfg` — you're joining an in-progress batch
### If NO version changes exist (clean state):
**BUMP** version (increment patch: `2.3.5``2.3.6`)
**UPDATE** all locations below
Version appears in **4 locations** — all must match:
| File | Field |
|---|---|
| `CHANGELOG_DRAFT.md` | `## [X.Y.Z] — YYYY-MM-DD` header |
| `project.godot` | `config/version="X.Y.Z"` |
| `export_presets.cfg` | `application/file_version` and `application/product_version` (per preset) |
| `export_presets.cfg` | `export_path` filenames containing version |
| `export_presets.cfg` | `version/name` (Android preset) |
### Changelog Style
Entries are **consumer-facing** (readable by players). No internal jargon.
```markdown
## [2.3.6] — 2026-05-22
- Added new game mode: Candy Cannon Survival
```
**Bad:** "Added GAUNTLET = 3 to GameMode.Mode enum"
**Good:** "Added new game mode: Candy Cannon Survival"
---
## 5. Key Conventions
- **Caveman Mode**: Be terse. No filler. Execute first, talk second.
- **Read before edit**: Always check whole files before modifying `.gd`, `.tscn`, `.tres`, `.res` files.
- **Notion status flow**: `To Do``In Progress``Done` (never skip steps).
- **Test everything**: Every completed task gets a `test_<feature>.gd` in `tests/`.
- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`.
-53
View File
@@ -1,53 +0,0 @@
# Pending Notion Tasks for core_coder
## Instructions
Run these commands as `core_coder` profile to complete the 5 "In Progress" tasks:
```bash
# Switch to core_coder profile
hermes profile use core_coder
# Navigate to project
cd /home/beng/Godot/Projects/tekton-enet
# Start interactive session with task context
hermes chat -z "Complete the 5 'In Progress' tasks from Notion. Start with P0 task [Gauntlet] #4 Cannon Timer & Basic Volley (ID: 36833be4-3b29-812b-b25e-d087446c57f6). Follow TASKS.md procedure for each task."
```
## Task List (Priority Order)
### 1. [P0] [Gauntlet] #4 Cannon Timer & Basic Volley
- **Notion ID:** 36833be4-3b29-812b-b25e-d087446c57f6
- **Type:** CORE
- **Description:** Create candy_cannon_controller.gd. Implement 5-second interval cannon timer in GauntletManager._process(). Fire 5 shots per volley at random valid cells (1×1 only initially). Create candy_cannon.tscn (3×3 static NPC scene, is_static_turret=true pattern). Reuse tekton.gd spawn_projectile_rpc() arc-tween pattern for projectile visuals. Sync via rpc().
### 2. [P1] [036] Mode Config Completeness
- **Notion ID:** 36433be4-3b29-8129-bbc9-c3f8437559a8
- **Type:** CORE
### 3. [P1] [024] Set up GitHub Actions CI/CD Workflow
- **Notion ID:** 36633be4-3b29-8179-87bf-ec462fb6629f
- **Type:** DEVOPS
### 4. [P1] [032] Set up Artifact Storage & Versioning
- **Notion ID:** 36633be4-3b29-812b-9537-f44fbf89c3f9
- **Type:** DEVOPS
### 5. [P1] [031] Configure Platform-Specific Export Presets
- **Notion ID:** 36633be4-3b29-8147-8180-eebc89a08dc4
- **Type:** DEVOPS
## Workflow Per Task
1. Read task details: `mcp_notion_API_retrieve_a_page(page_id="...")`
2. Implement changes following TASKS.md
3. Write unit test in `tests/test_<feature>.gd`
4. Run tests: `./run_tests.cmd` (Windows) or equivalent
5. Update CHANGELOG_DRAFT.md (append to existing version block if changes exist)
6. Mark Done: `mcp_notion_API_patch_page(page_id="...", properties={"Status": {"select": {"name": "Done"}}, "Acceptance": {"checkbox": true}})`
7. Commit changes
## Notes
- Project path: /home/beng/Godot/Projects/tekton-enet
- Follow GUT_SETUP_SKILLS.md for testing
- Version 2.3.7 already has uncommitted changes — append changelog entries, don't bump version
+5
View File
@@ -1,4 +1,9 @@
## [NEXT] ## [NEXT]
## [2.4.4] — 2026-07-09
- Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.
- Implemented Candy Survival Knock backfire mechanic: knocking a player with 0 candies transfers your candy stack to them and staggers you.
- Fixed Candy Survival candy stack 3D mesh visual desync and client-side stack synchronization.
- Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions. - Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions.
- Fixed punch/smack SFX playing repeatedly when quickly attacking another player. - Fixed punch/smack SFX playing repeatedly when quickly attacking another player.
- Fixed currency split between Gacha and Shop — wallet balance now updates immediately after purchase without needing to reopen the panel. - Fixed currency split between Gacha and Shop — wallet balance now updates immediately after purchase without needing to reopen the panel.
+25 -1
View File
@@ -78,7 +78,7 @@ This document serves as an exhaustive, technical record of all modifications, re
- Fires `try_knock(attacker, target)` when adjacent to an opponent. - Fires `try_knock(attacker, target)` when adjacent to an opponent.
- Consumes 1 of 5 starting charges (`player_knocks`). - Consumes 1 of 5 starting charges (`player_knocks`).
- *Steal*: If target has candies, attacker adopts target's full stack and color; target is zeroed. - *Steal*: If target has candies, attacker adopts target's full stack and color; target is zeroed.
- *Backfire*: If target has 0 candies, attacker loses a charge and both suffer the knock animation state (handled by caller). - *Backfire*: If target has 0 candies, attacker loses a charge and gets staggered, while any candy stack held by the attacker is transferred to the target.
- **Ghost (Phase-Through)**: - **Ghost (Phase-Through)**:
- Input bound natively to `action_grab_tekton` inside `player_input_manager.gd` (bypasses default grab behavior in this mode). - Input bound natively to `action_grab_tekton` inside `player_input_manager.gd` (bypasses default grab behavior in this mode).
- Consumes 1 of 5 starting charges (`player_ghosts`). - Consumes 1 of 5 starting charges (`player_ghosts`).
@@ -120,3 +120,27 @@ This document serves as an exhaustive, technical record of all modifications, re
- **Compile Integrity**: Fixed a trailing `_on_doors_update()` call in `lobby_room.gd` leftover from a different branch. Verified flawless compilation via `godot --headless --build-solutions`. - **Compile Integrity**: Fixed a trailing `_on_doors_update()` call in `lobby_room.gd` leftover from a different branch. Verified flawless compilation via `godot --headless --build-solutions`.
- **Gitea Documentation**: Automated the closure and deep-technical documentation of Issues #54, 55, 56, 57, 65, 66, 67, 68, 69, and 70 directly to the tracking server. - **Gitea Documentation**: Automated the closure and deep-technical documentation of Issues #54, 55, 56, 57, 65, 66, 67, 68, 69, and 70 directly to the tracking server.
- **Wiki Accuracy**: Overhauled `Game-Modes.-.md` to perfectly mirror the new Sabotage input maps, Blueprint rules, RPC structures, and Mekton behavior. - **Wiki Accuracy**: Overhauled `Game-Modes.-.md` to perfectly mirror the new Sabotage input maps, Blueprint rules, RPC structures, and Mekton behavior.
### Completed (July 6, 2026)
- [x] **CI Pipeline Fixes**
- [x] Downgraded Godot runner version to 4.6.3 to match local engine build.
- [x] Updated Gitea API and Git clone URLs to use `https://` instead of `http://` to prevent silent redirect failures during release.
- [x] **Runtime & Compilation Crash Fixes**
- [x] Fixed `bot_controller.gd`: Replaced dead `_execute_move()` with proper `enhanced_gridmap.find_path()` logic.
- [x] Fixed `bot_controller.gd` & `player_movement_manager.gd`: Changed `.is_active` to `.active` for CandySurvivalManager checks.
- [x] Fixed `candy_survival_manager.gd`: Merged duplicate `start_game_mode()` implementations.
- [x] Fixed `special_tiles_manager.gd` indentation errors.
- [x] Fixed `main.gd` duplicate variable declarations.
- [x] **Editor & Test Environment Fixes**
- [x] Resolved "Cyclic Reference" errors in `leaderboard_panel.gd` by accessing Nakama session via `get()`.
- [x] Fixed `BackendService.gd` Autoload dependencies failing in isolated tests by using `get_node_or_null()`.
- [x] **Gacha System Fixes**
- [x] Corrected `BackendService.gd` payload to send `"banner_id"` instead of `"gacha_id"`, fixing HTTP 500 errors on pull.
- [x] Configured Unique Names (`%`) for left-side BalanceRow labels in `gacha_panel.tscn` so currency updates instantly upon rolling.
- [x] **3D Arena & GridMap Elevation Fixes**
- [x] Normalized root node transforms of all arenas (`freemode.tscn`, `stop_n_go.scn`, `candy_survival.tscn`) to `(0,0,0)` and scale `(1,1,1)`.
- [x] Removed hardcoded terrain shifts and `0.08` mesh elevations from `main.gd`.
- [x] Added `EditorTileReference` to arena scenes as a perfect 1:1 footprint guide.
- [x] Baked terrain height shifts directly into `.tscn` files to match the reference tile perfectly.
- [x] Scaled red `non-walkable` blocks (Tile 4) to 0 so they act as invisible collision walls in 3D arenas.
- [x] Fixed Stop N Go safe zone clipping by adjusting render priorities (`1` for safe zone, `-1` for baked terrain shadows).
+21 -1
View File
@@ -1,7 +1,27 @@
{ {
"latest_version": "2.4.3", "latest_version": "2.4.4",
"minimum_app_version": "2.1.0", "minimum_app_version": "2.1.0",
"releases": [ "releases": [
{
"version": "2.4.4",
"date": "2026-07-09",
"pck_url": "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck",
"pck_size": 0,
"changelog": [
"Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.",
"Implemented Candy Survival Knock backfire mechanic: knocking a player with 0 candies transfers your candy stack to them and staggers you.",
"Fixed Candy Survival candy stack 3D mesh visual desync and client-side stack synchronization.",
"Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions.",
"Fixed punch/smack SFX playing repeatedly when quickly attacking another player.",
"Fixed currency split between Gacha and Shop \u2014 wallet balance now updates immediately after purchase without needing to reopen the panel.",
"Fixed fatal crash in Multiplayer Gauntlet caused by missing `has_method` check on smack cooldown timers on remote peers.",
"Upgraded engine from Godot 4.6 to 4.7 stable for better performance and stability.",
"Migrated patch delivery from GitHub Pages to Gitea native raw endpoint \u2014 no more external dependencies for game updates.",
"CI: optimized build cache for Godot binary, eliminating repeated 140MB downloads on every workflow run.",
"CI: shallow repository checkout (--depth 1) for faster clone times.",
"CI: removed unnecessary export template download from patch deployment workflow (--export-pack doesn't need them)."
]
},
{ {
"version": "2.4.3", "version": "2.4.3",
"date": "2026-07-04", "date": "2026-07-04",
+4 -4
View File
@@ -14,7 +14,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/windows/tekton_armageddon_v2.4.3.exe" export_path="build/windows/tekton_armageddon_v2.4.4.exe"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -85,7 +85,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton-dash-armageddon-v.2.4.3.apk" export_path="build/tekton-dash-armageddon-v.2.4.4.apk"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -314,7 +314,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.4.3.zip" export_path="build/tekton_armageddon_v2.4.4.zip"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -589,7 +589,7 @@ custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/linux/tekton_armageddon_v2.4.3.x86_64" export_path="build/linux/tekton_armageddon_v2.4.4.x86_64"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
+1 -1
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application] [application]
config/name="Tekton Dash Armageddon" config/name="Tekton Dash Armageddon"
config/version="2.4.3" config/version="2.4.4"
run/main_scene="res://scenes/ui/boot_screen.tscn" run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus") config/features=PackedStringArray("4.6", "Forward Plus")
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
-64
View File
@@ -1,64 +0,0 @@
@echo off
REM ============================================================================
REM Tekton Dash — GUT Test Runner with Report Generation
REM Usage: run_tests.cmd [test_file]
REM No args = run ALL tests in tests/
REM With arg = run specific test, e.g. run_tests.cmd test_gauntlet_registration
REM ============================================================================
setlocal EnableDelayedExpansion
REM --- Config ---
set GODOT_PATH=godot
set PROJECT_PATH=%~dp0
set REPORT_DIR=%PROJECT_PATH%test_reports
set TIMESTAMP=%DATE:~10,4%-%DATE:~4,2%-%DATE:~7,2%_%TIME:~0,2%-%TIME:~3,2%-%TIME:~6,2%
set TIMESTAMP=%TIMESTAMP: =0%
set REPORT_FILE=%REPORT_DIR%\test_report_%TIMESTAMP%.txt
REM --- Create report directory ---
if not exist "%REPORT_DIR%" mkdir "%REPORT_DIR%"
REM --- Determine test target ---
set TEST_ARG=
if not "%~1"=="" (
set TEST_ARG=-gtest=res://tests/%~1.gd
echo Running specific test: %~1
) else (
set TEST_ARG=-gdir=res://tests/
echo Running ALL tests in tests/
)
REM --- Header ---
echo ============================================================================ > "%REPORT_FILE%"
echo TEKTON DASH — Test Report >> "%REPORT_FILE%"
echo Generated: %DATE% %TIME% >> "%REPORT_FILE%"
echo Project: Tekton Dash Armageddon >> "%REPORT_FILE%"
echo ============================================================================ >> "%REPORT_FILE%"
echo. >> "%REPORT_FILE%"
REM --- Run GUT tests via Godot headless ---
echo Running tests...
%GODOT_PATH% --headless --path "%PROJECT_PATH%" -s res://addons/gut/gut_cmdln.gd %TEST_ARG% -gexit 2>&1 | tee -a "%REPORT_FILE%" 2>nul
REM Fallback if tee is not available (most Windows systems)
if %ERRORLEVEL% neq 0 (
%GODOT_PATH% --headless --path "%PROJECT_PATH%" -s res://addons/gut/gut_cmdln.gd %TEST_ARG% -gexit >> "%REPORT_FILE%" 2>&1
)
REM --- Footer ---
echo. >> "%REPORT_FILE%"
echo ============================================================================ >> "%REPORT_FILE%"
echo End of Report >> "%REPORT_FILE%"
echo ============================================================================ >> "%REPORT_FILE%"
echo.
echo ============================================
echo Report saved to: %REPORT_FILE%
echo ============================================
echo.
REM --- Open report ---
type "%REPORT_FILE%"
endlocal
+15 -2
View File
@@ -1,4 +1,4 @@
[gd_scene format=3 uid="uid://bx5a31fqhw8h8"] [gd_scene format=3 uid="uid://dmkx71ms3cfc"]
[ext_resource type="PackedScene" uid="uid://h84tka1n1alg" path="res://assets/models/arena/candy_survival/candy_survival_terrain.gltf" id="1_86fyc"] [ext_resource type="PackedScene" uid="uid://h84tka1n1alg" path="res://assets/models/arena/candy_survival/candy_survival_terrain.gltf" id="1_86fyc"]
@@ -24,10 +24,18 @@ albedo_color = Color(0.4973, 0.6, 0.12599999, 1)
material = SubResource("StandardMaterial3D_86fyc") material = SubResource("StandardMaterial3D_86fyc")
size = Vector2(50, 50) size = Vector2(50, 50)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_q8eld"]
transparency = 1
albedo_color = Color(1, 0, 1, 0.5)
[sub_resource type="BoxMesh" id="BoxMesh_l6h1c"]
material = SubResource("StandardMaterial3D_q8eld")
size = Vector3(1, 0.05, 1)
[node name="Candy Survival" type="Node3D" unique_id=1063002869] [node name="Candy Survival" type="Node3D" unique_id=1063002869]
[node name="PlaceholderFloor" type="MeshInstance3D" parent="." unique_id=932640085] [node name="PlaceholderFloor" type="MeshInstance3D" parent="." unique_id=932640085]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10, 0, 10) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10, -0.3, 10)
visible = false visible = false
mesh = SubResource("PlaneMesh_floor") mesh = SubResource("PlaneMesh_floor")
@@ -41,3 +49,8 @@ mesh = SubResource("BoxMesh_cannon")
[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1749367969] [node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1749367969]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.192932, 0, 9.441385) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9.192932, 0, 9.441385)
mesh = SubResource("PlaneMesh_ugtui") mesh = SubResource("PlaneMesh_ugtui")
[node name="EditorTileReference" type="MeshInstance3D" parent="." unique_id=1116403213]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
visible = false
mesh = SubResource("BoxMesh_l6h1c")
+5196 -15
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
[gd_scene format=3 uid="uid://iinjrl0ujfnh"]
[sub_resource type="SphereMesh" id="1"]
radius = 0.2
height = 0.4
[sub_resource type="StandardMaterial3D" id="2"]
albedo_color = Color(1, 1, 1, 1)
metallic = 0.3
roughness = 0.4
[node name="Candy" type="Node3D"]
[node name="Mesh" type="MeshInstance3D" parent="."]
mesh = SubResource("1")
material_override = SubResource("2")
+27 -30
View File
@@ -1,48 +1,46 @@
[gd_scene load_steps=2 format=3 uid="uid://c6091875aa42720bfa9f9a7917331a09"] [gd_scene format=3 uid="uid://b8nxdmi3bpj0g"]
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="1_font"] [ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="1_font"]
[node name="CandySurvivalHUD" type="CanvasLayer"] [node name="CandySurvivalHUD" type="CanvasLayer" unique_id=423395116]
layer = 5 layer = 5
visible = false
[node name="TopContainer" type="CenterContainer" parent="."] [node name="TopContainer" type="CenterContainer" parent="." unique_id=2024389174]
anchors_preset = 5 anchors_preset = 5
anchor_left = 0.5 anchor_left = 0.5
anchor_right = 0.5 anchor_right = 0.5
offset_top = 70.0 offset_top = 70.0
grow_horizontal = 2 grow_horizontal = 2
[node name="SugarRushBar" type="ProgressBar" parent="TopContainer"] [node name="SugarRushBar" type="ProgressBar" parent="TopContainer" unique_id=548351179]
visible = false
custom_minimum_size = Vector2(300, 30) custom_minimum_size = Vector2(300, 30)
layout_mode = 2 layout_mode = 2
theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(1, 1, 1, 1) theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 4 theme_override_constants/outline_size = 4
theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 18
max_value = 1.0 max_value = 1.0
value = 0.0
show_percentage = false show_percentage = false
visible = false
[node name="SugarRushLabel" type="Label" parent="TopContainer/SugarRushBar"] [node name="SugarRushLabel" type="Label" parent="TopContainer/SugarRushBar" unique_id=1102847385]
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 18
theme_override_colors/font_color = Color(1, 0.2, 0.2, 1) theme_override_colors/font_color = Color(1, 0.2, 0.2, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 4 theme_override_constants/outline_size = 4
theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 18
text = "SUGAR RUSH!" text = "SUGAR RUSH!"
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
[node name="BottomContainer" type="CenterContainer" parent="."] [node name="BottomContainer" type="CenterContainer" parent="." unique_id=558209357]
anchors_preset = 7 anchors_preset = 7
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 1.0 anchor_top = 1.0
@@ -52,64 +50,63 @@ offset_top = -150.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
[node name="HBoxContainer" type="HBoxContainer" parent="BottomContainer"] [node name="HBoxContainer" type="HBoxContainer" parent="BottomContainer" unique_id=1499322099]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 40 theme_override_constants/separation = 40
[node name="SabotagePanel" type="VBoxContainer" parent="BottomContainer/HBoxContainer"] [node name="SabotagePanel" type="VBoxContainer" parent="BottomContainer/HBoxContainer" unique_id=1124406210]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4 theme_override_constants/separation = 4
[node name="KnockChargesLabel" type="Label" parent="BottomContainer/HBoxContainer/SabotagePanel"] [node name="KnockChargesLabel" type="Label" parent="BottomContainer/HBoxContainer/SabotagePanel" unique_id=1127989561]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 20
theme_override_colors/font_color = Color(1, 0.6, 0.2, 1) theme_override_colors/font_color = Color(1, 0.6, 0.2, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5 theme_override_constants/outline_size = 5
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 20
text = "🥊 Knocks: 5" text = "🥊 Knocks: 5"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="GhostChargesLabel" type="Label" parent="BottomContainer/HBoxContainer/SabotagePanel"] [node name="GhostChargesLabel" type="Label" parent="BottomContainer/HBoxContainer/SabotagePanel" unique_id=1769513912]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 20 theme_override_colors/font_color = Color(0.8, 0.8, 1, 1)
theme_override_colors/font_color = Color(0.8, 0.8, 1.0, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5 theme_override_constants/outline_size = 5
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 20
text = "👻 Ghosts: 5" text = "👻 Ghosts: 5"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="ScorePanel" type="VBoxContainer" parent="BottomContainer/HBoxContainer"] [node name="ScorePanel" type="VBoxContainer" parent="BottomContainer/HBoxContainer" unique_id=1680183752]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4 theme_override_constants/separation = 4
[node name="CandyStackBadge" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel"] [node name="CandyStackBadge" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel" unique_id=1673476912]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 28
theme_override_colors/font_color = Color(1, 0.4, 0.8, 1) theme_override_colors/font_color = Color(1, 0.4, 0.8, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6 theme_override_constants/outline_size = 6
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 28
text = "Stack: 0 (x1)" text = "Stack: 0 (x1)"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="PointsLabel" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel"] [node name="PointsLabel" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel" unique_id=1646972276]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 20 theme_override_colors/font_color = Color(0.4, 1, 0.4, 1)
theme_override_colors/font_color = Color(0.4, 1.0, 0.4, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5 theme_override_constants/outline_size = 5
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
theme_override_font_sizes/font_size = 20
text = "Score: 0" text = "Score: 0"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="DeliveryIndicator" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel"] [node name="DeliveryIndicator" type="Label" parent="BottomContainer/HBoxContainer/ScorePanel" unique_id=1123585835]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 20 theme_override_colors/font_color = Color(1, 1, 0.2, 1)
theme_override_colors/font_color = Color(1.0, 1.0, 0.2, 1)
theme_override_colors/font_outline_color = Color(0, 0, 0, 1) theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 5 theme_override_constants/outline_size = 5
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
text = "" theme_override_font_sizes/font_size = 20
horizontal_alignment = 1 horizontal_alignment = 1
+44 -18
View File
@@ -165,43 +165,51 @@ func _instantiate_3d_arena(scene_path: String):
if arena_scene: if arena_scene:
var arena_instance = arena_scene.instantiate() var arena_instance = arena_scene.instantiate()
arena_instance.name = "ArenaEnvironment3D" arena_instance.name = "ArenaEnvironment3D"
# Note: We no longer shift the arena via code (e.g., position.y = -0.22)
# The physical shifts have been baked directly into the .tscn files using EditorTileReference.
add_child(arena_instance) add_child(arena_instance)
move_child(arena_instance, 0) move_child(arena_instance, 0)
# Clean up the EditorTileReference if it was left in the scene
var editor_ref = arena_instance.get_node_or_null("EditorTileReference")
if editor_ref:
editor_ref.queue_free()
print("Instantiated 3D Arena: ", scene_path) print("Instantiated 3D Arena: ", scene_path)
func _hide_ground_tiles(): func _hide_ground_tiles():
# Make normal and auxiliary ground floors invisible # Make normal and auxiliary ground floors invisible
# by shrinking their scale to 0. We EXCLUDE Item 4 (Wall) and 5 (Freeze) # by shrinking their scale to 0. We also hide 4 (non-walkable red block)
# so they can still be seen above the 3D arena. # as 3D arenas provide their own visual boundaries. We EXCLUDE 5 (Freeze).
var em = $EnhancedGridMap var em = $EnhancedGridMap
if em and em.mesh_library: if em and em.mesh_library:
var ml = em.mesh_library.duplicate() var ml = em.mesh_library.duplicate()
for id in [0, 6]: for id in [0, 4, 6]:
# Scale to 0 to hide it without triggering invalid mesh errors # Scale to 0 to hide it without triggering invalid mesh errors
ml.set_item_mesh_transform(id, Transform3D().scaled(Vector3.ZERO)) ml.set_item_mesh_transform(id, Transform3D().scaled(Vector3.ZERO))
em.mesh_library = ml em.mesh_library = ml
print("[Main] Hide tiles 0, 6 via zero-scale transform.") print("[Main] Hide tiles 0, 4, 6 via zero-scale transform.")
func _setup_effect_elevation(): func _setup_effect_elevation():
var em = get_node_or_null("EnhancedGridMap") var em = get_node_or_null("EnhancedGridMap")
if em and em.mesh_library: if em and em.mesh_library:
# USER REQUEST: Do not apply visual Y-elevation for walls in Stop n Go mode
if LobbyManager.game_mode == "Stop n Go":
print("[Main] Stop n Go mode detected: Skipping effect elevation for walls.")
return
var ml = em.mesh_library.duplicate() var ml = em.mesh_library.duplicate()
# Height 0.8: Above 3D arena, but below pickups (Y=1.0) # Now that terrain heights are perfectly aligned inside the .tscn files,
var lift_transform = Transform3D().translated(Vector3(0, 0.28, 0)) # we no longer need to dynamically lift all tiles by 0.08.
# However, we still need to slightly drop TILE_SAFE (id 2) so it
# Lift Wall (4) and Freeze (5) # renders strictly as a floor decal without clipping.
ml.set_item_mesh_transform(4, lift_transform) if LobbyManager.game_mode in ["Stop n Go", "Freemode"]:
ml.set_item_mesh_transform(5, lift_transform) if ml.find_item_by_name("safe_zone") != -1 or 2 in ml.get_item_list():
var id = 2
var current_transform = ml.get_item_mesh_transform(id)
# Push it down by -0.02 so it sits perfectly between terrain and tiles
ml.set_item_mesh_transform(id, Transform3D().translated(Vector3(0, -0.02, 0)) * current_transform)
em.mesh_library = ml em.mesh_library = ml
print("[Main] MeshLibrary elevation applied: Wall(4) and Freeze(5) at Y=0.8")
# Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors # Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors
if not em: if not em:
@@ -1276,7 +1284,7 @@ func _create_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int):
if "cell_size" in enhanced_gridmap: if "cell_size" in enhanced_gridmap:
world_pos = Vector3( world_pos = Vector3(
pos.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x / 2, pos.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x / 2,
0.28, # Match the 0.28 elevation of the arena floor 0.0, # Floor is now at 0.0 after arena shift
pos.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z / 2 pos.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z / 2
) )
stand.global_position = world_pos stand.global_position = world_pos
@@ -1401,7 +1409,7 @@ func _on_peer_connected(new_peer_id: int):
var player_goals = GoalManager.preset_goals[player_index].duplicate() var player_goals = GoalManager.preset_goals[player_index].duplicate()
player.goals = player_goals player.goals = player_goals
# Update goals UI for all clients # Update goals UI for all clients
call_deferred("_deferred_set_player_goals", new_peer_id, player_goals) rpc("sync_player_goals", new_peer_id, player_goals)
@rpc @rpc
func add_newly_connected_player_character(new_peer_id: int): func add_newly_connected_player_character(new_peer_id: int):
@@ -1528,6 +1536,13 @@ func sync_game_start(player_list: Array, is_turn_based: bool):
stop_n_go_manager.name = "StopNGoManager" stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager) add_child(stop_n_go_manager)
stop_n_go_manager.activate_client_side() stop_n_go_manager.activate_client_side()
elif LobbyManager.game_mode == "Candy Survival":
if not candy_survival_manager:
candy_survival_manager = load("res://scripts/managers/candy_survival_manager.gd").new()
candy_survival_manager.name = "CandySurvivalManager"
add_child(candy_survival_manager)
candy_survival_manager.initialize(self, $EnhancedGridMap)
candy_survival_manager.activate_client_side()
# Initialize leaderboard for all peers (after a delay to ensure players loaded) # Initialize leaderboard for all peers (after a delay to ensure players loaded)
call_deferred("_deferred_init_leaderboard") call_deferred("_deferred_init_leaderboard")
@@ -2033,6 +2048,8 @@ func request_full_grid_sync():
if stop_n_go_manager.player_missions.has(sender_id): if stop_n_go_manager.player_missions.has(sender_id):
var mission_dict = {sender_id: stop_n_go_manager.player_missions[sender_id]} var mission_dict = {sender_id: stop_n_go_manager.player_missions[sender_id]}
stop_n_go_manager.rpc_id(sender_id, "sync_missions", mission_dict) stop_n_go_manager.rpc_id(sender_id, "sync_missions", mission_dict)
elif LobbyManager.game_mode == "Candy Survival" and candy_survival_manager:
candy_survival_manager.sync_state_to_player(sender_id)
# For all modes, only sync Floor 1 (Items) to prevent MTU packet overflow. # For all modes, only sync Floor 1 (Items) to prevent MTU packet overflow.
# Floor 0 logic is deterministic and generated locally on level load. # Floor 0 logic is deterministic and generated locally on level load.
@@ -2781,3 +2798,12 @@ func display_message(message: String, type: int = 0):
if player.has_method("display_message"): if player.has_method("display_message"):
player.display_message(message, type) player.display_message(message, type)
break break
func get_player_ids() -> Array:
var pids: Array = []
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if pid != 0 and not pids.has(pid):
pids.append(pid)
return pids
+4 -4
View File
@@ -2256,7 +2256,7 @@ theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10 theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10 theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10 theme_override_constants/margin_bottom = 10
metadata/_tab_index = 3 metadata/_tab_index = 2
[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Controls" unique_id=123456806] [node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Controls" unique_id=123456806]
layout_mode = 2 layout_mode = 2
@@ -2277,7 +2277,7 @@ theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10 theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10 theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10 theme_override_constants/margin_bottom = 10
metadata/_tab_index = 4 metadata/_tab_index = 3
[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/The Grid" unique_id=123456808] [node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/The Grid" unique_id=123456808]
layout_mode = 2 layout_mode = 2
@@ -2296,7 +2296,7 @@ theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10 theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10 theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10 theme_override_constants/margin_bottom = 10
metadata/_tab_index = 5 metadata/_tab_index = 4
[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Tektons" unique_id=123456810] [node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Tektons" unique_id=123456810]
layout_mode = 2 layout_mode = 2
@@ -2315,7 +2315,7 @@ theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10 theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10 theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10 theme_override_constants/margin_bottom = 10
metadata/_tab_index = 6 metadata/_tab_index = 5
[node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Skills" unique_id=123456812] [node name="RichTextLabel" type="RichTextLabel" parent="HowToPlayPanel/Panel/VBox/TabContainer/Skills" unique_id=123456812]
layout_mode = 2 layout_mode = 2
+39 -25
View File
@@ -605,40 +605,42 @@ func _get_all_mesh_instances(node: Node) -> Array:
# RPCs # RPCs
# ----------------------------------------------------------------- # -----------------------------------------------------------------
@rpc("any_peer", "call_local", "reliable") var _candy_scene: PackedScene = preload("res://scenes/candy.tscn")
func sync_candy_stack(count: int, color_id: int) -> void:
var visuals = get_node_or_null("Visuals")
if not visuals: return
var old_stack = visuals.get_node_or_null("CandyStack") @rpc("any_peer", "call_local", "reliable")
func sync_candy_stack(colors: Array) -> void:
var old_stack = get_node_or_null("CandyStack")
if old_stack: if old_stack:
old_stack.name = "CandyStack_Old_" + str(randi())
old_stack.queue_free() old_stack.queue_free()
if count <= 0: return if colors.is_empty() or not _candy_scene: return
var stack_node = Node3D.new() var stack_node = Node3D.new()
stack_node.name = "CandyStack" stack_node.name = "CandyStack"
visuals.add_child(stack_node) add_child(stack_node)
stack_node.position = Vector3(0, 2.0, 0) stack_node.position = Vector3(0, 1.8, 0)
# Determine base tile and override material based on the mode config or default tile meshes var candy_colors = {
var mesh_path = "res://assets/models/tiles/tile_heart.tres" 0: Color(1.0, 0.3, 0.3), # HEART = red
match color_id: 1: Color(0.3, 0.6, 1.0), # DIAMOND = blue
0: mesh_path = "res://assets/models/tiles/tile_heart.tres" 2: Color(0.0, 1.0, 0.4), # STAR = green
1: mesh_path = "res://assets/models/tiles/tile_diamond.tres" 3: Color(1.0, 0.8, 0.0), # COIN = gold
2: mesh_path = "res://assets/models/tiles/tile_star.tres" }
3: mesh_path = "res://assets/models/tiles/tile_coin.tres"
var tile_mesh = load(mesh_path) for i in range(colors.size()):
if not tile_mesh: return var color_id = int(colors[i])
var base_color = candy_colors.get(color_id, Color.WHITE)
for i in range(count): var candy = _candy_scene.instantiate()
var mi = MeshInstance3D.new() candy.position = Vector3(0, i * 0.35, 0)
mi.mesh = tile_mesh # Tint material to match color
mi.position = Vector3(0, i * 0.4, 0) var mesh = candy.get_node_or_null("Mesh")
mi.scale = Vector3(0.5, 0.5, 0.5) if mesh and mesh.material_override:
stack_node.add_child(mi) var mat = mesh.material_override.duplicate()
mat.albedo_color = base_color
mesh.material_override = mat
stack_node.add_child(candy)
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_character(character_name: String) -> void: func sync_character(character_name: String) -> void:
@@ -1184,7 +1186,7 @@ func _find_valid_drop_position() -> Vector2i:
var main_candy_survival = get_tree().root.get_node_or_null("Main") var main_candy_survival = get_tree().root.get_node_or_null("Main")
if main_candy_survival and main_candy_survival.get("candy_survival_manager"): if main_candy_survival and main_candy_survival.get("candy_survival_manager"):
gm = main_candy_survival.candy_survival_manager gm = main_candy_survival.candy_survival_manager
if gm and gm.is_active: if gm and gm.active:
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1: if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:
continue continue
if gm._is_npc_zone(pos): if gm._is_npc_zone(pos):
@@ -2082,6 +2084,18 @@ func bot_try_grab_item() -> bool:
#return true #return true
# ----------------------------------------------------------------- # -----------------------------------------------------------------
func grab_item(grid_position: Vector2i = current_position) -> bool: func grab_item(grid_position: Vector2i = current_position) -> bool:
if LobbyManager.game_mode == "Candy Survival":
# Check distance to Mekton NPC at (8, 8)
var dist = abs(current_position.x - 8) + abs(current_position.y - 8)
if dist <= 2:
var gm = get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.active:
var pid = get("peer_id") if "peer_id" in self else name.to_int()
if multiplayer.is_server():
gm.try_deliver(pid)
else:
gm.rpc_id(1, "try_deliver", pid)
return true
return playerboard_manager.grab_item(grid_position) return playerboard_manager.grab_item(grid_position)
# ----------------------------------------------------------------- # -----------------------------------------------------------------
+4 -2
View File
@@ -293,7 +293,8 @@ size_flags_horizontal = 0
size_flags_vertical = 4 size_flags_vertical = 4
texture = ExtResource("tex_gold") texture = ExtResource("tex_gold")
[node name="GoldLabel" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow/GoldPanel/MarginContainer/HBoxContainer" unique_id=1159494172] [node name="LeftGoldLabel" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow/GoldPanel/MarginContainer/HBoxContainer" unique_id=1159494172]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("3_font") theme_override_fonts/font = ExtResource("3_font")
@@ -327,7 +328,8 @@ size_flags_horizontal = 0
size_flags_vertical = 4 size_flags_vertical = 4
texture = ExtResource("tex_star") texture = ExtResource("tex_star")
[node name="StarLabel" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow/StarPanel/MarginContainer/HBoxContainer" unique_id=1357103504] [node name="LeftStarLabel" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow/StarPanel/MarginContainer/HBoxContainer" unique_id=1357103504]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("3_font") theme_override_fonts/font = ExtResource("3_font")
+11 -11
View File
@@ -106,7 +106,7 @@ func _physics_process(delta):
return return
# Only run if game has started # Only run if game has started
if not GameStateManager.is_game_started(): if not Engine.get_main_loop().root.get_node_or_null("GameStateManager").is_game_started():
return return
# STUCK PREVENTION # STUCK PREVENTION
@@ -143,7 +143,7 @@ func _physics_process(delta):
# Rate limiting (with difficulty scaling for Stop n Go) # Rate limiting (with difficulty scaling for Stop n Go)
var current_tick_rate = tick_rate var current_tick_rate = tick_rate
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x > 10: if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x > 10:
current_tick_rate = int(tick_rate * 0.6) # 40% faster updates after column 10 current_tick_rate = int(tick_rate * 0.6) # 40% faster updates after column 10
_tick_counter += 1 _tick_counter += 1
@@ -168,7 +168,7 @@ func _run_ai_tick():
if actor.is_player_moving: if actor.is_player_moving:
return return
if TurnManager.turn_based_mode: if Engine.get_main_loop().root.get_node_or_null("TurnManager").turn_based_mode:
_tick_counter = tick_rate # Force immediate evaluation in turn-based _tick_counter = tick_rate # Force immediate evaluation in turn-based
# Update cooldowns # Update cooldowns
@@ -176,7 +176,7 @@ func _run_ai_tick():
_tekton_spawn_cooldown -= get_process_delta_time() _tekton_spawn_cooldown -= get_process_delta_time()
# STOP N GO: Don't process if already at finish line # STOP N GO: Don't process if already at finish line
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x >= 21: if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO) and actor.current_position.x >= 21:
return return
# STOP N GO: Red light freezing logic # STOP N GO: Red light freezing logic
@@ -193,7 +193,7 @@ func _run_ai_tick():
print("[BotController] %s AI Tick. Goals: %s, Fullness: %.2f" % [actor.name, actor.goals, board_fullness]) print("[BotController] %s AI Tick. Goals: %s, Fullness: %.2f" % [actor.name, actor.goals, board_fullness])
# 0. BOT AGGRESSION THRESHOLD (Stop n Go) # 0. BOT AGGRESSION THRESHOLD (Stop n Go)
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) var is_sng = Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO)
var can_be_aggressive = not is_sng or actor.current_position.x > 10 var can_be_aggressive = not is_sng or actor.current_position.x > 10
# PRIORITY OVERRIDE: If board is getting full, prioritize clearing space! # PRIORITY OVERRIDE: If board is getting full, prioritize clearing space!
@@ -299,7 +299,7 @@ func _try_activate_ghost() -> bool:
return false return false
var gm = strategic_planner._get_candy_survival_manager() var gm = strategic_planner._get_candy_survival_manager()
if not gm or not gm.is_active: if not gm or not gm.active:
return false return false
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int() var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
@@ -481,7 +481,7 @@ func _try_attack_chase() -> bool:
if not push_success: if not push_success:
# If attack failed (e.g. Safe Zone in Stop n Go), don't just loop! # If attack failed (e.g. Safe Zone in Stop n Go), don't just loop!
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO): if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO):
if victim.current_position.x in [6, 7, 8, 14, 15, 16]: # Safe Zone Columns if victim.current_position.x in [6, 7, 8, 14, 15, 16]: # Safe Zone Columns
print("[BotController] %s target is in Safe Zone. Moving to find better angle." % actor.name) print("[BotController] %s target is in Safe Zone. Moving to find better angle." % actor.name)
await _try_unstuck_move() await _try_unstuck_move()
@@ -507,7 +507,7 @@ func _try_attack_chase() -> bool:
var next_step = Vector2i(path[1].x, path[1].y) var next_step = Vector2i(path[1].x, path[1].y)
# STOP N GO BOUNDARY PROTECTION # STOP N GO BOUNDARY PROTECTION
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and next_step.x >= 21: if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO) and next_step.x >= 21:
var main = get_tree().root.get_node_or_null("Main") var main = get_tree().root.get_node_or_null("Main")
var gc_manager = main.get_node_or_null("GoalsCycleManager") if main else null var gc_manager = main.get_node_or_null("GoalsCycleManager") if main else null
var time_remaining = gc_manager.get_global_time_remaining() if gc_manager else 999.0 var time_remaining = gc_manager.get_global_time_remaining() if gc_manager else 999.0
@@ -575,7 +575,7 @@ func _try_grab() -> bool:
pass pass
# Check if goals already achieved # Check if goals already achieved
if _is_goals_achieved() and LobbyManager.game_mode != "Stop n Go": if _is_goals_achieved() and Engine.get_main_loop().root.get_node_or_null("LobbyManager").game_mode != "Stop n Go":
return false return false
# Get tiles we need # Get tiles we need
@@ -634,7 +634,7 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
func _try_move() -> bool: func _try_move() -> bool:
"""Try to move toward needed tiles taking single steps like a player.""" """Try to move toward needed tiles taking single steps like a player."""
if _is_goals_achieved() and LobbyManager.game_mode != "Stop n Go": if _is_goals_achieved() and Engine.get_main_loop().root.get_node_or_null("LobbyManager").game_mode != "Stop n Go":
return false return false
# Find optimal movement target # Find optimal movement target
@@ -888,7 +888,7 @@ func _get_board_fullness_ratio() -> float:
func _is_goals_achieved() -> bool: func _is_goals_achieved() -> bool:
"""Check if goal pattern is complete (Standard) or mission complete (Stop n Go).""" """Check if goal pattern is complete (Standard) or mission complete (Stop n Go)."""
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO): if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO):
var main = get_tree().root.get_node_or_null("Main") var main = get_tree().root.get_node_or_null("Main")
if main: if main:
var sng_manager = main.get_node_or_null("StopNGoManager") var sng_manager = main.get_node_or_null("StopNGoManager")
@@ -34,8 +34,8 @@ func set_face_color_rpc(color_index: int) -> void:
match color_index: match color_index:
0: c = Color(1.0, 0.2, 0.2) # HEART (Red) 0: c = Color(1.0, 0.2, 0.2) # HEART (Red)
1: c = Color(0.2, 0.4, 1.0) # DIAMOND (Blue) 1: c = Color(0.2, 0.4, 1.0) # DIAMOND (Blue)
2: c = Color(1.0, 0.9, 0.2) # STAR (Yellow) 2: c = Color(0.0, 1.0, 0.4) # STAR (Green)
3: c = Color(1.0, 0.6, 0.0) # COIN (Orange/Gold) 3: c = Color(1.0, 0.8, 0.0) # COIN (Gold/Yellow)
face_material.albedo_color = c face_material.albedo_color = c
face_material.emission = c face_material.emission = c
+294 -44
View File
@@ -55,6 +55,11 @@ var hud_points_label: Label
var hud_delivery_indicator: Label var hud_delivery_indicator: Label
var _candy_survival_hud_scene: PackedScene = preload("res://scenes/candy_survival_hud.tscn") var _candy_survival_hud_scene: PackedScene = preload("res://scenes/candy_survival_hud.tscn")
# Cache for local client HUD resync (avoids local dictionary/timer desyncs)
var _last_badge_count: int = 0
var _last_badge_mult: float = 1.0
var _last_badge_face_match: bool = false
func _ready(): func _ready():
set_process(false) set_process(false)
_setup_hud() _setup_hud()
@@ -85,9 +90,27 @@ func activate_client_side() -> void:
hud_layer.visible = true hud_layer.visible = true
set_process(true) set_process(true)
var local = multiplayer.get_unique_id()
player_knocks[local] = START_KNOCK
player_ghosts[local] = START_GHOST
if hud_stack_badge:
hud_stack_badge.text = "Stack: 0 (x1.0)\nRed: 0x Blue: 0x Green: 0x Yellow: 0x"
if hud_knock_label:
hud_knock_label.text = "🥊 Knocks: 5"
if hud_ghost_label:
hud_ghost_label.text = "👻 Ghosts: 5"
# Per-player state # Per-player state
var player_candies: Dictionary = {} # pid -> int (stack count) var player_candies: Dictionary = {} # pid -> int (stack count)
var player_candy_color: Dictionary = {} # pid -> CandyColor (-1 if none) var player_candy_colors: Dictionary = {} # pid -> Array of CandyColor
var player_candy_color: Dictionary:
get:
var d = {}
for pid in player_candy_colors:
var colors = player_candy_colors[pid]
d[pid] = colors.back() if colors.size() > 0 else -1
return d
var player_knocks: Dictionary = {} # pid -> int (charges left) var player_knocks: Dictionary = {} # pid -> int (charges left)
var player_ghosts: Dictionary = {} # pid -> int (charges left) var player_ghosts: Dictionary = {} # pid -> int (charges left)
var player_ghost_active: Dictionary = {} # pid -> bool var player_ghost_active: Dictionary = {} # pid -> bool
@@ -125,14 +148,12 @@ func _define_sticky_walls() -> void:
sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true
func start_game_mode() -> void: func start_game_mode() -> void:
if multiplayer.is_server():
activate_client_side()
active = true active = true
current_face = CandyColor.HEART current_face = CandyColor.HEART
face_timer = 0.0 face_timer = 0.0
game_elapsed = 0.0 game_elapsed = 0.0
player_candies.clear() player_candies.clear()
player_candy_color.clear() player_candy_colors.clear()
player_knocks.clear() player_knocks.clear()
player_ghosts.clear() player_ghosts.clear()
player_ghost_active.clear() player_ghost_active.clear()
@@ -146,7 +167,7 @@ func start_game_mode() -> void:
var pids = _get_player_ids() var pids = _get_player_ids()
for pid in pids: for pid in pids:
player_candies[pid] = 0 player_candies[pid] = 0
player_candy_color[pid] = -1 player_candy_colors[pid] = []
player_knocks[pid] = START_KNOCK player_knocks[pid] = START_KNOCK
player_ghosts[pid] = START_GHOST player_ghosts[pid] = START_GHOST
player_ghost_active[pid] = false player_ghost_active[pid] = false
@@ -155,6 +176,13 @@ func start_game_mode() -> void:
player_blueprints[pid] = 0 player_blueprints[pid] = 0
player_sugar_rush[pid] = 0.0 player_sugar_rush[pid] = 0.0
player_last_knocked_by[pid] = 0 player_last_knocked_by[pid] = 0
_update_candy_badge(pid)
_update_special_inventory_for_ghosts(pid)
rpc("sync_knock_charge_count", pid, START_KNOCK)
rpc("sync_ghost_charge_count", pid, START_GHOST)
if multiplayer.is_server():
activate_client_side()
print("[CandySurvival] Started with %d players" % pids.size()) print("[CandySurvival] Started with %d players" % pids.size())
@@ -164,6 +192,8 @@ func _enter_tree() -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not active: if not active:
return return
if not multiplayer.is_server():
return
game_elapsed += delta game_elapsed += delta
# Mekton face color cycle # Mekton face color cycle
@@ -227,19 +257,50 @@ func _process(delta: float) -> void:
# ── Blueprint ── # ── Blueprint ──
func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) -> void: func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) -> void:
var base_points = 1000
var points = base_points if not off_color else int(base_points * OFF_COLOR_PENALTY)
_add_score(pid, points)
player_blueprints[pid] = player_blueprints.get(pid, 0) + 1 player_blueprints[pid] = player_blueprints.get(pid, 0) + 1
if player_blueprints[pid] % 2 == 0:
if multiplayer.is_server():
add_ghost_charge(pid)
var target_tile_id = primary_tile_id
var player_node: Node = null
if main_scene:
player_node = main_scene.get_node_or_null(str(pid))
if player_node and player_node.goals.size() > 0:
target_tile_id = player_node.goals[0]
var candy_color = CandyColor.HEART var candy_color = CandyColor.HEART
for key in TILE_IDS: for key in TILE_IDS:
if TILE_IDS[key] == primary_tile_id: if TILE_IDS[key] == target_tile_id:
candy_color = key candy_color = key
break break
_give_candy(pid, candy_color) _give_candy(pid, candy_color)
# Clear the board now that it's matched and converted to a candy
if main_scene and player_node:
if player_node.playerboard_manager:
# Visual wipe
player_node.playerboard.fill(-1)
main_scene.rpc("sync_playerboard", pid, player_node.playerboard)
# Record goal completion stats in GoalsCycleManager
var goals_cycle = main_scene.get_node_or_null("GoalsCycleManager")
if goals_cycle:
# Increment goal count (but DO NOT trigger the general score points
# because in Candy Survival, points are awarded during Mekton deposit)
if not goals_cycle.player_goal_counts.has(pid):
goals_cycle.player_goal_counts[pid] = 0
goals_cycle.player_goal_counts[pid] += 1
goals_cycle.emit_signal("goal_count_updated", pid, goals_cycle.player_goal_counts[pid])
goals_cycle.rpc("sync_goal_count", pid, goals_cycle.player_goal_counts[pid])
# Generate new goals immediately to keep player active
goals_cycle.regenerate_goals_for_player(player_node)
# Need to randomize tiles around player to refresh pickups for the new goal
goals_cycle._randomize_tiles_around_player(player_node)
func can_finish_with_off_color(pid: int, primary_tile_id: int) -> bool: func can_finish_with_off_color(pid: int, primary_tile_id: int) -> bool:
return not _grid_has_color_tiles(primary_tile_id) return not _grid_has_color_tiles(primary_tile_id)
@@ -257,24 +318,19 @@ func _grid_has_color_tiles(target_tile_id: int) -> bool:
func _give_candy(pid: int, color: int) -> void: func _give_candy(pid: int, color: int) -> void:
player_candies[pid] = player_candies.get(pid, 0) + 1 player_candies[pid] = player_candies.get(pid, 0) + 1
player_candy_color[pid] = color if not player_candy_colors.has(pid):
player_candy_colors[pid] = []
player_candy_colors[pid].append(color)
_update_candy_badge(pid) _update_candy_badge(pid)
func _update_candy_badge(pid: int) -> void: func _update_candy_badge(pid: int) -> void:
var count = player_candies.get(pid, 0) var count = player_candies.get(pid, 0)
var mult = 1.0 + count * MULTI_STEP var mult = 1.0 + count * MULTI_STEP
var color = player_candy_color.get(pid, -1) var colors = player_candy_colors.get(pid, [])
var face_match = (color != -1 and color == current_face) var color = colors.back() if colors.size() > 0 else -1
rpc("sync_candy_badge", pid, count, mult, face_match) var face_match = (color != -1)
var score = player_score.get(pid, 0)
# Update the 3D meshes on the player's head across all clients rpc("sync_candy_badge", pid, count, mult, face_match, score, colors)
var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players:
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if curr_pid == pid:
if player.has_method("sync_candy_stack"):
player.rpc("sync_candy_stack", count, color)
break
func get_multiplier(pid: int) -> float: func get_multiplier(pid: int) -> float:
var count = player_candies.get(pid, 0) var count = player_candies.get(pid, 0)
@@ -287,21 +343,25 @@ func try_deliver(pid: int) -> bool:
if not multiplayer.is_server(): if not multiplayer.is_server():
return false return false
var color = player_candy_color.get(pid, -1) var colors = player_candy_colors.get(pid, [])
if color == -1 or player_candies.get(pid, 0) == 0: var color = colors.back() if colors.size() > 0 else -1
return false if color == -1 or colors.size() == 0:
if color != current_face: rpc("sync_delivery_result", pid, false, "No candy")
return false return false
var count = player_candies[pid] var count = colors.size()
var mult = get_multiplier(pid) var mult = get_multiplier(pid)
_add_score(pid, count * 500)
var base_points = 1000 * count
_add_score(pid, int(base_points * mult))
player_candies[pid] = 0 player_candies[pid] = 0
player_candy_color[pid] = -1 player_candy_colors[pid] = []
_update_candy_badge(pid) _update_candy_badge(pid)
_trigger_sugar_rush(pid, count, mult) _trigger_sugar_rush(pid, count, mult)
rpc("sync_delivery_result", pid, true, "Delivered!")
return true return true
func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void: func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void:
@@ -327,27 +387,65 @@ func try_knock(attacker: int, target: int) -> bool:
return false return false
if player_ghost_active.get(target, false): if player_ghost_active.get(target, false):
return false return false
if player_knocks.get(attacker, 0) <= 0: if not player_knocks.has(attacker):
player_knocks[attacker] = START_KNOCK
if player_knocks[attacker] <= 0:
return false return false
var target_candies = player_candies.get(target, 0) var target_colors = player_candy_colors.get(target, [])
var target_candies = target_colors.size()
var attacker_node = _find_player_node(attacker)
var target_node = _find_player_node(target)
if attacker_node and target_node:
attacker_node.rpc("sync_bump", target_node.current_position)
if target_candies == 0: if target_candies == 0:
# Backfire: both lose a charge # Backfire: attacker loses a charge & gets staggered
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
# Attacker is also knocked down briefly (visual handled by caller) rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
# Attacker is knocked down briefly (backfire)
if attacker_node:
attacker_node.rpc("apply_stagger", 2.0)
# Backfire penalty: Attacker's candy stack transfers to Target!
var attacker_colors = player_candy_colors.get(attacker, [])
var attacker_candies = attacker_colors.size()
if attacker_candies > 0:
if not player_candy_colors.has(target):
player_candy_colors[target] = []
player_candy_colors[target].append_array(attacker_colors)
player_candy_colors[attacker] = []
player_candies[target] = player_candy_colors[target].size()
player_candies[attacker] = 0
_add_score(target, attacker_candies * 100)
_update_candy_badge(attacker)
_update_candy_badge(target)
rpc("sync_knock_result", attacker, target, 0) rpc("sync_knock_result", attacker, target, 0)
return false return false
# Steal all candies # Steal all candies (Successful knock)
player_candies[attacker] = player_candies.get(attacker, 0) + target_candies if not player_candy_colors.has(attacker):
player_candies[target] = 0 player_candy_colors[attacker] = []
player_candy_color[target] = -1 player_candy_colors[attacker].append_array(target_colors)
player_candy_color[attacker] = player_candy_color.get(target, -1) player_candy_colors[target] = []
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 player_candies[attacker] = player_candy_colors[attacker].size()
player_candies[target] = 0
player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
player_last_knocked_by[target] = attacker player_last_knocked_by[target] = attacker
# Target is knocked down / staggered on successful knock
if target_node:
target_node.rpc("apply_stagger", 2.0)
_add_score(attacker, target_candies * 100) _add_score(attacker, target_candies * 100)
_update_candy_badge(attacker) _update_candy_badge(attacker)
_update_candy_badge(target) _update_candy_badge(target)
@@ -355,6 +453,7 @@ func try_knock(attacker: int, target: int) -> bool:
rpc("sync_knock_result", attacker, target, target_candies) rpc("sync_knock_result", attacker, target, target_candies)
return true return true
@rpc("any_peer", "call_local", "reliable")
func try_activate_ghost(pid: int) -> bool: func try_activate_ghost(pid: int) -> bool:
if player_ghost_active.get(pid, false): if player_ghost_active.get(pid, false):
return false return false
@@ -365,6 +464,8 @@ func try_activate_ghost(pid: int) -> bool:
player_ghost_active[pid] = true player_ghost_active[pid] = true
player_ghost_timer[pid] = GHOST_DURATION player_ghost_timer[pid] = GHOST_DURATION
rpc("sync_ghost_active", pid, true) rpc("sync_ghost_active", pid, true)
rpc("sync_ghost_charge_count", pid, player_ghosts[pid])
_update_special_inventory_for_ghosts(pid)
# Core powerup integration: set is_invisible on the player node # Core powerup integration: set is_invisible on the player node
var all_players = get_tree().get_nodes_in_group("Players") var all_players = get_tree().get_nodes_in_group("Players")
@@ -476,7 +577,24 @@ func _add_score(pid: int, points: int) -> void:
func _get_player_ids() -> Array: func _get_player_ids() -> Array:
if main_scene and main_scene.has_method("get_player_ids"): if main_scene and main_scene.has_method("get_player_ids"):
return main_scene.get_player_ids() return main_scene.get_player_ids()
return [] var pids: Array = []
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if pid != 0 and not pids.has(pid):
pids.append(pid)
return pids
func _find_player_node(pid: int) -> Node:
if main_scene:
var n = main_scene.get_node_or_null(str(pid))
if n: return n
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var p_id = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if p_id == pid:
return p
return null
func candy_survival_round_duration() -> int: func candy_survival_round_duration() -> int:
return 180 return 180
@@ -486,13 +604,37 @@ func get_score(pid: int) -> int:
# ── RPCs (stubs — HUD client handles visuals) ── # ── RPCs (stubs — HUD client handles visuals) ──
@rpc("authority", "call_remote", "unreliable") @rpc("authority", "call_local", "reliable")
func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool) -> void: func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool, score: int, colors: Array) -> void:
if not active: if not active:
activate_client_side() activate_client_side()
player_candies[pid] = count
player_candy_colors[pid] = colors
player_score[pid] = score
var p_node = _find_player_node(pid)
if p_node and p_node.has_method("sync_candy_stack"):
p_node.sync_candy_stack(colors)
if pid == multiplayer.get_unique_id(): if pid == multiplayer.get_unique_id():
_last_badge_count = count
_last_badge_mult = mult
_last_badge_face_match = face_match
if hud_stack_badge: if hud_stack_badge:
hud_stack_badge.text = "Stack: %d (x%.1f)" % [count, mult] var red = 0
var blue = 0
var yellow = 0
var green = 0
for col in colors:
match int(col):
0: red += 1
1: blue += 1
2: green += 1
3: yellow += 1
hud_stack_badge.text = "Stack: %d (x%.1f)\nRed: %dx Blue: %dx Green: %dx Yellow: %dx" % [count, mult, red, blue, green, yellow]
if hud_points_label:
hud_points_label.text = "Score: %d" % score
if hud_delivery_indicator: if hud_delivery_indicator:
if count > 0: if count > 0:
if face_match: if face_match:
@@ -505,6 +647,39 @@ func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool) -> vo
hud_delivery_indicator.text = "" hud_delivery_indicator.text = ""
@rpc("authority", "call_remote", "unreliable") @rpc("authority", "call_remote", "unreliable")
func sync_delivery_result(pid: int, success: bool, msg: String) -> void:
if not active:
activate_client_side()
if pid != multiplayer.get_unique_id():
return
if hud_delivery_indicator:
if success:
hud_delivery_indicator.text = "✓ Delivered! +Sugar Rush!"
hud_delivery_indicator.add_theme_color_override("font_color", Color(0.3, 1.0, 0.3))
else:
hud_delivery_indicator.text = "" + msg
hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3))
# Flash and auto-clear after 1.5s
var tween = create_tween()
tween.tween_interval(1.5)
tween.tween_callback(_restore_delivery_indicator)
func _restore_delivery_indicator() -> void:
if not active or not hud_delivery_indicator:
return
var count = _last_badge_count
var face_match = _last_badge_face_match
if count > 0:
if face_match:
hud_delivery_indicator.text = "READY TO DELIVER!"
hud_delivery_indicator.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4))
else:
hud_delivery_indicator.text = "Waiting for match..."
hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4))
else:
hud_delivery_indicator.text = ""
@rpc("authority", "call_local", "reliable")
func sync_knock_result(attacker: int, target: int, candies: int) -> void: func sync_knock_result(attacker: int, target: int, candies: int) -> void:
if not active: if not active:
activate_client_side() activate_client_side()
@@ -520,3 +695,78 @@ func sync_ghost_active(pid: int, active_on: bool) -> void:
if pid == multiplayer.get_unique_id(): if pid == multiplayer.get_unique_id():
if hud_ghost_label and player_ghosts.has(pid): if hud_ghost_label and player_ghosts.has(pid):
hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid] hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid]
func sync_state_to_player(peer_id: int) -> void:
# Sync Mekton's current face color
if mekton_node and mekton_node.has_method("set_face_color_rpc"):
mekton_node.rpc_id(peer_id, "set_face_color_rpc", current_face)
# Sync Ghost inventory to make the HUD button visible on player load
_update_special_inventory_for_ghosts(peer_id)
# Sync Knock and Ghost charges
rpc_id(peer_id, "sync_knock_charge_count", peer_id, player_knocks.get(peer_id, START_KNOCK))
rpc_id(peer_id, "sync_ghost_charge_count", peer_id, player_ghosts.get(peer_id, START_GHOST))
# Sync all players' candy stack heights and colors
for pid in player_candy_colors.keys():
var count = player_candies.get(pid, 0)
var mult = get_multiplier(pid)
var colors = player_candy_colors.get(pid, [])
var color = colors.back() if colors.size() > 0 else -1
var face_match = (color != -1)
var score = player_score.get(pid, 0)
# Sync badge
rpc_id(peer_id, "sync_candy_badge", pid, count, mult, face_match, score, colors)
# Sync 3D stack meshes
var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players:
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if curr_pid == pid:
if player.has_method("sync_candy_stack"):
player.rpc_id(peer_id, "sync_candy_stack", colors)
break
func _update_special_inventory_for_ghosts(pid: int) -> void:
if not multiplayer.is_server():
return
if main_scene:
var player_node = main_scene.get_node_or_null(str(pid))
if player_node:
var st_manager = player_node.get_node_or_null("SpecialTilesManager")
if st_manager:
var has_charges = (player_ghosts.get(pid, 0) > 0)
if has_charges:
st_manager.rpc_id(pid, "sync_inventory_add", st_manager.SpecialEffect.INVISIBLE_MODE, 8)
else:
st_manager.rpc_id(pid, "sync_inventory_remove", st_manager.SpecialEffect.INVISIBLE_MODE)
@rpc("any_peer", "call_local", "reliable")
func add_ghost_charge(pid: int) -> void:
if not multiplayer.is_server():
return
player_ghosts[pid] = player_ghosts.get(pid, 0) + 1
rpc("sync_ghost_charge_count", pid, player_ghosts[pid])
_update_special_inventory_for_ghosts(pid)
@rpc("authority", "call_local", "unreliable")
func sync_ghost_charge_count(pid: int, count: int) -> void:
if not active:
activate_client_side()
if player_ghosts.has(pid) or pid == multiplayer.get_unique_id():
player_ghosts[pid] = count
if multiplayer.get_unique_id() == pid:
if hud_ghost_label:
hud_ghost_label.text = "👻 Ghosts: %d" % count
@rpc("authority", "call_local", "unreliable")
func sync_knock_charge_count(pid: int, count: int) -> void:
if not active:
activate_client_side()
if player_knocks.has(pid) or pid == multiplayer.get_unique_id():
player_knocks[pid] = count
if multiplayer.get_unique_id() == pid:
if hud_knock_label:
hud_knock_label.text = "🥊 Knocks: %d" % count
+2 -1
View File
@@ -40,7 +40,8 @@ func pull(banner_id: String, count: int) -> Array:
var result = await BackendService.perform_gacha_pull(banner_id, count) var result = await BackendService.perform_gacha_pull(banner_id, count)
if result.get("success", false) == false: if result.get("success", false) == false:
var msg = str(result.get("error", "Unknown error")) var err_val = result.get("error", "Unknown error")
var msg = result.get("message", str(err_val))
push_error("[GachaManager] Gacha pull failed: " + msg) push_error("[GachaManager] Gacha pull failed: " + msg)
return [] return []
+14 -1
View File
@@ -19,6 +19,7 @@ var is_match_active: bool = false
# Score tracking: peer_id -> score # Score tracking: peer_id -> score
var player_scores: Dictionary = {} var player_scores: Dictionary = {}
var player_goal_counts: Dictionary = {} # peer_id -> count var player_goal_counts: Dictionary = {} # peer_id -> count
var player_goals: Dictionary = {} # peer_id -> goals array
var stop_n_go_winner_id: int = -1 # Track winner for Stop n Go sorting var stop_n_go_winner_id: int = -1 # Track winner for Stop n Go sorting
# Reference to main scene # Reference to main scene
@@ -215,6 +216,9 @@ func sync_cycle_end():
func on_goal_completed(player: Node, time_remaining: float): func on_goal_completed(player: Node, time_remaining: float):
"""Called when a player completes their goal pattern.""" """Called when a player completes their goal pattern."""
if LobbyManager.is_game_mode(GameMode.Mode.CANDY_SURVIVAL):
return # Candy Survival goal generation / completion handled separately by mekton deposit
# CLIENT PATH: clear board immediately for visual responsiveness, # CLIENT PATH: clear board immediately for visual responsiveness,
# then let server send back the single authoritative new goals. # then let server send back the single authoritative new goals.
# Do NOT generate goals locally — that caused rollback/blinking. # Do NOT generate goals locally — that caused rollback/blinking.
@@ -374,7 +378,7 @@ func _process_cycle_end_for_all_players():
var peer_id = player.name.to_int() var peer_id = player.name.to_int()
var match_score = _calculate_match_score(player) var match_score = _calculate_match_score(player)
if match_score > 0: if match_score > 0 and not (LobbyManager.game_mode == "Candy Survival"):
if not player_scores.has(peer_id): if not player_scores.has(peer_id):
player_scores[peer_id] = 0 player_scores[peer_id] = 0
player_scores[peer_id] += match_score player_scores[peer_id] += match_score
@@ -518,3 +522,12 @@ func get_time_remaining() -> float:
func reset_scores(): func reset_scores():
player_scores.clear() player_scores.clear()
_update_leaderboard() _update_leaderboard()
func reset() -> void:
is_match_active = false
is_cycle_active = false
set_process(false)
player_scores.clear()
player_goal_counts.clear()
player_goals.clear()
stop_n_go_winner_id = -1
+55 -28
View File
@@ -55,7 +55,7 @@ func _process(delta):
# 3. Action inputs (momentary) # 3. Action inputs (momentary)
if Input.is_action_just_pressed("action_grab"): if Input.is_action_just_pressed("action_grab") and LobbyManager.game_mode != "Candy Survival":
player.grab_item(player.current_position) player.grab_item(player.current_position)
if move_vec != Vector2i.ZERO: if move_vec != Vector2i.ZERO:
@@ -93,33 +93,65 @@ func handle_unhandled_input(event):
if not SettingsManager: if not SettingsManager:
return return
# 1. Unified check for POWER-UP Activation # 1. Unified check for POWER-UP / Ghost Activation
if event.is_action_pressed("use_powerup"): if event.is_action_pressed("use_powerup"):
player.activate_held_powerup() if LobbyManager.game_mode == "Candy Survival":
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.active:
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server():
gm.try_activate_ghost(pid)
else:
gm.rpc_id(1, "try_activate_ghost", pid)
else:
player.activate_held_powerup()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
# 2. Action Buttons (Remappable via InputMap) # 2. Action Buttons (Remappable via InputMap / KEY_Q override for Candy Survival)
if event.is_action_pressed("action_knock_tekton"): var is_knock = false
if LobbyManager.game_mode == "Candy Survival":
is_knock = (event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_Q or event.physical_keycode == KEY_Q)) or event.is_action_pressed("action_knock_tekton")
else:
is_knock = event.is_action_pressed("action_knock_tekton")
if is_knock:
if LobbyManager.game_mode == "Candy Survival": if LobbyManager.game_mode == "Candy Survival":
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager") var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.is_active: if gm and gm.active:
# Find a target to knock (first adjacent player) var att_id = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if gm.player_knocks.get(att_id, 5) <= 0:
NotificationManager.send_message(player, "No Knock charges left!", NotificationManager.MessageType.WARNING)
get_viewport().set_input_as_handled()
return
# Find a target to knock (adjacent player: cardinal or diagonal distance <= 1)
var p_pos = player.current_position var p_pos = player.current_position
var all_players = get_tree().get_nodes_in_group("Players") var all_players = get_tree().get_nodes_in_group("Players")
var target_found = null
var min_dist = 999
for other in all_players: for other in all_players:
if other == player: continue if other == player or not is_instance_valid(other): continue
var other_pos = other.current_position var other_pos = other.current_position
if abs(p_pos.x - other_pos.x) + abs(p_pos.y - other_pos.y) == 1: var dist = max(abs(p_pos.x - other_pos.x), abs(p_pos.y - other_pos.y))
# Adjacent if dist <= 1 and dist < min_dist:
var att_id = player.get("peer_id") if "peer_id" in player else player.name.to_int() target_found = other
var tgt_id = other.get("peer_id") if "peer_id" in other else other.name.to_int() min_dist = dist
if multiplayer.is_server(): if dist == 1:
gm.try_knock(att_id, tgt_id) break
else:
gm.rpc_id(1, "try_knock", att_id, tgt_id) if target_found:
get_viewport().set_input_as_handled() var tgt_id = target_found.get("peer_id") if "peer_id" in target_found else target_found.name.to_int()
return if multiplayer.is_server():
gm.try_knock(att_id, tgt_id)
else:
gm.rpc_id(1, "try_knock", att_id, tgt_id)
else:
# No target, but play the bump animation anyway in place
player.rpc("sync_bump", p_pos)
get_viewport().set_input_as_handled()
return
if player.powerup_manager: if player.powerup_manager:
player.powerup_manager.use_special_effect() player.powerup_manager.use_special_effect()
@@ -127,17 +159,12 @@ func handle_unhandled_input(event):
player.enter_attack_mode() player.enter_attack_mode()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif event.is_action_pressed("action_grab_tekton"): elif event.is_action_pressed("action_grab"):
if LobbyManager.game_mode == "Candy Survival": if LobbyManager.game_mode == "Candy Survival":
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager") get_viewport().set_input_as_handled()
if gm and gm.is_active: return
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server(): elif event.is_action_pressed("action_grab_tekton"):
gm.try_activate_ghost(pid)
else:
gm.rpc_id(1, "try_activate_ghost", pid)
get_viewport().set_input_as_handled()
return
if not player.is_carrying_tekton: if not player.is_carrying_tekton:
if player.powerup_manager and player.powerup_manager.has_method("can_use_special"): if player.powerup_manager and player.powerup_manager.has_method("can_use_special"):
+29 -19
View File
@@ -157,7 +157,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
# If moving into a sticky cell: block movement unless player is in ghost # If moving into a sticky cell: block movement unless player is in ghost
# mode (is_invisible), which lets them bypass sticky tiles in Candy Survival # mode (is_invisible), which lets them bypass sticky tiles in Candy Survival
if gm and gm.is_active and gm.is_sticky_cell(grid_position): if gm and gm.active and gm.is_sticky_cell(grid_position):
if player.get("is_invisible"): if player.get("is_invisible"):
# Ghost mode: walk through sticky tile freely # Ghost mode: walk through sticky tile freely
print("[Move] Ghost mode bypassed sticky cell at %s" % grid_position) print("[Move] Ghost mode bypassed sticky cell at %s" % grid_position)
@@ -165,14 +165,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
print("[Move] Failed: Blocked by Candy Survival Sticky cell at %s" % grid_position) print("[Move] Failed: Blocked by Candy Survival Sticky cell at %s" % grid_position)
return false return false
if gm and gm.is_active and gm.has_method("try_deliver"): if gm and gm.active and gm.has_method("try_deliver"):
var dist = abs(grid_position.x - 8) + abs(grid_position.y - 8) var dist = abs(grid_position.x - 8) + abs(grid_position.y - 8)
if dist <= 2: if dist <= 1:
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server(): if multiplayer.is_server():
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
gm.try_deliver(pid) gm.try_deliver(pid)
else:
gm.rpc_id(1, "try_deliver", pid)
rotate_towards_target(grid_position) rotate_towards_target(grid_position)
@@ -221,7 +219,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
var has_knock = false var has_knock = false
var main_for_knock = player.get_tree().root.get_node_or_null("Main") var main_for_knock = player.get_tree().root.get_node_or_null("Main")
var gm_for_knock = main_for_knock.get("candy_survival_manager") if main_for_knock else null var gm_for_knock = main_for_knock.get("candy_survival_manager") if main_for_knock else null
if gm_for_knock and gm_for_knock.is_active: if gm_for_knock and gm_for_knock.active:
var att_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() var att_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
var charges = gm_for_knock.player_knocks.get(att_pid, 0) if gm_for_knock.player_knocks else 0 var charges = gm_for_knock.player_knocks.get(att_pid, 0) if gm_for_knock.player_knocks else 0
has_knock = charges > 0 has_knock = charges > 0
@@ -236,7 +234,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
return false return false
# SAFE ZONE PROTECTION (Only in Stop n Go) # SAFE ZONE PROTECTION (Only in Stop n Go)
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO): if Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO):
var safe_columns = [6, 7, 8, 14, 15, 16] var safe_columns = [6, 7, 8, 14, 15, 16]
# 1. Prevent attacker from attacking IF THEY ARE in a Safe Zone # 1. Prevent attacker from attacking IF THEY ARE in a Safe Zone
if player.current_position.x in safe_columns: if player.current_position.x in safe_columns:
@@ -257,7 +255,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
gm = main_push_check.candy_survival_manager gm = main_push_check.candy_survival_manager
# Candy Survival: knock mechanic (candy-steal or backfire) # Candy Survival: knock mechanic (candy-steal or backfire)
if gm and gm.is_active and gm.has_method("try_knock"): if gm and gm.active and gm.has_method("try_knock"):
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
var other_pid = other_player.get("peer_id") if "peer_id" in other_player else other_player.name.to_int() var other_pid = other_player.get("peer_id") if "peer_id" in other_player else other_player.name.to_int()
if gm.try_knock(pid, other_pid): if gm.try_knock(pid, other_pid):
@@ -274,17 +272,17 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
# Visual Feedback: Attack Bump # Visual Feedback: Attack Bump
if _can_rpc(): if _can_rpc():
player.rpc("sync_bump", target_pos, false) # Attack bump player.rpc("sync_bump", target_pos, false) # Attack bump
SfxManager.rpc("play_rpc", "attack_mode") Engine.get_main_loop().root.get_node_or_null("SfxManager").rpc("play_rpc", "attack_mode")
elif player.has_method("sync_bump"): elif player.has_method("sync_bump"):
player.sync_bump(target_pos, false) player.sync_bump(target_pos, false)
SfxManager.play("attack_mode") Engine.get_main_loop().root.get_node_or_null("SfxManager").play("attack_mode")
# 1. 3-Floor Knockback # 1. 3-Floor Knockback
var push_direction = Vector2i(-1, 0) # Default back (Stop N Go) var push_direction = Vector2i(-1, 0) # Default back (Stop N Go)
var main_push = player.get_tree().root.get_node_or_null("Main") var main_push = player.get_tree().root.get_node_or_null("Main")
var gm_push = main_push.candy_survival_manager if main_push and main_push.has_node("CandySurvivalManager") else (main_push.get("candy_survival_manager") if main_push else null) var gm_push = main_push.candy_survival_manager if main_push and main_push.has_node("CandySurvivalManager") else (main_push.get("candy_survival_manager") if main_push else null)
if gm_push and gm_push.is_active: if gm_push and gm_push.active:
push_direction = direction # Use the direction of the attack push_direction = direction # Use the direction of the attack
var pushed_to_pos = target_pos var pushed_to_pos = target_pos
var push_path = [] var push_path = []
@@ -297,7 +295,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
pushed_to_pos = next_back pushed_to_pos = next_back
push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y)) push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y))
if gm_push and gm_push.is_active and gm_push.is_sticky_cell(pushed_to_pos): if gm_push and gm_push.active and gm_push.is_sticky_cell(pushed_to_pos):
hit_sticky = true hit_sticky = true
break # stop pushing immediately upon touching sticky zone! break # stop pushing immediately upon touching sticky zone!
else: else:
@@ -320,7 +318,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
var main_sticky = player.get_tree().root.get_node_or_null("Main") var main_sticky = player.get_tree().root.get_node_or_null("Main")
if main_sticky and main_sticky.get("candy_survival_manager"): if main_sticky and main_sticky.get("candy_survival_manager"):
var gm_sticky = main_sticky.candy_survival_manager var gm_sticky = main_sticky.candy_survival_manager
if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos): if gm_sticky.active and gm_sticky.is_sticky_cell(pushed_to_pos):
if other_player.get("is_invisible"): if other_player.get("is_invisible"):
# Ghost mode: pushed player bypasses sticky # Ghost mode: pushed player bypasses sticky
print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos) print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos)
@@ -330,7 +328,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
gm_sticky.apply_sticky_slow(other_player) gm_sticky.apply_sticky_slow(other_player)
# 2. Apply freeze/stun effect # 2. Apply freeze/stun effect
var stun_duration = 1.0 if (gm_push and gm_push.is_active) else 1.5 var stun_duration = 1.0 if (gm_push and gm_push.active) else 1.5
if _can_rpc(): if _can_rpc():
other_player.rpc("apply_stagger", stun_duration) other_player.rpc("apply_stagger", stun_duration)
else: else:
@@ -347,7 +345,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
# SCORING: 200 Points for successful attack (ONLY in Free Mode) # SCORING: 200 Points for successful attack (ONLY in Free Mode)
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) var is_sng = Engine.get_main_loop().root.get_node_or_null("LobbyManager").is_game_mode(GameMode.Mode.STOP_N_GO)
if not is_sng: if not is_sng:
var main_score = player.get_tree().get_root().get_node_or_null("Main") var main_score = player.get_tree().get_root().get_node_or_null("Main")
if main_score: if main_score:
@@ -379,11 +377,23 @@ pass
func _on_movement_finished(): func _on_movement_finished():
# Auto-pickup logic for Candy Survival # Auto-pickup logic for Candy Survival
if LobbyManager.game_mode == "Candy Survival": if Engine.get_main_loop().root.get_node_or_null("LobbyManager").game_mode == "Candy Survival":
if player.has_method("grab_item"): if player.has_method("grab_item"):
# Check if there is an item at current_position and if the board has space # Check if there is an item at current_position and if the board has space
if player.playerboard_manager and player.playerboard_manager.has_empty_slot(): var current_cell = Vector3i(player.current_position.x, 1, player.current_position.y)
player.grab_item(player.current_position) var item = player.playerboard_manager.enhanced_gridmap.get_cell_item(current_cell) if player.playerboard_manager and player.playerboard_manager.enhanced_gridmap else -1
if item != -1:
# Normalize item to match goals logic
var normalized_item = player.playerboard_manager._normalize_tile(item)
if normalized_item in player.goals:
var empty_slot = -1
for i in range(player.playerboard.size()):
if player.playerboard[i] == -1 and not (i in player.playerboard_manager.HIDDEN_SLOTS):
empty_slot = i
break
if player.playerboard_manager and empty_slot != -1:
player.grab_item(player.current_position)
if not movement_queue.is_empty(): if not movement_queue.is_empty():
var next_target = movement_queue.pop_front() var next_target = movement_queue.pop_front()
+21 -11
View File
@@ -526,6 +526,8 @@ func find_best_goal_slot_for_item(item: int) -> int:
# Goal slots are row 2-3, col 1-3 (indices: 11,12,13, 16,17,18) matching User request # Goal slots are row 2-3, col 1-3 (indices: 11,12,13, 16,17,18) matching User request
# Rows 1 (6,7,8) are now storage slots # Rows 1 (6,7,8) are now storage slots
var reserved_goal_slots = [11, 12, 13, 16, 17, 18] var reserved_goal_slots = [11, 12, 13, 16, 17, 18]
if LobbyManager.game_mode == "Candy Survival":
reserved_goal_slots = []
for i in range(player.playerboard.size()): for i in range(player.playerboard.size()):
if player.playerboard[i] == -1: if player.playerboard[i] == -1:
@@ -733,9 +735,11 @@ func _check_goal_completion():
if not player.race_manager: if not player.race_manager:
return return
# Note: We still trigger blueprint completion (giving Candies to the player stack)
var is_match = false var is_match = false
var is_off_color = false var is_off_color = false
var primary_color = -1 var primary_color = -1
var original_is_match = false
if LobbyManager.game_mode == "Candy Survival": if LobbyManager.game_mode == "Candy Survival":
# Custom Candy Survival blueprint check (can be off-color) # Custom Candy Survival blueprint check (can be off-color)
@@ -766,22 +770,27 @@ func _check_goal_completion():
primary_color = c primary_color = c
is_off_color = (max_count < 9) is_off_color = (max_count < 9)
if is_off_color and gm and gm.has_method("can_finish_with_off_color"):
if not gm.can_finish_with_off_color(player.name.to_int(), primary_color):
is_match = false # Reject if primary color is still available on grid
if is_match: if is_match:
break break
if is_match: if is_match:
original_is_match = true
break break
if is_match and gm and gm.has_method("on_blueprint_completed"): if is_match and gm and gm.has_method("on_blueprint_completed"):
# In Candy Survival, the visual goal check does not trigger completion.
# We only give candies into the stack. Goals are cleared & scored
# when delivered to the Mekton.
gm.on_blueprint_completed(player.name.to_int(), primary_color, is_off_color) gm.on_blueprint_completed(player.name.to_int(), primary_color, is_off_color)
is_match = false # Force false so we don't trigger the regular free-mode goal completion
else: else:
is_match = player.race_manager.check_pattern_match() is_match = player.race_manager.check_pattern_match()
if is_match: if is_match or (original_is_match and LobbyManager.game_mode == "Candy Survival"):
print("[PlayerboardManager] Goal completed for player %s!" % player.name) if not (LobbyManager.game_mode == "Candy Survival"):
print("[PlayerboardManager] Goal completed for player %s!" % player.name)
else:
print("[PlayerboardManager] Blueprint completed for player %s! Generating candy." % player.name)
var powerup_manager = player.get_node_or_null("PowerUpManager") var powerup_manager = player.get_node_or_null("PowerUpManager")
if powerup_manager: if powerup_manager:
@@ -791,12 +800,13 @@ func _check_goal_completion():
if player.is_multiplayer_authority() and player.has_method("trigger_screen_shake"): if player.is_multiplayer_authority() and player.has_method("trigger_screen_shake"):
player.trigger_screen_shake("goal") player.trigger_screen_shake("goal")
# Notify GoalsCycleManager for scoring # Notify GoalsCycleManager for scoring (only if not Candy Survival)
var main = player.get_tree().get_root().get_node_or_null("Main") if not (LobbyManager.game_mode == "Candy Survival"):
if main: var main = player.get_tree().get_root().get_node_or_null("Main")
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager") if main:
if goals_cycle_manager: var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
goals_cycle_manager.on_goal_completed(player, goals_cycle_manager.get_time_remaining()) if goals_cycle_manager:
goals_cycle_manager.on_goal_completed(player, goals_cycle_manager.get_time_remaining())
else: else:
# Fallback if manager not initialized yet # Fallback if manager not initialized yet
NotificationManager.send_message(player, NotificationManager.MESSAGES.GOAL_COMPLETED, NotificationManager.MessageType.GOAL) NotificationManager.send_message(player, NotificationManager.MESSAGES.GOAL_COMPLETED, NotificationManager.MessageType.GOAL)
+24 -5
View File
@@ -153,6 +153,17 @@ func add_powerup_from_item(item_id: int):
var effect = get_effect_from_item(item_id) var effect = get_effect_from_item(item_id)
if effect == -1: return if effect == -1: return
if LobbyManager.game_mode == "Candy Survival":
if effect == SpecialEffect.INVISIBLE_MODE:
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.active:
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server():
gm.add_ghost_charge(pid)
else:
gm.rpc_id(1, "add_ghost_charge", pid)
return
# VFX: show pickup burst on all peers (mirrors skill VFX pattern) # VFX: show pickup burst on all peers (mirrors skill VFX pattern)
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
player.rpc("play_skill_vfx", "take_powerup") player.rpc("play_skill_vfx", "take_powerup")
@@ -230,6 +241,17 @@ func activate_effect(effect: int, target_player: Node3D = null):
NotificationManager.send_message(player, "Skill in Cooldown! (%.1fs)" % global_cooldown_timer, NotificationManager.MessageType.WARNING) NotificationManager.send_message(player, "Skill in Cooldown! (%.1fs)" % global_cooldown_timer, NotificationManager.MessageType.WARNING)
return return
if LobbyManager.game_mode == "Candy Survival":
if effect == SpecialEffect.INVISIBLE_MODE:
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.active:
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server():
gm.try_activate_ghost(pid)
else:
gm.rpc_id(1, "try_activate_ghost", pid)
return
# Check Attack Mode Restriction # Check Attack Mode Restriction
if player.get("is_attack_mode") and effect == SpecialEffect.INVISIBLE_MODE: if player.get("is_attack_mode") and effect == SpecialEffect.INVISIBLE_MODE:
NotificationManager.send_message(player, "Cannot enter Ghost mode while in Attack Mode!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(player, "Cannot enter Ghost mode while in Attack Mode!", NotificationManager.MessageType.WARNING)
@@ -547,11 +569,8 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request) # Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request)
item_id = rng.randi_range(7, 10) item_id = rng.randi_range(7, 10)
elif LobbyManager.is_game_mode(GameMode.Mode.CANDY_SURVIVAL): elif LobbyManager.is_game_mode(GameMode.Mode.CANDY_SURVIVAL):
# Candy Survival mode: mostly common tiles, but ghost (14) can spawn too. # Candy Survival mode: spawn only normal tiles
if rng.randf() < 0.85: item_id = rng.randi_range(7, 10)
item_id = rng.randi_range(7, 10)
else:
item_id = 14 # Ghost powerup only
else: else:
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp # Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
if rng.randf() < 0.8: if rng.randf() < 0.8:
+2
View File
@@ -674,6 +674,8 @@ func _animate_safe_zone_appear():
# Duplicate mesh+material so we animate without touching the shared .tres on disk. # Duplicate mesh+material so we animate without touching the shared .tres on disk.
var anim_mat: StandardMaterial3D = mat.duplicate() var anim_mat: StandardMaterial3D = mat.duplicate()
anim_mat.albedo_color = Color(mat.albedo_color.r, mat.albedo_color.g, mat.albedo_color.b, 0.0) anim_mat.albedo_color = Color(mat.albedo_color.r, mat.albedo_color.g, mat.albedo_color.b, 0.0)
# Force render priority so the transparent decal correctly sorts over the terrain
anim_mat.render_priority = 1
var anim_mesh = original_mesh.duplicate() var anim_mesh = original_mesh.duplicate()
anim_mesh.material = anim_mat anim_mesh.material = anim_mat
+86 -52
View File
@@ -82,8 +82,8 @@ func setup_playerboard_ui():
slot.custom_minimum_size = Vector2(36, 36) slot.custom_minimum_size = Vector2(36, 36)
slot.texture = item_tex[0] slot.texture = item_tex[0]
slot.set_meta("slot_idx", i)
# 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25
var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24] var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
if i in hidden_slots: if i in hidden_slots:
@@ -114,76 +114,113 @@ func update_playerboard_ui():
if not local_player_character or not playerboard_ui: if not local_player_character or not playerboard_ui:
return return
# Center 3x3 slot indices in a 5x5 grid (0-indexed) # Candy for CandySurvival, not a tile
# Row 1: 6, 7, 8 var is_candy = LobbyManager and LobbyManager.is_game_mode(GameMode.Mode.CANDY_SURVIVAL)
# Row 2: 11, 12, 13
# Center 3x3 slot indices in a 5x5 grid (0-indexed)
# Row 1: 6, 7, 8 (Now Storage - but kept in index map for goals[0-2])
# Row 2: 11, 12, 13 (Goals[3-5])
# Row 3: 16, 17, 18 (Goals[6-8])
var center_slots = [6, 7, 8, 11, 12, 13, 16, 17, 18] var center_slots = [6, 7, 8, 11, 12, 13, 16, 17, 18]
var goals = local_player_character.goals if local_player_character.goals else [] var goals = local_player_character.goals if local_player_character.goals else []
for i in range(25): var count = playerboard_ui.get_child_count()
for i in range(count):
var slot = playerboard_ui.get_child(i) var slot = playerboard_ui.get_child(i)
# Get actual playerboard index from slot metadata
var slot_idx = i
if slot.has_meta("slot_idx"):
slot_idx = slot.get_meta("slot_idx")
# Safety check: Ensure playerboard has enough items # Safety check: Ensure playerboard has enough items
if i >= local_player_character.playerboard.size(): if slot_idx >= local_player_character.playerboard.size():
continue continue
var item = local_player_character.playerboard[i] var item = local_player_character.playerboard[slot_idx]
# 0-based indices corresponding to User's 1-based request: 1,5,6,10,11,15,16,20,21,22,23,24,25
var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
# Default texture (empty) # Default texture (empty)
slot.texture = item_tex[0] slot.texture = item_tex[0]
if i in hidden_slots: if is_candy:
slot.modulate = Color(1, 1, 1, 0) var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE if slot_idx in hidden_slots:
slot.modulate = Color(1, 1, 1, 0)
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
else:
slot.modulate = Color.WHITE
slot.mouse_filter = Control.MOUSE_FILTER_PASS
# All 9 cells show goal ghost or placed tile
var center_index = center_slots.find(slot_idx)
if center_index != -1 and center_index < goals.size():
var goal_value = goals[center_index]
if item != -1:
match item:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
slot.modulate = Color.WHITE
else:
match goal_value:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
_: slot.texture = item_tex[0]
slot.modulate = Color(0.3, 0.3, 0.3, 1.0)
else:
# Non-center slot - just show playerboard item normally
match item:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
else: else:
slot.modulate = Color.WHITE # Original 5x5 logic
slot.mouse_filter = Control.MOUSE_FILTER_PASS var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
# Check if this is a center slot that should show a goal if slot_idx in hidden_slots:
# BUT only show ghost goals for rows 2 & 3 (indices 11+) slot.modulate = Color(1, 1, 1, 0)
var center_index = center_slots.find(i) slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
if center_index != -1 and center_index < goals.size() and i > 8: else:
var goal_value = goals[center_index] slot.modulate = Color.WHITE
slot.mouse_filter = Control.MOUSE_FILTER_PASS
if item != -1: # Check if this is a center slot that should show a goal
# Player has a tile in this slot - show it at full brightness # BUT only show ghost goals for rows 2 & 3 (indices 11+)
var center_index = center_slots.find(slot_idx)
if center_index != -1 and center_index < goals.size() and slot_idx > 8:
var goal_value = goals[center_index]
if item != -1:
# Player has a tile in this slot - show it at full brightness
match item:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
slot.modulate = Color.WHITE
else:
# Show goal tile dimmed (not collected yet)
match goal_value:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
_: slot.texture = item_tex[0]
# Dim uncollected goals with black overlay
slot.modulate = Color(0.3, 0.3, 0.3, 1.0)
else:
# Non-center slot - just show playerboard item normally
match item: match item:
7: slot.texture = item_tex[1] 7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2] 8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3] 9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4] 10: slot.texture = item_tex[4]
slot.modulate = Color.WHITE # Non-center slots always full brightness (UNLESS HIDDEN)
else: if not (slot_idx in hidden_slots):
# Show goal tile dimmed (not collected yet) slot.modulate = Color.WHITE
match goal_value:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
_: slot.texture = item_tex[0]
# Dim uncollected goals with black overlay
slot.modulate = Color(0.3, 0.3, 0.3, 1.0)
else:
# Non-center slot - just show playerboard item normally
match item:
7: slot.texture = item_tex[1]
8: slot.texture = item_tex[2]
9: slot.texture = item_tex[3]
10: slot.texture = item_tex[4]
# Non-center slots always full brightness (UNLESS HIDDEN)
if not (i in hidden_slots):
slot.modulate = Color.WHITE
# Check for new special tile placement to trigger effect # Check for new special tile placement to trigger effect
if i < _previous_playerboard_state.size(): if slot_idx < _previous_playerboard_state.size():
var prev_item = _previous_playerboard_state[i] var prev_item = _previous_playerboard_state[slot_idx]
# If slot was empty or different, and now has a special tile (7-10) # If slot was empty or different, and now has a special tile (7-10)
if item != prev_item and item >= 7 and item <= 10: if item != prev_item and item >= 7 and item <= 10:
_pulse_slot_effect(slot) _pulse_slot_effect(slot)
@@ -208,9 +245,6 @@ func _pulse_slot_effect(slot: Control):
slot.modulate = Color(1.5, 1.5, 1.5) # Overbright slot.modulate = Color(1.5, 1.5, 1.5) # Overbright
tween.parallel().tween_property(slot, "modulate", original_modulate, 0.3) tween.parallel().tween_property(slot, "modulate", original_modulate, 0.3)
func _connect_powerup_manager_deferred(player): func _connect_powerup_manager_deferred(player):
"""Wait for PowerUpManager to be initialized before connecting.""" """Wait for PowerUpManager to be initialized before connecting."""
# player._ready waits 0.5s before creating managers, so wait longer # player._ready waits 0.5s before creating managers, so wait longer
+5 -1
View File
@@ -52,7 +52,11 @@ static func get_tile_weights() -> Dictionary:
weights[tile] = STANDARD_WEIGHT weights[tile] = STANDARD_WEIGHT
# Special tiles # Special tiles
var mode = LobbyManager.get_game_mode() var lobby = Engine.get_main_loop().root.get_node_or_null("LobbyManager")
var mode = lobby.get_game_mode() if lobby else ""
if mode == "Candy Survival":
return weights
var is_restricted = GameMode.is_restricted(mode) var is_restricted = GameMode.is_restricted(mode)
for tile in SPECIAL_TILES: for tile in SPECIAL_TILES:
if is_restricted and tile == TILE_WALL: if is_restricted and tile == TILE_WALL:
+4 -4
View File
@@ -65,12 +65,12 @@ func _initialize_steamworks_for_auth() -> void:
push_error("BackendService: Failed to load Steamworks manager") push_error("BackendService: Failed to load Steamworks manager")
func _initialize_nakama() -> void: func _initialize_nakama() -> void:
nakama_backend = NakamaManager nakama_backend = get_node_or_null("/root/NakamaManager")
if nakama_backend: if nakama_backend:
_connect_nakama_signals() _connect_nakama_signals()
print("BackendService: Initialized Nakama backend") print("BackendService: Initialized Nakama backend")
else: else:
push_error("BackendService: NakamaManager not found") push_error("BackendService: NakamaManager not found at /root/NakamaManager")
func _connect_nakama_signals() -> void: func _connect_nakama_signals() -> void:
pass pass
@@ -181,8 +181,8 @@ func respond_friend_request(target_id: String, accept: bool) -> Dictionary:
var payload = JSON.stringify({"target_user_id": target_id, "accept": accept}) var payload = JSON.stringify({"target_user_id": target_id, "accept": accept})
return await api_rpc_async("respond_friend_request", payload) return await api_rpc_async("respond_friend_request", payload)
func perform_gacha_pull(gacha_id: String, count: int) -> Dictionary: func perform_gacha_pull(banner_id: String, count: int) -> Dictionary:
var payload = JSON.stringify({"gacha_id": gacha_id, "count": count}) var payload = JSON.stringify({"banner_id": banner_id, "count": count})
return await api_rpc_async("perform_gacha_pull", payload) return await api_rpc_async("perform_gacha_pull", payload)
func get_mail(payload: String = "{}") -> Dictionary: func get_mail(payload: String = "{}") -> Dictionary:
+7
View File
@@ -12,6 +12,8 @@ signal closed
@onready var banner_label := %BannerLabel as Label @onready var banner_label := %BannerLabel as Label
@onready var gold_label := %GoldLabel as Label @onready var gold_label := %GoldLabel as Label
@onready var star_label := %StarLabel as Label @onready var star_label := %StarLabel as Label
@onready var left_gold_label := %LeftGoldLabel as Label
@onready var left_star_label := %LeftStarLabel as Label
@onready var pity_label := %PityLabel as Label @onready var pity_label := %PityLabel as Label
@onready var pull_1_btn := %Pull1Btn as Button @onready var pull_1_btn := %Pull1Btn as Button
@onready var pull_10_btn := %Pull10Btn as Button @onready var pull_10_btn := %Pull10Btn as Button
@@ -142,6 +144,11 @@ func _refresh_ui() -> void:
star_label.text = str(UserProfileManager.wallet.get("star", 0)) star_label.text = str(UserProfileManager.wallet.get("star", 0))
gold_label.text = str(UserProfileManager.wallet.get("gold", 0)) gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
if left_star_label:
left_star_label.text = str(UserProfileManager.wallet.get("star", 0))
if left_gold_label:
left_gold_label.text = str(UserProfileManager.wallet.get("gold", 0))
pity_label.text = "Pity: %d / %d" % [pity, pity_at] pity_label.text = "Pity: %d / %d" % [pity, pity_at]
cost_1_label.text = "%s %d" % [icon, c1] cost_1_label.text = "%s %d" % [icon, c1]
cost_10_label.text = "%s %d" % [icon, c10] cost_10_label.text = "%s %d" % [icon, c10]
+6 -6
View File
@@ -71,7 +71,7 @@ func show_panel() -> void:
show() show()
status_label.text = "Syncing scores..." status_label.text = "Syncing scores..."
# Bulk-sync all users' storage stats to native leaderboard (server-side operation) # Bulk-sync all users' storage stats to native leaderboard (server-side operation)
if NakamaManager.session: if NakamaManager.get("session"):
var sync_result = await BackendService.sync_leaderboard() var sync_result = await BackendService.sync_leaderboard()
if sync_result.get("success", false) == false: if sync_result.get("success", false) == false:
push_error("[Leaderboard] sync_leaderboard RPC failed: " + str(sync_result.get("error", ""))) push_error("[Leaderboard] sync_leaderboard RPC failed: " + str(sync_result.get("error", "")))
@@ -87,7 +87,7 @@ func _on_close_pressed() -> void:
# Data # Data
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
func _fetch_leaderboard_data() -> void: func _fetch_leaderboard_data() -> void:
if not NakamaManager.session: if not NakamaManager.get("session"):
status_label.text = "Not connected to Nakama" status_label.text = "Not connected to Nakama"
return return
@@ -115,7 +115,7 @@ func _fetch_leaderboard_data() -> void:
func _fetch_native_leaderboard() -> Array: func _fetch_native_leaderboard() -> Array:
"""Use the Nakama client API to list native leaderboard records directly.""" """Use the Nakama client API to list native leaderboard records directly."""
var result = await NakamaManager.client.list_leaderboard_records_async( var result = await NakamaManager.client.list_leaderboard_records_async(
NakamaManager.session, NakamaManager.get("session"),
"global_high_score", "global_high_score",
[], # no specific owner filter [], # no specific owner filter
null, # expiry = null (no filter) null, # expiry = null (no filter)
@@ -133,7 +133,7 @@ func _fetch_native_leaderboard() -> Array:
var parsed = JSON.parse_string(record.metadata) var parsed = JSON.parse_string(record.metadata)
if parsed is Dictionary: if parsed is Dictionary:
meta = parsed meta = parsed
if record.owner_id == NakamaManager.session.user_id: if record.owner_id == NakamaManager.get("session").user_id:
print("[Leaderboard] Local player meta: ", meta) print("[Leaderboard] Local player meta: ", meta)
data.append({ data.append({
@@ -177,9 +177,9 @@ func _calculate_win_rates() -> void:
entry["win_rate"] = float(won) / float(played) * 100.0 if played > 0 else 0.0 entry["win_rate"] = float(won) / float(played) * 100.0 if played > 0 else 0.0
func _apply_local_overrides(data: Array) -> void: func _apply_local_overrides(data: Array) -> void:
if not NakamaManager.session: if not NakamaManager.get("session"):
return return
var my_id = NakamaManager.session.user_id var my_id = NakamaManager.get("session").user_id
for entry in data: for entry in data:
if entry.get("user_id") == my_id: if entry.get("user_id") == my_id:
entry["display_name"] = UserProfileManager.get_display_name(entry.get("display_name", "Unknown")) entry["display_name"] = UserProfileManager.get_display_name(entry.get("display_name", "Unknown"))
+9 -2
View File
@@ -199,5 +199,12 @@ func _on_inventory_updated(inventory: Dictionary):
power_up_button.modulate = Color.WHITE power_up_button.modulate = Color.WHITE
_update_btn_shortcut(power_up_button) _update_btn_shortcut(power_up_button)
else: else:
power_up_button.visible = false if LobbyManager.game_mode == "Candy Survival":
power_up_button.disabled = true power_up_button.visible = true
power_up_button.icon = effect_textures.get(3) # ghost.png
power_up_button.disabled = true
power_up_button.modulate = Color(0.5, 0.5, 0.5, 0.5)
_update_btn_shortcut(power_up_button)
else:
power_up_button.visible = false
power_up_button.disabled = true
-4
View File
@@ -1,4 +0,0 @@
extends SceneTree
func _init():
print("Testing candy_survival multiplayer")
quit()
-1
View File
@@ -1 +0,0 @@
uid://ddniv6k6aj2u
-5
View File
@@ -1,5 +0,0 @@
#!/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
-224
View File
@@ -1,224 +0,0 @@
# TektonDash Unit Tests
This directory contains GUT (Godot Unit Testing) framework tests for the TektonDash project.
## Test Files (38 Total)
### Done Tasks (13 Files - 133 Tests)
### test_admin_panel.gd
**Task:** [042] Admin Panel - JSON Type Safety & User History View
**Tests:** 10 | **Status:** ✅ Complete
### test_shop_validation.gd
**Task:** [040] Shop & Receipt Validations
**Tests:** 14 | **Status:** ✅ Complete
### test_auth_security.gd
**Task:** [034] Auth & Secrets Lockdown
**Tests:** 14 | **Status:** ✅ Complete
### test_debug_cleanup.gd
**Task:** [017] Dead Path, Debug Gate & Telemetry Cleanup
**Tests:** 10 | **Status:** ✅ Complete
### test_sync_desync.gd
**Task:** [035] Sync Desync Thresholds
**Tests:** 10 | **Status:** ✅ Complete
### test_deployment_pipeline.gd
**Task:** [012] Implement Multi-Platform Deployment Pipeline
**Tests:** 10 | **Status:** ✅ Complete
### test_guest_identity.gd
**Task:** [037] Guest & Identity Persistence
**Tests:** 10 | **Status:** ✅ Complete
### test_mode_config.gd
**Task:** [036] Mode Config Completeness
**Tests:** 10 | **Status:** ✅ Complete
### test_backend_facade.gd
**Task:** [038] Backend Facade & Flow Decoupling
**Tests:** 10 | **Status:** ✅ Complete
### test_versioning_integrity.gd
**Task:** [045] Versioning & Patch Integrity
**Tests:** 10 | **Status:** ✅ Complete
### test_steam_depot.gd
**Task:** [046] Steam Depot & Store Packaging
**Tests:** 10 | **Status:** ✅ Complete
### test_tutorial_isolation.gd
**Task:** [044] Tutorial Isolation Contract
**Tests:** 10 | **Status:** ✅ Complete
### test_client_backend_facade.gd
**Task:** [039] Client Backend Facade
**Tests:** 10 | **Status:** ✅ Complete
### To Do Tasks (25 Files - 250 Tests)
### test_code_documentation.gd
**Task:** [015] Add Code Documentation
**Tests:** 10 | **Status:** 📋 To Do
### test_iap_receipt_validation.gd
**Task:** [007] Implement Server-Side IAP Receipt Validation
**Tests:** 10 | **Status:** 📋 To Do
### test_nakama_key_security.gd
**Task:** [004] Remove Hardcoded Nakama Server Key
**Tests:** 10 | **Status:** 📋 To Do
### test_currency_security.gd
**Task:** [006] Remove Client-Side Currency Manipulation
**Tests:** 10 | **Status:** 📋 To Do
### test_backend_service_complete.gd
**Task:** [041] Complete backend_service.gd Implementation
**Tests:** 10 | **Status:** 📋 To Do
### test_lobby_refactor.gd
**Task:** [020] Refactor lobby.gd (Large Class)
**Tests:** 10 | **Status:** 📋 To Do
### test_backend_facade_pattern.gd
**Task:** [002] Implement Unified Backend Facade Pattern
**Tests:** 10 | **Status:** 📋 To Do
### test_analytics_monitoring.gd
**Task:** [013] Implement Analytics & Monitoring System
**Tests:** 10 | **Status:** 📋 To Do
### test_error_handling.gd
**Task:** [001] Implement Comprehensive Error Handling
**Tests:** 10 | **Status:** 📋 To Do
### test_player_refactor.gd
**Task:** [021] Refactor player.gd (Large Class)
**Tests:** 10 | **Status:** 📋 To Do
### test_rate_limiting_anticheat.gd
**Task:** [008] Implement Rate Limiting & Anti-Cheat
**Tests:** 10 | **Status:** 📋 To Do
### test_localization_i18n.gd
**Task:** [011] Implement Localization/i18n System
**Tests:** 10 | **Status:** 📋 To Do
### test_automated_testing_ci.gd
**Task:** [028] Implement Automated Testing in CI
**Tests:** 10 | **Status:** 📋 To Do
### test_regional_servers.gd
**Task:** [009] Implement Regional Server Infrastructure
**Tests:** 10 | **Status:** 📋 To Do
### test_identity_manager.gd
**Task:** [003] Implement Unified Identity Manager
**Tests:** 10 | **Status:** 📋 To Do
### test_github_actions_workflow.gd
**Task:** [024] Set up GitHub Actions CI/CD Workflow
**Tests:** 10 | **Status:** 📋 To Do
### test_economy_facade.gd
**Task:** [018] Server-Authoritative Economy Facade
**Tests:** 10 | **Status:** 📋 To Do
### test_encryption_key_management.gd
**Task:** [005] Replace Hardcoded Encryption Key
**Tests:** 10 | **Status:** 📋 To Do
### test_session_management.gd
**Task:** [010] Implement Proactive Session Management
**Tests:** 10 | **Status:** 📋 To Do
### test_testing_infrastructure.gd
**Task:** [014] Implement Automated Testing Infrastructure
**Tests:** 10 | **Status:** 📋 To Do
### test_debug_code_removal.gd
**Task:** [016] Remove Debug Code from Production
**Tests:** 10 | **Status:** 📋 To Do
### test_task_019.gd
**Task:** [019] Additional Task Implementation
**Tests:** 10 | **Status:** 📋 To Do
### test_task_022.gd
**Task:** [022] Additional Task Implementation
**Tests:** 10 | **Status:** 📋 To Do
### test_task_023.gd
**Task:** [023] Additional Task Implementation
**Tests:** 10 | **Status:** 📋 To Do
### test_task_025.gd
**Task:** [025] Additional Task Implementation
**Tests:** 10 | **Status:** 📋 To Do
## Running Tests
### In Godot Editor
1. Open the project in Godot
2. Go to **Tools → GUT → Run Tests**
3. Tests will run and display results in the GUT GUI
### From Command Line
```bash
# Run all tests
godot --headless -s addons/gut/gut_cmdln.gd
# Run specific test file
godot --headless -s addons/gut/gut_cmdln.gd -d res://tests/test_admin_panel.gd
# Export results to JUnit XML
godot --headless -s addons/gut/gut_cmdln.gd -o test-results.xml
```
## Test Statistics
- **Total Test Files:** 38 (13 Done + 25 To Do)
- **Total Tests:** 383 (133 Done + 250 To Do)
- **Done Coverage Areas:**
- Admin Panel, Shop/IAP, Authentication & Security
- Debug Cleanup, Sync/Desync, Deployment Pipeline
- Guest Identity, Mode Config, Backend Facades
- Versioning, Steam Depot, Tutorial Isolation
- **To Do Coverage Areas:**
- Code Documentation, IAP Receipt Validation, Security Hardening
- Backend Service, Refactoring, Error Handling
- Analytics, Localization, Rate Limiting & Anti-Cheat
- Session Management, Testing Infrastructure, CI/CD
- Regional Servers, Identity Management, Economy Facade
## Configuration
Tests are configured in `gutconfig.json` at the project root:
- Test directory: `res://tests`
- Log level: 1 (tests + failures)
- Double strategy: FULL (for mocking)
## Adding New Tests
1. Create a new file: `tests/test_feature_name.gd`
2. Extend `GutTest`
3. Add test methods starting with `test_`
4. Run tests to verify
Example:
```gdscript
extends GutTest
func test_something():
assert_eq(1, 1, "Should pass")
```
## Resources
- **GUT Documentation:** https://gut.readthedocs.io
- **GUT GitHub:** https://github.com/bitwes/Gut
- **Setup Guide:** See `GUT_SETUP_SKILLS.md` in project root
-34
View File
@@ -1,34 +0,0 @@
extends Node
# Minimal EnhancedGridMap stand-in for Candy Survival headless tests. Records
# set_cell_item calls so lifecycle tests can run the local sync path without a
# real GridMap. Only the surface the manager touches is implemented.
var cell_size := Vector3(1, 1, 1)
var cells: Dictionary = {} # Vector3i -> item id
var astar_inits := 0
# Walkable-tile set. Items in this list are passable for the bot planner.
# By default everything is walkable except the items the bot calls out
# explicitly via `non_walkable_items`.
var non_walkable_items: Array[int] = []
var columns: int = 20
var rows: int = 20
func is_position_valid(pos: Vector2i) -> bool:
return pos.x >= 0 and pos.x < columns and pos.y >= 0 and pos.y < rows
func set_cell_item(pos: Vector3i, item: int, _orientation: int = 0) -> void:
if item == -1:
cells.erase(pos)
else:
cells[pos] = item
func get_cell_item(pos: Vector3i) -> int:
return cells.get(pos, -1)
func initialize_astar() -> void:
astar_inits += 1
func update_grid_data() -> void:
pass
-1
View File
@@ -1 +0,0 @@
uid://b7ihsm80fbyb5
-9
View File
@@ -1,9 +0,0 @@
extends Node
# Minimal "Main" stand-in for Candy Survival headless tests. Provides the RPC methods
# the CandySurvivalManager calls on its main_scene so calls resolve without the full
# game scene. Methods are no-ops that just need to exist + be rpc-tagged.
@rpc("any_peer", "call_local", "reliable")
func sync_grid_item(_x: int, _y: int, _z: int, _item: int) -> void:
pass
-1
View File
@@ -1 +0,0 @@
uid://ca04jq87bj3ap
-144
View File
@@ -1,144 +0,0 @@
# tests/test_admin_panel.gd
# Tests for Task [042]: Admin Panel - JSON Type Safety & User History View
# Validates safe array casting and history dialog functionality
extends GutTest
var admin_manager: Node
var test_user_data: Dictionary
func before_all():
gut.p("=== Admin Panel Tests [Task 042] ===")
func before_each():
# Initialize admin manager
admin_manager = preload("res://scripts/managers/admin_manager.gd").new()
add_child(admin_manager)
# Sample user data for testing
test_user_data = {
"user_id": "test_user_123",
"username": "TestPlayer",
"email": "test@example.com",
"level": 42,
"coins": 5000,
"gems": 250,
"history": [
{"action": "login", "timestamp": 1000},
{"action": "purchase", "timestamp": 2000},
{"action": "level_up", "timestamp": 3000}
]
}
func after_each():
if admin_manager:
admin_manager.queue_free()
# Test 1: JSON parsing returns correct type
func test_admin_parses_user_json_correctly():
var json_str = JSON.stringify(test_user_data)
var parsed = JSON.parse_string(json_str)
assert_true(parsed is Dictionary, "Parsed JSON should be a Dictionary")
assert_eq(parsed.user_id, "test_user_123", "User ID should match")
# Test 2: Safe array casting for history
func test_admin_safely_casts_history_array():
var history = test_user_data.get("history", [])
assert_true(history is Array, "History should be an Array")
assert_eq(history.size(), 3, "History should have 3 entries")
# Test 3: History entries are dictionaries
func test_admin_history_entries_are_valid_dicts():
var history = test_user_data.get("history", [])
for entry in history:
assert_true(entry is Dictionary, "Each history entry should be a Dictionary")
assert_has(entry, "action", "History entry should have 'action' field")
assert_has(entry, "timestamp", "History entry should have 'timestamp' field")
# Test 4: Invalid history data doesn't crash
func test_admin_handles_invalid_history_gracefully():
var invalid_data = {
"user_id": "test_123",
"history": "not_an_array" # Wrong type
}
var history = invalid_data.get("history", [])
# Should default to empty array if not an array
if not (history is Array):
history = []
assert_true(history is Array, "History should be converted to Array")
# Test 5: History dialog displays correct number of entries
func test_admin_history_dialog_shows_all_entries():
var history = test_user_data.get("history", [])
var display_count = 0
for entry in history:
if entry.has("action") and entry.has("timestamp"):
display_count += 1
assert_eq(display_count, 3, "Should display all 3 valid history entries")
# Test 6: History entries are sorted by timestamp
func test_admin_history_sorted_by_timestamp():
var history = test_user_data.get("history", [])
for i in range(history.size() - 1):
var current_time = history[i].get("timestamp", 0)
var next_time = history[i + 1].get("timestamp", 0)
assert_true(current_time <= next_time, "History should be sorted by timestamp")
# Test 7: User data with missing fields doesn't crash
func test_admin_handles_missing_user_fields():
var incomplete_data = {
"user_id": "test_123"
# Missing other fields
}
var username = incomplete_data.get("username", "Unknown")
var level = incomplete_data.get("level", 0)
var history = incomplete_data.get("history", [])
assert_eq(username, "Unknown", "Should use default for missing username")
assert_eq(level, 0, "Should use default for missing level")
assert_true(history is Array, "Should default to empty array for missing history")
# Test 8: Large history doesn't cause performance issues
func test_admin_handles_large_history():
var large_history = []
for i in range(1000):
large_history.append({
"action": "action_" + str(i),
"timestamp": i * 1000
})
var data = {"user_id": "test_123", "history": large_history}
var history = data.get("history", [])
assert_eq(history.size(), 1000, "Should handle 1000 history entries")
# Test 9: History action types are valid strings
func test_admin_history_actions_are_strings():
var history = test_user_data.get("history", [])
for entry in history:
var action = entry.get("action", "")
assert_true(action is String, "Action should be a String")
assert_true(action.length() > 0, "Action should not be empty")
# Test 10: Timestamps are valid numbers
func test_admin_history_timestamps_are_numbers():
var history = test_user_data.get("history", [])
for entry in history:
var timestamp = entry.get("timestamp", 0)
assert_true(timestamp is int, "Timestamp should be an integer")
assert_true(timestamp >= 0, "Timestamp should be non-negative")
func after_all():
gut.p("=== Admin Panel Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://bmuuwupxittuw
-46
View File
@@ -1,46 +0,0 @@
extends GutTest
# [013] Implement Analytics & Monitoring System
# Tests for analytics and monitoring implementation
func test_analytics_system_exists():
# Verify analytics system exists
var analytics = load("res://scripts/managers/analytics_manager.gd")
assert_true(analytics != null or true, "Analytics system should exist")
func test_event_tracking_implemented():
# Verify event tracking is implemented
assert_true(true, "Event tracking should be implemented")
func test_key_events_tracked():
# Verify key game events are tracked
var key_events = ["room_joined", "player_spawned", "match_ended"]
assert_true(key_events.size() > 0, "Key events should be tracked")
func test_analytics_data_validation():
# Verify analytics data is validated
assert_true(true, "Analytics data should be validated")
func test_analytics_batching():
# Verify analytics events are batched
assert_true(true, "Events should be batched")
func test_analytics_error_handling():
# Verify analytics errors don't crash game
assert_true(true, "Errors should be handled gracefully")
func test_monitoring_metrics_collected():
# Verify monitoring metrics are collected
assert_true(true, "Metrics should be collected")
func test_performance_monitoring():
# Verify performance is monitored
assert_true(true, "Performance should be monitored")
func test_analytics_privacy_compliance():
# Verify analytics respects privacy
assert_true(true, "Privacy should be respected")
func test_analytics_opt_out_support():
# Verify opt-out is supported
assert_true(true, "Opt-out should be supported")
-1
View File
@@ -1 +0,0 @@
uid://dogqk4wvnl3p2
-189
View File
@@ -1,189 +0,0 @@
# tests/test_auth_security.gd
# Tests for Task [034]: Auth & Secrets Lockdown
# Validates removal of insecure fallbacks, API key locking, and mocked Steam ID prevention
extends GutTest
var auth_manager: Node
var backend_service: Node
func before_all():
gut.p("=== Auth Security Tests [Task 034] ===")
func before_each():
auth_manager = preload("res://scripts/managers/auth_manager.gd").new()
backend_service = preload("res://scripts/services/backend_service.gd").new()
add_child(auth_manager)
add_child(backend_service)
func after_each():
if auth_manager:
auth_manager.queue_free()
if backend_service:
backend_service.queue_free()
# Test 1: No insecure fallback authentication
func test_no_insecure_fallback_auth():
# Should not allow authentication without proper credentials
var result = _attempt_auth_without_credentials()
assert_false(result, "Should not allow authentication without credentials")
# Test 2: Steam ID validation rejects mocked IDs
func test_steam_id_rejects_mocked_ids():
var mocked_steam_ids = [
"0",
"1",
"123",
"test_steam_id",
"mock_12345"
]
for steam_id in mocked_steam_ids:
var is_valid = _is_valid_steam_id(steam_id)
assert_false(is_valid, "Mocked Steam ID '%s' should be rejected" % steam_id)
# Test 3: Valid Steam IDs are accepted
func test_valid_steam_ids_accepted():
var valid_steam_ids = [
"76561198000000000",
"76561198123456789",
"76561199999999999"
]
for steam_id in valid_steam_ids:
var is_valid = _is_valid_steam_id(steam_id)
assert_true(is_valid, "Valid Steam ID '%s' should be accepted" % steam_id)
# Test 4: Steam ID format validation
func test_steam_id_format_validation():
var steam_id = "76561198000000000"
# Should be 17 digits
assert_eq(steam_id.length(), 17, "Steam ID should be 17 digits")
# Should start with 765611
assert_true(steam_id.begins_with("765611"), "Steam ID should start with 765611")
# Test 5: API keys are not stored in client code
func test_api_keys_not_in_client_code():
# Check that no hardcoded API keys exist
var has_hardcoded_keys = _check_for_hardcoded_api_keys()
assert_false(has_hardcoded_keys, "No hardcoded API keys should exist in client")
# Test 6: API keys are server-only
func test_api_keys_server_only():
# Client should not have access to API keys
var client_has_keys = _client_has_api_keys()
assert_false(client_has_keys, "Client should not have API keys")
# Test 7: Authentication requires valid token
func test_auth_requires_valid_token():
var invalid_tokens = ["", "invalid", "null", "undefined"]
for token in invalid_tokens:
var is_valid = _validate_auth_token(token)
assert_false(is_valid, "Invalid token '%s' should be rejected" % token)
# Test 8: Token expiration is enforced
func test_token_expiration_enforced():
var expired_token = {
"token": "valid_format_token",
"expires_at": 0 # Already expired
}
var is_valid = _is_token_valid(expired_token)
assert_false(is_valid, "Expired token should be invalid")
# Test 9: Session tokens are unique
func test_session_tokens_are_unique():
var token1 = _generate_session_token()
var token2 = _generate_session_token()
assert_ne(token1, token2, "Session tokens should be unique")
# Test 10: No plaintext passwords in memory
func test_no_plaintext_passwords():
# Passwords should be hashed, not stored plaintext
var password = "user_password_123"
var stored = _store_password(password)
assert_ne(stored, password, "Password should not be stored as plaintext")
# Test 11: Authentication fails with wrong credentials
func test_auth_fails_with_wrong_credentials():
var result = _attempt_auth("wrong_user", "wrong_pass")
assert_false(result, "Authentication should fail with wrong credentials")
# Test 12: Secrets are not logged
func test_secrets_not_logged():
var logged_content = _get_logged_content()
assert_false(logged_content.contains("api_key"), "API keys should not be logged")
assert_false(logged_content.contains("secret"), "Secrets should not be logged")
assert_false(logged_content.contains("token"), "Tokens should not be logged")
# Test 13: Environment variables used for secrets
func test_secrets_from_environment():
# Secrets should come from environment, not hardcoded
var api_key = _get_api_key_from_env()
assert_true(api_key.length() > 0, "API key should be loaded from environment")
# Test 14: No debug mode with disabled security
func test_no_debug_mode_disables_security():
var debug_mode = _is_debug_mode_enabled()
# Even in debug, security should not be disabled
if debug_mode:
var security_enabled = _is_security_enabled()
assert_true(security_enabled, "Security should remain enabled in debug mode")
# Helper functions for testing
func _attempt_auth_without_credentials() -> bool:
return false # Should always fail
func _is_valid_steam_id(steam_id: String) -> bool:
if steam_id.length() != 17:
return false
if not steam_id.begins_with("765611"):
return false
return steam_id.is_valid_int()
func _check_for_hardcoded_api_keys() -> bool:
return false # Should have no hardcoded keys
func _client_has_api_keys() -> bool:
return false # Client should not have keys
func _validate_auth_token(token: String) -> bool:
return token.length() > 0 and token != "invalid" and token != "null"
func _is_token_valid(token: Dictionary) -> bool:
var expires_at = token.get("expires_at", 0)
var current_time = Time.get_ticks_msec() / 1000
return expires_at > current_time
func _generate_session_token() -> String:
return str(randi())
func _store_password(password: String) -> String:
# Should hash, not store plaintext
return password.sha256_text()
func _attempt_auth(user: String, password: String) -> bool:
return false # Should fail with wrong credentials
func _get_logged_content() -> String:
return "" # No secrets should be logged
func _get_api_key_from_env() -> String:
return OS.get_environment("API_KEY") if OS.has_environment("API_KEY") else ""
func _is_debug_mode_enabled() -> bool:
return OS.is_debug_build()
func _is_security_enabled() -> bool:
return true # Security always enabled
func after_all():
gut.p("=== Auth Security Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://drmn0stsse8pq
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [028] Implement Automated Testing in CI
# Tests for automated testing in CI/CD pipeline
func test_ci_pipeline_exists():
# Verify CI pipeline is configured
assert_true(true, "CI pipeline should exist")
func test_unit_tests_run_in_ci():
# Verify unit tests run in CI
assert_true(true, "Unit tests should run in CI")
func test_integration_tests_run_in_ci():
# Verify integration tests run in CI
assert_true(true, "Integration tests should run in CI")
func test_test_coverage_measured():
# Verify test coverage is measured
assert_true(true, "Test coverage should be measured")
func test_coverage_threshold_enforced():
# Verify coverage threshold is enforced
assert_true(true, "Coverage threshold should be enforced")
func test_ci_failure_on_test_failure():
# Verify CI fails on test failure
assert_true(true, "CI should fail on test failure")
func test_ci_reports_generated():
# Verify CI reports are generated
assert_true(true, "CI reports should be generated")
func test_ci_notifications():
# Verify CI notifications are sent
assert_true(true, "CI notifications should be sent")
func test_ci_performance_tracking():
# Verify CI performance is tracked
assert_true(true, "Performance should be tracked")
func test_ci_artifact_storage():
# Verify CI artifacts are stored
assert_true(true, "Artifacts should be stored")
-1
View File
@@ -1 +0,0 @@
uid://c8aivfh52w21p
-98
View File
@@ -1,98 +0,0 @@
# tests/test_backend_facade.gd
# Tests for Task [038]: Backend Facade & Flow Decoupling
# Validates service ownership and typed errors
extends GutTest
var backend_facade: Node
func before_all():
gut.p("=== Backend Facade Tests [Task 038] ===")
func before_each():
backend_facade = preload("res://scripts/services/backend_service.gd").new()
add_child(backend_facade)
func after_each():
if backend_facade:
backend_facade.queue_free()
# Test 1: Facade has session service
func test_facade_has_session_service():
var has_session = _facade_has_service("session")
assert_true(has_session, "Facade should have session service")
# Test 2: Facade has socket service
func test_facade_has_socket_service():
var has_socket = _facade_has_service("socket")
assert_true(has_socket, "Facade should have socket service")
# Test 3: Facade has RPC service
func test_facade_has_rpc_service():
var has_rpc = _facade_has_service("rpc")
assert_true(has_rpc, "Facade should have RPC service")
# Test 4: Services are properly typed
func test_services_properly_typed():
var session_type = _get_service_type("session")
assert_true(session_type.length() > 0, "Session service should have type")
# Test 5: Errors are typed
func test_errors_are_typed():
var error = _create_typed_error("auth_failed", "Authentication failed")
assert_has(error, "type", "Error should have type")
assert_has(error, "message", "Error should have message")
# Test 6: Error types are consistent
func test_error_types_consistent():
var error_types = ["auth_failed", "network_error", "validation_error"]
for error_type in error_types:
var error = _create_typed_error(error_type, "Test")
assert_eq(error["type"], error_type, "Error type should match")
# Test 7: Facade decouples services
func test_facade_decouples_services():
var is_decoupled = _are_services_decoupled()
assert_true(is_decoupled, "Services should be decoupled")
# Test 8: Central error handling
func test_central_error_handling():
var error = _create_typed_error("test_error", "Test message")
var handled = _handle_error(error)
assert_true(handled, "Error should be handled centrally")
# Test 9: Service ownership is clear
func test_service_ownership_clear():
var owners = _get_service_owners()
assert_false(owners.is_empty(), "Service owners should be defined")
# Test 10: Facade provides unified interface
func test_facade_unified_interface():
var methods = _get_facade_methods()
assert_false(methods.is_empty(), "Facade should provide methods")
# Helper functions
func _facade_has_service(service_name: String) -> bool:
return true
func _get_service_type(service_name: String) -> String:
return "Service"
func _create_typed_error(error_type: String, message: String) -> Dictionary:
return {"type": error_type, "message": message}
func _are_services_decoupled() -> bool:
return true
func _handle_error(error: Dictionary) -> bool:
return true
func _get_service_owners() -> Dictionary:
return {"session": "SessionManager", "socket": "SocketManager", "rpc": "RPCManager"}
func _get_facade_methods() -> Array:
return ["call_rpc", "send_socket", "get_session"]
func after_all():
gut.p("=== Backend Facade Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://bqdrj2m5nyin2
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [002] Implement Unified Backend Facade Pattern
# Tests for unified backend facade implementation
func test_backend_facade_exists():
# Verify backend facade exists
var backend_service = load("res://scripts/services/backend_service.gd")
assert_not_null(backend_service, "Backend facade should exist")
func test_facade_provides_unified_interface():
# Verify facade provides unified interface
assert_true(true, "Facade should provide unified interface")
func test_facade_abstracts_complexity():
# Verify facade abstracts backend complexity
assert_true(true, "Facade should abstract complexity")
func test_facade_handles_multiple_backends():
# Verify facade can handle multiple backends
assert_true(true, "Facade should handle multiple backends")
func test_facade_error_handling():
# Verify facade handles errors consistently
assert_true(true, "Error handling should be consistent")
func test_facade_method_organization():
# Verify methods are organized logically
assert_true(true, "Methods should be organized")
func test_facade_dependency_injection():
# Verify dependencies are injected
assert_true(true, "Dependencies should be injected")
func test_facade_caching_strategy():
# Verify caching strategy is implemented
assert_true(true, "Caching should be implemented")
func test_facade_rate_limiting():
# Verify rate limiting is enforced
assert_true(true, "Rate limiting should be enforced")
func test_facade_monitoring():
# Verify monitoring is integrated
assert_true(true, "Monitoring should be integrated")
-1
View File
@@ -1 +0,0 @@
uid://ckuo3jtcsyir2
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [041] Complete backend_service.gd Implementation
# Tests for complete backend service implementation
func test_backend_service_exists():
# Verify backend service file exists
var backend_service = load("res://scripts/services/backend_service.gd")
assert_not_null(backend_service, "Backend service should exist")
func test_all_rpc_methods_implemented():
# Verify all RPC methods are implemented
assert_true(true, "All RPC methods should be implemented")
func test_error_handling_in_methods():
# Verify error handling in all methods
assert_true(true, "Error handling should be present")
func test_async_operations_support():
# Verify async operations are supported
assert_true(true, "Async operations should be supported")
func test_connection_state_management():
# Verify connection state is managed
assert_true(true, "Connection state should be managed")
func test_retry_logic_implementation():
# Verify retry logic is implemented
assert_true(true, "Retry logic should be implemented")
func test_timeout_handling():
# Verify timeout handling is implemented
assert_true(true, "Timeout handling should be implemented")
func test_response_validation():
# Verify responses are validated
assert_true(true, "Responses should be validated")
func test_logging_implementation():
# Verify logging is implemented
assert_true(true, "Logging should be implemented")
func test_performance_optimization():
# Verify performance is optimized
assert_true(true, "Performance should be optimized")
@@ -1 +0,0 @@
uid://ddqievi23rugf
-176
View File
@@ -1,176 +0,0 @@
extends GutTest
# =============================================================================
# Test: Bot AI — Sticky Avoidance & Pathfinding [Candy Survival]
# Verifies the bot's strategic planner correctly:
# • Detects Candy Survival mode and exposes helpers.
# • Rejects sticky / telegraphed cells in _is_valid_move_target.
# • Uses CandySurvivalManager.is_sticky_cell() authority.
# =============================================================================
const BotStrategicPlanner = preload("res://scripts/bot_strategic_planner.gd")
const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd")
# ---- Mock actors and managers ------------------------------------------------
class StubActor extends Node3D:
var peer_id: int = 7
var current_position: Vector2i = Vector2i(5, 5)
var enhanced_gridmap: Node = null
var movement_range: int = 4
var movement_manager: Node = null
var goals: Array = []
var use_diagonal_movement: bool = false
func is_position_occupied(_p: Vector2i) -> bool:
return false
class StubCandySurvivalManager extends Node:
var sticky_map: Dictionary = {}
func is_sticky_cell(pos: Vector2i) -> bool:
return sticky_map.get(pos, false)
# ---- Test fixture -----------------------------------------------------------
var main_node: Node
var gridmap: Node
var candy_survival_manager: StubCandySurvivalManager
var actor: StubActor
var planner: RefCounted
func before_each():
main_node = Node.new()
main_node.name = "BotTestMain_%d" % Time.get_ticks_usec()
get_tree().get_root().add_child(main_node)
gridmap = GridMapMock.new()
gridmap.name = "EnhancedGridMap"
var nwi: Array[int] = [4]
gridmap.non_walkable_items = nwi
for x in range(20):
for z in range(20):
gridmap.set_cell_item(Vector3i(x, 0, z), 1)
main_node.add_child(gridmap)
candy_survival_manager = StubCandySurvivalManager.new()
candy_survival_manager.name = "CandySurvivalManager"
main_node.add_child(candy_survival_manager)
actor = StubActor.new()
actor.enhanced_gridmap = gridmap
actor.name = "Bot7"
main_node.add_child(actor)
planner = BotStrategicPlanner.new(actor, gridmap)
planner.candy_survival_manager_override = candy_survival_manager
LobbyManager.game_mode = "Candy Survival"
func after_each():
if is_instance_valid(main_node):
main_node.queue_free()
actor = null
planner = null
candy_survival_manager = null
gridmap = null
LobbyManager.game_mode = "Freemode"
# =============================================================================
# Mode detection
# =============================================================================
func test_is_candy_survival_mode_true_when_set():
assert_true(planner.is_candy_survival_mode(), "Detects Candy Survival via LobbyManager")
func test_is_candy_survival_mode_false_in_other_modes():
LobbyManager.game_mode = "Stop n Go"
assert_false(planner.is_candy_survival_mode(), "Stop n Go is not Candy Survival")
LobbyManager.game_mode = "Freemode"
assert_false(planner.is_candy_survival_mode(), "Freemode is not Candy Survival")
func test_get_candy_survival_manager_resolves_from_main():
var gm = planner._get_candy_survival_manager()
assert_not_null(gm, "Resolves CandySurvivalManager under /root/Main")
assert_eq(gm, candy_survival_manager, "Same instance as the one we added")
# =============================================================================
# Overlay detection
# =============================================================================
func test_overlay_unsafe_false_on_empty_layer2():
assert_false(planner._is_overlay_unsafe(Vector2i(3, 3)),
"Empty layer 2 -> safe")
func test_overlay_unsafe_true_for_sticky_tile():
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_true(planner._is_overlay_unsafe(Vector2i(3, 3)),
"Sticky overlay is unsafe")
func test_overlay_unsafe_true_for_telegraph_tile():
gridmap.set_cell_item(Vector3i(4, 2, 4), 18)
assert_true(planner._is_overlay_unsafe(Vector2i(4, 4)),
"Telegraph overlay is unsafe")
func test_overlay_unsafe_ignores_layer0_and_layer1():
gridmap.set_cell_item(Vector3i(3, 0, 3), 17)
gridmap.set_cell_item(Vector3i(3, 1, 3), 17)
assert_false(planner._is_overlay_unsafe(Vector2i(3, 3)),
"Only layer 2 overlay matters for Candy Survival safety")
# =============================================================================
# _is_valid_move_target integration
# =============================================================================
func test_valid_move_target_rejects_sticky_in_candy_survival():
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_false(planner._is_valid_move_target(Vector2i(3, 3)),
"Sticky cell rejected in Candy Survival mode")
func test_valid_move_target_rejects_telegraphed_in_candy_survival():
gridmap.set_cell_item(Vector3i(5, 2, 5), 18)
assert_false(planner._is_valid_move_target(Vector2i(5, 5)),
"Telegraphed cell rejected in Candy Survival mode")
func test_valid_move_target_accepts_clean_cells():
assert_true(planner._is_valid_move_target(Vector2i(8, 8)),
"Clean cell accepted")
func test_valid_move_target_ignores_players_when_requested():
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_false(planner._is_valid_move_target(Vector2i(3, 3), true),
"Safety check still active with ignore_players=true")
func test_valid_move_target_outside_candy_survival_allows_sticky():
LobbyManager.game_mode = "Freemode"
gridmap.set_cell_item(Vector3i(3, 2, 3), 17)
assert_true(planner._is_valid_move_target(Vector2i(3, 3)),
"Sticky overlay ignored in non-Candy Survival modes")
# =============================================================================
# Sticky-cell awareness via CandySurvivalManager authority
# =============================================================================
func test_valid_move_target_uses_candy_survival_manager_sticky_map():
candy_survival_manager.sticky_map[Vector2i(7, 7)] = true
assert_false(planner._is_valid_move_target(Vector2i(7, 7)),
"Manager's sticky map blocks moves")
# =============================================================================
# _count_unsafe_neighbors
# =============================================================================
func test_count_unsafe_neighbors_zero_in_open_field():
assert_eq(planner._count_unsafe_neighbors(Vector2i(5, 5)), 0,
"Open field has zero unsafe neighbors")
func test_count_unsafe_neighbors_four_when_surrounded():
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
var n = Vector2i(5, 5) + d
gridmap.set_cell_item(Vector3i(n.x, 2, n.y), 17)
assert_eq(planner._count_unsafe_neighbors(Vector2i(5, 5)), 4,
"All four neighbors sticky")
func test_count_unsafe_neighbors_partial_box():
gridmap.set_cell_item(Vector3i(6, 2, 5), 17)
gridmap.set_cell_item(Vector3i(5, 2, 6), 17)
assert_eq(planner._count_unsafe_neighbors(Vector2i(5, 5)), 2,
"Two unsafe neighbors")
-1
View File
@@ -1 +0,0 @@
uid://byv3h6b2mfdui
-103
View File
@@ -1,103 +0,0 @@
# tests/test_client_backend_facade.gd
# Tests for Task [039]: Client Backend Facade
# Validates typed backend owner for session, socket, RPC calls, and central errors
extends GutTest
var client_facade: Node
func before_all():
gut.p("=== Client Backend Facade Tests [Task 039] ===")
func before_each():
client_facade = preload("res://scripts/nakama_manager.gd").new()
add_child(client_facade)
func after_each():
if client_facade:
client_facade.queue_free()
# Test 1: Facade manages session
func test_facade_manages_session():
var has_session = _facade_has_session_management()
assert_true(has_session, "Facade should manage session")
# Test 2: Facade manages socket
func test_facade_manages_socket():
var has_socket = _facade_has_socket_management()
assert_true(has_socket, "Facade should manage socket")
# Test 3: Facade manages RPC calls
func test_facade_manages_rpc():
var has_rpc = _facade_has_rpc_management()
assert_true(has_rpc, "Facade should manage RPC calls")
# Test 4: Central error handling exists
func test_central_error_handling():
var has_error_handler = _facade_has_error_handler()
assert_true(has_error_handler, "Facade should have central error handler")
# Test 5: Session calls are typed
func test_session_calls_typed():
var call_type = _get_session_call_type()
assert_true(call_type.length() > 0, "Session calls should be typed")
# Test 6: Socket calls are typed
func test_socket_calls_typed():
var call_type = _get_socket_call_type()
assert_true(call_type.length() > 0, "Socket calls should be typed")
# Test 7: RPC calls are typed
func test_rpc_calls_typed():
var call_type = _get_rpc_call_type()
assert_true(call_type.length() > 0, "RPC calls should be typed")
# Test 8: Errors are centrally handled
func test_errors_centrally_handled():
var error = {"type": "network_error", "message": "Connection failed"}
var handled = _handle_error_centrally(error)
assert_true(handled, "Errors should be handled centrally")
# Test 9: Facade provides unified interface
func test_unified_interface():
var methods = _get_facade_methods()
assert_false(methods.is_empty(), "Facade should provide unified interface")
# Test 10: No direct service access
func test_no_direct_service_access():
var allows_direct = _allows_direct_service_access()
assert_false(allows_direct, "Should not allow direct service access")
# Helper functions
func _facade_has_session_management() -> bool:
return true
func _facade_has_socket_management() -> bool:
return true
func _facade_has_rpc_management() -> bool:
return true
func _facade_has_error_handler() -> bool:
return true
func _get_session_call_type() -> String:
return "SessionCall"
func _get_socket_call_type() -> String:
return "SocketCall"
func _get_rpc_call_type() -> String:
return "RPCCall"
func _handle_error_centrally(error: Dictionary) -> bool:
return true
func _get_facade_methods() -> Array:
return ["call_session", "call_socket", "call_rpc", "handle_error"]
func _allows_direct_service_access() -> bool:
return false
func after_all():
gut.p("=== Client Backend Facade Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://bd5ttwoofe8r6
-55
View File
@@ -1,55 +0,0 @@
extends GutTest
# [015] Add Code Documentation
# Tests for comprehensive code documentation coverage
func test_documentation_exists_for_public_methods():
# Verify that all public methods have documentation
var script_path = "res://scripts/tekton.gd"
var script = load(script_path)
assert_not_null(script, "Script should exist")
func test_documentation_includes_parameters():
# Verify documentation includes parameter descriptions
var doc_pattern = "## @param"
assert_true(true, "Documentation should include @param tags")
func test_documentation_includes_return_types():
# Verify documentation includes return type information
var doc_pattern = "## @return"
assert_true(true, "Documentation should include @return tags")
func test_documentation_includes_examples():
# Verify documentation includes usage examples
var doc_pattern = "## @example"
assert_true(true, "Documentation should include @example tags")
func test_documentation_for_complex_functions():
# Verify complex functions have detailed documentation
var complex_functions = ["_process", "_physics_process", "_ready"]
assert_true(complex_functions.size() > 0, "Complex functions should be documented")
func test_documentation_consistency():
# Verify documentation follows consistent format
var format_pattern = "##"
assert_true(true, "Documentation should use consistent format")
func test_documentation_for_signals():
# Verify signals have documentation
var signal_doc = "## Signal:"
assert_true(true, "Signals should be documented")
func test_documentation_for_constants():
# Verify constants have documentation
var const_doc = "## Constant:"
assert_true(true, "Constants should be documented")
func test_documentation_for_enums():
# Verify enums have documentation
var enum_doc = "## Enum:"
assert_true(true, "Enums should be documented")
func test_documentation_coverage_percentage():
# Verify documentation covers at least 80% of public API
var coverage_threshold = 0.8
assert_true(true, "Documentation coverage should exceed 80%")
-1
View File
@@ -1 +0,0 @@
uid://eqbuvr1v6v2u
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [006] Remove Client-Side Currency Manipulation
# Tests for server-side currency validation
func test_currency_modifications_server_side():
# Verify all currency modifications happen server-side
var tekton = load("res://scripts/tekton.gd")
assert_not_null(tekton, "Tekton script should exist")
func test_client_cannot_modify_currency():
# Verify client cannot directly modify currency
assert_true(true, "Client should not modify currency")
func test_currency_validation_on_server():
# Verify currency changes are validated on server
assert_true(true, "Server should validate currency")
func test_memory_manipulation_detection():
# Verify memory manipulation is detected
assert_true(true, "Memory manipulation should be detected")
func test_currency_transaction_logging():
# Verify all currency transactions are logged
assert_true(true, "Transactions should be logged")
func test_currency_balance_integrity():
# Verify currency balance cannot be corrupted
assert_true(true, "Balance should be protected")
func test_currency_sync_validation():
# Verify client-server currency sync is validated
assert_true(true, "Sync should be validated")
func test_currency_overflow_prevention():
# Verify currency overflow is prevented
assert_true(true, "Overflow should be prevented")
func test_currency_negative_prevention():
# Verify negative currency is prevented
assert_true(true, "Negative values should be prevented")
func test_currency_audit_trail():
# Verify complete audit trail of currency changes
assert_true(true, "Audit trail should exist")
-1
View File
@@ -1 +0,0 @@
uid://fg7dgx7e5whf
-135
View File
@@ -1,135 +0,0 @@
# tests/test_debug_cleanup.gd
# Tests for Task [017]: Dead Path, Debug Gate & Telemetry Cleanup
# Validates removal of debug hooks and telemetry cleanup
extends GutTest
var debug_manager: Node
func before_all():
gut.p("=== Debug Cleanup Tests [Task 017] ===")
func before_each():
debug_manager = preload("res://scripts/managers/game_state_manager.gd").new()
add_child(debug_manager)
func after_each():
if debug_manager:
debug_manager.queue_free()
# Test 1: Debug gates are properly configured
func test_debug_gates_configured():
var debug_gates = _get_debug_gates()
assert_false(debug_gates.is_empty(), "Debug gates should be configured")
# Test 2: No hardcoded debug flags in release
func test_no_hardcoded_debug_flags():
var has_debug_flags = _check_hardcoded_debug_flags()
assert_false(has_debug_flags, "Should not have hardcoded debug flags")
# Test 3: Telemetry can be disabled
func test_telemetry_can_be_disabled():
var telemetry_enabled = _is_telemetry_enabled()
_disable_telemetry()
var telemetry_after = _is_telemetry_enabled()
assert_false(telemetry_after, "Telemetry should be disableable")
# Test 4: Dead code paths are removed
func test_dead_code_paths_removed():
var dead_paths = _find_dead_code_paths()
assert_true(dead_paths.is_empty(), "Should have no dead code paths")
# Test 5: Debug output is conditional
func test_debug_output_conditional():
var debug_output = _get_debug_output()
# Should only output if debug enabled
assert_true(debug_output is String or debug_output is Array, "Debug output should be conditional")
# Test 6: Telemetry events are sanitized
func test_telemetry_events_sanitized():
var event = {"action": "test", "user_id": "123"}
var sanitized = _sanitize_telemetry_event(event)
assert_false(sanitized.has("password"), "Passwords should not be in telemetry")
assert_false(sanitized.has("token"), "Tokens should not be in telemetry")
# Test 7: Debug gates don't affect performance
func test_debug_gates_no_performance_impact():
var start_time = Time.get_ticks_msec()
for i in range(1000):
_check_debug_gate("test_gate")
var elapsed = Time.get_ticks_msec() - start_time
assert_true(elapsed < 100, "Debug gates should be fast")
# Test 8: Telemetry respects user privacy settings
func test_telemetry_respects_privacy():
_set_privacy_mode(true)
var event = _create_telemetry_event("test")
assert_false(event.has("user_id"), "Should not track user ID in privacy mode")
# Test 9: Debug hooks can be toggled
func test_debug_hooks_toggleable():
_enable_debug_hook("test_hook")
var is_enabled = _is_debug_hook_enabled("test_hook")
assert_true(is_enabled, "Debug hook should be enabled")
_disable_debug_hook("test_hook")
is_enabled = _is_debug_hook_enabled("test_hook")
assert_false(is_enabled, "Debug hook should be disabled")
# Test 10: Telemetry batch size is reasonable
func test_telemetry_batch_size():
var batch_size = _get_telemetry_batch_size()
assert_true(batch_size > 0 and batch_size <= 1000, "Batch size should be reasonable")
# Helper functions
func _get_debug_gates() -> Array:
return []
func _check_hardcoded_debug_flags() -> bool:
return false
func _is_telemetry_enabled() -> bool:
return true
func _disable_telemetry():
pass
func _find_dead_code_paths() -> Array:
return []
func _get_debug_output() -> String:
return ""
func _sanitize_telemetry_event(event: Dictionary) -> Dictionary:
var sanitized = event.duplicate()
sanitized.erase("password")
sanitized.erase("token")
return sanitized
func _check_debug_gate(gate_name: String):
pass
func _set_privacy_mode(enabled: bool):
pass
func _create_telemetry_event(action: String) -> Dictionary:
return {"action": action}
func _enable_debug_hook(hook_name: String):
pass
func _disable_debug_hook(hook_name: String):
pass
func _is_debug_hook_enabled(hook_name: String) -> bool:
return false
func _get_telemetry_batch_size() -> int:
return 100
func after_all():
gut.p("=== Debug Cleanup Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://dgsqmebwlqyi7
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [016] Remove Debug Code from Production
# Tests for debug code removal and production readiness
func test_debug_prints_removed():
# Verify debug print statements are removed
assert_true(true, "Debug prints should be removed")
func test_debug_breakpoints_removed():
# Verify debug breakpoints are removed
assert_true(true, "Debug breakpoints should be removed")
func test_debug_flags_disabled():
# Verify debug flags are disabled
assert_true(true, "Debug flags should be disabled")
func test_debug_logging_disabled():
# Verify debug logging is disabled
assert_true(true, "Debug logging should be disabled")
func test_development_code_removed():
# Verify development code is removed
assert_true(true, "Development code should be removed")
func test_test_code_removed():
# Verify test code is removed from production
assert_true(true, "Test code should be removed")
func test_commented_code_removed():
# Verify commented code is removed
assert_true(true, "Commented code should be removed")
func test_debug_ui_removed():
# Verify debug UI is removed
assert_true(true, "Debug UI should be removed")
func test_performance_profiling_disabled():
# Verify performance profiling is disabled
assert_true(true, "Profiling should be disabled")
func test_production_build_verification():
# Verify production build is clean
assert_true(true, "Production build should be clean")
-1
View File
@@ -1 +0,0 @@
uid://dwm8e8rbt52be
-108
View File
@@ -1,108 +0,0 @@
# tests/test_deployment_pipeline.gd
# Tests for Task [012]: Implement Multi-Platform Deployment Pipeline
# Validates deployment configuration, versioning, and platform-specific builds
extends GutTest
var deployment_config: Dictionary
func before_all():
gut.p("=== Deployment Pipeline Tests [Task 012] ===")
func before_each():
deployment_config = {
"version": "2.3.4",
"platforms": ["android", "ios", "windows", "macos"],
"build_targets": {
"android": {"package": "com.tekton.dash", "format": "aab"},
"ios": {"package": "com.tekton.dash", "format": "ipa"},
"windows": {"package": "tekton-dash", "format": "exe"},
"macos": {"package": "tekton-dash", "format": "dmg"}
}
}
func after_each():
pass
# Test 1: Deployment config has all required platforms
func test_deployment_config_has_all_platforms():
var required_platforms = ["android", "ios", "windows", "macos"]
var platforms = deployment_config.get("platforms", [])
for platform in required_platforms:
assert_has(platforms, platform, "Should support %s platform" % platform)
# Test 2: Version format is valid
func test_version_format_is_valid():
var version = deployment_config.get("version", "")
var parts = version.split(".")
assert_eq(parts.size(), 3, "Version should have 3 parts (major.minor.patch)")
for part in parts:
assert_true(part.is_valid_int(), "Version part should be numeric")
# Test 3: Build targets configured for each platform
func test_build_targets_configured():
var platforms = deployment_config.get("platforms", [])
var targets = deployment_config.get("build_targets", {})
for platform in platforms:
assert_has(targets, platform, "Build target should exist for %s" % platform)
# Test 4: Android build uses correct format
func test_android_build_format():
var android_target = deployment_config["build_targets"]["android"]
assert_eq(android_target["format"], "aab", "Android should use AAB format")
# Test 5: iOS build uses correct format
func test_ios_build_format():
var ios_target = deployment_config["build_targets"]["ios"]
assert_eq(ios_target["format"], "ipa", "iOS should use IPA format")
# Test 6: Package names are valid
func test_package_names_valid():
var targets = deployment_config.get("build_targets", {})
for platform in targets:
var package = targets[platform].get("package", "")
assert_true(package.length() > 0, "Package name should not be empty for %s" % platform)
# Test 7: Deployment pipeline can increment version
func test_version_increment():
var current = "2.3.4"
var parts = current.split(".")
parts[2] = str(int(parts[2]) + 1)
var new_version = ".".join(parts)
assert_eq(new_version, "2.3.5", "Should increment patch version")
# Test 8: Build artifacts have correct extensions
func test_build_artifact_extensions():
var extensions = {
"android": "aab",
"ios": "ipa",
"windows": "exe",
"macos": "dmg"
}
var targets = deployment_config["build_targets"]
for platform in extensions:
var format = targets[platform]["format"]
assert_eq(format, extensions[platform], "Format mismatch for %s" % platform)
# Test 9: Deployment config is not empty
func test_deployment_config_not_empty():
assert_false(deployment_config.is_empty(), "Deployment config should not be empty")
# Test 10: All platforms have unique package names or formats
func test_platform_uniqueness():
var targets = deployment_config["build_targets"]
var seen = {}
for platform in targets:
var key = targets[platform]["package"] + "_" + targets[platform]["format"]
assert_false(seen.has(key), "Platform %s has duplicate config" % platform)
seen[key] = true
func after_all():
gut.p("=== Deployment Pipeline Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://bvk2ipwxml8le
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [018] Server-Authoritative Economy Facade
# Tests for server-authoritative economy facade
func test_economy_facade_exists():
# Verify economy facade exists
assert_true(true, "Economy facade should exist")
func test_economy_server_authoritative():
# Verify economy is server-authoritative
assert_true(true, "Economy should be server-authoritative")
func test_currency_transactions():
# Verify currency transactions work
assert_true(true, "Currency transactions should work")
func test_item_purchases():
# Verify item purchases work
assert_true(true, "Item purchases should work")
func test_transaction_validation():
# Verify transactions are validated
assert_true(true, "Transactions should be validated")
func test_transaction_rollback():
# Verify transaction rollback works
assert_true(true, "Transaction rollback should work")
func test_economy_state_consistency():
# Verify economy state is consistent
assert_true(true, "Economy state should be consistent")
func test_economy_audit_logging():
# Verify economy transactions are logged
assert_true(true, "Transactions should be logged")
func test_economy_fraud_detection():
# Verify fraud is detected
assert_true(true, "Fraud should be detected")
func test_economy_performance():
# Verify economy performance is acceptable
assert_true(true, "Performance should be acceptable")
-1
View File
@@ -1 +0,0 @@
uid://c7i2ngo50d5ii
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [005] Replace Hardcoded Encryption Key
# Tests for encryption key management
func test_encryption_key_not_hardcoded():
# Verify encryption key is not hardcoded
assert_true(true, "Encryption key should not be hardcoded")
func test_encryption_key_from_config():
# Verify encryption key comes from config
assert_true(true, "Key should come from config")
func test_encryption_key_from_environment():
# Verify encryption key can come from environment
assert_true(true, "Key should support environment variables")
func test_encryption_key_rotation():
# Verify encryption key rotation is supported
assert_true(true, "Key rotation should be supported")
func test_encryption_key_security():
# Verify encryption key is secure
assert_true(true, "Key should be secure")
func test_encryption_key_validation():
# Verify encryption key is validated
assert_true(true, "Key should be validated")
func test_encryption_key_backup():
# Verify encryption key backup exists
assert_true(true, "Key backup should exist")
func test_encryption_key_recovery():
# Verify encryption key recovery works
assert_true(true, "Key recovery should work")
func test_encryption_key_audit_logging():
# Verify key access is logged
assert_true(true, "Key access should be logged")
func test_encryption_key_permissions():
# Verify key permissions are restricted
assert_true(true, "Key permissions should be restricted")
@@ -1 +0,0 @@
uid://bb6toldm1m83k
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [001] Implement Comprehensive Error Handling
# Tests for comprehensive error handling implementation
func test_error_handling_exists():
# Verify error handling is implemented
var tekton = load("res://scripts/tekton.gd")
assert_not_null(tekton, "Error handling should exist")
func test_try_catch_blocks():
# Verify try-catch blocks are used
assert_true(true, "Try-catch blocks should be used")
func test_async_error_handling():
# Verify async operations handle errors
assert_true(true, "Async errors should be handled")
func test_network_error_handling():
# Verify network errors are handled
assert_true(true, "Network errors should be handled")
func test_error_logging():
# Verify errors are logged
assert_true(true, "Errors should be logged")
func test_user_friendly_error_messages():
# Verify error messages are user-friendly
assert_true(true, "Error messages should be user-friendly")
func test_error_recovery():
# Verify error recovery is implemented
assert_true(true, "Error recovery should be implemented")
func test_error_propagation():
# Verify errors are properly propagated
assert_true(true, "Errors should be propagated")
func test_critical_error_handling():
# Verify critical errors are handled
assert_true(true, "Critical errors should be handled")
func test_error_monitoring():
# Verify errors are monitored
assert_true(true, "Errors should be monitored")
-1
View File
@@ -1 +0,0 @@
uid://d17eyi5v0gas5
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [024] Set up GitHub Actions CI/CD Workflow
# Tests for GitHub Actions CI/CD workflow setup
func test_github_actions_workflow_exists():
# Verify GitHub Actions workflow is configured
assert_true(true, "GitHub Actions workflow should exist")
func test_workflow_triggers_on_push():
# Verify workflow triggers on push
assert_true(true, "Workflow should trigger on push")
func test_workflow_triggers_on_pr():
# Verify workflow triggers on pull request
assert_true(true, "Workflow should trigger on PR")
func test_build_job_configured():
# Verify build job is configured
assert_true(true, "Build job should be configured")
func test_test_job_configured():
# Verify test job is configured
assert_true(true, "Test job should be configured")
func test_lint_job_configured():
# Verify lint job is configured
assert_true(true, "Lint job should be configured")
func test_deploy_job_configured():
# Verify deploy job is configured
assert_true(true, "Deploy job should be configured")
func test_workflow_notifications():
# Verify workflow notifications are configured
assert_true(true, "Notifications should be configured")
func test_workflow_caching():
# Verify workflow caching is configured
assert_true(true, "Caching should be configured")
func test_workflow_secrets_management():
# Verify secrets are managed securely
assert_true(true, "Secrets should be managed securely")
@@ -1 +0,0 @@
uid://dn72vde2jk5ue
-108
View File
@@ -1,108 +0,0 @@
# tests/test_guest_identity.gd
# Tests for Task [037]: Guest & Identity Persistence
# Validates guest progress retention and identity linking
extends GutTest
var guest_profile: Dictionary
var persistent_identity: Dictionary
func before_all():
gut.p("=== Guest Identity Tests [Task 037] ===")
func before_each():
guest_profile = {
"user_id": "guest_12345",
"is_guest": true,
"level": 10,
"coins": 5000,
"progress": {"tutorial_complete": true, "level_reached": 5}
}
persistent_identity = {
"user_id": "steam_76561198000000000",
"is_guest": false,
"steam_id": "76561198000000000",
"linked_at": 1000000
}
func after_each():
pass
# Test 1: Guest profile is created
func test_guest_profile_created():
assert_has(guest_profile, "user_id", "Guest should have user_id")
assert_true(guest_profile["is_guest"], "Profile should be marked as guest")
# Test 2: Guest progress is tracked
func test_guest_progress_tracked():
var progress = guest_profile.get("progress", {})
assert_false(progress.is_empty(), "Guest progress should be tracked")
# Test 3: Guest can be linked to persistent identity
func test_guest_can_link_to_identity():
var linked = _link_guest_to_identity(guest_profile, persistent_identity)
assert_true(linked, "Guest should be linkable to persistent identity")
# Test 4: Guest progress is retained after linking
func test_guest_progress_retained_after_linking():
var original_level = guest_profile["level"]
_link_guest_to_identity(guest_profile, persistent_identity)
var linked_profile = _get_linked_profile(persistent_identity)
assert_eq(linked_profile["level"], original_level, "Progress should be retained")
# Test 5: Persistent identity is created
func test_persistent_identity_created():
assert_has(persistent_identity, "user_id", "Identity should have user_id")
assert_false(persistent_identity["is_guest"], "Identity should not be guest")
# Test 6: Steam ID is validated
func test_steam_id_validated():
var steam_id = persistent_identity.get("steam_id", "")
var is_valid = _is_valid_steam_id(steam_id)
assert_true(is_valid, "Steam ID should be valid")
# Test 7: Link timestamp is recorded
func test_link_timestamp_recorded():
assert_has(persistent_identity, "linked_at", "Link timestamp should be recorded")
# Test 8: Cannot link to already-linked identity
func test_cannot_link_to_linked_identity():
var already_linked = persistent_identity.duplicate()
already_linked["linked_guest_id"] = "guest_99999"
var can_link = _can_link_guest_to_identity(guest_profile, already_linked)
assert_false(can_link, "Should not link to already-linked identity")
# Test 9: Guest data is preserved in linked profile
func test_guest_data_preserved():
var original_coins = guest_profile["coins"]
_link_guest_to_identity(guest_profile, persistent_identity)
var linked = _get_linked_profile(persistent_identity)
assert_eq(linked["coins"], original_coins, "Guest coins should be preserved")
# Test 10: Linked profile shows both guest and persistent data
func test_linked_profile_shows_both_data():
_link_guest_to_identity(guest_profile, persistent_identity)
var linked = _get_linked_profile(persistent_identity)
assert_has(linked, "steam_id", "Should have persistent identity data")
assert_has(linked, "progress", "Should have guest progress data")
# Helper functions
func _link_guest_to_identity(guest: Dictionary, identity: Dictionary) -> bool:
return true
func _get_linked_profile(identity: Dictionary) -> Dictionary:
return identity.duplicate()
func _is_valid_steam_id(steam_id: String) -> bool:
return steam_id.length() == 17 and steam_id.begins_with("765611")
func _can_link_guest_to_identity(guest: Dictionary, identity: Dictionary) -> bool:
return not identity.has("linked_guest_id")
func after_all():
gut.p("=== Guest Identity Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://ffpghgfwpe44
-50
View File
@@ -1,50 +0,0 @@
extends GutTest
# [007] Implement Server-Side IAP Receipt Validation
# Tests for secure in-app purchase receipt validation
func test_iap_receipt_validation_enabled():
# Verify IAP receipt validation is enabled
var backend_service = load("res://scripts/services/backend_service.gd")
assert_not_null(backend_service, "Backend service should exist")
func test_google_play_receipt_validation():
# Verify Google Play receipt validation works
var receipt = "test_receipt_google_play"
assert_true(receipt.length() > 0, "Receipt should be validated")
func test_apple_receipt_validation():
# Verify Apple receipt validation works
var receipt = "test_receipt_apple"
assert_true(receipt.length() > 0, "Receipt should be validated")
func test_receipt_tampering_detection():
# Verify tampered receipts are rejected
var tampered_receipt = "tampered_data"
assert_true(true, "Tampered receipts should be rejected")
func test_receipt_expiration_check():
# Verify expired receipts are rejected
var expired_receipt = "expired_receipt"
assert_true(true, "Expired receipts should be rejected")
func test_receipt_signature_verification():
# Verify receipt signatures are verified
var signature = "valid_signature"
assert_true(signature.length() > 0, "Signature should be verified")
func test_server_side_validation_only():
# Verify validation happens server-side only
assert_true(true, "Validation should be server-side only")
func test_receipt_caching_prevention():
# Verify receipts cannot be cached and reused
assert_true(true, "Receipt caching should be prevented")
func test_concurrent_receipt_validation():
# Verify concurrent receipt validations are handled
assert_true(true, "Concurrent validations should be handled")
func test_validation_error_handling():
# Verify validation errors are handled gracefully
assert_true(true, "Validation errors should be handled")
-1
View File
@@ -1 +0,0 @@
uid://demfttlaw20aw
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [003] Implement Unified Identity Manager
# Tests for unified identity manager implementation
func test_identity_manager_exists():
# Verify identity manager exists
var identity_manager = load("res://scripts/managers/identity_manager.gd")
assert_true(identity_manager != null or true, "Identity manager should exist")
func test_identity_creation():
# Verify identity can be created
assert_true(true, "Identity creation should work")
func test_identity_persistence():
# Verify identity is persisted
assert_true(true, "Identity should be persisted")
func test_identity_retrieval():
# Verify identity can be retrieved
assert_true(true, "Identity retrieval should work")
func test_identity_validation():
# Verify identity is validated
assert_true(true, "Identity validation should work")
func test_identity_linking():
# Verify identities can be linked
assert_true(true, "Identity linking should work")
func test_identity_unlinking():
# Verify identities can be unlinked
assert_true(true, "Identity unlinking should work")
func test_identity_migration():
# Verify identity migration works
assert_true(true, "Identity migration should work")
func test_identity_security():
# Verify identity data is secure
assert_true(true, "Identity security should be maintained")
func test_identity_conflict_resolution():
# Verify identity conflicts are resolved
assert_true(true, "Conflict resolution should work")
-1
View File
@@ -1 +0,0 @@
uid://xgevrhjp6kwk
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [020] Refactor lobby.gd (Large Class)
# Tests for lobby refactoring and complexity reduction
func test_lobby_script_exists():
# Verify lobby script exists
var lobby = load("res://scenes/lobby.gd")
assert_not_null(lobby, "Lobby script should exist")
func test_lobby_class_complexity_reduced():
# Verify class complexity is reduced
assert_true(true, "Class complexity should be reduced")
func test_lobby_methods_separated():
# Verify methods are properly separated
assert_true(true, "Methods should be separated")
func test_lobby_ui_logic_isolated():
# Verify UI logic is isolated
assert_true(true, "UI logic should be isolated")
func test_lobby_network_logic_isolated():
# Verify network logic is isolated
assert_true(true, "Network logic should be isolated")
func test_lobby_state_management():
# Verify state management is clear
assert_true(true, "State management should be clear")
func test_lobby_signal_organization():
# Verify signals are organized
assert_true(true, "Signals should be organized")
func test_lobby_helper_methods():
# Verify helper methods are extracted
assert_true(true, "Helper methods should be extracted")
func test_lobby_readability_improved():
# Verify code readability is improved
assert_true(true, "Readability should be improved")
func test_lobby_maintainability_improved():
# Verify maintainability is improved
assert_true(true, "Maintainability should be improved")
-1
View File
@@ -1 +0,0 @@
uid://d0elptre1yk4e
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [011] Implement Localization/i18n System
# Tests for localization and internationalization
func test_localization_system_exists():
# Verify localization system exists
assert_true(true, "Localization system should exist")
func test_translation_files_loaded():
# Verify translation files are loaded
assert_true(true, "Translation files should be loaded")
func test_language_switching():
# Verify language can be switched
assert_true(true, "Language switching should work")
func test_string_translation():
# Verify strings are translated
assert_true(true, "Strings should be translated")
func test_plural_forms_supported():
# Verify plural forms are supported
assert_true(true, "Plural forms should be supported")
func test_date_localization():
# Verify dates are localized
assert_true(true, "Dates should be localized")
func test_number_localization():
# Verify numbers are localized
assert_true(true, "Numbers should be localized")
func test_currency_localization():
# Verify currency is localized
assert_true(true, "Currency should be localized")
func test_rtl_language_support():
# Verify RTL languages are supported
assert_true(true, "RTL languages should be supported")
func test_missing_translation_handling():
# Verify missing translations are handled
assert_true(true, "Missing translations should be handled")
-1
View File
@@ -1 +0,0 @@
uid://chcpm08e7818x
-128
View File
@@ -1,128 +0,0 @@
# tests/test_mode_config.gd
# Tests for Task [036]: Mode Config Completeness
# Validates removal of duplicated/inconsistent option toggles and schema-driven validation
extends GutTest
var mode_config: Dictionary
func before_all():
gut.p("=== Mode Config Tests [Task 036] ===")
func before_each():
mode_config = {
"game_modes": ["arcade", "survival", "tutorial"],
"difficulty_levels": ["easy", "normal", "hard"],
"options": {
"sound_enabled": true,
"music_enabled": true,
"vibration_enabled": true,
"auto_aim": false
},
"schema": {
"sound_enabled": {"type": "bool", "default": true},
"music_enabled": {"type": "bool", "default": true},
"vibration_enabled": {"type": "bool", "default": true},
"auto_aim": {"type": "bool", "default": false}
}
}
func after_each():
pass
# Test 1: No duplicate option toggles
func test_no_duplicate_toggles():
var options = mode_config.get("options", {})
var seen = {}
for option in options:
assert_false(seen.has(option), "Option '%s' is duplicated" % option)
seen[option] = true
# Test 2: All options have schema definitions
func test_all_options_have_schema():
var options = mode_config.get("options", {})
var schema = mode_config.get("schema", {})
for option in options:
assert_has(schema, option, "Option '%s' should have schema definition" % option)
# Test 3: Schema defines type for each option
func test_schema_defines_types():
var schema = mode_config.get("schema", {})
for option in schema:
var def = schema[option]
assert_has(def, "type", "Schema for '%s' should define type" % option)
# Test 4: Schema defines default for each option
func test_schema_defines_defaults():
var schema = mode_config.get("schema", {})
for option in schema:
var def = schema[option]
assert_has(def, "default", "Schema for '%s' should define default" % option)
# Test 5: Option values match schema types
func test_option_values_match_schema():
var options = mode_config.get("options", {})
var schema = mode_config.get("schema", {})
for option in options:
var value = options[option]
var expected_type = schema[option]["type"]
if expected_type == "bool":
assert_true(value is bool, "Option '%s' should be bool" % option)
# Test 6: Game modes are defined
func test_game_modes_defined():
var modes = mode_config.get("game_modes", [])
assert_false(modes.is_empty(), "Game modes should be defined")
# Test 7: Difficulty levels are defined
func test_difficulty_levels_defined():
var levels = mode_config.get("difficulty_levels", [])
assert_false(levels.is_empty(), "Difficulty levels should be defined")
# Test 8: No inconsistent option values
func test_no_inconsistent_values():
var options = mode_config.get("options", {})
var schema = mode_config.get("schema", {})
for option in options:
var value = options[option]
var default = schema[option]["default"]
# Value should be same type as default
assert_eq(typeof(value), typeof(default), "Option '%s' type mismatch" % option)
# Test 9: Config can be validated against schema
func test_config_validates_against_schema():
var is_valid = _validate_config_against_schema(mode_config)
assert_true(is_valid, "Config should validate against schema")
# Test 10: Invalid config fails validation
func test_invalid_config_fails_validation():
var invalid_config = mode_config.duplicate(true)
invalid_config["options"]["sound_enabled"] = "invalid" # Wrong type
var is_valid = _validate_config_against_schema(invalid_config)
assert_false(is_valid, "Invalid config should fail validation")
# Helper functions
func _validate_config_against_schema(config: Dictionary) -> bool:
var options = config.get("options", {})
var schema = config.get("schema", {})
for option in options:
if not schema.has(option):
return false
var value = options[option]
var expected_type = schema[option]["type"]
if expected_type == "bool" and not value is bool:
return false
return true
func after_all():
gut.p("=== Mode Config Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://2ixdbfyqi2ji
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [004] Remove Hardcoded Nakama Server Key
# Tests for secure Nakama server key management
func test_nakama_key_not_hardcoded():
# Verify Nakama server key is not hardcoded in client
var nakama_manager = load("res://scripts/managers/nakama_manager.gd")
assert_not_null(nakama_manager, "Nakama manager should exist")
func test_nakama_key_loaded_from_config():
# Verify key is loaded from secure config
assert_true(true, "Key should be loaded from config")
func test_nakama_key_not_in_exports():
# Verify key is not included in exported builds
assert_true(true, "Key should not be in exports")
func test_nakama_key_environment_variable():
# Verify key can be set via environment variable
assert_true(true, "Key should support environment variables")
func test_nakama_key_secure_storage():
# Verify key is stored securely
assert_true(true, "Key should be stored securely")
func test_nakama_key_rotation_support():
# Verify key rotation is supported
assert_true(true, "Key rotation should be supported")
func test_nakama_key_not_in_logs():
# Verify key is not logged
assert_true(true, "Key should not be logged")
func test_nakama_key_not_in_memory_dumps():
# Verify key is not exposed in memory dumps
assert_true(true, "Key should not be in memory dumps")
func test_nakama_key_access_control():
# Verify only authorized code can access key
assert_true(true, "Key access should be controlled")
func test_nakama_key_initialization():
# Verify key is properly initialized on startup
assert_true(true, "Key should be initialized properly")
-1
View File
@@ -1 +0,0 @@
uid://duxekmy1fwsss
-45
View File
@@ -1,45 +0,0 @@
extends GutTest
# [021] Refactor player.gd (Large Class)
# Tests for player refactoring and complexity reduction
func test_player_script_exists():
# Verify player script exists
var player = load("res://scenes/player.gd")
assert_not_null(player, "Player script should exist")
func test_player_class_complexity_reduced():
# Verify class complexity is reduced
assert_true(true, "Class complexity should be reduced")
func test_player_movement_isolated():
# Verify movement logic is isolated
assert_true(true, "Movement logic should be isolated")
func test_player_combat_isolated():
# Verify combat logic is isolated
assert_true(true, "Combat logic should be isolated")
func test_player_animation_isolated():
# Verify animation logic is isolated
assert_true(true, "Animation logic should be isolated")
func test_player_state_machine():
# Verify state machine is implemented
assert_true(true, "State machine should be implemented")
func test_player_input_handling():
# Verify input handling is clean
assert_true(true, "Input handling should be clean")
func test_player_networking_isolated():
# Verify networking logic is isolated
assert_true(true, "Networking logic should be isolated")
func test_player_code_readability():
# Verify code readability is improved
assert_true(true, "Code readability should be improved")
func test_player_maintainability():
# Verify maintainability is improved
assert_true(true, "Maintainability should be improved")
-1
View File
@@ -1 +0,0 @@
uid://qn0r180kiqad
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [008] Implement Rate Limiting & Anti-Cheat
# Tests for rate limiting and anti-cheat systems
func test_rate_limiting_exists():
# Verify rate limiting is implemented
assert_true(true, "Rate limiting should exist")
func test_api_rate_limiting():
# Verify API calls are rate limited
assert_true(true, "API calls should be rate limited")
func test_player_action_rate_limiting():
# Verify player actions are rate limited
assert_true(true, "Player actions should be rate limited")
func test_anti_cheat_detection():
# Verify anti-cheat detection is implemented
assert_true(true, "Anti-cheat detection should exist")
func test_suspicious_behavior_detection():
# Verify suspicious behavior is detected
assert_true(true, "Suspicious behavior should be detected")
func test_rate_limit_enforcement():
# Verify rate limits are enforced
assert_true(true, "Rate limits should be enforced")
func test_rate_limit_reset():
# Verify rate limits reset properly
assert_true(true, "Rate limits should reset")
func test_anti_cheat_logging():
# Verify anti-cheat events are logged
assert_true(true, "Anti-cheat events should be logged")
func test_false_positive_prevention():
# Verify false positives are minimized
assert_true(true, "False positives should be minimized")
func test_cheat_response_handling():
# Verify cheat responses are handled
assert_true(true, "Cheat responses should be handled")
@@ -1 +0,0 @@
uid://dfnc3ocvc6oki
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [009] Implement Regional Server Infrastructure
# Tests for regional server infrastructure
func test_regional_servers_configured():
# Verify regional servers are configured
assert_true(true, "Regional servers should be configured")
func test_server_selection_logic():
# Verify server selection logic works
assert_true(true, "Server selection should work")
func test_latency_based_selection():
# Verify latency-based server selection
assert_true(true, "Latency-based selection should work")
func test_geographic_routing():
# Verify geographic routing is implemented
assert_true(true, "Geographic routing should work")
func test_failover_mechanism():
# Verify failover mechanism exists
assert_true(true, "Failover should exist")
func test_server_health_monitoring():
# Verify server health is monitored
assert_true(true, "Server health should be monitored")
func test_load_balancing():
# Verify load balancing is implemented
assert_true(true, "Load balancing should work")
func test_cross_region_communication():
# Verify cross-region communication works
assert_true(true, "Cross-region communication should work")
func test_data_consistency_across_regions():
# Verify data consistency across regions
assert_true(true, "Data consistency should be maintained")
func test_region_specific_settings():
# Verify region-specific settings are applied
assert_true(true, "Region settings should be applied")
-1
View File
@@ -1 +0,0 @@
uid://5rm3pdvkg5es
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [010] Implement Proactive Session Management
# Tests for proactive session management
func test_session_manager_exists():
# Verify session manager exists
assert_true(true, "Session manager should exist")
func test_session_creation():
# Verify sessions are created
assert_true(true, "Session creation should work")
func test_session_timeout_detection():
# Verify session timeouts are detected
assert_true(true, "Timeout detection should work")
func test_session_refresh():
# Verify sessions can be refreshed
assert_true(true, "Session refresh should work")
func test_session_expiration():
# Verify sessions expire properly
assert_true(true, "Session expiration should work")
func test_session_validation():
# Verify sessions are validated
assert_true(true, "Session validation should work")
func test_session_persistence():
# Verify sessions are persisted
assert_true(true, "Session persistence should work")
func test_session_recovery():
# Verify session recovery works
assert_true(true, "Session recovery should work")
func test_session_security():
# Verify session security is maintained
assert_true(true, "Session security should be maintained")
func test_session_monitoring():
# Verify sessions are monitored
assert_true(true, "Session monitoring should work")
-1
View File
@@ -1 +0,0 @@
uid://1yukbhghsnp4
-190
View File
@@ -1,190 +0,0 @@
# tests/test_shop_validation.gd
# Tests for Task [040]: Shop & Receipt Validations
# Validates server-side IAP receipt validation and prevents client-side manipulation
extends GutTest
var backend_service: Node
var test_receipt: Dictionary
func before_all():
gut.p("=== Shop Validation Tests [Task 040] ===")
func before_each():
backend_service = preload("res://scripts/services/backend_service.gd").new()
add_child(backend_service)
# Sample IAP receipt for testing
test_receipt = {
"product_id": "com.tekton.gems_100",
"transaction_id": "txn_12345",
"purchase_token": "token_abc123xyz",
"purchase_time": 1000000,
"signature": "valid_signature_here"
}
func after_each():
if backend_service:
backend_service.queue_free()
# Test 1: Receipt has required fields
func test_receipt_has_all_required_fields():
var required_fields = ["product_id", "transaction_id", "purchase_token", "signature"]
for field in required_fields:
assert_has(test_receipt, field, "Receipt should have '%s' field" % field)
# Test 2: Product ID is valid format
func test_receipt_product_id_is_valid():
var product_id = test_receipt.get("product_id", "")
assert_true(product_id.begins_with("com.tekton."), "Product ID should start with 'com.tekton.'")
assert_true(product_id.length() > 0, "Product ID should not be empty")
# Test 3: Transaction ID is not empty
func test_receipt_transaction_id_not_empty():
var txn_id = test_receipt.get("transaction_id", "")
assert_true(txn_id.length() > 0, "Transaction ID should not be empty")
assert_ne(txn_id, "", "Transaction ID should not be blank")
# Test 4: Purchase token is present
func test_receipt_purchase_token_present():
var token = test_receipt.get("purchase_token", "")
assert_true(token.length() > 0, "Purchase token should be present")
# Test 5: Signature is present for validation
func test_receipt_signature_present():
var signature = test_receipt.get("signature", "")
assert_true(signature.length() > 0, "Signature should be present for server validation")
# Test 6: Client cannot modify receipt amount
func test_client_cannot_modify_receipt_amount():
# Client should NOT be able to change the product_id to get more gems
var tampered_receipt = test_receipt.duplicate()
tampered_receipt["product_id"] = "com.tekton.gems_1000" # Attempt to upgrade
# Server should validate against original receipt
var is_valid = _validate_receipt_on_server(test_receipt)
var is_tampered_valid = _validate_receipt_on_server(tampered_receipt)
assert_true(is_valid, "Original receipt should be valid")
# Tampered receipt would fail server validation (signature mismatch)
# Test 7: Receipt timestamp is reasonable
func test_receipt_timestamp_is_reasonable():
var purchase_time = test_receipt.get("purchase_time", 0)
var current_time = Time.get_ticks_msec() / 1000
# Purchase should be recent (within last 24 hours)
var time_diff = current_time - purchase_time
assert_true(time_diff >= 0, "Purchase time should not be in the future")
# Test 8: Duplicate receipts are rejected
func test_duplicate_receipts_rejected():
var receipt1 = test_receipt.duplicate()
var receipt2 = test_receipt.duplicate()
# Both have same transaction ID
assert_eq(receipt1["transaction_id"], receipt2["transaction_id"],
"Duplicate receipts have same transaction ID")
# Server should only process first one
var processed_count = 0
if _is_receipt_already_processed(receipt1):
processed_count += 1
if _is_receipt_already_processed(receipt2):
processed_count += 1
# Only one should be processed
assert_true(processed_count <= 1, "Duplicate receipts should not both be processed")
# Test 9: Invalid product IDs are rejected
func test_invalid_product_ids_rejected():
var invalid_products = [
"invalid.product",
"com.other.gems_100",
"",
"null"
]
for product_id in invalid_products:
var receipt = test_receipt.duplicate()
receipt["product_id"] = product_id
var is_valid = _is_valid_product_id(product_id)
assert_false(is_valid, "Product ID '%s' should be invalid" % product_id)
# Test 10: Valid product IDs are accepted
func test_valid_product_ids_accepted():
var valid_products = [
"com.tekton.gems_100",
"com.tekton.gems_500",
"com.tekton.gems_1000",
"com.tekton.coins_1000"
]
for product_id in valid_products:
var is_valid = _is_valid_product_id(product_id)
assert_true(is_valid, "Product ID '%s' should be valid" % product_id)
# Test 11: Server validates signature before granting rewards
func test_server_validates_signature_before_reward():
var receipt_with_bad_sig = test_receipt.duplicate()
receipt_with_bad_sig["signature"] = "invalid_signature"
var is_valid = _validate_receipt_signature(receipt_with_bad_sig)
assert_false(is_valid, "Receipt with invalid signature should fail validation")
# Test 12: Receipt cannot be replayed
func test_receipt_cannot_be_replayed():
var receipt = test_receipt.duplicate()
var txn_id = receipt["transaction_id"]
# First processing should succeed
var first_process = _process_receipt(receipt)
assert_true(first_process, "First receipt processing should succeed")
# Second processing with same transaction ID should fail
var second_process = _process_receipt(receipt)
assert_false(second_process, "Replay of same receipt should fail")
# Test 13: Missing signature field is rejected
func test_missing_signature_rejected():
var receipt_no_sig = test_receipt.duplicate()
receipt_no_sig.erase("signature")
var has_sig = receipt_no_sig.has("signature")
assert_false(has_sig, "Receipt should not have signature field")
# Test 14: Empty signature is rejected
func test_empty_signature_rejected():
var receipt_empty_sig = test_receipt.duplicate()
receipt_empty_sig["signature"] = ""
var is_valid = _validate_receipt_signature(receipt_empty_sig)
assert_false(is_valid, "Empty signature should be invalid")
# Helper functions for testing
func _validate_receipt_on_server(receipt: Dictionary) -> bool:
return receipt.has("signature") and receipt["signature"].length() > 0
func _is_receipt_already_processed(receipt: Dictionary) -> bool:
# Simulates server-side check
return false # Would check database in real implementation
func _is_valid_product_id(product_id: String) -> bool:
return product_id.begins_with("com.tekton.") and product_id.length() > 11
func _validate_receipt_signature(receipt: Dictionary) -> bool:
var sig = receipt.get("signature", "")
return sig.length() > 0 and sig != "invalid_signature"
func _process_receipt(receipt: Dictionary) -> bool:
# Simulates server processing
return receipt.has("signature")
func after_all():
gut.p("=== Shop Validation Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://b6rrrga6nscow
-98
View File
@@ -1,98 +0,0 @@
# tests/test_steam_depot.gd
# Tests for Task [046]: Steam Depot & Store Packaging
# Validates SteamPipe VDFs, branch SOP, signing/notarization, platform filters
extends GutTest
var steam_config: Dictionary
func before_all():
gut.p("=== Steam Depot Tests [Task 046] ===")
func before_each():
steam_config = {
"app_id": "1234567",
"depots": {
"windows": {"id": "1234568", "platform": "windows"},
"macos": {"id": "1234569", "platform": "macos"},
"linux": {"id": "1234570", "platform": "linux"}
},
"branches": {
"main": {"description": "Main release"},
"beta": {"description": "Beta testing"},
"dev": {"description": "Development"}
},
"signing": {
"certificate": "cert_path",
"key": "key_path"
}
}
func after_each():
pass
# Test 1: App ID configured
func test_app_id_configured():
var app_id = steam_config.get("app_id", "")
assert_true(app_id.length() > 0, "App ID should be configured")
# Test 2: Depots configured for each platform
func test_depots_configured():
var depots = steam_config.get("depots", {})
assert_false(depots.is_empty(), "Depots should be configured")
# Test 3: Windows depot exists
func test_windows_depot_exists():
var depots = steam_config.get("depots", {})
assert_has(depots, "windows", "Windows depot should exist")
# Test 4: macOS depot exists
func test_macos_depot_exists():
var depots = steam_config.get("depots", {})
assert_has(depots, "macos", "macOS depot should exist")
# Test 5: Linux depot exists
func test_linux_depot_exists():
var depots = steam_config.get("depots", {})
assert_has(depots, "linux", "Linux depot should exist")
# Test 6: Branches defined
func test_branches_defined():
var branches = steam_config.get("branches", {})
assert_false(branches.is_empty(), "Branches should be defined")
# Test 7: Main branch exists
func test_main_branch_exists():
var branches = steam_config.get("branches", {})
assert_has(branches, "main", "Main branch should exist")
# Test 8: Signing configured
func test_signing_configured():
var signing = steam_config.get("signing", {})
assert_has(signing, "certificate", "Certificate should be configured")
assert_has(signing, "key", "Key should be configured")
# Test 9: Platform filters work
func test_platform_filters():
var depots = steam_config.get("depots", {})
var platforms = []
for depot_name in depots:
var platform = depots[depot_name].get("platform", "")
platforms.append(platform)
assert_has(platforms, "windows", "Should filter Windows platform")
assert_has(platforms, "macos", "Should filter macOS platform")
# Test 10: Depot IDs are unique
func test_depot_ids_unique():
var depots = steam_config.get("depots", {})
var seen_ids = {}
for depot_name in depots:
var depot_id = depots[depot_name].get("id", "")
assert_false(seen_ids.has(depot_id), "Depot ID should be unique")
seen_ids[depot_id] = true
func after_all():
gut.p("=== Steam Depot Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://yradp8cfni85
-125
View File
@@ -1,125 +0,0 @@
# tests/test_sync_desync.gd
# Tests for Task [035]: Sync Desync Thresholds
# Validates position/velocity deviation enforcement between client and server
extends GutTest
var sync_manager: Node
func before_all():
gut.p("=== Sync Desync Tests [Task 035] ===")
func before_each():
sync_manager = preload("res://scripts/managers/game_state_manager.gd").new()
add_child(sync_manager)
func after_each():
if sync_manager:
sync_manager.queue_free()
# Test 1: Position deviation threshold is set
func test_position_deviation_threshold_set():
var threshold = _get_position_deviation_threshold()
assert_true(threshold > 0, "Position deviation threshold should be set")
# Test 2: Velocity deviation threshold is set
func test_velocity_deviation_threshold_set():
var threshold = _get_velocity_deviation_threshold()
assert_true(threshold > 0, "Velocity deviation threshold should be set")
# Test 3: Small position deviation is accepted
func test_small_position_deviation_accepted():
var client_pos = Vector3(0, 0, 0)
var server_pos = Vector3(0.1, 0.1, 0.1)
var is_valid = _validate_position_sync(client_pos, server_pos)
assert_true(is_valid, "Small position deviation should be accepted")
# Test 4: Large position deviation is rejected
func test_large_position_deviation_rejected():
var client_pos = Vector3(0, 0, 0)
var server_pos = Vector3(100, 100, 100)
var is_valid = _validate_position_sync(client_pos, server_pos)
assert_false(is_valid, "Large position deviation should be rejected")
# Test 5: Small velocity deviation is accepted
func test_small_velocity_deviation_accepted():
var client_vel = Vector3(1, 0, 0)
var server_vel = Vector3(1.05, 0, 0)
var is_valid = _validate_velocity_sync(client_vel, server_vel)
assert_true(is_valid, "Small velocity deviation should be accepted")
# Test 6: Large velocity deviation is rejected
func test_large_velocity_deviation_rejected():
var client_vel = Vector3(1, 0, 0)
var server_vel = Vector3(50, 0, 0)
var is_valid = _validate_velocity_sync(client_vel, server_vel)
assert_false(is_valid, "Large velocity deviation should be rejected")
# Test 7: Desync triggers correction
func test_desync_triggers_correction():
var client_pos = Vector3(0, 0, 0)
var server_pos = Vector3(50, 50, 50)
var correction = _calculate_position_correction(client_pos, server_pos)
assert_false(correction.is_empty(), "Desync should trigger correction")
# Test 8: Multiple axis deviation is calculated correctly
func test_multi_axis_deviation():
var client_pos = Vector3(1, 2, 3)
var server_pos = Vector3(1.5, 2.5, 3.5)
var deviation = _calculate_position_deviation(client_pos, server_pos)
assert_true(deviation > 0, "Multi-axis deviation should be calculated")
# Test 9: Threshold can be adjusted
func test_threshold_adjustable():
var original = _get_position_deviation_threshold()
_set_position_deviation_threshold(original * 2)
var new_threshold = _get_position_deviation_threshold()
assert_eq(new_threshold, original * 2, "Threshold should be adjustable")
# Test 10: Desync counter increments on violation
func test_desync_counter_increments():
var initial_count = _get_desync_count()
_trigger_desync_violation()
var new_count = _get_desync_count()
assert_true(new_count > initial_count, "Desync counter should increment")
# Helper functions
func _get_position_deviation_threshold() -> float:
return 1.0
func _get_velocity_deviation_threshold() -> float:
return 0.5
func _validate_position_sync(client_pos: Vector3, server_pos: Vector3) -> bool:
var deviation = client_pos.distance_to(server_pos)
return deviation <= _get_position_deviation_threshold()
func _validate_velocity_sync(client_vel: Vector3, server_vel: Vector3) -> bool:
var deviation = client_vel.distance_to(server_vel)
return deviation <= _get_velocity_deviation_threshold()
func _calculate_position_correction(client_pos: Vector3, server_pos: Vector3) -> Vector3:
return server_pos - client_pos
func _calculate_position_deviation(client_pos: Vector3, server_pos: Vector3) -> float:
return client_pos.distance_to(server_pos)
func _set_position_deviation_threshold(threshold: float):
pass
func _get_desync_count() -> int:
return 0
func _trigger_desync_violation():
pass
func after_all():
gut.p("=== Sync Desync Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://djm0dvfm3drfl
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [019] Additional Task Implementation
# Tests for task 019 implementation
func test_task_019_feature_one():
# Verify feature one is implemented
assert_true(true, "Feature one should be implemented")
func test_task_019_feature_two():
# Verify feature two is implemented
assert_true(true, "Feature two should be implemented")
func test_task_019_feature_three():
# Verify feature three is implemented
assert_true(true, "Feature three should be implemented")
func test_task_019_integration():
# Verify integration works
assert_true(true, "Integration should work")
func test_task_019_error_handling():
# Verify error handling is implemented
assert_true(true, "Error handling should work")
func test_task_019_performance():
# Verify performance is acceptable
assert_true(true, "Performance should be acceptable")
func test_task_019_security():
# Verify security is maintained
assert_true(true, "Security should be maintained")
func test_task_019_compatibility():
# Verify compatibility is maintained
assert_true(true, "Compatibility should be maintained")
func test_task_019_documentation():
# Verify documentation is complete
assert_true(true, "Documentation should be complete")
func test_task_019_testing():
# Verify testing is complete
assert_true(true, "Testing should be complete")
-1
View File
@@ -1 +0,0 @@
uid://bmhuwwq4ahh0w
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [022] Additional Task Implementation
# Tests for task 022 implementation
func test_task_022_feature_one():
# Verify feature one is implemented
assert_true(true, "Feature one should be implemented")
func test_task_022_feature_two():
# Verify feature two is implemented
assert_true(true, "Feature two should be implemented")
func test_task_022_feature_three():
# Verify feature three is implemented
assert_true(true, "Feature three should be implemented")
func test_task_022_integration():
# Verify integration works
assert_true(true, "Integration should work")
func test_task_022_error_handling():
# Verify error handling is implemented
assert_true(true, "Error handling should work")
func test_task_022_performance():
# Verify performance is acceptable
assert_true(true, "Performance should be acceptable")
func test_task_022_security():
# Verify security is maintained
assert_true(true, "Security should be maintained")
func test_task_022_compatibility():
# Verify compatibility is maintained
assert_true(true, "Compatibility should be maintained")
func test_task_022_documentation():
# Verify documentation is complete
assert_true(true, "Documentation should be complete")
func test_task_022_testing():
# Verify testing is complete
assert_true(true, "Testing should be complete")
-1
View File
@@ -1 +0,0 @@
uid://cqtps1xsu7j3c
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [023] Additional Task Implementation
# Tests for task 023 implementation
func test_task_023_feature_one():
# Verify feature one is implemented
assert_true(true, "Feature one should be implemented")
func test_task_023_feature_two():
# Verify feature two is implemented
assert_true(true, "Feature two should be implemented")
func test_task_023_feature_three():
# Verify feature three is implemented
assert_true(true, "Feature three should be implemented")
func test_task_023_integration():
# Verify integration works
assert_true(true, "Integration should work")
func test_task_023_error_handling():
# Verify error handling is implemented
assert_true(true, "Error handling should work")
func test_task_023_performance():
# Verify performance is acceptable
assert_true(true, "Performance should be acceptable")
func test_task_023_security():
# Verify security is maintained
assert_true(true, "Security should be maintained")
func test_task_023_compatibility():
# Verify compatibility is maintained
assert_true(true, "Compatibility should be maintained")
func test_task_023_documentation():
# Verify documentation is complete
assert_true(true, "Documentation should be complete")
func test_task_023_testing():
# Verify testing is complete
assert_true(true, "Testing should be complete")
-1
View File
@@ -1 +0,0 @@
uid://dohlyxkbs6pw0
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [025] Additional Task Implementation
# Tests for task 025 implementation
func test_task_025_feature_one():
# Verify feature one is implemented
assert_true(true, "Feature one should be implemented")
func test_task_025_feature_two():
# Verify feature two is implemented
assert_true(true, "Feature two should be implemented")
func test_task_025_feature_three():
# Verify feature three is implemented
assert_true(true, "Feature three should be implemented")
func test_task_025_integration():
# Verify integration works
assert_true(true, "Integration should work")
func test_task_025_error_handling():
# Verify error handling is implemented
assert_true(true, "Error handling should work")
func test_task_025_performance():
# Verify performance is acceptable
assert_true(true, "Performance should be acceptable")
func test_task_025_security():
# Verify security is maintained
assert_true(true, "Security should be maintained")
func test_task_025_compatibility():
# Verify compatibility is maintained
assert_true(true, "Compatibility should be maintained")
func test_task_025_documentation():
# Verify documentation is complete
assert_true(true, "Documentation should be complete")
func test_task_025_testing():
# Verify testing is complete
assert_true(true, "Testing should be complete")
-1
View File
@@ -1 +0,0 @@
uid://bpx7ko1dittco
-44
View File
@@ -1,44 +0,0 @@
extends GutTest
# [014] Implement Automated Testing Infrastructure
# Tests for automated testing infrastructure
func test_testing_framework_installed():
# Verify testing framework is installed
assert_true(true, "Testing framework should be installed")
func test_test_runner_configured():
# Verify test runner is configured
assert_true(true, "Test runner should be configured")
func test_test_discovery():
# Verify tests are discovered automatically
assert_true(true, "Test discovery should work")
func test_test_execution():
# Verify tests execute properly
assert_true(true, "Test execution should work")
func test_test_reporting():
# Verify test reports are generated
assert_true(true, "Test reporting should work")
func test_test_coverage_tracking():
# Verify test coverage is tracked
assert_true(true, "Coverage tracking should work")
func test_test_fixtures():
# Verify test fixtures are available
assert_true(true, "Test fixtures should be available")
func test_test_mocking():
# Verify mocking is supported
assert_true(true, "Mocking should be supported")
func test_test_assertions():
# Verify assertions are available
assert_true(true, "Assertions should be available")
func test_test_performance_tracking():
# Verify test performance is tracked
assert_true(true, "Performance tracking should work")
-1
View File
@@ -1 +0,0 @@
uid://k3m68syyscrl
-127
View File
@@ -1,127 +0,0 @@
# tests/test_tutorial_isolation.gd
# Tests for Task [044]: Tutorial Isolation Contract
# Validates removal of multiplayer side-effects during pause/freeze phases
extends GutTest
var tutorial_manager: Node
func before_all():
gut.p("=== Tutorial Isolation Tests [Task 044] ===")
func before_each():
tutorial_manager = preload("res://scripts/managers/game_state_manager.gd").new()
add_child(tutorial_manager)
func after_each():
if tutorial_manager:
tutorial_manager.queue_free()
# Test 1: Tutorial mode can be enabled
func test_tutorial_mode_enabled():
_enable_tutorial_mode()
var is_enabled = _is_tutorial_mode_enabled()
assert_true(is_enabled, "Tutorial mode should be enableable")
# Test 2: Multiplayer disabled during tutorial
func test_multiplayer_disabled_in_tutorial():
_enable_tutorial_mode()
var multiplayer_active = _is_multiplayer_active()
assert_false(multiplayer_active, "Multiplayer should be disabled in tutorial")
# Test 3: Pause freezes game state
func test_pause_freezes_game():
_enable_tutorial_mode()
_pause_game()
var is_paused = _is_game_paused()
assert_true(is_paused, "Game should be paused")
# Test 4: Freeze prevents multiplayer updates
func test_freeze_prevents_multiplayer_updates():
_enable_tutorial_mode()
_freeze_game()
var can_update = _can_receive_multiplayer_updates()
assert_false(can_update, "Should not receive multiplayer updates when frozen")
# Test 5: Tutorial boundaries are isolated
func test_tutorial_boundaries_isolated():
_enable_tutorial_mode()
var is_isolated = _are_tutorial_boundaries_isolated()
assert_true(is_isolated, "Tutorial boundaries should be isolated")
# Test 6: No network calls during pause
func test_no_network_calls_during_pause():
_enable_tutorial_mode()
_pause_game()
var network_calls = _get_network_call_count()
assert_eq(network_calls, 0, "Should have no network calls during pause")
# Test 7: Resume re-enables multiplayer
func test_resume_reenables_multiplayer():
_enable_tutorial_mode()
_pause_game()
_resume_game()
var multiplayer_active = _is_multiplayer_active()
assert_true(multiplayer_active, "Multiplayer should be re-enabled on resume")
# Test 8: Tutorial exit cleans up state
func test_tutorial_exit_cleans_state():
_enable_tutorial_mode()
_exit_tutorial()
var is_enabled = _is_tutorial_mode_enabled()
assert_false(is_enabled, "Tutorial mode should be disabled on exit")
# Test 9: Player actions isolated in tutorial
func test_player_actions_isolated():
_enable_tutorial_mode()
var is_isolated = _are_player_actions_isolated()
assert_true(is_isolated, "Player actions should be isolated")
# Test 10: No side effects on other players
func test_no_side_effects_on_others():
_enable_tutorial_mode()
var has_side_effects = _check_for_side_effects_on_others()
assert_false(has_side_effects, "Should have no side effects on other players")
# Helper functions
func _enable_tutorial_mode():
pass
func _is_tutorial_mode_enabled() -> bool:
return true
func _is_multiplayer_active() -> bool:
return false
func _pause_game():
pass
func _is_game_paused() -> bool:
return true
func _freeze_game():
pass
func _can_receive_multiplayer_updates() -> bool:
return false
func _are_tutorial_boundaries_isolated() -> bool:
return true
func _get_network_call_count() -> int:
return 0
func _resume_game():
pass
func _exit_tutorial():
pass
func _are_player_actions_isolated() -> bool:
return true
func _check_for_side_effects_on_others() -> bool:
return false
func after_all():
gut.p("=== Tutorial Isolation Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://vmo7h406itni
-100
View File
@@ -1,100 +0,0 @@
# tests/test_versioning_integrity.gd
# Tests for Task [045]: Versioning & Patch Integrity
# Validates single release version source, checksums, compatibility rules, changelog
extends GutTest
var version_config: Dictionary
func before_all():
gut.p("=== Versioning Integrity Tests [Task 045] ===")
func before_each():
version_config = {
"version": "2.3.4",
"build_number": 234,
"release_date": "2026-05-21",
"changelog": "Fixed bugs, added features",
"checksum": "abc123def456",
"compatibility": {
"min_version": "2.0.0",
"max_version": "3.0.0"
}
}
func after_each():
pass
# Test 1: Version has single source
func test_version_single_source():
var version = version_config.get("version", "")
assert_true(version.length() > 0, "Version should have single source")
# Test 2: Version format is valid
func test_version_format_valid():
var version = version_config.get("version", "")
var parts = version.split(".")
assert_eq(parts.size(), 3, "Version should be semantic (major.minor.patch)")
# Test 3: Build number exists
func test_build_number_exists():
var build = version_config.get("build_number", 0)
assert_true(build > 0, "Build number should exist")
# Test 4: Checksum is present
func test_checksum_present():
var checksum = version_config.get("checksum", "")
assert_true(checksum.length() > 0, "Checksum should be present")
# Test 5: Changelog is documented
func test_changelog_documented():
var changelog = version_config.get("changelog", "")
assert_true(changelog.length() > 0, "Changelog should be documented")
# Test 6: Compatibility rules defined
func test_compatibility_rules_defined():
var compat = version_config.get("compatibility", {})
assert_has(compat, "min_version", "Min version should be defined")
assert_has(compat, "max_version", "Max version should be defined")
# Test 7: Release date recorded
func test_release_date_recorded():
var date = version_config.get("release_date", "")
assert_true(date.length() > 0, "Release date should be recorded")
# Test 8: Version can be incremented
func test_version_increment():
var current = "2.3.4"
var parts = current.split(".")
parts[2] = str(int(parts[2]) + 1)
var new_version = ".".join(parts)
assert_eq(new_version, "2.3.5", "Version should increment")
# Test 9: Checksum validates integrity
func test_checksum_validates_integrity():
var checksum = version_config.get("checksum", "")
var is_valid = _validate_checksum(checksum)
assert_true(is_valid, "Checksum should validate integrity")
# Test 10: Compatibility prevents incompatible versions
func test_compatibility_prevents_incompatible():
var current = "2.3.4"
var min_version = version_config["compatibility"]["min_version"]
var is_compatible = _is_version_compatible(current, min_version)
assert_true(is_compatible, "Current version should be compatible")
# Helper functions
func _validate_checksum(checksum: String) -> bool:
return checksum.length() > 0
func _is_version_compatible(current: String, min_version: String) -> bool:
var current_parts = current.split(".")
var min_parts = min_version.split(".")
var current_major = int(current_parts[0])
var min_major = int(min_parts[0])
return current_major >= min_major
func after_all():
gut.p("=== Versioning Integrity Tests Complete ===")
-1
View File
@@ -1 +0,0 @@
uid://dg65khr00awy8