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).
This commit is contained in:
2026-05-24 06:56:57 +08:00
parent 01ff0d4434
commit 7380161743
17 changed files with 3434 additions and 101 deletions
+1366
View File
@@ -0,0 +1,1366 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Candy Cannon Survival — Technical Documentation</title>
<meta name="description" content="Technical implementation documentation for the Candy Cannon Survival (Gauntlet) game mode in Tekton Dash">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #111118;
--bg-card: #16161f;
--bg-card-hover: #1c1c28;
--bg-code: #1a1a26;
--border: #2a2a3a;
--border-glow: #ff6bb5;
--text-primary: #e8e6f0;
--text-secondary: #8b89a0;
--text-muted: #5a586e;
--accent-pink: #ff6bb5;
--accent-pink-dim: #ff6bb540;
--accent-candy: #ff85c8;
--accent-purple: #a855f7;
--accent-blue: #6366f1;
--accent-cyan: #22d3ee;
--accent-green: #34d399;
--accent-yellow: #fbbf24;
--accent-orange: #fb923c;
--accent-red: #f87171;
--new-badge: #34d399;
--existing-badge: #6366f1;
--adapt-badge: #fbbf24;
--glass: rgba(22, 22, 31, 0.7);
--glass-border: rgba(255, 107, 181, 0.12);
--shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.5);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--accent-pink-dim) transparent;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
}
/* === AMBIENT BG === */
.ambient-bg {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
}
.ambient-bg .orb {
position: absolute;
border-radius: 50%;
filter: blur(120px);
opacity: 0.15;
animation: orbFloat 20s ease-in-out infinite;
}
.ambient-bg .orb:nth-child(1) {
width: 600px; height: 600px;
background: var(--accent-pink);
top: -200px; left: -100px;
animation-delay: 0s;
}
.ambient-bg .orb:nth-child(2) {
width: 500px; height: 500px;
background: var(--accent-purple);
bottom: -150px; right: -100px;
animation-delay: -7s;
}
.ambient-bg .orb:nth-child(3) {
width: 400px; height: 400px;
background: var(--accent-blue);
top: 50%; left: 50%;
transform: translate(-50%, -50%);
animation-delay: -14s;
}
@keyframes orbFloat {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(30px, -40px) scale(1.05); }
50% { transform: translate(-20px, 20px) scale(0.95); }
75% { transform: translate(40px, 30px) scale(1.02); }
}
/* === LAYOUT === */
.wrapper {
position: relative;
z-index: 1;
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
}
/* === HERO === */
.hero {
padding: 80px 0 60px;
text-align: center;
position: relative;
}
.hero-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
border-radius: 100px;
background: var(--accent-pink-dim);
border: 1px solid var(--accent-pink);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--accent-candy);
margin-bottom: 24px;
}
.hero-badge .dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent-pink);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.hero h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.1;
background: linear-gradient(135deg, #fff 0%, var(--accent-candy) 50%, var(--accent-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 16px;
}
.hero .subtitle {
font-size: 18px;
color: var(--text-secondary);
max-width: 640px;
margin: 0 auto 40px;
}
.hero-stats {
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
}
.hero-stat {
text-align: center;
}
.hero-stat .value {
font-size: 2rem;
font-weight: 800;
color: var(--accent-candy);
}
.hero-stat .label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
/* === NAV === */
.sticky-nav {
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: rgba(10,10,15,0.85);
border-bottom: 1px solid var(--border);
padding: 0;
margin-bottom: 48px;
}
.nav-inner {
display: flex;
gap: 4px;
padding: 8px 0;
overflow-x: auto;
scrollbar-width: none;
}
.nav-inner::-webkit-scrollbar { display: none; }
.nav-link {
padding: 8px 16px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
white-space: nowrap;
transition: all 0.2s;
}
.nav-link:hover, .nav-link.active {
background: var(--accent-pink-dim);
color: var(--accent-candy);
}
/* === SECTIONS === */
section {
margin-bottom: 64px;
}
.section-header {
margin-bottom: 32px;
}
.section-header h2 {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
display: flex;
align-items: center;
gap: 12px;
}
.section-header h2 .icon {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
border-radius: 10px;
font-size: 18px;
}
.section-header p {
color: var(--text-secondary);
margin-top: 8px;
max-width: 700px;
}
/* === CARDS === */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
transition: all 0.3s;
}
.card:hover {
border-color: var(--glass-border);
background: var(--bg-card-hover);
box-shadow: 0 0 30px var(--accent-pink-dim);
}
/* === GLOSSARY === */
.glossary-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 16px;
}
.glossary-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px;
display: flex;
gap: 16px;
transition: all 0.3s;
position: relative;
overflow: hidden;
}
.glossary-item::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 4px; height: 100%;
border-radius: 4px 0 0 4px;
}
.glossary-item.new::before { background: var(--new-badge); }
.glossary-item.adapt::before { background: var(--adapt-badge); }
.glossary-item.existing::before { background: var(--existing-badge); }
.glossary-item:hover {
border-color: var(--glass-border);
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
}
.glossary-icon {
width: 44px; height: 44px;
min-width: 44px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
}
.glossary-item.new .glossary-icon { background: rgba(52,211,153,0.12); }
.glossary-item.adapt .glossary-icon { background: rgba(251,191,36,0.12); }
.glossary-item.existing .glossary-icon { background: rgba(99,102,241,0.12); }
.glossary-content h3 {
font-size: 15px;
font-weight: 700;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.glossary-content p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
}
.glossary-content .code-ref {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--accent-cyan);
background: rgba(34,211,238,0.08);
padding: 2px 8px;
border-radius: 4px;
margin-top: 6px;
display: inline-block;
}
/* === BADGES === */
.badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 100px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.badge-new { background: rgba(52,211,153,0.15); color: var(--new-badge); }
.badge-adapt { background: rgba(251,191,36,0.15); color: var(--adapt-badge); }
.badge-existing { background: rgba(99,102,241,0.15); color: var(--existing-badge); }
/* === LEGEND === */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 24px;
padding: 16px 20px;
background: var(--bg-secondary);
border-radius: 12px;
border: 1px solid var(--border);
}
.legend-item {
display: flex; align-items: center; gap: 8px;
font-size: 13px; color: var(--text-secondary);
}
.legend-dot {
width: 10px; height: 10px;
border-radius: 3px;
}
.legend-dot.new { background: var(--new-badge); }
.legend-dot.adapt { background: var(--adapt-badge); }
.legend-dot.existing { background: var(--existing-badge); }
/* === ARCHITECTURE DIAGRAM === */
.arch-diagram {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: 16px;
padding: 32px;
overflow-x: auto;
}
.arch-tree {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 2;
}
.arch-tree .node { color: var(--accent-cyan); font-weight: 600; }
.arch-tree .new-node { color: var(--accent-green); font-weight: 600; }
.arch-tree .label { color: var(--text-muted); }
.arch-tree .connector { color: var(--text-muted); }
.arch-tree .tag-new {
font-size: 10px;
background: rgba(52,211,153,0.15);
color: var(--new-badge);
padding: 1px 6px;
border-radius: 4px;
margin-left: 4px;
font-weight: 700;
}
/* === TABLES === */
.table-wrap {
overflow-x: auto;
border-radius: 14px;
border: 1px solid var(--border);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead th {
background: var(--bg-secondary);
padding: 14px 16px;
text-align: left;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
tbody td {
padding: 12px 16px;
border-bottom: 1px solid rgba(42,42,58,0.5);
vertical-align: top;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: rgba(255,107,181,0.03); }
tbody td:first-child { font-weight: 600; }
.td-code {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--accent-cyan);
}
/* === REUSE TABLE === */
.reuse-direct { color: var(--accent-green); font-weight: 600; }
.reuse-heavy { color: var(--accent-cyan); font-weight: 600; }
.reuse-partial { color: var(--accent-yellow); font-weight: 600; }
.reuse-adapt { color: var(--accent-orange); font-weight: 600; }
.reuse-pattern { color: var(--accent-purple); font-weight: 600; }
/* === PHASE TIMELINE === */
.timeline {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 768px) { .timeline { grid-template-columns: 1fr; } }
.phase-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 24px;
position: relative;
overflow: hidden;
}
.phase-card::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 3px;
}
.phase-card.phase-1::after { background: linear-gradient(90deg, var(--accent-green), var(--accent-cyan)); }
.phase-card.phase-2::after { background: linear-gradient(90deg, var(--accent-yellow), var(--accent-orange)); }
.phase-card.phase-3::after { background: linear-gradient(90deg, var(--accent-orange), var(--accent-red)); }
.phase-time {
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.phase-card h3 {
font-size: 18px;
font-weight: 800;
margin-bottom: 12px;
}
.phase-card ul {
list-style: none;
padding: 0;
}
.phase-card li {
font-size: 13px;
color: var(--text-secondary);
padding: 4px 0;
padding-left: 16px;
position: relative;
}
.phase-card li::before {
content: '';
position: absolute;
left: 0;
color: var(--accent-pink);
font-weight: 700;
}
/* === FILE LISTS === */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.file-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.2s;
}
.file-card:hover {
border-color: var(--glass-border);
transform: translateY(-1px);
}
.file-icon {
width: 36px; height: 36px; min-width: 36px;
border-radius: 8px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
}
.file-card.new-file .file-icon { background: rgba(52,211,153,0.12); }
.file-card.mod-file .file-icon { background: rgba(251,191,36,0.12); }
.file-info h4 {
font-size: 13px;
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
}
.file-info p {
font-size: 12px;
color: var(--text-muted);
}
/* === CODE BLOCKS === */
.code-block {
background: var(--bg-code);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 12.5px;
line-height: 1.8;
}
.code-block .kw { color: var(--accent-purple); }
.code-block .fn { color: var(--accent-cyan); }
.code-block .str { color: var(--accent-green); }
.code-block .cm { color: var(--text-muted); font-style: italic; }
.code-block .num { color: var(--accent-orange); }
.code-block .type { color: var(--accent-yellow); }
/* === RISK CARDS === */
.risk-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 12px;
}
.risk-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
}
.risk-indicator {
width: 36px; height: 36px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 16px;
background: rgba(248,113,113,0.12);
}
.risk-content h4 { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
.risk-content p { font-size: 12px; color: var(--text-secondary); }
/* === PRIORITY LIST === */
.priority-list {
counter-reset: priority;
}
.priority-item {
display: flex;
gap: 16px;
padding: 16px 0;
border-bottom: 1px solid rgba(42,42,58,0.4);
align-items: flex-start;
}
.priority-item:last-child { border-bottom: none; }
.priority-num {
counter-increment: priority;
width: 36px; height: 36px; min-width: 36px;
border-radius: 10px;
background: var(--accent-pink-dim);
color: var(--accent-candy);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
font-weight: 800;
}
.priority-info h4 { font-size: 14px; font-weight: 700; }
.priority-info p {
font-size: 12px;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
}
/* === NETWORK TABLE ACCENTS === */
.sync-method {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--accent-cyan);
}
/* === FOOTER === */
footer {
text-align: center;
padding: 48px 0;
border-top: 1px solid var(--border);
color: var(--text-muted);
font-size: 13px;
}
/* === SEARCH === */
.search-bar {
position: relative;
margin-bottom: 20px;
}
.search-bar input {
width: 100%;
padding: 12px 16px 12px 44px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
outline: none;
transition: border-color 0.2s;
}
.search-bar input:focus {
border-color: var(--accent-pink);
}
.search-bar input::placeholder {
color: var(--text-muted);
}
.search-bar .search-icon {
position: absolute;
left: 14px; top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 16px;
pointer-events: none;
}
/* === FILTER BTNS === */
.filter-btns {
display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap;
}
.filter-btn {
padding: 6px 14px;
border-radius: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
transition: all 0.2s;
}
.filter-btn:hover, .filter-btn.active {
border-color: var(--accent-pink);
color: var(--accent-candy);
background: var(--accent-pink-dim);
}
/* === SCROLL TOP === */
.scroll-top {
position: fixed;
bottom: 32px; right: 32px;
width: 44px; height: 44px;
border-radius: 12px;
background: var(--accent-pink);
color: #fff;
border: none;
cursor: pointer;
font-size: 18px;
display: none;
align-items: center; justify-content: center;
box-shadow: 0 4px 20px var(--accent-pink-dim);
transition: transform 0.2s;
z-index: 200;
}
.scroll-top:hover { transform: translateY(-2px); }
.scroll-top.show { display: flex; }
</style>
</head>
<body>
<div class="ambient-bg">
<div class="orb"></div>
<div class="orb"></div>
<div class="orb"></div>
</div>
<div class="wrapper">
<!-- ============ HERO ============ -->
<header class="hero">
<div class="hero-badge"><span class="dot"></span> Technical Documentation</div>
<h1>Candy Cannon Survival</h1>
<p class="subtitle">Gauntlet Mode — Implementation blueprint mapping GDD mechanics to existing Tekton Dash systems</p>
<div class="hero-stats">
<div class="hero-stat"><div class="value">70%</div><div class="label">Code Reuse</div></div>
<div class="hero-stat"><div class="value">4</div><div class="label">New Files</div></div>
<div class="hero-stat"><div class="value">7</div><div class="label">Modified Files</div></div>
<div class="hero-stat"><div class="value">12</div><div class="label">New Terms</div></div>
<div class="hero-stat"><div class="value">10</div><div class="label">Reused Terms</div></div>
</div>
</header>
<!-- ============ NAV ============ -->
<div class="sticky-nav">
<nav class="nav-inner wrapper">
<a class="nav-link" href="#glossary">Glossary</a>
<a class="nav-link" href="#architecture">Architecture</a>
<a class="nav-link" href="#reuse">Reuse Map</a>
<a class="nav-link" href="#phases">Phases</a>
<a class="nav-link" href="#systems">Systems</a>
<a class="nav-link" href="#files">Files</a>
<a class="nav-link" href="#network">Network</a>
<a class="nav-link" href="#priority">Priority</a>
<a class="nav-link" href="#risks">Risks</a>
</nav>
</div>
<!-- ============ GLOSSARY ============ -->
<section id="glossary">
<div class="section-header">
<h2><span class="icon" style="background:rgba(255,107,181,0.12)">📖</span> Glossary</h2>
<p>All terms used in Gauntlet mode — categorized by whether they're new, adapted, or already implemented in Tekton Dash.</p>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot new"></div> New — unique to Gauntlet</div>
<div class="legend-item"><div class="legend-dot adapt"></div> Adapted — modified from existing mechanic</div>
<div class="legend-item"><div class="legend-dot existing"></div> Existing — already in game, reused as-is</div>
</div>
<div class="search-bar">
<span class="search-icon">🔍</span>
<input type="text" id="glossarySearch" placeholder="Search terms..." oninput="filterGlossary()">
</div>
<div class="filter-btns">
<button class="filter-btn active" onclick="setFilter('all', this)">All</button>
<button class="filter-btn" onclick="setFilter('new', this)">🟢 New Only</button>
<button class="filter-btn" onclick="setFilter('adapt', this)">🟡 Adapted</button>
<button class="filter-btn" onclick="setFilter('existing', this)">🔵 Existing</button>
</div>
<div class="glossary-grid" id="glossaryGrid">
<!-- ===== NEW TERMS ===== -->
<div class="glossary-item new" data-type="new" data-name="sticky cell">
<div class="glossary-icon">🍬</div>
<div class="glossary-content">
<h3>Sticky Cell <span class="badge badge-new">New</span></h3>
<p>A grid cell hit by the Candy Cannon that becomes impassable. Players stepping onto or pushed into a sticky cell are <em>trapped</em>. Remains until cleansed or round ends. Rendered as Layer 2 overlay (pink translucent mesh, ID 17).</p>
<span class="code-ref">TILE_STICKY = 17 → GridMap Layer 2</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="telegraph">
<div class="glossary-icon"></div>
<div class="glossary-content">
<h3>Telegraph <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">TILE_TELEGRAPH = 18 → rpc("sync_telegraph")</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="candy cannon">
<div class="glossary-icon">💥</div>
<div class="glossary-content">
<h3>Candy Cannon <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">CandyCannonController → candy_cannon.tscn</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="volley">
<div class="glossary-icon">🎯</div>
<div class="glossary-content">
<h3>Volley <span class="badge badge-new">New</span></h3>
<p>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).</p>
<span class="code-ref">_fire_volley() → cannon_interval = 5.0</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="impact size">
<div class="glossary-icon">📐</div>
<div class="glossary-content">
<h3>Impact Size <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">phase_weights[phase_idx]["2x2"]</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="trapped">
<div class="glossary-icon">🪤</div>
<div class="glossary-content">
<h3>Trapped <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">trapped_players: Dict → rpc("sync_trapped")</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="cleanser">
<div class="glossary-icon"></div>
<div class="glossary-content">
<h3>Cleanser <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">player_cleansers[peer_id] → GoalsCycleManager signal</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="clash">
<div class="glossary-icon">💫</div>
<div class="glossary-content">
<h3>Clash <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">clash detection → 0.5s window, server authority</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="charged state">
<div class="glossary-icon">🔋</div>
<div class="glossary-content">
<h3>Charged State <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">smack_charged[player_id] → 3.0s window</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="anti-unfairness">
<div class="glossary-icon">⚖️</div>
<div class="glossary-content">
<h3>Anti-Unfairness <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">last_targeted_player_id → EnhancedGridMap.initialize_astar()</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="route blocking">
<div class="glossary-icon">🚧</div>
<div class="glossary-content">
<h3>Route Blocking <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">_get_route_blocking_target() → 25% weight</span>
</div>
</div>
<div class="glossary-item new" data-type="new" data-name="gauntlet arena">
<div class="glossary-icon">🏟️</div>
<div class="glossary-content">
<h3>Gauntlet Arena <span class="badge badge-new">New</span></h3>
<p>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.</p>
<span class="code-ref">ARENA_SIZE = 20 → gauntlet.tscn</span>
</div>
</div>
<!-- ===== ADAPTED TERMS ===== -->
<div class="glossary-item adapt" data-type="adapt" data-name="smack">
<div class="glossary-icon">👊</div>
<div class="glossary-content">
<h3>Smack <span class="badge badge-adapt">Adapted</span></h3>
<p>Gauntlet-specific melee push. Adapts existing <code>try_push()</code> 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.</p>
<span class="code-ref">PlayerMovementManager.try_push() → smack_cooldowns</span>
</div>
</div>
<div class="glossary-item adapt" data-type="adapt" data-name="phase">
<div class="glossary-icon">⏱️</div>
<div class="glossary-content">
<h3>Phase <span class="badge badge-adapt">Adapted</span></h3>
<p>Three escalation phases in Gauntlet: <strong>Open Arena</strong> (060s), <strong>Route Pressure</strong> (60120s), <strong>Survival Endgame</strong> (120180s). Adapts StopNGoManager's Go/Stop phase pattern but uses time-elapsed triggers instead of cycle signals.</p>
<span class="code-ref">enum Phase { OPEN_ARENA, ROUTE_PRESSURE, SURVIVAL_ENDGAME }</span>
</div>
</div>
<div class="glossary-item adapt" data-type="adapt" data-name="bot ai cannon avoidance">
<div class="glossary-icon">🤖</div>
<div class="glossary-content">
<h3>Bot AI — Cannon Avoidance <span class="badge badge-adapt">Adapted</span></h3>
<p>Extends BotStrategicPlanner with Gauntlet-specific logic: telegraph awareness, sticky path planning, safe-zone pathfinding. Adapts existing bot movement heuristics to factor in shrinking arena.</p>
<span class="code-ref">BotStrategicPlanner → new evaluate_gauntlet()</span>
</div>
</div>
<!-- ===== EXISTING TERMS ===== -->
<div class="glossary-item existing" data-type="existing" data-name="attack mode">
<div class="glossary-icon">⚔️</div>
<div class="glossary-content">
<h3>Attack Mode <span class="badge badge-existing">Existing</span></h3>
<p>Existing player state toggled via PowerUpManager when boost bar is full. In Gauntlet, <em>not used directly</em> — replaced by Smack mechanic. The push physics from <code>try_push()</code> are reused but the activation logic differs.</p>
<span class="code-ref">PowerUpManager.is_attack_mode → NOT used in Gauntlet</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="stagger">
<div class="glossary-icon">😵</div>
<div class="glossary-content">
<h3>Stagger <span class="badge badge-existing">Existing</span></h3>
<p>Existing 1.5s movement disable after being push-attacked. Gauntlet's Smack uses a shorter 1.0s stun, but the underlying <code>apply_stagger()</code> function is reused with a duration parameter.</p>
<span class="code-ref">PlayerMovementManager.apply_stagger(duration)</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="mission / goals">
<div class="glossary-icon">🎯</div>
<div class="glossary-content">
<h3>Mission / Goals <span class="badge badge-existing">Existing</span></h3>
<p>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).</p>
<span class="code-ref">GoalManager → GoalsCycleManager.goal_count_updated</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="layer 2 overlay">
<div class="glossary-icon">🗂️</div>
<div class="glossary-content">
<h3>Layer 2 Overlay <span class="badge badge-existing">Existing</span></h3>
<p>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.</p>
<span class="code-ref">GridMap.set_cell_item(Vector3i(x, 2, z), id)</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="try push">
<div class="glossary-icon">🫸</div>
<div class="glossary-content">
<h3>try_push() <span class="badge badge-existing">Existing</span></h3>
<p>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.</p>
<span class="code-ref">PlayerMovementManager.try_push(target, direction)</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="screen shake">
<div class="glossary-icon">📳</div>
<div class="glossary-content">
<h3>Screen Shake <span class="badge badge-existing">Existing</span></h3>
<p>Camera shake effect triggered via RPC. Used on cannon impact with "medium" intensity. Already implemented system-wide.</p>
<span class="code-ref">player.rpc("trigger_screen_shake", "medium")</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="tekton projectile">
<div class="glossary-icon">🎪</div>
<div class="glossary-content">
<h3>Tekton Projectile <span class="badge badge-existing">Existing</span></h3>
<p>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.</p>
<span class="code-ref">tekton.gd → spawn_projectile_rpc(target, duration)</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="rpc sync pattern">
<div class="glossary-icon">📡</div>
<div class="glossary-content">
<h3>RPC Sync Pattern <span class="badge badge-existing">Existing</span></h3>
<p>Server-authoritative state sync via <code>@rpc("authority", "call_local", "reliable")</code>. All Gauntlet state changes (sticky, phase, trap, cleanser) use this identical pattern.</p>
<span class="code-ref">@rpc("authority", "call_local", "reliable")</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="timed match">
<div class="glossary-icon"></div>
<div class="glossary-content">
<h3>Timed Match <span class="badge badge-existing">Existing</span></h3>
<p>Global match timer from GoalsCycleManager. Gauntlet passes 180s duration. System handles countdown, HUD timer, and match-end trigger.</p>
<span class="code-ref">goals_cycle_manager.start_match(180.0)</span>
</div>
</div>
<div class="glossary-item existing" data-type="existing" data-name="special tiles manager">
<div class="glossary-icon">💎</div>
<div class="glossary-content">
<h3>SpecialTilesManager <span class="badge badge-existing">Existing</span></h3>
<p>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.</p>
<span class="code-ref">SpecialTilesManager.inventory → mode-based restrictions</span>
</div>
</div>
</div>
</section>
<!-- ============ ARCHITECTURE ============ -->
<section id="architecture">
<div class="section-header">
<h2><span class="icon" style="background:rgba(99,102,241,0.12)">🏗️</span> Architecture</h2>
<p>How GauntletManager slots into the existing manager tree, following the StopNGoManager pattern exactly.</p>
</div>
<div class="arch-diagram">
<div class="arch-tree">
<span class="node">main.gd</span><br>
<span class="connector">├──</span> <span class="node">_init_managers()</span> <span class="label">← instantiate GauntletManager</span><br>
<span class="connector">├──</span> <span class="node">_setup_host_game()</span> <span class="label">← arena setup branch</span><br>
<span class="connector">├──</span> <span class="node">_start_game()</span> <span class="label">← start_game_mode() call</span><br>
<span class="connector"></span><br>
<span class="new-node">GauntletManager</span> <span class="tag-new">NEW</span><br>
<span class="connector">├──</span> <span class="new-node">_setup_arena()</span> <span class="label">← 20×20 grid, center 3×3 NPC zone</span><br>
<span class="connector">├──</span> <span class="new-node">_setup_hud()</span> <span class="label">← mission label, cleanser indicator</span><br>
<span class="connector">├──</span> <span class="new-node">start_game_mode()</span> <span class="label">← start cannon timer, spawn tiles</span><br>
<span class="connector">├──</span> <span class="new-node">_process()</span> <span class="label">← cannon volley timer, phase escalation</span><br>
<span class="connector">├──</span> <span class="new-node">CandyCannonController</span> <span class="tag-new">NEW</span> <span class="label">← targeting, volley fire</span><br>
<span class="connector">├──</span> <span class="new-node">StickyCell system</span> <span class="tag-new">NEW</span> <span class="label">← Layer 2 overlay, trap logic</span><br>
<span class="connector">├──</span> <span class="new-node">Cleanser system</span> <span class="tag-new">NEW</span> <span class="label">← powerup via missions</span><br>
<span class="connector">├──</span> <span class="new-node">Smack system</span> <span class="tag-new">NEW</span> <span class="label">← modified push with charge/cooldown</span><br>
<span class="connector">└──</span> <span class="new-node">Win condition</span> <span class="label">← highest score at timer end</span><br>
</div>
</div>
</section>
<!-- ============ REUSE MAP ============ -->
<section id="reuse">
<div class="section-header">
<h2><span class="icon" style="background:rgba(52,211,153,0.12)">♻️</span> Reuse Map</h2>
<p>How each GDD feature maps to existing systems — showing what's reused vs what's new.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th>GDD Feature</th><th>Existing System</th><th>Reuse</th><th>New Work</th></tr>
</thead>
<tbody>
<tr><td>Game Mode Registration</td><td class="td-code">GameMode.gd + LobbyManager</td><td><span class="reuse-direct">Direct</span></td><td>Add enum + strings</td></tr>
<tr><td>20×20 Arena</td><td class="td-code">StopNGoManager._setup_arena()</td><td><span class="reuse-heavy">Heavy</span></td><td>Custom layout, same API</td></tr>
<tr><td>Tile Collection / Scoring</td><td class="td-code">GoalsCycleManager</td><td><span class="reuse-direct">Direct</span></td><td>Reuse as-is</td></tr>
<tr><td>Mission System</td><td class="td-code">GoalManager + goals_cycle_manager</td><td><span class="reuse-direct">Direct</span></td><td>Same 3×3 pattern matching</td></tr>
<tr><td>Timed Match</td><td class="td-code">GoalsCycleManager.start_match()</td><td><span class="reuse-direct">Direct</span></td><td>Pass 180s duration</td></tr>
<tr><td>Player Movement</td><td class="td-code">PlayerMovementManager</td><td><span class="reuse-direct">Direct</span></td><td>No changes</td></tr>
<tr><td>Powerup System</td><td class="td-code">SpecialTilesManager</td><td><span class="reuse-partial">Partial</span></td><td>Cleanser = new type</td></tr>
<tr><td>Smack Mechanic</td><td class="td-code">PlayerMovementManager.try_push()</td><td><span class="reuse-adapt">Adapt</span></td><td>Modified push rules</td></tr>
<tr><td>Candy Cannon NPC</td><td class="td-code">tekton.gd + TektonController</td><td><span class="reuse-pattern">Pattern</span></td><td>New NPC, reuses projectile</td></tr>
<tr><td>Sticky Cells</td><td class="td-code">StopNGoManager safe zone overlay</td><td><span class="reuse-pattern">Pattern</span></td><td>New tile type, same layer</td></tr>
<tr><td>Telegraph VFX</td><td class="td-code">VFXManager / animation.gd</td><td><span class="reuse-pattern">Pattern</span></td><td>New animations, same system</td></tr>
<tr><td>HUD</td><td class="td-code">StopNGoManager._setup_hud()</td><td><span class="reuse-direct">Direct</span></td><td>Mode-specific labels</td></tr>
<tr><td>Network Sync</td><td class="td-code">RPC patterns</td><td><span class="reuse-direct">Direct</span></td><td>Same patterns</td></tr>
<tr><td>Lobby Settings</td><td class="td-code">LobbyManager signal/sync</td><td><span class="reuse-direct">Direct</span></td><td>Gauntlet settings</td></tr>
<tr><td>Bot AI</td><td class="td-code">BotController + BotStrategicPlanner</td><td><span class="reuse-adapt">Adapt</span></td><td>Cannon avoidance strategy</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ PHASES ============ -->
<section id="phases">
<div class="section-header">
<h2><span class="icon" style="background:rgba(251,191,36,0.12)">🌊</span> Phase Timeline</h2>
<p>Three escalation phases that control cannon intensity and impact size distribution.</p>
</div>
<div class="timeline">
<div class="phase-card phase-1">
<div class="phase-time">0:00 — 1:00</div>
<h3>Open Arena</h3>
<ul>
<li>Collect tiles, learn the mission</li>
<li>Slow candy pressure</li>
<li>1×1 shots: <strong>60%</strong></li>
<li>1×2 shots: <strong>40%</strong></li>
<li>2×2 shots: <strong>0%</strong></li>
<li>~60 impacts total</li>
</ul>
</div>
<div class="phase-card phase-2">
<div class="phase-time">1:00 — 2:00</div>
<h3>Route Pressure</h3>
<ul>
<li>Candy shapes arena topology</li>
<li>Smack becomes dangerous</li>
<li>1×1 shots: <strong>30%</strong></li>
<li>1×2 shots: <strong>55%</strong></li>
<li>2×2 shots: <strong>15%</strong></li>
<li>Cleanser used strategically</li>
</ul>
</div>
<div class="phase-card phase-3">
<div class="phase-time">2:00 — 3:00</div>
<h3>Survival Endgame</h3>
<ul>
<li>~80% arena is sticky</li>
<li>Safe zones limited, high tension</li>
<li>1×1 shots: <strong>15%</strong></li>
<li>1×2 shots: <strong>55%</strong></li>
<li>2×2 shots: <strong>30%</strong></li>
<li>Aggressive route-blocking allowed</li>
</ul>
</div>
</div>
</section>
<!-- ============ CORE SYSTEMS ============ -->
<section id="systems">
<div class="section-header">
<h2><span class="icon" style="background:rgba(168,85,247,0.12)">⚙️</span> Core Systems</h2>
<p>Deep-dive into the four new systems and how they integrate.</p>
</div>
<!-- Sticky Cell -->
<div class="card" style="margin-bottom: 16px;">
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">🍬 Sticky Cell System</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Feature</th><th>Implementation</th></tr></thead>
<tbody>
<tr><td>Visual</td><td>Layer 2 overlay — transparent candy-pink mesh (ID 17)</td></tr>
<tr><td>Movement Block</td><td class="td-code">PlayerMovementManager.simple_move_to() — add sticky check alongside wall check</td></tr>
<tr><td>Trap on Step</td><td class="td-code">GauntletManager._check_player_on_sticky() in _process()</td></tr>
<tr><td>Trap on Push</td><td class="td-code">PlayerMovementManager.try_push() — check landing cell</td></tr>
<tr><td>Cleanser Bypass</td><td>Temporary flag (like <code>is_invisible</code> wall bypass)</td></tr>
<tr><td>Network Sync</td><td class="td-code">main.rpc("sync_grid_item", x, 2, z, 17)</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Smack -->
<div class="card" style="margin-bottom: 16px;">
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">👊 Smack vs Attack Mode</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Property</th><th>Current Attack Mode</th><th>Gauntlet Smack</th></tr></thead>
<tbody>
<tr><td>Charge Source</td><td>Boost bar fills to 100</td><td>8s auto-refill cooldown</td></tr>
<tr><td>Activation</td><td>Toggle <code>is_attack_mode</code></td><td>3s charged window (pink model)</td></tr>
<tr><td>Push Distance</td><td>3 cells backward</td><td>3 cells in push direction</td></tr>
<tr><td>Stagger Duration</td><td>1.5s <code>apply_stagger()</code></td><td>1.0s stun</td></tr>
<tr><td>Sticky Landing</td><td>N/A</td><td>Trapped on first sticky cell</td></tr>
<tr><td>Clash Rule</td><td>N/A</td><td>Both stunned, no push, bars consumed</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Cleanser -->
<div class="card" style="margin-bottom: 16px;">
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">✨ Cleanser Power-Up</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Property</th><th>Value</th></tr></thead>
<tbody>
<tr><td>Unlock Trigger</td><td class="td-code">GoalsCycleManager.goal_count_updated → count % 2 == 0</td></tr>
<tr><td>Storage</td><td class="td-code">GauntletManager.player_cleansers[peer_id] = 1</td></tr>
<tr><td>Effect</td><td>5 cells movement through sticky — crossed cells become passable</td></tr>
<tr><td>Sync</td><td class="td-code">rpc("sync_cleanser_state", peer_id, count)</td></tr>
<tr><td>Clear Sticky</td><td class="td-code">main.rpc("sync_grid_item", x, 2, z, -1)</td></tr>
<tr><td>Inventory Limit</td><td>1 per player</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Cannon Targeting -->
<div class="card">
<h3 style="margin-bottom:16px; display:flex; align-items:center; gap:8px;">🎯 Cannon Targeting Intelligence</h3>
<div class="table-wrap">
<table>
<thead><tr><th>Roll %</th><th>Target Strategy</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td><strong>60%</strong></td><td>Near a player (not same as last)</td><td>Direct pressure</td></tr>
<tr><td><strong>25%</strong></td><td>Route-blocking bottleneck</td><td>Cut escape paths</td></tr>
<tr><td><strong>10%</strong></td><td>Random non-sticky area</td><td>Spread coverage</td></tr>
<tr><td><strong>5%</strong></td><td>Previously sticky / chaos</td><td>Unpredictability</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ============ FILES ============ -->
<section id="files">
<div class="section-header">
<h2><span class="icon" style="background:rgba(34,211,238,0.12)">📁</span> File Changes</h2>
</div>
<h3 style="margin-bottom:12px; font-size:14px; color:var(--accent-green);">New Files</h3>
<div class="file-grid" style="margin-bottom:28px">
<div class="file-card new-file">
<div class="file-icon">📜</div>
<div class="file-info"><h4>gauntlet_manager.gd</h4><p>Core mode logic, phases, sticky cells, cleanser, smack</p></div>
</div>
<div class="file-card new-file">
<div class="file-icon">📜</div>
<div class="file-info"><h4>candy_cannon_controller.gd</h4><p>Cannon targeting, volley fire, telegraph</p></div>
</div>
<div class="file-card new-file">
<div class="file-icon">🎬</div>
<div class="file-info"><h4>gauntlet.tscn</h4><p>3D arena environment scene</p></div>
</div>
<div class="file-card new-file">
<div class="file-icon">🎬</div>
<div class="file-info"><h4>candy_cannon.tscn</h4><p>Candy Cannon NPC (3×3, static)</p></div>
</div>
</div>
<h3 style="margin-bottom:12px; font-size:14px; color:var(--accent-yellow);">Modified Files</h3>
<div class="file-grid">
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>game_mode.gd</h4><p>Add GAUNTLET = 3 enum, string mappings</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>lobby_manager.gd</h4><p>Mode list, gauntlet settings, area mapping</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>main.gd</h4><p>Manager init, arena setup branch, start branch</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>player_movement_manager.gd</h4><p>Sticky check in move + push</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>goals_cycle_manager.gd</h4><p>Cleanser grant on 2nd goal</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>special_tiles_manager.gd</h4><p>Gauntlet powerup restrictions</p></div>
</div>
<div class="file-card mod-file">
<div class="file-icon">✏️</div>
<div class="file-info"><h4>MeshLibrary .tres</h4><p>Add TILE_STICKY (17) and TILE_TELEGRAPH (18)</p></div>
</div>
</div>
</section>
<!-- ============ NETWORK ============ -->
<section id="network">
<div class="section-header">
<h2><span class="icon" style="background:rgba(248,113,113,0.12)">📡</span> Network Sync</h2>
<p>All sync follows existing RPC patterns — no new networking paradigms needed.</p>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Data</th><th>Sync Method</th><th>Existing Pattern</th></tr></thead>
<tbody>
<tr><td>Sticky Cells</td><td class="sync-method">main.rpc("sync_grid_item", x, 2, z, 17)</td><td>Safe zone / freeze overlay</td></tr>
<tr><td>Telegraph</td><td class="sync-method">rpc("sync_telegraph", targets_array)</td><td>StopNGoManager.sync_phase()</td></tr>
<tr><td>Phase Changes</td><td class="sync-method">rpc("sync_gauntlet_phase", idx, elapsed)</td><td>StopNGoManager.sync_phase()</td></tr>
<tr><td>Trap State</td><td class="sync-method">player.rpc("sync_trapped", true)</td><td>player.rpc("sync_stop_freeze")</td></tr>
<tr><td>Cleanser Grant</td><td class="sync-method">rpc("sync_cleanser", peer_id, count)</td><td>goals_cycle_manager.sync_goal_count()</td></tr>
<tr><td>Smack State</td><td class="sync-method">player.rpc("sync_smack_state", charged)</td><td>player.rpc("sync_modulate")</td></tr>
<tr><td>Cannon NPC</td><td colspan="2" style="color:var(--text-muted)">Static scene — no movement sync needed</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ PRIORITY ============ -->
<section id="priority">
<div class="section-header">
<h2><span class="icon" style="background:rgba(251,191,36,0.12)">📋</span> Implementation Priority</h2>
</div>
<div class="card">
<div class="priority-list">
<div class="priority-item"><div class="priority-num">1</div><div class="priority-info"><h4>Game Mode Registration</h4><p>game_mode.gd, lobby_manager.gd, main.gd</p></div></div>
<div class="priority-item"><div class="priority-num">2</div><div class="priority-info"><h4>Arena Setup</h4><p>gauntlet_manager._setup_arena(), 20×20 grid</p></div></div>
<div class="priority-item"><div class="priority-num">3</div><div class="priority-info"><h4>Tile Spawning</h4><p>StopNGoManager._spawn_mission_tiles() pattern</p></div></div>
<div class="priority-item"><div class="priority-num">4</div><div class="priority-info"><h4>Cannon Timer + Volley</h4><p>5s interval, 5 shots, 1×1 only</p></div></div>
<div class="priority-item"><div class="priority-num">5</div><div class="priority-info"><h4>Sticky Cell System</h4><p>Layer 2 overlay, movement block, trap detection</p></div></div>
<div class="priority-item"><div class="priority-num">6</div><div class="priority-info"><h4>Telegraph VFX</h4><p>Warning glow → impact transition</p></div></div>
<div class="priority-item"><div class="priority-num">7</div><div class="priority-info"><h4>Impact Sizes</h4><p>1×2 and 2×2 shapes, phase weights</p></div></div>
<div class="priority-item"><div class="priority-num">8</div><div class="priority-info"><h4>Smack Mechanic</h4><p>Modified push with cooldown/charge</p></div></div>
<div class="priority-item"><div class="priority-num">9</div><div class="priority-info"><h4>Cleanser</h4><p>Unlock tracking, sticky bypass</p></div></div>
<div class="priority-item"><div class="priority-num">10</div><div class="priority-info"><h4>Targeting Intelligence</h4><p>Player proximity, route blocking, anti-unfairness</p></div></div>
<div class="priority-item"><div class="priority-num">11</div><div class="priority-info"><h4>Bot AI</h4><p>Cannon avoidance, sticky path planning</p></div></div>
<div class="priority-item"><div class="priority-num">12</div><div class="priority-info"><h4>Polish</h4><p>VFX, SFX, HUD animations, 3D scene</p></div></div>
</div>
</div>
</section>
<!-- ============ RISKS ============ -->
<section id="risks">
<div class="section-header">
<h2><span class="icon" style="background:rgba(248,113,113,0.12)">⚠️</span> Risk Assessment</h2>
</div>
<div class="risk-grid">
<div class="risk-card">
<div class="risk-indicator">🗂️</div>
<div class="risk-content">
<h4>Layer 2 Conflict</h4>
<p>GridMap Layer 2 used by freeze/safe overlays. <strong>Mitigated:</strong> Gauntlet mode is exclusive — no freeze/safe tiles exist.</p>
</div>
</div>
<div class="risk-card">
<div class="risk-indicator">📊</div>
<div class="risk-content">
<h4>20×20 Grid Performance</h4>
<p>400 cells + overlays. <strong>Mitigated:</strong> Existing 23×12 and 14×14 arenas work fine; 20×20 comparable.</p>
</div>
</div>
<div class="risk-card">
<div class="risk-indicator">🚫</div>
<div class="risk-content">
<h4>Impossible Arenas</h4>
<p>Cannon could seal all paths. <strong>Mitigated:</strong> AStar pathfinding check before each volley.</p>
</div>
</div>
<div class="risk-card">
<div class="risk-indicator">🔢</div>
<div class="risk-content">
<h4>MeshLibrary ID Collision</h4>
<p>IDs 1718 might exist. <strong>Mitigated:</strong> Verify max ID in .tres before adding.</p>
</div>
</div>
<div class="risk-card">
<div class="risk-indicator">⏱️</div>
<div class="risk-content">
<h4>Smack Clash Timing</h4>
<p>Network latency affects clash detection. <strong>Mitigated:</strong> Server-authoritative timestamp, 0.5s window.</p>
</div>
</div>
</div>
</section>
</div>
<footer>
<div class="wrapper">
Tekton Dash — Candy Cannon Survival Technical Docs · Generated from gauntlet-technical-implementation.md
</div>
</footer>
<button class="scroll-top" id="scrollTop" onclick="window.scrollTo({top:0,behavior:'smooth'})"></button>
<script>
// Scroll-to-top button
window.addEventListener('scroll', () => {
document.getElementById('scrollTop').classList.toggle('show', window.scrollY > 400);
});
// Active nav link
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('.nav-link');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(s => {
if (window.scrollY >= s.offsetTop - 120) current = s.id;
});
navLinks.forEach(l => {
l.classList.toggle('active', l.getAttribute('href') === '#' + current);
});
});
// Glossary filter
let activeFilter = 'all';
function setFilter(type, btn) {
activeFilter = type;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
filterGlossary();
}
function filterGlossary() {
const q = document.getElementById('glossarySearch').value.toLowerCase();
document.querySelectorAll('.glossary-item').forEach(item => {
const name = item.dataset.name;
const type = item.dataset.type;
const text = item.textContent.toLowerCase();
const matchFilter = activeFilter === 'all' || type === activeFilter;
const matchSearch = !q || text.includes(q);
item.style.display = matchFilter && matchSearch ? '' : 'none';
});
}
</script>
</body>
</html>
+391
View File
@@ -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) |
+234
View File
@@ -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 | 48 |
| 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
- 68 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:001:00 | 5/volley | Mostly 1×1, 1×2 | Slow arena pressure |
| 1:002:00 | 5/volley | Mostly 1×2, some 2×2 | Route cutting |
| 2:003:00 | 5/volley | More 2×2 | Strong endgame pressure |
| Phase | 1×1 | 1×2 | 2×2 |
|---|---|---|---|
| 0:001:00 | 60% | 40% | 0% |
| 1:002:00 | 30% | 55% | 15% |
| 2:003: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:001:00)
- Collect tiles, understand mission
- Slow candy pressure (1×1, 1×2 mostly)
### Phase 2 — Route Pressure (1:002: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:003: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