diff --git a/.agents/workflows/do-task.md b/.agents/workflows/do-task.md new file mode 100644 index 0000000..1c9d703 --- /dev/null +++ b/.agents/workflows/do-task.md @@ -0,0 +1,164 @@ +--- +description: This document tells AI agents how to work on Tekton Dash tasks end-to-end. +--- + +# SKILLS.md — 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=36633be43b29803891cd000c6f6e5c5f + +### 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: "" + 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_.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_.gd` in `tests/`. +- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`. diff --git a/.clinerules/workflows/Do Task.md b/.clinerules/workflows/Do Task.md new file mode 100644 index 0000000..16a6212 --- /dev/null +++ b/.clinerules/workflows/Do Task.md @@ -0,0 +1,160 @@ +# SKILLS.md — 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=36633be43b29803891cd000c6f6e5c5f + +### 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: "" + 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_.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_.gd` in `tests/`. +- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`. diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index 7b0baeb..8b71938 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -1,3 +1,9 @@ +## [2.3.6] — 2026-05-22 +- Added new game mode: **Candy Cannon Survival** — dodge candy volleys from a giant cannon in the center of the arena! +- New 20×20 arena with a central Candy Cannon obstacle and three escalating phases: Open Arena, Route Pressure, and Survival. +- Candy Cannon Survival is now selectable from the lobby game mode list with its own dedicated arena. +- The arena now spawns collectible tiles (Hearts, Diamonds, Stars, Coins) with pattern-matching missions — complete goals while dodging candy! + ## [2.3.5] — 2026-05-22 - Refactored `lobby.gd` into modular helper classes (`LobbyChat`, `LobbyMainMenu`, `LobbyRoomList`, `LobbyRoom`) to reduce file size and improve maintainability. - Externalized Nakama connection config in `nakama_manager.gd` — server key, host, port, and scheme now read from environment variables with ProjectSettings fallback. diff --git a/SKILLS.md b/SKILLS.md new file mode 100644 index 0000000..16a6212 --- /dev/null +++ b/SKILLS.md @@ -0,0 +1,160 @@ +# SKILLS.md — 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=36633be43b29803891cd000c6f6e5c5f + +### 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: "" + 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_.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_.gd` in `tests/`. +- **GUT framework**: Tests use the Godot Unit Test (GUT) addon at `addons/gut/`. diff --git a/TEMP_REPORT.md b/TEMP_REPORT.md deleted file mode 100644 index d91886f..0000000 --- a/TEMP_REPORT.md +++ /dev/null @@ -1,86 +0,0 @@ -# [ ADT ] Report: Nakama Friend System & Steam Auth Implementation - -## Session Summary -Implemented comprehensive Nakama friend system with lobby invites, direct messaging, global chat, and Steam authentication fallback. Migrated all UI from dynamic node creation to scene-based architecture. - -## Completed Changes - -### Server (Nakama) -✅ Added `afterAuthenticateSteam` hook in `tekton_admin.js` to set Steam persona name as display name -✅ Added `rpcSendLobbyInvite` RPC for sending lobby invite notifications to friends - -### Authentication -✅ Implemented Steam login fallback to email-style auth in `auth_manager.gd` for dev/testing without publisher key -✅ Set Steam username as display name with default password matching username -✅ Set `is_guest = false` and `auth_mode = "steam"` on Steam login - -### Friend System -✅ Created `FriendManager.gd` autoload singleton for Nakama friend list, DM channels, and lobby invite notifications -✅ Registered FriendManager in `project.godot` autoload -✅ Added `nakama_id` field to LobbyManager player data -✅ Extended `request_room_info` RPC to include `nakama_id` for all players - -### Lobby UI -✅ Added optional `InviteBtn` reference in `lobby.gd` with safe node resolution -✅ Connected `invite_btn.pressed` to open invite friends dialog -✅ Connected FriendManager signals for lobby invite notifications -✅ Added + Friend buttons in player slots with friend status display -✅ Implemented invite friends popup using `invite_friends_dialog.tscn` scene -✅ Implemented lobby invite notification popup using `lobby_invite_popup.tscn` scene -✅ Added social panel modal opening from lobby - -### Social Panel -✅ Created `social_panel.tscn` with full UI layout (tabs, friend list, global chat, DM views) -✅ Created `social_panel.gd` with `@onready` references (no dynamic UI creation) -✅ Implemented friend list using `friend_row.tscn` instances -✅ Implemented global chat and DM tabs with UI nodes -✅ Handled DM history and message sending - -### UI Scenes (All .tscn based, no .new() calls) -✅ Created `friend_row.tscn` + `friend_row.gd` (friend list row with chat/accept/decline/remove buttons) -✅ Created `invite_friends_dialog.tscn` + `invite_friends_dialog.gd` (invite friends popup) -✅ Created `invite_row.tscn` + `invite_row.gd` (single invite row with invite button) -✅ Created `lobby_invite_popup.tscn` + `lobby_invite_popup.gd` (lobby invite notification dialog) - -### Profile Panel -✅ Fixed account type display to include Steam auth mode in `profile_panel.gd` - -### Version & Changelog -✅ Updated `CHANGELOG_DRAFT.md` with all changes (7 player-facing entries) -✅ Bumped version to 2.1.9 via `generate_version_json.py` -✅ Updated `assets/data/version.json` with new version and changelog - -## Files Modified -- `server/nakama/tekton_admin.js` -- `scripts/managers/auth_manager.gd` -- `scripts/managers/friend_manager.gd` -- `scripts/managers/lobby_manager.gd` -- `scripts/ui/profile_panel.gd` -- `scripts/ui/social_panel.gd` -- `scripts/ui/friend_row.gd` -- `scripts/ui/invite_friends_dialog.gd` -- `scripts/ui/invite_row.gd` -- `scripts/ui/lobby_invite_popup.gd` -- `scenes/lobby.gd` -- `project.godot` -- `CHANGELOG_DRAFT.md` -- `assets/data/version.json` - -## Files Created -- `scenes/ui/social_panel.tscn` -- `scenes/ui/friend_row.tscn` -- `scenes/ui/invite_friends_dialog.tscn` -- `scenes/ui/invite_row.tscn` -- `scenes/ui/lobby_invite_popup.tscn` - -## Version Bumped -**2.1.8 → 2.1.9** (2026-04-29) - -## Player-Facing Changelog -- Added friend system with friend list, direct messaging, and lobby invitations -- Improved Steam login support -- Added Social Panel with Friends, Global Chat, and Direct Message tabs -- Added ability to add/remove friends from lobby player slots -- Added Invite Friends button to lobby -- Added lobby invite notifications -- Fixed account type display in profile panel diff --git a/docs/gauntlet-technical-docs.html b/docs/gauntlet-technical-docs.html new file mode 100644 index 0000000..f95f1aa --- /dev/null +++ b/docs/gauntlet-technical-docs.html @@ -0,0 +1,1366 @@ + + + + + +Candy Cannon Survival — Technical Documentation + + + + + + + + +
+
+
+
+
+ +
+ + +
+
Technical Documentation
+

Candy Cannon Survival

+

Gauntlet Mode — Implementation blueprint mapping GDD mechanics to existing Tekton Dash systems

+
+
70%
Code Reuse
+
4
New Files
+
7
Modified Files
+
12
New Terms
+
10
Reused Terms
+
+
+ + + + + +
+
+

📖 Glossary

+

All terms used in Gauntlet mode — categorized by whether they're new, adapted, or already implemented in Tekton Dash.

+
+ +
+
New — unique to Gauntlet
+
Adapted — modified from existing mechanic
+
Existing — already in game, reused as-is
+
+ + + +
+ + + + +
+ +
+ + +
+
🍬
+
+

Sticky Cell New

+

A grid cell hit by the Candy Cannon that becomes impassable. Players stepping onto or pushed into a sticky cell are trapped. Remains until cleansed or round ends. Rendered as Layer 2 overlay (pink translucent mesh, ID 17).

+ TILE_STICKY = 17 → GridMap Layer 2 +
+
+ +
+
+
+

Telegraph New

+

1-second warning before cannon impact. Target cell glows pink/candy color with a shadow preview and charge-up sound. Uses temporary overlay tile (ID 18) on Layer 2, animated alpha 0→1 over 0.8s, then replaced by Sticky Cell on impact.

+ TILE_TELEGRAPH = 18 → rpc("sync_telegraph") +
+
+ +
+
💥
+
+

Candy Cannon New

+

Central NPC occupying a permanent 3×3 zone at arena center. Fires volleys of 5 candy shots every 5 seconds, creating sticky cells. Static body — cannot be grabbed, thrown, or interacted with. Not a Tekton — it's a dedicated hazard entity.

+ CandyCannonController → candy_cannon.tscn +
+
+ +
+
🎯
+
+

Volley New

+

A batch of 5 simultaneous cannon shots fired at different target cells. One volley fires every 5 seconds (36 total over 3 minutes = 180 impacts). Each shot in a volley has an independent impact size roll (1×1, 1×2, or 2×2).

+ _fire_volley() → cannon_interval = 5.0 +
+
+ +
+
📐
+
+

Impact Size New

+

The footprint of each cannon shot. Three sizes: 1×1 (single cell), 1×2 (two adjacent), 2×2 (four cells square). Distribution changes per phase — early favors 1×1, endgame favors 2×2.

+ phase_weights[phase_idx]["2x2"] +
+
+ +
+
🪤
+
+

Trapped New

+

Player state when standing on a sticky cell. Cannot move normally. Escape only via Cleanser power-up. Players can be trapped by stepping onto sticky, being pushed into sticky, or direct cannon hit. Trapped players keep their score but are out of active play.

+ trapped_players: Dict → rpc("sync_trapped") +
+
+ +
+
+
+

Cleanser New

+

Power-up earned by completing 2 missions. Allows 5 cells of movement through sticky candy, cleansing traversed cells back to walkable. Inventory limit: 1. Cannot activate while stunned. 0.3s activation delay.

+ player_cleansers[peer_id] → GoalsCycleManager signal +
+
+ +
+
💫
+
+

Clash New

+

When two players activate Smack simultaneously (within 0.5s) and are in range of each other. Both get stunned for 1.0s, no push occurs, both smack bars are consumed. Server-authoritative timestamp comparison.

+ clash detection → 0.5s window, server authority +
+
+ +
+
🔋
+
+

Charged State New

+

3-second window after Smack activation where the player model turns pink. If a target enters range during this window, the smack triggers. If no target is hit within 3s, energy is consumed with no effect.

+ smack_charged[player_id] → 3.0s window +
+
+ +
+
⚖️
+
+

Anti-Unfairness New

+

Targeting rules preventing the cannon from feeling random/cheap. No same-player twice in a row, 2×2 never directly on player, path validation ensures escape routes exist (except final 30s). Uses AStar pathfinding.

+ last_targeted_player_id → EnhancedGridMap.initialize_astar() +
+
+ +
+
🚧
+
+

Route Blocking New

+

Cannon targeting strategy (25% chance) that places sticky cells on pathfinding bottlenecks — narrow corridors between sticky regions. Forces players to reroute. Calculated using EnhancedGridMap neighbor analysis.

+ _get_route_blocking_target() → 25% weight +
+
+ +
+
🏟️
+
+

Gauntlet Arena New

+

20×20 cell arena with 391 playable cells (400 minus 3×3 NPC zone). Players spawn at outer edges/corners. Target: 80% sticky coverage by round end (313 cells), leaving ~78 safe cells.

+ ARENA_SIZE = 20 → gauntlet.tscn +
+
+ + +
+
👊
+
+

Smack Adapted

