Files
tekton/docs/gauntlet-technical-implementation.md
T
adtpdn 7380161743 feat: add Candy Cannon Survival game mode with collectible tiles
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).
2026-05-24 06:56:57 +08:00

16 KiB
Raw Blame History

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

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:

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

# 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
  1. Game Mode Registrationgame_mode.gd, lobby_manager.gd, main.gd branches
  2. Arena Setupgauntlet_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)