# 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) |