+

Gauntlet-specific melee push. Adapts existing try_push() from Attack Mode but replaces boost-meter gating with 8s auto-refill cooldown, adds 3s charged window, sticky landing trap, and clash detection. Push distance: 3 cells.

+ PlayerMovementManager.try_push() → smack_cooldowns +
+
+ +
+
⏱️
+
+

Phase Adapted

+

Three escalation phases in Gauntlet: Open Arena (0–60s), Route Pressure (60–120s), Survival Endgame (120–180s). Adapts StopNGoManager's Go/Stop phase pattern but uses time-elapsed triggers instead of cycle signals.

+ enum Phase { OPEN_ARENA, ROUTE_PRESSURE, SURVIVAL_ENDGAME } +
+
+ +
+
🤖
+
+

Bot AI — Cannon Avoidance Adapted

+

Extends BotStrategicPlanner with Gauntlet-specific logic: telegraph awareness, sticky path planning, safe-zone pathfinding. Adapts existing bot movement heuristics to factor in shrinking arena.

+ BotStrategicPlanner → new evaluate_gauntlet() +
+
+ + +
+
⚔️
+
+

Attack Mode Existing

+

Existing player state toggled via PowerUpManager when boost bar is full. In Gauntlet, not used directly — replaced by Smack mechanic. The push physics from try_push() are reused but the activation logic differs.

+ PowerUpManager.is_attack_mode → NOT used in Gauntlet +
+
+ +
+
😵
+
+

Stagger Existing

+

Existing 1.5s movement disable after being push-attacked. Gauntlet's Smack uses a shorter 1.0s stun, but the underlying apply_stagger() function is reused with a duration parameter.

+ PlayerMovementManager.apply_stagger(duration) +
+
+ +
+
🎯
+
+

Mission / Goals Existing

+

3×3 pattern-matching tile collection system. Reused as-is from GoalManager + GoalsCycleManager. In Gauntlet, completing every 2 missions also triggers Cleanser unlock (new hook on existing signal).

+ GoalManager → GoalsCycleManager.goal_count_updated +
+
+ +
+
🗂️
+
+

Layer 2 Overlay Existing

+

GridMap's Y=2 layer used for visual overlays (safe zones in Stop N Go, freeze in Freemode, highlights). Gauntlet uses it for Sticky Cell and Telegraph meshes. No conflict — modes are mutually exclusive.

+ GridMap.set_cell_item(Vector3i(x, 2, z), id) +
+
+ +
+
🫸
+
+

try_push() Existing

+

Player push mechanic in PlayerMovementManager. Pushes target 3 cells backward. Gauntlet's Smack wraps this with direction-based push, sticky landing detection, and clash rules.

+ PlayerMovementManager.try_push(target, direction) +
+
+ +
+
📳
+
+

Screen Shake Existing

+

Camera shake effect triggered via RPC. Used on cannon impact with "medium" intensity. Already implemented system-wide.

+ player.rpc("trigger_screen_shake", "medium") +
+
+ +
+
🎪
+
+

Tekton Projectile Existing

+

Arc-tween projectile from Tekton NPC. Candy Cannon reuses this exact visual pattern (spawn_projectile_rpc) — creating a mesh, arc-tweening position, then freeing on arrival.

+ tekton.gd → spawn_projectile_rpc(target, duration) +
+
+ +
+
📡
+
+

RPC Sync Pattern Existing

+

Server-authoritative state sync via @rpc("authority", "call_local", "reliable"). All Gauntlet state changes (sticky, phase, trap, cleanser) use this identical pattern.

+ @rpc("authority", "call_local", "reliable") +
+
+ +
+
+
+

Timed Match Existing

+

Global match timer from GoalsCycleManager. Gauntlet passes 180s duration. System handles countdown, HUD timer, and match-end trigger.

+ goals_cycle_manager.start_match(180.0) +
+
+ +
+
💎
+
+

SpecialTilesManager Existing

+

Handles power-up tiles, inventory, and effects. Gauntlet restricts certain powerups (like Stop N Go restrictions) and adds Cleanser as a new inventory slot via the existing signal/slot system.

+ SpecialTilesManager.inventory → mode-based restrictions +
+
+ +
+
+ + +
+
+

🏗️ Architecture

+

How GauntletManager slots into the existing manager tree, following the StopNGoManager pattern exactly.

+
+
+
+main.gd
+├── _init_managers() ← instantiate GauntletManager
+├── _setup_host_game() ← arena setup branch
+├── _start_game() ← start_game_mode() call
+
+GauntletManager NEW
+├── _setup_arena() ← 20×20 grid, center 3×3 NPC zone
+├── _setup_hud() ← mission label, cleanser indicator
+├── start_game_mode() ← start cannon timer, spawn tiles
+├── _process() ← cannon volley timer, phase escalation
+├── CandyCannonController NEW ← targeting, volley fire
+├── StickyCell system NEW ← Layer 2 overlay, trap logic
+├── Cleanser system NEW ← powerup via missions
+├── Smack system NEW ← modified push with charge/cooldown
+└── Win condition ← highest score at timer end
+
+
+
+ + +
+
+

♻️ Reuse Map

+

How each GDD feature maps to existing systems — showing what's reused vs what's new.

+
+
+ + + + + + + + + + + + + + + + + + + + + +
GDD FeatureExisting SystemReuseNew Work
Game Mode RegistrationGameMode.gd + LobbyManagerDirectAdd enum + strings
20×20 ArenaStopNGoManager._setup_arena()HeavyCustom layout, same API
Tile Collection / ScoringGoalsCycleManagerDirectReuse as-is
Mission SystemGoalManager + goals_cycle_managerDirectSame 3×3 pattern matching
Timed MatchGoalsCycleManager.start_match()DirectPass 180s duration
Player MovementPlayerMovementManagerDirectNo changes
Powerup SystemSpecialTilesManagerPartialCleanser = new type
Smack MechanicPlayerMovementManager.try_push()AdaptModified push rules
Candy Cannon NPCtekton.gd + TektonControllerPatternNew NPC, reuses projectile
Sticky CellsStopNGoManager safe zone overlayPatternNew tile type, same layer
Telegraph VFXVFXManager / animation.gdPatternNew animations, same system
HUDStopNGoManager._setup_hud()DirectMode-specific labels
Network SyncRPC patternsDirectSame patterns
Lobby SettingsLobbyManager signal/syncDirectGauntlet settings
Bot AIBotController + BotStrategicPlannerAdaptCannon avoidance strategy
+
+
+ + +
+
+

🌊 Phase Timeline

+

Three escalation phases that control cannon intensity and impact size distribution.

+
+
+
+
0:00 — 1:00
+

Open Arena

+
    +
  • Collect tiles, learn the mission
  • +
  • Slow candy pressure
  • +
  • 1×1 shots: 60%
  • +
  • 1×2 shots: 40%
  • +
  • 2×2 shots: 0%
  • +
  • ~60 impacts total
  • +
+
+
+
1:00 — 2:00
+

Route Pressure

+
    +
  • Candy shapes arena topology
  • +
  • Smack becomes dangerous
  • +
  • 1×1 shots: 30%
  • +
  • 1×2 shots: 55%
  • +
  • 2×2 shots: 15%
  • +
  • Cleanser used strategically
  • +
+
+
+
2:00 — 3:00
+

Survival Endgame

+
    +
  • ~80% arena is sticky
  • +
  • Safe zones limited, high tension
  • +
  • 1×1 shots: 15%
  • +
  • 1×2 shots: 55%
  • +
  • 2×2 shots: 30%
  • +
  • Aggressive route-blocking allowed
  • +
+
+
+
+ + +
+
+

⚙️ Core Systems

+

Deep-dive into the four new systems and how they integrate.

+
+ + +
+

🍬 Sticky Cell System

+
+ + + + + + + + + + +
FeatureImplementation
VisualLayer 2 overlay — transparent candy-pink mesh (ID 17)
Movement BlockPlayerMovementManager.simple_move_to() — add sticky check alongside wall check
Trap on StepGauntletManager._check_player_on_sticky() in _process()
Trap on PushPlayerMovementManager.try_push() — check landing cell
Cleanser BypassTemporary flag (like is_invisible wall bypass)
Network Syncmain.rpc("sync_grid_item", x, 2, z, 17)
+
+
+ + +
+

👊 Smack vs Attack Mode

+
+ + + + + + + + + + +
PropertyCurrent Attack ModeGauntlet Smack
Charge SourceBoost bar fills to 1008s auto-refill cooldown
ActivationToggle is_attack_mode3s charged window (pink model)
Push Distance3 cells backward3 cells in push direction
Stagger Duration1.5s apply_stagger()1.0s stun
Sticky LandingN/ATrapped on first sticky cell
Clash RuleN/ABoth stunned, no push, bars consumed
+
+
+ + +
+

✨ Cleanser Power-Up

+
+ + + + + + + + + + +
PropertyValue
Unlock TriggerGoalsCycleManager.goal_count_updated → count % 2 == 0
StorageGauntletManager.player_cleansers[peer_id] = 1
Effect5 cells movement through sticky — crossed cells become passable
Syncrpc("sync_cleanser_state", peer_id, count)
Clear Stickymain.rpc("sync_grid_item", x, 2, z, -1)
Inventory Limit1 per player
+
+
+ + +
+

🎯 Cannon Targeting Intelligence

+
+ + + + + + + + +
Roll %Target StrategyPurpose
60%Near a player (not same as last)Direct pressure
25%Route-blocking bottleneckCut escape paths
10%Random non-sticky areaSpread coverage
5%Previously sticky / chaosUnpredictability
+
+
+
+ + +
+
+

📁 File Changes

+
+ +

New Files

+
+
+
📜
+

gauntlet_manager.gd

Core mode logic, phases, sticky cells, cleanser, smack

+
+
+
📜
+

candy_cannon_controller.gd

Cannon targeting, volley fire, telegraph

+
+
+
🎬
+

gauntlet.tscn

3D arena environment scene

+
+
+
🎬
+

candy_cannon.tscn

Candy Cannon NPC (3×3, static)

+
+
+ +

Modified Files

+
+
+
✏️
+

game_mode.gd

Add GAUNTLET = 3 enum, string mappings

+
+
+
✏️
+

lobby_manager.gd

Mode list, gauntlet settings, area mapping

+
+
+
✏️
+

main.gd

Manager init, arena setup branch, start branch

+
+
+
✏️
+

player_movement_manager.gd

Sticky check in move + push

+
+
+
✏️
+

goals_cycle_manager.gd

Cleanser grant on 2nd goal

+
+
+
✏️
+

special_tiles_manager.gd

Gauntlet powerup restrictions

+
+
+
✏️
+

MeshLibrary .tres

Add TILE_STICKY (17) and TILE_TELEGRAPH (18)

+
+
+
+ + +
+
+

📡 Network Sync

+

All sync follows existing RPC patterns — no new networking paradigms needed.

+
+
+ + + + + + + + + + + +
DataSync MethodExisting Pattern
Sticky Cellsmain.rpc("sync_grid_item", x, 2, z, 17)Safe zone / freeze overlay
Telegraphrpc("sync_telegraph", targets_array)StopNGoManager.sync_phase()
Phase Changesrpc("sync_gauntlet_phase", idx, elapsed)StopNGoManager.sync_phase()
Trap Stateplayer.rpc("sync_trapped", true)player.rpc("sync_stop_freeze")
Cleanser Grantrpc("sync_cleanser", peer_id, count)goals_cycle_manager.sync_goal_count()
Smack Stateplayer.rpc("sync_smack_state", charged)player.rpc("sync_modulate")
Cannon NPCStatic scene — no movement sync needed
+
+
+ + +
+
+

