Files
tekton/docs/gauntlet-technical-implementation.md
T
2026-06-08 12:19:34 +08:00

392 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) |