Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 950b1969d8 | |||
| 286d0ce069 | |||
| 04e40f5b76 | |||
| 90ada5ace4 | |||
| ffbc3956ee | |||
| 6dae67fce4 | |||
| baeb2f6cfc | |||
| 7c3e55e904 | |||
| 4fd46af3c0 | |||
| c114dd5175 | |||
| b4ac0afda7 | |||
| 48b748849b | |||
| f1ef43cb38 | |||
| b879c7b684 | |||
| ce515365a7 | |||
| b86011a6c6 | |||
| 4bf9f7aee1 | |||
| d3322f14ad | |||
| adad53e94d | |||
| 14d46b8823 | |||
| acd927eef6 | |||
| af6870f6a9 |
@@ -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/`.
|
|
||||||
@@ -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
|
|
||||||
@@ -20,8 +20,8 @@ jobs:
|
|||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
run: |
|
run: |
|
||||||
git config --global credential.helper store
|
git config --global credential.helper store
|
||||||
echo "http://god:${{ secrets.TEKTON_RELEASE_TOKEN }}@git.klud.top" > ~/.git-credentials
|
echo "https://god:${{ secrets.TEKTON_RELEASE_TOKEN }}@git.klud.top" > ~/.git-credentials
|
||||||
git clone http://git.klud.top/danchie/tekton.git .
|
git clone https://git.klud.top/danchie/tekton.git .
|
||||||
git checkout $TAG_NAME
|
git checkout $TAG_NAME
|
||||||
|
|
||||||
- name: Setup Godot (Cached)
|
- name: Setup Godot (Cached)
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
- name: Create Gitea Release
|
- name: Create Gitea Release
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
API="http://git.klud.top/api/v1/repos/danchie/tekton/releases"
|
API="https://git.klud.top/api/v1/repos/danchie/tekton/releases"
|
||||||
TAG="$TAG_NAME"
|
TAG="$TAG_NAME"
|
||||||
echo "Checking existing release for $TAG..."
|
echo "Checking existing release for $TAG..."
|
||||||
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG" 2>/dev/null || echo "")
|
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG" 2>/dev/null || echo "")
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: multipart/form-data" \
|
-H "Content-Type: multipart/form-data" \
|
||||||
-F "attachment=@build/tekton_armageddon_windows_${TAG_NAME}.zip" \
|
-F "attachment=@build/tekton_armageddon_windows_${TAG_NAME}.zip" \
|
||||||
"http://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||||
echo "Windows uploaded"
|
echo "Windows uploaded"
|
||||||
|
|
||||||
- name: Upload Linux asset
|
- name: Upload Linux asset
|
||||||
@@ -126,7 +126,7 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: multipart/form-data" \
|
-H "Content-Type: multipart/form-data" \
|
||||||
-F "attachment=@build/tekton_armageddon_linux_${TAG_NAME}.zip" \
|
-F "attachment=@build/tekton_armageddon_linux_${TAG_NAME}.zip" \
|
||||||
"http://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||||
echo "Linux uploaded"
|
echo "Linux uploaded"
|
||||||
|
|
||||||
- name: Upload macOS asset
|
- name: Upload macOS asset
|
||||||
@@ -137,7 +137,7 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: multipart/form-data" \
|
-H "Content-Type: multipart/form-data" \
|
||||||
-F "attachment=@build/tekton_armageddon_macos_${TAG_NAME}.zip" \
|
-F "attachment=@build/tekton_armageddon_macos_${TAG_NAME}.zip" \
|
||||||
"http://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
|
||||||
echo "macOS uploaded"
|
echo "macOS uploaded"
|
||||||
else
|
else
|
||||||
echo "macOS asset not built, skipping"
|
echo "macOS asset not built, skipping"
|
||||||
@@ -150,5 +150,5 @@ jobs:
|
|||||||
-H "Authorization: token $GITEA_TOKEN" \
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"draft":false}' \
|
-d '{"draft":false}' \
|
||||||
"http://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID"
|
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID"
|
||||||
echo "Published: https://git.klud.top/danchie/tekton/releases/tag/$TAG_NAME"
|
echo "Published: https://git.klud.top/danchie/tekton/releases/tag/$TAG_NAME"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 http://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git .
|
git clone --depth 1 https://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git .
|
||||||
git config user.name "god"
|
git config user.name "god"
|
||||||
git config user.email "god@noreply.git.klud.top"
|
git config user.email "god@noreply.git.klud.top"
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
git init
|
git init
|
||||||
git config user.name "god"
|
git config user.name "god"
|
||||||
git config user.email "god@noreply.git.klud.top"
|
git config user.email "god@noreply.git.klud.top"
|
||||||
git remote add origin http://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git
|
git remote add origin https://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git
|
||||||
git checkout -b patches
|
git checkout -b patches
|
||||||
git add .
|
git add .
|
||||||
git commit -m "patch ${{ github.event.inputs.version }}"
|
git commit -m "patch ${{ github.event.inputs.version }}"
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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")
|
||||||
+25
-17
@@ -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
|
||||||
|
|||||||
+4
-4
@@ -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
|
||||||
|
|||||||
+26
-25
@@ -605,40 +605,41 @@ 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.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(1.0, 0.8, 0.0), # STAR = gold
|
||||||
1: mesh_path = "res://assets/models/tiles/tile_diamond.tres"
|
3: Color(0.0, 1.0, 0.4), # COIN = green
|
||||||
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 = 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 +1185,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):
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -87,7 +92,14 @@ func activate_client_side() -> void:
|
|||||||
|
|
||||||
# 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
|
||||||
@@ -132,7 +144,7 @@ func start_game_mode() -> void:
|
|||||||
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 +158,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
|
||||||
@@ -164,6 +176,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 +241,46 @@ 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
|
||||||
|
|
||||||
|
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,13 +298,16 @@ 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 color = colors.back() if colors.size() > 0 else -1
|
||||||
var face_match = (color != -1 and color == current_face)
|
var face_match = (color != -1 and color == current_face)
|
||||||
rpc("sync_candy_badge", pid, count, mult, face_match)
|
rpc("sync_candy_badge", pid, count, mult, face_match)
|
||||||
|
|
||||||
@@ -273,7 +317,7 @@ func _update_candy_badge(pid: int) -> void:
|
|||||||
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||||
if curr_pid == pid:
|
if curr_pid == pid:
|
||||||
if player.has_method("sync_candy_stack"):
|
if player.has_method("sync_candy_stack"):
|
||||||
player.rpc("sync_candy_stack", count, color)
|
player.rpc("sync_candy_stack", colors)
|
||||||
break
|
break
|
||||||
|
|
||||||
func get_multiplier(pid: int) -> float:
|
func get_multiplier(pid: int) -> float:
|
||||||
@@ -287,21 +331,28 @@ 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
|
||||||
|
if color == -1 or colors.size() == 0:
|
||||||
|
rpc("sync_delivery_result", pid, false, "No candy")
|
||||||
return false
|
return false
|
||||||
if color != current_face:
|
if color != current_face:
|
||||||
|
rpc("sync_delivery_result", pid, false, "Wrong color")
|
||||||
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:
|
||||||
@@ -330,7 +381,8 @@ func try_knock(attacker: int, target: int) -> bool:
|
|||||||
if player_knocks.get(attacker, 0) <= 0:
|
if player_knocks.get(attacker, 0) <= 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()
|
||||||
|
|
||||||
if target_candies == 0:
|
if target_candies == 0:
|
||||||
# Backfire: both lose a charge
|
# Backfire: both lose a charge
|
||||||
@@ -340,10 +392,13 @@ func try_knock(attacker: int, target: int) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
# Steal all candies
|
# Steal all candies
|
||||||
player_candies[attacker] = player_candies.get(attacker, 0) + target_candies
|
if not player_candy_colors.has(attacker):
|
||||||
|
player_candy_colors[attacker] = []
|
||||||
|
player_candy_colors[attacker].append_array(target_colors)
|
||||||
|
player_candy_colors[target] = []
|
||||||
|
|
||||||
|
player_candies[attacker] = player_candy_colors[attacker].size()
|
||||||
player_candies[target] = 0
|
player_candies[target] = 0
|
||||||
player_candy_color[target] = -1
|
|
||||||
player_candy_color[attacker] = player_candy_color.get(target, -1)
|
|
||||||
|
|
||||||
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
|
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
|
||||||
player_last_knocked_by[target] = attacker
|
player_last_knocked_by[target] = attacker
|
||||||
@@ -355,6 +410,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
|
||||||
@@ -491,6 +547,10 @@ func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool) -> vo
|
|||||||
if not active:
|
if not active:
|
||||||
activate_client_side()
|
activate_client_side()
|
||||||
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]
|
hud_stack_badge.text = "Stack: %d (x%.1f)" % [count, mult]
|
||||||
if hud_delivery_indicator:
|
if hud_delivery_indicator:
|
||||||
@@ -504,6 +564,39 @@ func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool) -> vo
|
|||||||
else:
|
else:
|
||||||
hud_delivery_indicator.text = ""
|
hud_delivery_indicator.text = ""
|
||||||
|
|
||||||
|
@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_remote", "unreliable")
|
@rpc("authority", "call_remote", "unreliable")
|
||||||
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:
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,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.
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func handle_unhandled_input(event):
|
|||||||
if event.is_action_pressed("action_knock_tekton"):
|
if event.is_action_pressed("action_knock_tekton"):
|
||||||
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)
|
# Find a target to knock (first adjacent player)
|
||||||
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")
|
||||||
@@ -127,11 +127,24 @@ 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_interact"):
|
||||||
|
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_deliver(pid)
|
||||||
|
else:
|
||||||
|
gm.rpc_id(1, "try_deliver", pid)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
elif event.is_action_pressed("action_grab_tekton"):
|
elif event.is_action_pressed("action_grab_tekton"):
|
||||||
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:
|
||||||
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()
|
||||||
|
# In Candy Survival, action_grab_tekton activates Ghost mode
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
gm.try_activate_ghost(pid)
|
gm.try_activate_ghost(pid)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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,10 @@ 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 <= 2:
|
||||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
pass # Delivery must be triggered by 'action_interact' input
|
||||||
if multiplayer.is_server():
|
|
||||||
gm.try_deliver(pid)
|
|
||||||
else:
|
|
||||||
gm.rpc_id(1, "try_deliver", pid)
|
|
||||||
|
|
||||||
rotate_towards_target(grid_position)
|
rotate_towards_target(grid_position)
|
||||||
|
|
||||||
@@ -221,7 +217,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 +232,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 +253,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 +270,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 +293,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 +316,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 +326,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 +343,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,10 +375,22 @@ 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)
|
||||||
|
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)
|
player.grab_item(player.current_position)
|
||||||
|
|
||||||
if not movement_queue.is_empty():
|
if not movement_queue.is_empty():
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
if not (LobbyManager.game_mode == "Candy Survival"):
|
||||||
print("[PlayerboardManager] Goal completed for player %s!" % player.name)
|
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,7 +800,8 @@ 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)
|
||||||
|
if not (LobbyManager.game_mode == "Candy Survival"):
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if main:
|
if main:
|
||||||
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,32 +114,69 @@ 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:
|
||||||
|
var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
|
||||||
|
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:
|
||||||
|
# Original 5x5 logic
|
||||||
|
var hidden_slots = [0, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24]
|
||||||
|
|
||||||
|
if slot_idx in hidden_slots:
|
||||||
slot.modulate = Color(1, 1, 1, 0)
|
slot.modulate = Color(1, 1, 1, 0)
|
||||||
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
else:
|
else:
|
||||||
@@ -148,8 +185,8 @@ func update_playerboard_ui():
|
|||||||
|
|
||||||
# Check if this is a center slot that should show a goal
|
# Check if this is a center slot that should show a goal
|
||||||
# BUT only show ghost goals for rows 2 & 3 (indices 11+)
|
# BUT only show ghost goals for rows 2 & 3 (indices 11+)
|
||||||
var center_index = center_slots.find(i)
|
var center_index = center_slots.find(slot_idx)
|
||||||
if center_index != -1 and center_index < goals.size() and i > 8:
|
if center_index != -1 and center_index < goals.size() and slot_idx > 8:
|
||||||
var goal_value = goals[center_index]
|
var goal_value = goals[center_index]
|
||||||
|
|
||||||
if item != -1:
|
if item != -1:
|
||||||
@@ -178,12 +215,12 @@ func update_playerboard_ui():
|
|||||||
9: slot.texture = item_tex[3]
|
9: slot.texture = item_tex[3]
|
||||||
10: slot.texture = item_tex[4]
|
10: slot.texture = item_tex[4]
|
||||||
# Non-center slots always full brightness (UNLESS HIDDEN)
|
# Non-center slots always full brightness (UNLESS HIDDEN)
|
||||||
if not (i in hidden_slots):
|
if not (slot_idx in hidden_slots):
|
||||||
slot.modulate = Color.WHITE
|
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
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ 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 mode = Engine.get_main_loop().root.get_node_or_null("LobbyManager").get_game_mode()
|
||||||
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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
extends SceneTree
|
|
||||||
func _init():
|
|
||||||
print("Testing candy_survival multiplayer")
|
|
||||||
quit()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://ddniv6k6aj2u
|
|
||||||
@@ -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
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://b7ihsm80fbyb5
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://ca04jq87bj3ap
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bmuuwupxittuw
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dogqk4wvnl3p2
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://drmn0stsse8pq
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://c8aivfh52w21p
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bqdrj2m5nyin2
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://ckuo3jtcsyir2
|
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://byv3h6b2mfdui
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bd5ttwoofe8r6
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://eqbuvr1v6v2u
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://fg7dgx7e5whf
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dgsqmebwlqyi7
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dwm8e8rbt52be
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bvk2ipwxml8le
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://c7i2ngo50d5ii
|
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://d17eyi5v0gas5
|
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://ffpghgfwpe44
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://demfttlaw20aw
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://xgevrhjp6kwk
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://d0elptre1yk4e
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://chcpm08e7818x
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://2ixdbfyqi2ji
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://duxekmy1fwsss
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://qn0r180kiqad
|
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://5rm3pdvkg5es
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://1yukbhghsnp4
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://b6rrrga6nscow
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://yradp8cfni85
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://djm0dvfm3drfl
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bmhuwwq4ahh0w
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://cqtps1xsu7j3c
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dohlyxkbs6pw0
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://bpx7ko1dittco
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://k3m68syyscrl
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://vmo7h406itni
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://dg65khr00awy8
|
|
||||||
Reference in New Issue
Block a user