📋 Implementation Priority

+
+
+
+
1

Game Mode Registration

game_mode.gd, lobby_manager.gd, main.gd

+
2

Arena Setup

gauntlet_manager._setup_arena(), 20×20 grid

+
3

Tile Spawning

StopNGoManager._spawn_mission_tiles() pattern

+
4

Cannon Timer + Volley

5s interval, 5 shots, 1×1 only

+
5

Sticky Cell System

Layer 2 overlay, movement block, trap detection

+
6

Telegraph VFX

Warning glow → impact transition

+
7

Impact Sizes

1×2 and 2×2 shapes, phase weights

+
8

Smack Mechanic

Modified push with cooldown/charge

+
9

Cleanser

Unlock tracking, sticky bypass

+
10

Targeting Intelligence

Player proximity, route blocking, anti-unfairness

+
11

Bot AI

Cannon avoidance, sticky path planning

+
12

Polish

VFX, SFX, HUD animations, 3D scene

+
+
+
+ + +
+
+

⚠️ Risk Assessment

+
+
+
+
🗂️
+
+

Layer 2 Conflict

+

GridMap Layer 2 used by freeze/safe overlays. Mitigated: Gauntlet mode is exclusive — no freeze/safe tiles exist.

+
+
+
+
📊
+
+

20×20 Grid Performance

+

400 cells + overlays. Mitigated: Existing 23×12 and 14×14 arenas work fine; 20×20 comparable.

+
+
+
+
🚫
+
+

Impossible Arenas

+

Cannon could seal all paths. Mitigated: AStar pathfinding check before each volley.

+
+
+
+
🔢
+
+

MeshLibrary ID Collision

+

IDs 17–18 might exist. Mitigated: Verify max ID in .tres before adding.

+
+
+
+
⏱️
+
+

Smack Clash Timing

+

Network latency affects clash detection. Mitigated: Server-authoritative timestamp, 0.5s window.

