7380161743
Version bump to 2.3.6. New game mode features 20×20 arena with central cannon obstacle, three escalating phases (Open Arena, Route Pressure, Survival), and collectible tiles (Hearts, Diamonds, Stars, Coins) with pattern-matching missions. Players dodge candy volleys while completing collection goals. Updated export paths and version strings across all platforms (Windows, Android, Web, Linux).
392 lines
16 KiB
Markdown
392 lines
16 KiB
Markdown
# 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) |
|