+
+
+
+
+ +
+ +
+
+ Tekton Dash — Candy Cannon Survival Technical Docs · Generated from gauntlet-technical-implementation.md +
+
+ + + + + + + diff --git a/docs/gauntlet-technical-implementation.md b/docs/gauntlet-technical-implementation.md new file mode 100644 index 0000000..84e5c9b --- /dev/null +++ b/docs/gauntlet-technical-implementation.md @@ -0,0 +1,391 @@ +# Candy Cannon Survival (Gauntlet) — Technical Implementation Plan + +## 1. Feasibility Summary + +**Verdict: Feasible.** The existing codebase provides ~70% of the infrastructure needed. The game mode architecture is modular — each mode has its own manager (`StopNGoManager`, `PortalModeManager`) that handles arena setup, HUD, phase logic, and win conditions. A new `GauntletManager` follows this identical pattern. + +### Reuse Breakdown + +| GDD Feature | Existing System | Reuse Level | New Work | +|---|---|---|---| +| Game Mode registration | `GameMode.gd` enum + `LobbyManager` | **Direct** | Add enum entry + strings | +| 20×20 Arena setup | `StopNGoManager._setup_arena()` pattern | **Heavy** | Custom layout, same GridMap API | +| Tile collection / scoring | `GoalsCycleManager` | **Direct** | Reuse goal completion + scoring | +| Mission system (goals) | `GoalManager` + `goals_cycle_manager.gd` | **Direct** | Same 3×3 pattern matching | +| Timed match (3 min) | `GoalsCycleManager.start_match()` | **Direct** | Pass 180s duration | +| Player movement | `PlayerMovementManager` | **Direct** | No changes | +| Powerup system | `SpecialTilesManager` | **Partial** | Cleanser is a new powerup type | +| Attack/Push mechanic | `PlayerMovementManager.try_push()` | **Adapt** | Smack = modified push with new rules | +| NPC (Candy Cannon) | `tekton.gd` + `TektonController` | **Pattern** | New NPC, reuses projectile/animation patterns | +| Sticky cells | `StopNGoManager` safe zone overlay (Layer 2) | **Pattern** | New tile type, same GridMap layer approach | +| Telegraph VFX | `VFXManager` / `animation.gd` | **Pattern** | New animations, same system | +| HUD | `StopNGoManager._setup_hud()` pattern | **Direct** | Mode-specific labels | +| Network sync | RPC patterns throughout codebase | **Direct** | Same `rpc()` / `sync_*` patterns | +| Lobby settings | `LobbyManager` signal/sync pattern | **Direct** | Add gauntlet-specific settings | +| Bot AI | `BotController` + `BotStrategicPlanner` | **Adapt** | New strategy for cannon avoidance | + +--- + +## 2. Architecture Overview + +``` +main.gd +├── _init_managers() ← Add GauntletManager instantiation (same as StopNGoManager pattern) +├── _setup_host_game() ← Add gauntlet arena setup branch +├── _start_game() ← Add gauntlet start_game_mode() call +│ +GauntletManager (NEW) +├── _setup_arena() ← 20×20 grid, center 3×3 NPC zone +├── _setup_hud() ← Mission label, cleanser indicator +├── start_game_mode() ← Start cannon timer, spawn tiles +├── _process() ← Cannon volley timer, phase escalation +├── CandyCannonController ← Targeting logic, volley fire +├── StickyCell system ← Layer 2 overlay, trap logic +├── Cleanser system ← New powerup unlocked via missions +├── Smack system ← Modified push with charge/cooldown +└── Win condition ← Highest score at timer end +``` + +--- + +## 3. File-by-File Implementation + +### 3.1 Game Mode Registration + +#### `scripts/game_mode.gd` +```gdscript +enum Mode { + FREEMODE = 0, + STOP_N_GO = 1, + TEKTON_DOORS = 2, + GAUNTLET = 3 # NEW +} + +# Add to from_string(), mode_to_string(), get_all_modes(), is_restricted() +``` + +#### `scripts/managers/lobby_manager.gd` +- Add `"Candy Cannon Survival"` to `available_game_modes` +- Add `_update_available_areas()` entry → `"Gauntlet Arena"` +- Add gauntlet-specific lobby settings (mirroring Stop N Go pattern): + - `gauntlet_round_duration: int = 180` + - `gauntlet_cannon_interval: int = 5` + - `gauntlet_volley_size: int = 5` + - Corresponding `set_gauntlet_*()`, `sync_gauntlet_*()` RPCs + - Corresponding signals + +--- + +### 3.2 Core Manager — `gauntlet_manager.gd` (NEW) + +**Location:** `scripts/managers/gauntlet_manager.gd` + +**Pattern source:** `StopNGoManager` + `PortalModeManager` + +``` +class_name GauntletManager +extends Node + +# Signals +signal phase_changed(phase_index: int) +signal cannon_fired(targets: Array) +signal player_trapped(player_id: int) +signal cleanser_granted(player_id: int) + +# Constants +const ARENA_SIZE = 20 +const NPC_SIZE = 3 +const NPC_CENTER = Vector2i(9, 9) # Center of 20×20 +const TILE_STICKY = 17 # New MeshLibrary item ID +const TILE_WALKABLE = 0 +const TILE_OBSTACLE = 4 + +# Phase timing +enum Phase { OPEN_ARENA, ROUTE_PRESSURE, SURVIVAL_ENDGAME } +var current_phase: Phase = Phase.OPEN_ARENA +var elapsed_time: float = 0.0 + +# Cannon state +var cannon_timer: float = 0.0 +var cannon_interval: float = 5.0 +var volley_size: int = 5 +var sticky_cells: Dictionary = {} # Vector2i → true +var last_targeted_player_id: int = -1 + +# Smack state (per-player) +var smack_cooldowns: Dictionary = {} # player_id → float (time remaining) +var smack_charged: Dictionary = {} # player_id → float (charge window remaining) + +# Cleanser tracking +var player_mission_completions: Dictionary = {} # player_id → int +var player_cleansers: Dictionary = {} # player_id → int (0 or 1) + +# Trapped players +var trapped_players: Dictionary = {} # player_id → true +``` + +#### Key methods (mapped to existing patterns): + +| Method | Pattern Source | Purpose | +|---|---|---| +| `_setup_arena()` | `StopNGoManager._setup_arena()` | 20×20 grid, center 3×3 NPC block, walkable floor | +| `_setup_hud()` | `StopNGoManager._setup_hud()` | Mission label, cleanser indicator | +| `start_game_mode()` | `StopNGoManager.start_game_mode()` | Initialize cannon, spawn tiles, activate HUD | +| `_process(delta)` | `StopNGoManager._process()` | Tick cannon timer, fire volleys, update phase | +| `_fire_volley()` | NEW (uses `tekton.gd` projectile pattern) | Select targets, telegraph, apply sticky | +| `_apply_sticky(pos)` | `StopNGoManager._spawn_dynamic_safe_zone()` (Layer 2 overlay) | Set GridMap Layer 2 to TILE_STICKY | +| `_check_player_trapped(player)` | `StopNGoManager._is_in_safe_zone()` (inverted) | Check if player is on sticky cell | +| `check_win_condition()` | `StopNGoManager.check_win_condition()` | Highest score at match end | +| `sync_phase()` RPC | `StopNGoManager.sync_phase()` | Broadcast phase to clients | +| `sync_sticky_cells()` RPC | `main.rpc("sync_grid_item")` | Sync sticky cell state | + +--- + +### 3.3 Candy Cannon NPC — `candy_cannon_controller.gd` (NEW) + +**Location:** `scripts/controllers/candy_cannon_controller.gd` + +**Pattern source:** `TektonController` + `tekton.gd` projectile system + +``` +class_name CandyCannonController +extends Node + +var gauntlet_manager: GauntletManager +var npc_center: Vector2i +var gridmap: Node + +# Targeting weights per phase +var phase_weights: Array = [ + # Phase 0 (Open Arena): 1×1=60%, 1×2=40%, 2×2=0% + {"1x1": 0.6, "1x2": 0.4, "2x2": 0.0}, + # Phase 1 (Route Pressure): 1×1=30%, 1×2=55%, 2×2=15% + {"1x1": 0.3, "1x2": 0.55, "2x2": 0.15}, + # Phase 2 (Survival): 1×1=15%, 1×2=55%, 2×2=30% + {"1x1": 0.15, "1x2": 0.55, "2x2": 0.30} +] +``` + +**Targeting logic** reuses the `_is_position_valid()` and `get_neighbors()` from `EnhancedGridMap`, and `get_nodes_in_group("Players")` for player-proximity targeting. + +**Projectile visuals** reuse `tekton.gd`'s `spawn_projectile_rpc()` pattern (arc tween from cannon → target cell). + +--- + +### 3.4 Sticky Cell System + +**Approach:** Use GridMap Layer 2 (same as `StopNGoManager` safe zone overlay and `SpecialTilesManager` freeze overlay). + +**New MeshLibrary item:** `TILE_STICKY = 17` — Pink/candy-colored semi-transparent panel (same approach as TILE_SAFE = 2). + +| Feature | Implementation | +|---|---| +| Visual | Layer 2 overlay with transparent candy-pink mesh | +| Movement block | `PlayerMovementManager.simple_move_to()` — add sticky check alongside wall check | +| Trap on step | `GauntletManager._check_player_on_sticky()` in `_process()` | +| Trap on push | `PlayerMovementManager.try_push()` — check landing cell for sticky | +| Cleanser pass-through | Similar to `is_invisible` wall bypass — temporary flag | + +**Network sync:** Use existing `main.rpc("sync_grid_item", x, 2, z, TILE_STICKY)` — identical to how safe zones and freeze overlays sync. + +--- + +### 3.5 Telegraph System + +**Pattern source:** `StopNGoManager`'s `sync_all_safe_zones_vfx()` + `_animate_safe_zone_appear()` + +1. Server selects target cells +2. `rpc("sync_telegraph", targets)` — all clients show pink glow +3. 1-second delay (Timer) +4. `rpc("sync_impact", targets)` — apply sticky, VFX, screen shake + +**Visual approach:** +- Reuse Layer 2 overlay with a temporary "warning" tile ID (e.g., `TILE_TELEGRAPH = 18`) +- Animate alpha 0 → 1 over 0.8s (same `_animate_safe_zone_appear()` tween pattern) +- On impact: replace with `TILE_STICKY`, play `screen_shake_manager` via `player.rpc("trigger_screen_shake", "medium")` + +--- + +### 3.6 Smack Mechanic + +**Pattern source:** `PowerUpManager.use_special_effect()` + `PlayerMovementManager.try_push()` + +The smack mechanic is a reskin of the existing Attack Mode push, with modifications: + +| Property | Current Attack Mode | Gauntlet Smack | +|---|---|---| +| Charge source | `PowerUpManager.current_boost >= 100` | 8s cooldown timer (auto-refill) | +| Activation | Toggle `is_attack_mode` | 3s charged window (pink model) | +| Push distance | 3 cells backward (X=-1) | 3 cells in push direction | +| Stagger | 1.5s `apply_stagger()` | 1.0s stun | +| Sticky landing | N/A | Trapped on first sticky cell in path | +| Clash | N/A | Both stunned, no push, bars consumed | + +**Implementation in GauntletManager:** +- New per-player smack state (cooldown, charged flag) +- Override or extend `PlayerMovementManager.try_push()` behavior when in gauntlet mode +- Sticky landing check: iterate push path, stop at first sticky cell → call `trap_player()` +- Clash detection: if two players activate smack within 0.5s of each other and are in range + +--- + +### 3.7 Cleanser Power-Up + +**Pattern source:** `SpecialTilesManager.inventory` system + +| Property | Implementation | +|---|---| +| Unlock trigger | `GoalsCycleManager.goal_count_updated` signal — grant when `count % 2 == 0` | +| Storage | `GauntletManager.player_cleansers[peer_id] = 1` | +| Activation | New input action or existing powerup key | +| Effect | For 5 cells of movement, ignore sticky checks + clear sticky overlay on traversed cells | +| Sync | `rpc("sync_cleanser_state", peer_id, count)` | +| Clear sticky | `main.rpc("sync_grid_item", x, 2, z, -1)` — same as safe zone clear | + +--- + +### 3.8 Candy Cannon NPC Scene — `candy_cannon.tscn` (NEW) + +**Pattern source:** `tekton.tscn` + `static_tekton_stand.tscn` + +- 3×3 footprint centered at `(9, 9)` in 20×20 grid +- Static body (non-movable, non-interactable) +- Animated mesh (cannon rotation, firing animation) +- No grab/throw/knock interactions (like `is_static_turret = true`) + +--- + +### 3.9 Arena Scene — `gauntlet.tscn` (NEW) or `gauntlet.scn` + +**Location:** `scenes/arena/gauntlet.tscn` + +**Pattern source:** `scenes/arena/freemode.tscn`, `scenes/arena/stop_n_go.scn` + +- 3D environment for the gauntlet arena +- Referenced in `main.gd._apply_arena_background()` under `"Gauntlet Arena"` match case + +--- + +### 3.10 Integration Points in `main.gd` + +Following the exact pattern of StopNGoManager / PortalModeManager: + +```gdscript +# _init_managers() — Add after portal_mode_manager block: +if LobbyManager.game_mode == "Candy Cannon Survival": + gauntlet_manager = load("res://scripts/managers/gauntlet_manager.gd").new() + gauntlet_manager.name = "GauntletManager" + add_child(gauntlet_manager) + gauntlet_manager.initialize(self, $EnhancedGridMap) + +# _setup_host_game() — Add arena setup branch: +elif LobbyManager.game_mode == "Candy Cannon Survival" and gauntlet_manager: + gauntlet_manager._setup_arena() + +# _start_game() — Add game mode start: +elif LobbyManager.game_mode == "Candy Cannon Survival": + if gauntlet_manager: + gauntlet_manager.start_game_mode() + if goals_cycle_manager: + var match_duration = LobbyManager.get_match_duration() + goals_cycle_manager.start_match(float(match_duration)) +``` + +--- + +## 4. New Files Summary + +| File | Type | Purpose | +|---|---|---| +| `scripts/managers/gauntlet_manager.gd` | Script | Core mode logic, phases, sticky cells, cleanser, smack | +| `scripts/controllers/candy_cannon_controller.gd` | Script | Cannon targeting, volley fire, telegraph | +| `scenes/arena/gauntlet.tscn` | Scene | 3D arena environment | +| `scenes/candy_cannon.tscn` | Scene | Candy Cannon NPC (3×3, static) | + +## 5. Modified Files Summary + +| File | Changes | +|---|---| +| `scripts/game_mode.gd` | Add `GAUNTLET = 3` enum, string mappings | +| `scripts/managers/lobby_manager.gd` | Add mode to available list, gauntlet settings, area mapping | +| `scenes/main.gd` | Add gauntlet_manager init, arena setup branch, start branch | +| `scripts/managers/player_movement_manager.gd` | Add sticky cell check in `simple_move_to()`, sticky landing in push | +| `scripts/managers/goals_cycle_manager.gd` | Cleanser grant on every 2nd goal completion (gauntlet mode only) | +| `scripts/managers/special_tiles_manager.gd` | Restrict certain powerups in gauntlet mode (like Stop N Go restrictions) | +| MeshLibrary `.tres` | Add TILE_STICKY (17) and TILE_TELEGRAPH (18) mesh items | + +## 6. Anti-Unfairness Implementation + +```gdscript +# In CandyCannonController._select_targets(): +func _select_targets(count: int) -> Array[Vector2i]: + var targets: Array[Vector2i] = [] + var players = get_tree().get_nodes_in_group("Players") + + for i in range(count): + var roll = randf() + var target: Vector2i + + if roll < 0.60: + # Near a player (not same as last targeted) + target = _get_near_player_target(players) + elif roll < 0.85: + # Route-blocking (pathfinding bottleneck) + target = _get_route_blocking_target() + elif roll < 0.95: + # Random non-sticky + target = _get_random_non_sticky_target() + else: + # Chaos (anywhere) + target = _get_random_target() + + targets.append(target) + + return targets + +# Anti-unfairness rules: +# 1. last_targeted_player_id tracking prevents same-player targeting +# 2. 2×2 shots never placed directly ON a player (offset by 1) +# 3. Path validation: ensure at least one path from each active player +# to a non-sticky region (using EnhancedGridMap.initialize_astar()) +# 4. Exception: final 30s allows aggressive blocking +``` + +## 7. Network Considerations + +All sync follows existing patterns: + +| Data | Sync Method | Existing Pattern | +|---|---|---| +| Sticky cells | `main.rpc("sync_grid_item", x, 2, z, 17)` | Safe zone / freeze overlay | +| Telegraph | `rpc("sync_telegraph", targets_array)` | `StopNGoManager.sync_phase()` | +| Phase changes | `rpc("sync_gauntlet_phase", phase_idx, elapsed)` | `StopNGoManager.sync_phase()` | +| Trap state | `player.rpc("sync_trapped", true)` | `player.rpc("sync_stop_freeze", true)` | +| Cleanser grant | `rpc("sync_cleanser", peer_id, count)` | `goals_cycle_manager.sync_goal_count()` | +| Smack state | `player.rpc("sync_smack_state", charged)` | `player.rpc("sync_modulate", color)` | +| Cannon NPC | Static scene, no movement sync needed | `static_tekton_stand.tscn` | + +## 8. Implementation Priority (Recommended Order) + +1. **Game Mode Registration** — `game_mode.gd`, `lobby_manager.gd`, `main.gd` branches +2. **Arena Setup** — `gauntlet_manager._setup_arena()`, 20×20 grid, NPC zone block +3. **Tile Spawning** — Reuse `StopNGoManager._spawn_mission_tiles()` pattern +4. **Cannon Timer + Volley** — Basic 5s interval, 5 shots, 1×1 only (no sizes yet) +5. **Sticky Cell System** — Layer 2 overlay, movement blocking, trap detection +6. **Telegraph VFX** — Warning glow → impact +7. **Impact Sizes** — 1×2 and 2×2 shapes, phase-based weights +8. **Smack Mechanic** — Modified push with cooldown/charge +9. **Cleanser** — Unlock tracking, activated movement through sticky +10. **Targeting Intelligence** — Player proximity, route blocking, anti-unfairness +11. **Bot AI** — Cannon avoidance, sticky path planning +12. **Polish** — VFX, SFX, HUD animations, 3D arena scene + +## 9. Risk Assessment + +| Risk | Mitigation | +|---|---| +| GridMap Layer 2 conflict with existing freeze/safe overlays | Gauntlet mode is exclusive — no freeze/safe tiles in this mode | +| 20×20 grid performance (400 cells + overlays) | Existing 23×12 (Stop N Go) and 14×14 (Tekton Doors) work fine; 20×20 is comparable | +| Cannon targeting causing impossible arenas | Anti-unfairness pathfinding check via `EnhancedGridMap.initialize_astar()` | +| New MeshLibrary items (17, 18) colliding with existing IDs | Verify current max ID in `.tres` before adding | +| Smack clash detection timing | Use server-authoritative timestamp comparison (< 0.5s window) | diff --git a/docs/gauntlet.md b/docs/gauntlet.md new file mode 100644 index 0000000..e8e0f67 --- /dev/null +++ b/docs/gauntlet.md @@ -0,0 +1,234 @@ +# Candy Cannon Survival (Gauntlet) — Game Design Document + +## 1. High Concept + +A time-limited survival-collection arena mode where players move across a grid of colored tiles, collect mission-specific tiles for points, and survive a central NPC that fires candy cannon shots across the arena. Candy impacts turn cells sticky and unpassable. Players must keep scoring while the arena slowly becomes dangerous. The stage pressure escalates over 3 minutes. By the end of the round, around 80% of the playable arena is sticky candy, leaving only 20% safe space. + +## 2. Core Fantasy + +Players feel like they are racing through a colorful board-game arena while the floor is being eaten by candy chaos. The round starts open and playful, then becomes tighter, more tactical, and more desperate as safe routes disappear. + +## 3. Game Mode Summary + +| Property | Value | +|---|---| +| Mode Name | Candy Cannon Survival | +| Round Duration | 3 minutes | +| Recommended Players | 4–8 | +| Arena Type | Cell/grid-based arena | +| Primary Goal | Score points by collecting mission-required colored tiles | +| Secondary Goal | Survive until the timer ends | +| Main Hazard | Central NPC candy cannon creates sticky unpassable cells | +| Player Interaction | Smack/sabotage other players into danger | +| Comeback Tool | Cleanser power-up after completing 2 missions | + +## 4. Recommended Arena Setup + +| Metric | Value | +|---|---| +| Arena Size | 20×20 cells | +| Total Cells | 400 | +| Central NPC footprint | 3×3 = 9 cells | +| Playable Cells | 391 | +| Target sticky coverage (3 min) | 80% = 313 sticky cells | +| Remaining safe cells | 78 | + +## 5. Arena Layout + +- Grid: 20×20 square grid +- Each cell contains a colored tile +- Center 3×3 area occupied by Candy Cannon NPC (permanently blocked) +- Player spawns near outer edges / corners +- Mission tiles distributed across arena + +**Spawn positions:** +- 4 players: top-left, top-right, bottom-left, bottom-right outer quadrants +- 6–8 players: side-edge and corner-adjacent spawns + +## 6. Candy Cannon NPC + +- Position: Exact center, 3×3 cell area +- Fires candy cannon shots at cells +- Impacted cells become **sticky** + +### Sticky Cell Rules + +- Cannot be passed through +- Cannot be collected from +- Traps players who step onto it +- Traps players pushed into it +- Remains sticky until cleansed or round ends + +### Player Hit Rule + +- Direct hit → player is **trapped** (not eliminated) +- If they have Cleanser, they may escape +- Without Cleanser, they are out of active play + +## 7. Candy Cannon Timing & Math + +- Cannon fires **one volley every 5 seconds** +- Each volley: **5 shots** at different target cells +- 180 seconds / 5 = **36 volleys** +- 36 × 5 = **180 total impacts** + +### Telegraph + +- **1 second** telegraph before impact (pink glow, syrup preview, charge sound, final flash) + +### Impact Size Mix (full round) + +| Size | Chance | +|---|---| +| 1×1 (1 cell) | 35% | +| 1×2 (2 cells) | 50% | +| 2×2 (4 cells) | 15% | + +### Phase-Based Volley Pattern + +| Time | Shots | Mix | Purpose | +|---|---|---|---| +| 0:00–1:00 | 5/volley | Mostly 1×1, 1×2 | Slow arena pressure | +| 1:00–2:00 | 5/volley | Mostly 1×2, some 2×2 | Route cutting | +| 2:00–3:00 | 5/volley | More 2×2 | Strong endgame pressure | + +| Phase | 1×1 | 1×2 | 2×2 | +|---|---|---|---| +| 0:00–1:00 | 60% | 40% | 0% | +| 1:00–2:00 | 30% | 55% | 15% | +| 2:00–3:00 | 15% | 55% | 30% | + +## 8. Cannon Targeting Logic — Controlled Chaos + +| Chance | Target | +|---|---| +| 60% | Non-sticky area near a player | +| 25% | Non-sticky area blocking common routes | +| 10% | Random non-sticky area | +| 5% | Previously sticky / semi-blocked (chaos) | + +### Anti-Unfairness Rules + +- Do not target same player twice in a row +- Do not place 2×2 directly on a player without warning +- Do not fully seal all exits from a pocket (except final 30s) +- Maintain at least one path from each active player to a safe region when possible + +## 9. Telegraph System + +- **1 second** warning before impact +- Target cell glows pink/candy color +- Shadow or syrup splash preview +- Impact sound charges up +- Final 0.2s flash before landing + +## 10. Sticky Trap Rules + +- Step onto sticky cell → **trapped** +- Cannot move normally +- Escape only via Cleanser +- Direct cannon hit → trapped (Cleanser may save) +- Pushed into sticky → trapped (pusher rewarded indirectly) + +## 11. Cleanser Power-Up + +| Property | Value | +|---|---| +| Unlock | Every 2 completed missions | +| Effect | Move up to 5 cells through sticky candy | +| Cleanse | Crossed sticky cells become passable again | +| Ends | After 5 cells or when stopping on a safe cell | +| Inventory limit | 1 | +| Cannot activate | While stunned | +| Activation delay | ~0.3 seconds | + +## 12. Smack / Sabotage Mechanic + +| Property | Value | +|---|---| +| Smack energy refill | 8 seconds (full bar) | +| Smack activation | Model turns pink for 3 seconds (charged state) | +| Trigger | Target enters range during charged state | +| Cancel | No target within 3s → energy consumed, no effect | +| Effect on hit | Target pushed **3 cells** away, stunned after landing | +| Sticky landing | Target trapped on first sticky cell touched | + +### Smack Clash Rule + +- Two players activate smack simultaneously, both in range → **both smacked, both stunned, no push, both bars consumed** +- Stun duration for clash: **1.0 second** + +## 13. Win Condition + +- Highest score at end of 3 minutes wins +- Trapped/eliminated players keep earned score +- Surviving players receive survival bonus + +## 14. Flow Phases + +### Phase 1 — Open Arena (0:00–1:00) + +- Collect tiles, understand mission +- Slow candy pressure (1×1, 1×2 mostly) + +### Phase 2 — Route Pressure (1:00–2:00) + +- Candy shapes arena +- Smack becomes dangerous +- Cleanser used strategically +- 1×2 and 2×2 shots common, targeting near players/paths + +### Phase 3 — Survival Endgame (2:00–3:00) + +- Arena mostly sticky, safe zones limited +- Secure final points or focus survival +- 2×2 shots frequent, rare 1×3 line shots possible +- Aggressive route-blocking allowed + +## 15. Balance Recommendations (Starting Values) + +| System | Value | +|---|---| +| Arena size | 20×20 | +| NPC size | 3×3 center | +| Round duration | 180 seconds | +| Cannon interval | 5 seconds | +| Telegraph duration | 1 second | +| Target sticky coverage | 80% playable cells | +| Smack charge window | 3 seconds | +| Smack push distance | 3 cells | +| Smack energy refill | 8 seconds | +| Stun duration | 1.0 second | +| Cleanser unlock | Every 2 completed missions | +| Cleanser movement limit | 5 cells | +| Cleanser inventory limit | 1 | + +## 16. Risk Areas & Solutions + +| Risk | Solution | +|---|---| +| Arena impossible too early | Reduce early 2×2 shots; preserve escape paths | +| Smack too strong | Increase refill to 10s; reduce push to 2 cells; brief invuln after smack | +| Cleanser too weak | Increase movement to 6 cells; clear adjacent cells | +| Cannon feels random/unfair | Readable telegraphs; avoid targeting same player; avoid full enclosure before final 30s | + +## 17. Prototype Setup + +- Arena: 20×20 +- NPC: 3×3 center +- Players: 4 +- Round time: 3 minutes +- Cannon interval: 5 seconds +- Volley size: 5 shots +- Telegraph: 1 second +- Impact pattern as defined in Phase-Based table above +- Smack refill: 8s / window: 3s / push: 3 cells / stun: 1s +- Cleanser: every 2 missions / 5 cells movement / hold 1 + +## 18. Design Notes + +- Mode is about **choosing** when to score, sabotage, save Cleanser, or abandon a mission +- Cannon creates **pressure**, not instant failure +- Smack creates **player-driven danger** +- Cleanser gives **recovery** and rewards brave play near danger +- Emotional curve: playful collection → tactical routing → chaotic survival \ No newline at end of file diff --git a/export_presets.cfg b/export_presets.cfg index 4d99304..5cfc6cd 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -8,7 +8,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.5.exe" +export_path="build/tekton_armageddon_v2.3.6.exe" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -42,8 +42,8 @@ application/modify_resources=false application/icon="" application/console_wrapper_icon="" application/icon_interpolation=4 -application/file_version="2.3.5" -application/product_version="2.3.5" +application/file_version="2.3.6" +application/product_version="2.3.6" application/company_name="DanchieGo" application/product_name="Tekton Armageddon" application/file_description="" @@ -80,7 +80,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton-dash-armageddon-v.2.3.5.apk" +export_path="build/tekton-dash-armageddon-v.2.3.6.apk" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -111,7 +111,7 @@ architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false version/code=3 -version/name="2.3.5" +version/name="2.3.6" package/unique_name="com.danchiego.$genname" package/name="Tekton Dash Armageddon" package/signed=true @@ -306,7 +306,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.5.zip" +export_path="build/tekton_armageddon_v2.3.6.zip" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 @@ -565,8 +565,8 @@ codesign/digest_algorithm=1 codesign/identity_type=0 application/modify_resources=false application/console_wrapper_icon="" -application/file_version="2.3.5" -application/product_version="2.3.5" +application/file_version="2.3.6" +application/product_version="2.3.6" application/company_name="DanchieGo" application/product_name="Tekton Armageddon" application/file_description="" @@ -582,7 +582,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="build/tekton_armageddon_v2.3.5.x86_64" +export_path="build/tekton_armageddon_v2.3.6.x86_64" patches=PackedStringArray() patch_delta_encoding=false patch_delta_compression_level_zstd=19 diff --git a/project.godot b/project.godot index 1eaa9dc..2b63bf9 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="Tekton Dash Armageddon" -config/version="2.3.5" +config/version="2.3.6" run/main_scene="res://scenes/ui/boot_screen.tscn" config/features=PackedStringArray("4.6", "Forward Plus") boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) diff --git a/run_tests.cmd b/run_tests.cmd new file mode 100644 index 0000000..6f11c14 --- /dev/null +++ b/run_tests.cmd @@ -0,0 +1,64 @@ +@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 diff --git a/scenes/main.gd b/scenes/main.gd index 203163f..8ed371f 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -16,6 +16,7 @@ var portal_mode_winner_id: int = -1 var is_match_ended: bool = false var obstacle_manager var portal_mode_manager +var gauntlet_manager var vfx_manager # Minimal local state @@ -147,6 +148,9 @@ func _apply_arena_background(): _hide_ground_tiles() "Tekton Doors Arena": texture_path = "res://assets/graphics/level_bg/placeholder_tekton_doors.jpg" + "Gauntlet Arena": + texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" + _hide_ground_tiles() "Classic", _: texture_path = "res://assets/graphics/level_bg/placeholder_classic.jpg" @@ -247,6 +251,13 @@ func _init_managers(): add_child(portal_mode_manager) portal_mode_manager.initialize(self , $EnhancedGridMap) + # Gauntlet manager for Candy Cannon Survival mode + if LobbyManager.game_mode == "Candy Cannon Survival": + gauntlet_manager = load("res://scripts/managers/gauntlet_manager.gd").new() + gauntlet_manager.name = "GauntletManager" + add_child(gauntlet_manager) + gauntlet_manager.initialize(self, $EnhancedGridMap) + # Screen shake manager for impact feedback screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new() screen_shake_manager.name = "ScreenShakeManager" @@ -609,6 +620,8 @@ func _setup_host_game(): stop_n_go_manager._setup_arena() elif LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: portal_mode_manager.setup_arena_locally() + elif LobbyManager.game_mode == "Candy Cannon Survival" and gauntlet_manager: + gauntlet_manager._setup_arena() else: # Randomize grid first to ensure Floor 0 is walkable for pre-calculation randomize_game_grid() @@ -715,6 +728,10 @@ func _setup_client_game(): if LobbyManager.game_mode == "Tekton Doors" and portal_mode_manager: portal_mode_manager.setup_arena_locally() + # Initialize arena locally for Candy Cannon Survival + if LobbyManager.game_mode == "Candy Cannon Survival" and gauntlet_manager: + gauntlet_manager._apply_arena_setup() + # Ensure local player setup (UI, controls) is verified var player_character = get_node_or_null(str(my_id)) if player_character: @@ -809,10 +826,14 @@ func _start_game(): if LobbyManager.game_mode == "Stop n Go" and stop_n_go_manager: stop_n_go_manager.setup_mission_tiles() stop_n_go_manager.spawn_initial_powerups() # Ensure power-ups exist before 1,2,3 Go + + # Gauntlet: Spawn mission tiles across 20x20 arena BEFORE countdown + if LobbyManager.game_mode == "Candy Cannon Survival" and gauntlet_manager: + gauntlet_manager.setup_mission_tiles() # Spawn Static Tektons and random tiles BEFORE countdown (Free Mode Only) # Exclude for Stop n Go and Tekton Doors - if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors": + if LobbyManager.game_mode != "Stop n Go" and LobbyManager.game_mode != "Tekton Doors" and LobbyManager.game_mode != "Candy Cannon Survival": spawn_static_tektons() # Tekton Doors: Randomize connections BEFORE countdown so colors show @@ -851,6 +872,13 @@ func _start_game(): if goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration)) + elif LobbyManager.game_mode == "Candy Cannon Survival": + if gauntlet_manager: + gauntlet_manager.start_game_mode() + + if goals_cycle_manager: + var match_duration = LobbyManager.get_match_duration() + goals_cycle_manager.start_match(float(match_duration), true) # Enable cycles for 3x3 pattern missions elif goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration)) diff --git a/scripts/game_mode.gd b/scripts/game_mode.gd index e395c4a..4cc987f 100644 --- a/scripts/game_mode.gd +++ b/scripts/game_mode.gd @@ -4,7 +4,8 @@ class_name GameMode enum Mode { FREEMODE = 0, STOP_N_GO = 1, - TEKTON_DOORS = 2 + TEKTON_DOORS = 2, + GAUNTLET = 3 } static func from_string(mode: String) -> Mode: @@ -15,6 +16,8 @@ static func from_string(mode: String) -> Mode: return Mode.STOP_N_GO "Tekton Doors": return Mode.TEKTON_DOORS + "Candy Cannon Survival": + return Mode.GAUNTLET _: return Mode.FREEMODE @@ -26,11 +29,13 @@ static func mode_to_string(mode: Mode) -> String: return "Stop n Go" Mode.TEKTON_DOORS: return "Tekton Doors" + Mode.GAUNTLET: + return "Candy Cannon Survival" _: return "Freemode" static func is_restricted(mode: Mode) -> bool: - return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS + return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET static func get_all_modes() -> Array[String]: - return ["Freemode", "Stop n Go", "Tekton Doors"] + return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Cannon Survival"] diff --git a/scripts/managers/gauntlet_manager.gd b/scripts/managers/gauntlet_manager.gd new file mode 100644 index 0000000..9b77fa3 --- /dev/null +++ b/scripts/managers/gauntlet_manager.gd @@ -0,0 +1,537 @@ +extends Node +class_name GauntletManager + +# GauntletManager - Handles Candy Cannon Survival (Gauntlet) game mode +# Pattern: StopNGoManager + PortalModeManager + +signal phase_changed(phase_index: int, phase_name: String) +signal cannon_fired(targets: Array) +signal player_trapped(player_id: int) +signal cleanser_granted(player_id: int) + +# ============================================================================= +# Constants +# ============================================================================= + +const ARENA_COLUMNS: int = 20 +const ARENA_ROWS: int = 20 +const NPC_SIZE: int = 3 +const NPC_CENTER: Vector2i = Vector2i(9, 9) # Center of 20x20 (0-indexed, center of 3x3 block) + +# Tile IDs (matching MeshLibrary) +const TILE_WALKABLE: int = 0 +const TILE_OBSTACLE: int = 4 +const TILE_STICKY: int = 17 # New candy-pink overlay (Layer 2) +const TILE_TELEGRAPH: int = 18 # Warning glow (Layer 2, temporary) + +# Phase timing thresholds (seconds elapsed) +const PHASE_1_START: float = 0.0 # Open Arena +const PHASE_2_START: float = 60.0 # Route Pressure +const PHASE_3_START: float = 120.0 # Survival Endgame + +# ============================================================================= +# Phase System +# ============================================================================= + +enum Phase { OPEN_ARENA, ROUTE_PRESSURE, SURVIVAL_ENDGAME } +var current_phase: Phase = Phase.OPEN_ARENA +var elapsed_time: float = 0.0 +var is_active: bool = false + +# ============================================================================= +# Cannon State +# ============================================================================= + +var cannon_timer: float = 0.0 +var cannon_interval: float = 5.0 # seconds between volleys +var volley_size: int = 5 +var sticky_cells: Dictionary = {} # Vector2i → true +var last_targeted_player_id: int = -1 + +# Phase-specific cannon parameters +var phase_configs: Array = [ + # Phase 0 (Open Arena): slow, small volleys + {"interval": 5.0, "volley": 5, "telegraph_time": 1.2}, + # Phase 1 (Route Pressure): faster, bigger volleys + {"interval": 4.0, "volley": 8, "telegraph_time": 1.0}, + # Phase 2 (Survival Endgame): rapid fire, huge volleys + {"interval": 3.0, "volley": 12, "telegraph_time": 0.8}, +] + +# ============================================================================= +# Smack State (per-player) +# ============================================================================= + +var smack_cooldowns: Dictionary = {} # player_id → float (time remaining) +var smack_charged: Dictionary = {} # player_id → float (charge window remaining) +const SMACK_COOLDOWN: float = 8.0 +const SMACK_CHARGE_WINDOW: float = 3.0 + +# ============================================================================= +# Cleanser Tracking +# ============================================================================= + +var player_mission_completions: Dictionary = {} # player_id → int +var player_cleansers: Dictionary = {} # player_id → int (0 or 1) + +# ============================================================================= +# Trapped Players +# ============================================================================= + +var trapped_players: Dictionary = {} # player_id → true + +# ============================================================================= +# References +# ============================================================================= + +var main_scene: Node = null +var gridmap: Node = null + +# HUD +var hud_layer: CanvasLayer +var phase_label: Label +var cleanser_label: Label + +# ============================================================================= +# Lifecycle +# ============================================================================= + +func _ready(): + set_process(false) + _setup_hud() + +func initialize(main: Node, grid: Node) -> void: + main_scene = main + gridmap = grid + print("[Gauntlet] Initialized with gridmap: ", gridmap.name if gridmap else "null") + +func _process(delta: float) -> void: + if not is_active: + return + + elapsed_time += delta + + # Phase escalation + _check_phase_transition() + + # Cannon timer (server only) + if multiplayer.is_server(): + cannon_timer -= delta + if cannon_timer <= 0.0: + _fire_volley() + cannon_timer = cannon_interval + +# ============================================================================= +# Game Mode Start +# ============================================================================= + +func start_game_mode() -> void: + if multiplayer.is_server(): + activate_client_side() + _start_phase(Phase.OPEN_ARENA) + +func activate_client_side() -> void: + is_active = true + if hud_layer: + hud_layer.visible = true + set_process(true) + +# ============================================================================= +# Phase Management +# ============================================================================= + +func _check_phase_transition() -> void: + var new_phase = current_phase + + if elapsed_time >= PHASE_3_START: + new_phase = Phase.SURVIVAL_ENDGAME + elif elapsed_time >= PHASE_2_START: + new_phase = Phase.ROUTE_PRESSURE + + if new_phase != current_phase: + _start_phase(new_phase) + +func _start_phase(phase: Phase) -> void: + current_phase = phase + var config = phase_configs[int(phase)] + cannon_interval = config["interval"] + volley_size = config["volley"] + cannon_timer = cannon_interval + + var phase_name = _phase_to_string(phase) + print("[Gauntlet] Phase changed to: ", phase_name) + + if _can_rpc(): + rpc("sync_phase", int(phase), phase_name) + + emit_signal("phase_changed", int(phase), phase_name) + +func _phase_to_string(phase: Phase) -> String: + match phase: + Phase.OPEN_ARENA: + return "Open Arena" + Phase.ROUTE_PRESSURE: + return "Route Pressure" + Phase.SURVIVAL_ENDGAME: + return "Survival!" + _: + return "Unknown" + +@rpc("authority", "call_local", "reliable") +func sync_phase(phase_index: int, phase_name: String) -> void: + if not is_active: + activate_client_side() + current_phase = phase_index as Phase + var config = phase_configs[phase_index] + cannon_interval = config["interval"] + volley_size = config["volley"] + _update_hud_phase(phase_name) + +# ============================================================================= +# Arena Setup +# ============================================================================= + +func _setup_arena() -> void: + """Called by host in main._setup_host_game()""" + if not gridmap: + gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: + gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: + push_error("[Gauntlet] No EnhancedGridMap found!") + return + + print("[Gauntlet] Setting up %dx%d Arena..." % [ARENA_COLUMNS, ARENA_ROWS]) + + # Sync to clients + if _can_rpc(): + rpc("sync_arena_setup") + + # Apply locally for server + _apply_arena_setup() + +@rpc("authority", "call_remote", "reliable") +func sync_arena_setup() -> void: + print("[Gauntlet] Client: Syncing Arena Setup (%dx%d)..." % [ARENA_COLUMNS, ARENA_ROWS]) + _apply_arena_setup() + +func _apply_arena_setup() -> void: + """Shared arena layout logic for host + clients.""" + if not gridmap: + gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: + gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return + + # Resize grid (bypass setters that wipe the map) + gridmap.set("columns", ARENA_COLUMNS) + gridmap.set("rows", ARENA_ROWS) + + # Clear all + gridmap.clear() + + # Build the 20x20 arena + for x in range(ARENA_COLUMNS): + for z in range(ARENA_ROWS): + var pos = Vector2i(x, z) + + # Center 3x3 block: NPC obstacle (Candy Cannon) + if _is_npc_zone(pos): + gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE) + gridmap.set_cell_item(Vector3i(x, 1, z), -1) + continue + + # Outer edge (row 0, row 19, col 0, col 19) — cannon spawn positions + # These are walkable but used as spawn reference + gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE) + gridmap.set_cell_item(Vector3i(x, 1, z), -1) + + gridmap.diagonal_movement = true + gridmap.update_grid_data() + gridmap.initialize_astar() + + print("[Gauntlet] Arena setup complete. Center NPC at (%d,%d), size %dx%d" % [ + NPC_CENTER.x, NPC_CENTER.y, NPC_SIZE, NPC_SIZE + ]) + +func _is_npc_zone(pos: Vector2i) -> bool: + """Check if a position is within the center 3x3 NPC zone.""" + var half = NPC_SIZE / 2 # integer division = 1 + var min_coord = NPC_CENTER - Vector2i(half, half) # (8, 8) + var max_coord = NPC_CENTER + Vector2i(half, half) # (10, 10) + return pos.x >= min_coord.x and pos.x <= max_coord.x and pos.y >= min_coord.y and pos.y <= max_coord.y + +# ============================================================================= +# Tile Spawning & Mission System (Task #3) +# ============================================================================= + +func setup_mission_tiles() -> void: + """Public wrapper called from main.gd before countdown. Server-only.""" + if multiplayer.is_server(): + _spawn_mission_tiles() + +func _spawn_mission_tiles() -> void: + """Distribute colored goal tiles across the 20x20 arena. + Follows StopNGoManager._spawn_mission_tiles() pattern. + Excludes center 3x3 NPC zone.""" + if not gridmap: + gridmap = get_parent().get_node_or_null("EnhancedGridMap") + if not gridmap: + gridmap = get_node_or_null("/root/Main/EnhancedGridMap") + if not gridmap: return + + # Goal items: Heart(7), Diamond(8), Star(9), Coin(10) + var goal_items = [7, 8, 9, 10] + var tiles_spawned: int = 0 + + for x in range(ARENA_COLUMNS): + for z in range(ARENA_ROWS): + var pos = Vector2i(x, z) + + # Skip NPC cannon zone (center 3x3) + if _is_npc_zone(pos): + continue + + # Check base floor — don't spawn on obstacles or void + var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z)) + if base_tile == TILE_OBSTACLE or base_tile == -1: + continue + + # Skip if something already exists on Layer 1 + var current_item = gridmap.get_cell_item(Vector3i(x, 1, z)) + if current_item != -1: + continue + + # Spawn tiles with 60% density (40% chance to skip) + if randf() > 0.6: + continue + + var tile_type = goal_items[randi() % goal_items.size()] + gridmap.set_cell_item(Vector3i(x, 1, z), tile_type) + tiles_spawned += 1 + + # Sync to clients + var main = get_node("/root/Main") + if main: + main.rpc("sync_grid_item", x, 1, z, tile_type) + + print("[Gauntlet] Spawned %d mission tiles across %dx%d arena" % [tiles_spawned, ARENA_COLUMNS, ARENA_ROWS]) + +# ============================================================================= +# Cannon Logic (Server Only) +# ============================================================================= + +func _fire_volley() -> void: + """Select target cells, telegraph, then apply sticky after delay.""" + if not multiplayer.is_server(): + return + + var targets = _select_targets() + if targets.is_empty(): + return + + var config = phase_configs[int(current_phase)] + var telegraph_time = config["telegraph_time"] + + # Telegraph phase — show warning + if _can_rpc(): + rpc("sync_telegraph", targets) + + # Wait telegraph duration, then apply impact + await get_tree().create_timer(telegraph_time).timeout + + if _can_rpc(): + rpc("sync_impact", targets) + + emit_signal("cannon_fired", targets) + +func _select_targets() -> Array: + """Pick target cells for this volley based on current phase weights.""" + var targets: Array = [] + var all_players = get_tree().get_nodes_in_group("Players") + + # Collect all valid walkable positions (excluding NPC zone and existing sticky) + var valid_positions: Array = [] + for x in range(ARENA_COLUMNS): + for z in range(ARENA_ROWS): + var pos = Vector2i(x, z) + if _is_npc_zone(pos): + continue + if sticky_cells.has(pos): + continue + valid_positions.append(pos) + + if valid_positions.is_empty(): + return targets + + # Simple targeting: mix of random + player-adjacent + var remaining = volley_size + + # 40% of volley near players + var player_targets = int(remaining * 0.4) + for i in range(player_targets): + if all_players.is_empty(): + break + # Pick a random player + var player = all_players[randi() % all_players.size()] + var player_pos = player.current_position if player.get("current_position") else Vector2i(10, 10) + + # Pick a cell near them (within 3 tiles) + var nearby = _get_nearby_valid_cells(player_pos, 3, valid_positions) + if not nearby.is_empty(): + var target = nearby[randi() % nearby.size()] + if target not in targets: + targets.append(target) + remaining -= 1 + + # Remaining: random scatter + valid_positions.shuffle() + for pos in valid_positions: + if remaining <= 0: + break + if pos not in targets: + targets.append(pos) + remaining -= 1 + + return targets + +func _get_nearby_valid_cells(center: Vector2i, radius: int, valid: Array) -> Array: + var result: Array = [] + for pos in valid: + if abs(pos.x - center.x) <= radius and abs(pos.y - center.y) <= radius: + result.append(pos) + return result + +# ============================================================================= +# Telegraph & Impact (RPCs) +# ============================================================================= + +@rpc("authority", "call_local", "reliable") +func sync_telegraph(targets: Array) -> void: + """Show warning overlay on target cells.""" + if not gridmap: return + for target in targets: + var pos = target as Vector2i + gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH) + +@rpc("authority", "call_local", "reliable") +func sync_impact(targets: Array) -> void: + """Apply sticky cells at target positions.""" + if not gridmap: return + for target in targets: + var pos = target as Vector2i + # Replace telegraph with sticky on Layer 2 + gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY) + sticky_cells[pos] = true + + # Screen shake for impact + if main_scene and main_scene.get("screen_shake_manager"): + main_scene.screen_shake_manager.shake(0.15, 4.0) + + # Check if any player is now trapped + _check_all_players_trapped() + +# ============================================================================= +# Sticky / Trap System +# ============================================================================= + +func is_sticky_cell(pos: Vector2i) -> bool: + return sticky_cells.has(pos) + +func _check_all_players_trapped() -> void: + if not multiplayer.is_server(): return + var all_players = get_tree().get_nodes_in_group("Players") + for player in all_players: + var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1) + if is_sticky_cell(pos) and not trapped_players.has(player.get("peer_id", -1)): + _trap_player(player) + +func _trap_player(player: Node) -> void: + var pid = player.get("peer_id", -1) + if pid == -1: return + trapped_players[pid] = true + print("[Gauntlet] Player %d TRAPPED at %s" % [pid, str(player.current_position)]) + emit_signal("player_trapped", pid) + + # TODO: Apply movement lockout, score penalty, visual feedback + # For now, just mark as trapped — will be expanded in Task #4 + +func clear_sticky_cell(pos: Vector2i) -> void: + """Used by Cleanser power-up to remove a sticky cell.""" + sticky_cells.erase(pos) + if gridmap: + gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1) + +# ============================================================================= +# HUD +# ============================================================================= + +func _setup_hud() -> void: + hud_layer = CanvasLayer.new() + hud_layer.layer = 5 + hud_layer.visible = false + add_child(hud_layer) + + var custom_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf") + + # Phase label (top-center) + var top_container = CenterContainer.new() + top_container.set_anchors_preset(Control.PRESET_CENTER_TOP) + top_container.grow_horizontal = Control.GROW_DIRECTION_BOTH + top_container.grow_vertical = Control.GROW_DIRECTION_END + top_container.offset_top = 70 + hud_layer.add_child(top_container) + + phase_label = Label.new() + phase_label.text = "🍬 OPEN ARENA" + phase_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + if custom_font: phase_label.add_theme_font_override("font", custom_font) + phase_label.add_theme_font_size_override("font_size", 24) + phase_label.add_theme_color_override("font_outline_color", Color.BLACK) + phase_label.add_theme_constant_override("outline_size", 6) + phase_label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.8)) # Candy pink + top_container.add_child(phase_label) + + # Cleanser label (bottom-center) + var bottom_container = CenterContainer.new() + bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) + bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH + bottom_container.grow_vertical = Control.GROW_DIRECTION_BEGIN + bottom_container.offset_bottom = -50 + hud_layer.add_child(bottom_container) + + cleanser_label = Label.new() + cleanser_label.text = "🧹 Cleanser: 0" + cleanser_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + if custom_font: cleanser_label.add_theme_font_override("font", custom_font) + cleanser_label.add_theme_font_size_override("font_size", 20) + cleanser_label.add_theme_color_override("font_outline_color", Color.BLACK) + cleanser_label.add_theme_constant_override("outline_size", 6) + bottom_container.add_child(cleanser_label) + +func _update_hud_phase(phase_name: String) -> void: + if phase_label: + var icon = "🍬" + match phase_name: + "Route Pressure": + icon = "⚠️" + phase_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.2)) # Warning gold + "Survival!": + icon = "💀" + phase_label.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3)) # Danger red + _: + phase_label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.8)) # Candy pink + phase_label.text = "%s %s" % [icon, phase_name.to_upper()] + +func update_cleanser_ui(count: int) -> void: + if cleanser_label: + cleanser_label.text = "🧹 Cleanser: %d" % count + +# ============================================================================= +# Utility +# ============================================================================= + +func _can_rpc() -> bool: + if not multiplayer.has_multiplayer_peer(): return false + if multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: return false + return true diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 3fe50af..413bf34 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -80,7 +80,7 @@ var rematch_votes: Array = [] # [player_id, ...] # Character and area selection var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"] var available_areas: Array[String] = [] -var available_game_modes: Array[String] = ["Freemode", "Stop n Go"] +var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Candy Cannon Survival"] var selected_area: String = "Freemode Arena" # Host-controlled var game_mode: String = "Freemode" # Host-controlled var local_character_index: int = 0 # Local player's character index @@ -135,6 +135,8 @@ func _update_available_areas(mode: String) -> void: available_areas = ["Freemode Arena", "Classic", "Colloseum"] "Stop n Go": available_areas = ["Stop N Go Arena"] + "Candy Cannon Survival": + available_areas = ["Gauntlet Arena"] _: available_areas = ["Classic"] diff --git a/tests/test_gauntlet_registration.gd b/tests/test_gauntlet_registration.gd new file mode 100644 index 0000000..a43dda4 --- /dev/null +++ b/tests/test_gauntlet_registration.gd @@ -0,0 +1,158 @@ +# tests/test_gauntlet_registration.gd +# Tests for [Gauntlet] #1 Game Mode Registration +# Validates GAUNTLET enum, string conversion, lobby integration, and arena setup + +extends GutTest + +func before_all(): + gut.p("=== Gauntlet Registration Tests [Gauntlet #1] ===") + +func after_each(): + pass + +# ============================================================================= +# GameMode Enum Tests +# ============================================================================= + +# Test 1: GAUNTLET enum value exists and equals 3 +func test_gauntlet_enum_exists(): + assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be enum value 3") + +# Test 2: All 4 modes are present in enum +func test_all_modes_in_enum(): + assert_eq(GameMode.Mode.FREEMODE, 0, "FREEMODE should be 0") + assert_eq(GameMode.Mode.STOP_N_GO, 1, "STOP_N_GO should be 1") + assert_eq(GameMode.Mode.TEKTON_DOORS, 2, "TEKTON_DOORS should be 2") + assert_eq(GameMode.Mode.GAUNTLET, 3, "GAUNTLET should be 3") + +# ============================================================================= +# String Conversion Tests +# ============================================================================= + +# Test 3: from_string recognizes "Candy Cannon Survival" +func test_from_string_candy_cannon(): + var result = GameMode.from_string("Candy Cannon Survival") + assert_eq(result, GameMode.Mode.GAUNTLET, "from_string should parse 'Candy Cannon Survival' as GAUNTLET") + +# Test 4: mode_to_string returns "Candy Cannon Survival" for GAUNTLET +func test_mode_to_string_gauntlet(): + var result = GameMode.mode_to_string(GameMode.Mode.GAUNTLET) + assert_eq(result, "Candy Cannon Survival", "mode_to_string should return 'Candy Cannon Survival'") + +# Test 5: Round-trip conversion is lossless +func test_round_trip_conversion(): + var mode_str = GameMode.mode_to_string(GameMode.Mode.GAUNTLET) + var mode_enum = GameMode.from_string(mode_str) + assert_eq(mode_enum, GameMode.Mode.GAUNTLET, "Round-trip should preserve GAUNTLET") + +# Test 6: All existing modes still round-trip correctly +func test_existing_modes_round_trip(): + for mode in [GameMode.Mode.FREEMODE, GameMode.Mode.STOP_N_GO, GameMode.Mode.TEKTON_DOORS]: + var s = GameMode.mode_to_string(mode) + var back = GameMode.from_string(s) + assert_eq(back, mode, "Round-trip failed for %s" % s) + +# Test 7: Unknown string defaults to FREEMODE +func test_unknown_string_defaults_freemode(): + var result = GameMode.from_string("NonExistentMode") + assert_eq(result, GameMode.Mode.FREEMODE, "Unknown mode string should default to FREEMODE") + +# ============================================================================= +# get_all_modes Tests +# ============================================================================= + +# Test 8: get_all_modes includes "Candy Cannon Survival" +func test_get_all_modes_includes_gauntlet(): + var modes = GameMode.get_all_modes() + assert_has(modes, "Candy Cannon Survival", "get_all_modes should include 'Candy Cannon Survival'") + +# Test 9: get_all_modes returns exactly 4 entries +func test_get_all_modes_count(): + var modes = GameMode.get_all_modes() + assert_eq(modes.size(), 4, "get_all_modes should return 4 modes") + +# Test 10: get_all_modes order is correct +func test_get_all_modes_order(): + var modes = GameMode.get_all_modes() + assert_eq(modes[0], "Freemode", "First mode should be Freemode") + assert_eq(modes[1], "Stop n Go", "Second mode should be Stop n Go") + assert_eq(modes[2], "Tekton Doors", "Third mode should be Tekton Doors") + assert_eq(modes[3], "Candy Cannon Survival", "Fourth mode should be Candy Cannon Survival") + +# ============================================================================= +# is_restricted Tests +# ============================================================================= + +# Test 11: GAUNTLET is a restricted mode +func test_gauntlet_is_restricted(): + var result = GameMode.is_restricted(GameMode.Mode.GAUNTLET) + assert_true(result, "GAUNTLET should be restricted (dedicated arena)") + +# Test 12: FREEMODE is NOT restricted +func test_freemode_not_restricted(): + var result = GameMode.is_restricted(GameMode.Mode.FREEMODE) + assert_false(result, "FREEMODE should not be restricted") + +# Test 13: All restricted modes are confirmed +func test_all_restricted_modes(): + assert_true(GameMode.is_restricted(GameMode.Mode.STOP_N_GO), "STOP_N_GO should be restricted") + assert_true(GameMode.is_restricted(GameMode.Mode.TEKTON_DOORS), "TEKTON_DOORS should be restricted") + assert_true(GameMode.is_restricted(GameMode.Mode.GAUNTLET), "GAUNTLET should be restricted") + +# ============================================================================= +# LobbyManager Integration Tests +# ============================================================================= + +# Test 14: Lobby available_game_modes includes "Candy Cannon Survival" +func test_lobby_modes_includes_gauntlet(): + var modes = LobbyManager.available_game_modes + assert_has(modes, "Candy Cannon Survival", "LobbyManager.available_game_modes should include 'Candy Cannon Survival'") + +# Test 15: gauntlet_manager.gd script file exists +func test_gauntlet_manager_script_exists(): + var script_exists = ResourceLoader.exists("res://scripts/managers/gauntlet_manager.gd") + assert_true(script_exists, "gauntlet_manager.gd should exist") + +# Test 16: GauntletManager class can be loaded +func test_gauntlet_manager_loads(): + var script = load("res://scripts/managers/gauntlet_manager.gd") + assert_not_null(script, "gauntlet_manager.gd should load without errors") + +# Test 17: GauntletManager has required methods +func test_gauntlet_manager_has_methods(): + var manager = GauntletManager.new() + assert_true(manager.has_method("_setup_arena"), "GauntletManager should have _setup_arena()") + assert_true(manager.has_method("_apply_arena_setup"), "GauntletManager should have _apply_arena_setup()") + assert_true(manager.has_method("start_game_mode"), "GauntletManager should have start_game_mode()") + assert_true(manager.has_method("initialize"), "GauntletManager should have initialize()") + manager.free() + +# Test 18: GauntletManager arena constants are correct +func test_gauntlet_arena_constants(): + assert_eq(GauntletManager.ARENA_COLUMNS, 20, "Arena should be 20 columns") + assert_eq(GauntletManager.ARENA_ROWS, 20, "Arena should be 20 rows") + assert_eq(GauntletManager.NPC_SIZE, 3, "NPC zone should be 3x3") + assert_eq(GauntletManager.NPC_CENTER, Vector2i(9, 9), "NPC center should be at (9,9)") + +# Test 19: NPC zone detection works +func test_npc_zone_detection(): + var manager = GauntletManager.new() + # Center of NPC zone + assert_true(manager._is_npc_zone(Vector2i(9, 9)), "Center (9,9) should be NPC zone") + # Edges of NPC zone + assert_true(manager._is_npc_zone(Vector2i(8, 8)), "Corner (8,8) should be NPC zone") + assert_true(manager._is_npc_zone(Vector2i(10, 10)), "Corner (10,10) should be NPC zone") + # Outside NPC zone + assert_false(manager._is_npc_zone(Vector2i(7, 9)), "Outside (7,9) should NOT be NPC zone") + assert_false(manager._is_npc_zone(Vector2i(11, 9)), "Outside (11,9) should NOT be NPC zone") + assert_false(manager._is_npc_zone(Vector2i(0, 0)), "Corner (0,0) should NOT be NPC zone") + manager.free() + +# Test 20: Phase enum has 3 phases +func test_gauntlet_phases(): + assert_eq(GauntletManager.Phase.OPEN_ARENA, 0, "OPEN_ARENA should be 0") + assert_eq(GauntletManager.Phase.ROUTE_PRESSURE, 1, "ROUTE_PRESSURE should be 1") + assert_eq(GauntletManager.Phase.SURVIVAL_ENDGAME, 2, "SURVIVAL_ENDGAME should be 2") + +func after_all(): + gut.p("=== Gauntlet Registration Tests Complete ===") diff --git a/tests/test_gauntlet_tile_spawning.gd b/tests/test_gauntlet_tile_spawning.gd new file mode 100644 index 0000000..f8c8548 --- /dev/null +++ b/tests/test_gauntlet_tile_spawning.gd @@ -0,0 +1,144 @@ +extends GutTest + +# ============================================================================= +# Test: Gauntlet Tile Spawning & Mission System (Task #3) +# ============================================================================= + +var GauntletManagerScript = load("res://scripts/managers/gauntlet_manager.gd") +var manager: GauntletManager + +func before_each(): + manager = GauntletManagerScript.new() + add_child(manager) + +func after_each(): + manager.queue_free() + +# ============================================================================= +# Arena Constants +# ============================================================================= + +func test_arena_size_20x20(): + assert_eq(manager.ARENA_COLUMNS, 20, "Arena should be 20 columns") + assert_eq(manager.ARENA_ROWS, 20, "Arena should be 20 rows") + +func test_npc_center_position(): + assert_eq(manager.NPC_CENTER, Vector2i(9, 9), "NPC center should be at (9,9)") + +func test_npc_size_3x3(): + assert_eq(manager.NPC_SIZE, 3, "NPC zone should be 3x3") + +# ============================================================================= +# NPC Zone Exclusion +# ============================================================================= + +func test_npc_zone_center_is_excluded(): + assert_true(manager._is_npc_zone(Vector2i(9, 9)), "Center (9,9) should be NPC zone") + +func test_npc_zone_corners_are_excluded(): + assert_true(manager._is_npc_zone(Vector2i(8, 8)), "Top-left (8,8) should be NPC zone") + assert_true(manager._is_npc_zone(Vector2i(10, 8)), "Top-right (10,8) should be NPC zone") + assert_true(manager._is_npc_zone(Vector2i(8, 10)), "Bottom-left (8,10) should be NPC zone") + assert_true(manager._is_npc_zone(Vector2i(10, 10)), "Bottom-right (10,10) should be NPC zone") + +func test_outside_npc_zone_not_excluded(): + assert_false(manager._is_npc_zone(Vector2i(7, 9)), "Left of NPC zone should NOT be excluded") + assert_false(manager._is_npc_zone(Vector2i(11, 9)), "Right of NPC zone should NOT be excluded") + assert_false(manager._is_npc_zone(Vector2i(9, 7)), "Above NPC zone should NOT be excluded") + assert_false(manager._is_npc_zone(Vector2i(9, 11)), "Below NPC zone should NOT be excluded") + +func test_arena_corners_not_excluded(): + assert_false(manager._is_npc_zone(Vector2i(0, 0)), "Top-left corner should be walkable") + assert_false(manager._is_npc_zone(Vector2i(19, 0)), "Top-right corner should be walkable") + assert_false(manager._is_npc_zone(Vector2i(0, 19)), "Bottom-left corner should be walkable") + assert_false(manager._is_npc_zone(Vector2i(19, 19)), "Bottom-right corner should be walkable") + +func test_npc_zone_total_cells(): + var npc_count = 0 + for x in range(manager.ARENA_COLUMNS): + for z in range(manager.ARENA_ROWS): + if manager._is_npc_zone(Vector2i(x, z)): + npc_count += 1 + assert_eq(npc_count, 9, "NPC zone should occupy exactly 9 cells (3x3)") + +func test_walkable_cells_count(): + # 20x20 = 400 total, minus 9 NPC = 391 walkable + var walkable = 400 - 9 + assert_eq(walkable, 391, "Should have 391 walkable cells") + +# ============================================================================= +# Tile Constants +# ============================================================================= + +func test_goal_tile_ids_valid(): + # Heart(7), Diamond(8), Star(9), Coin(10) — match StopNGoManager + var goal_items = [7, 8, 9, 10] + for item in goal_items: + assert_gt(item, 0, "Goal tile ID %d should be positive" % item) + assert_lt(item, 17, "Goal tile ID %d should not conflict with sticky(17)" % item) + +func test_tile_walkable_id(): + assert_eq(manager.TILE_WALKABLE, 0, "Walkable tile should be ID 0") + +func test_tile_obstacle_id(): + assert_eq(manager.TILE_OBSTACLE, 4, "Obstacle tile should be ID 4") + +func test_tile_sticky_id(): + assert_eq(manager.TILE_STICKY, 17, "Sticky tile should be ID 17") + +func test_tile_telegraph_id(): + assert_eq(manager.TILE_TELEGRAPH, 18, "Telegraph tile should be ID 18") + +# ============================================================================= +# Method Existence +# ============================================================================= + +func test_setup_mission_tiles_exists(): + assert_true(manager.has_method("setup_mission_tiles"), "Should have setup_mission_tiles()") + +func test_spawn_mission_tiles_exists(): + assert_true(manager.has_method("_spawn_mission_tiles"), "Should have _spawn_mission_tiles()") + +# ============================================================================= +# Sticky Cell System +# ============================================================================= + +func test_sticky_cells_initially_empty(): + assert_eq(manager.sticky_cells.size(), 0, "Sticky cells should start empty") + +func test_is_sticky_cell_false_for_clean(): + assert_false(manager.is_sticky_cell(Vector2i(5, 5)), "Clean cell should not be sticky") + +func test_is_sticky_cell_true_after_marking(): + manager.sticky_cells[Vector2i(5, 5)] = true + assert_true(manager.is_sticky_cell(Vector2i(5, 5)), "Marked cell should be sticky") + +func test_clear_sticky_cell(): + manager.sticky_cells[Vector2i(3, 3)] = true + manager.clear_sticky_cell(Vector2i(3, 3)) + assert_false(manager.is_sticky_cell(Vector2i(3, 3)), "Cleared cell should no longer be sticky") + +# ============================================================================= +# Phase Interaction with Tile Spawning +# ============================================================================= + +func test_initial_phase_is_open_arena(): + assert_eq(manager.current_phase, GauntletManager.Phase.OPEN_ARENA, "Should start in Open Arena") + +func test_phase_to_string_open_arena(): + assert_eq(manager._phase_to_string(GauntletManager.Phase.OPEN_ARENA), "Open Arena") + +func test_phase_to_string_route_pressure(): + assert_eq(manager._phase_to_string(GauntletManager.Phase.ROUTE_PRESSURE), "Route Pressure") + +func test_phase_to_string_survival(): + assert_eq(manager._phase_to_string(GauntletManager.Phase.SURVIVAL_ENDGAME), "Survival!") + +# ============================================================================= +# Match Timer Integration +# ============================================================================= + +func test_match_duration_180s(): + # Gauntlet uses 180s match (3 phases: 0-60, 60-120, 120-180) + var total = manager.PHASE_3_START + 60.0 # Phase 3 starts at 120, runs 60s + assert_eq(total, 180.0, "Total match should be 180 seconds")