diff --git a/src/game.ts b/src/game.ts
index 495a5a7..a0ea3d0 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -92,6 +92,22 @@ function buildBreakfastRush(world: World, blob: BlobHandle, skin: PaintSkin): Fi
splats: 2, count: 6, area: [5, 1.0, 2.5], drift: [0, 0.3, 0], bubbleRadius: 0.6,
})
+ // ---- ORANGE bubbles on the high shelf, just before the gap-jump lip.
+ // BOUNCE (jumpMul) is the buff, and this is the one spot in the game where
+ // jump height IS the problem — pop a couple and you clear the gap comfortably
+ // instead of relying on the recovery trampoline. Deliberately opt-in and
+ // deliberately costly: they're sitting in the GREEN grip zone, so grabbing
+ // orange dilutes your green. Route for the jump or route for the grip. ----
+ // impulse 1, not the default 3: at full strength the pops stacked and launched
+ // the blob 3.3u above the shelf right at the gap lip, which throws off a jump
+ // this course is finely tuned around. Here the bubbles are a paint pickup with
+ // a nudge — the BOUNCE buff is the payoff, not the boop.
+ createPaintBubbles(world, {
+ id: 'bubbles-gap', position: [0, 4.4, 5], color: 'orange', radius: 0.6,
+ splats: 2, count: 5, area: [3.5, 0.7, 2], drift: [0, 0.25, 0],
+ bubbleRadius: 0.55, impulse: 1,
+ })
+
// ---- paint-mines: first-through floor panels on the centre line ----
// REWARD: roll over it and it launches you forward + coats you GREEN (GRIP) —
// a shortcut booster for the runner who takes the middle. HAZARD: further down,
diff --git a/src/paint/coverage-math.test.ts b/src/paint/coverage-math.test.ts
index ac89e12..3fc0b26 100644
--- a/src/paint/coverage-math.test.ts
+++ b/src/paint/coverage-math.test.ts
@@ -9,6 +9,7 @@ import type { CoverageReport, PaintColor } from '../contracts'
import {
ACTIVATE,
SUPER,
+ BOUNCE_JUMP,
MEGA_SIZE,
MINI_SIZE,
MEGA_MASS,
@@ -88,6 +89,7 @@ near(buffStrength(0.45), 0.15 + 0.85 * 0.5, 'strength ramps linearly at midpoint
near(clean.massMul, 1, 'clean massMul 1')
near(clean.glow, 0, 'clean no glow')
near(clean.size, 1, 'clean size 1 (no grow/shrink in MVP)')
+ near(clean.jumpMul, 1, 'clean jumpMul 1')
}
{
// RED just under activation: no buff yet
@@ -113,6 +115,21 @@ near(buffStrength(0.45), 0.15 + 0.85 * 0.5, 'strength ramps linearly at midpoint
const m = computeModifiers(cov({ blue: 0.25 }))
ok(m.waterproof === true, 'blue 25% waterproof')
}
+{
+ // ORANGE = BOUNCE. Guards the wire that was dead for a whole release: jumpMul
+ // is plumbed controller<-BuffSystem<-here, so a regression to a constant 1
+ // silently removes the buff rather than failing anywhere visible.
+ near(computeModifiers(cov({ orange: 0.19 })).jumpMul, 1, 'orange 19% → no bounce yet')
+ const s = 0.15 + 0.85 * 0.5 // buffStrength(0.45)
+ near(
+ computeModifiers(cov({ orange: 0.45 })).jumpMul,
+ 1 + BOUNCE_JUMP * s,
+ 'orange 45% jumpMul tracks BOUNCE_JUMP',
+ )
+ const sup = computeModifiers(cov({ orange: SUPER }))
+ near(sup.jumpMul, 1 + BOUNCE_JUMP, 'orange 70% jumpMul maxed')
+ near(sup.glow, 1, 'orange 70% is super (glow)')
+}
{
// massMul clamps at 1.8 when fully painted (no purple/pink → base only)
const m = computeModifiers(cov({ red: 0.5, blue: 0.5 }))
diff --git a/src/paint/coverage-math.ts b/src/paint/coverage-math.ts
index 21ad760..30a3b75 100644
--- a/src/paint/coverage-math.ts
+++ b/src/paint/coverage-math.ts
@@ -54,6 +54,9 @@ export const SUPER = 0.70 // ≥70% one colour = super state
/** Strength floor at the activation threshold so the buff visibly "kicks in". */
const ACTIVATE_FLOOR = 0.15
+/** Extra take-off speed at full ORANGE strength: jumpMul 1.0 → 1.5 (BOUNCE). */
+export const BOUNCE_JUMP = 0.5
+
// ---- Scale buffs (GDD §5.3 / §6): purple MEGA + pink MINI (Lane G) ---------
// Both drive `modifiers.size` (the controller scales collider + visuals) and
// bias `massMul` on top of the paint=weight base. They oppose each other: a blob
@@ -112,6 +115,7 @@ export function dominantColor(cov: CoverageReport): PaintColor | null {
/**
* The core mapping: coverage -> modifiers (GDD §6).
* RED -> speedMul up to 1.6 (BURN)
+ * ORANGE -> jumpMul up to 1.5 (BOUNCE — rubberized, §6)
* GREEN -> grip up to 1.0 (GRIP)
* BLUE -> waterproof (SLICK)
* PURPLE -> size up to 1.45 + extra mass (MEGA — grow big & heavy, §5.3)
@@ -131,6 +135,10 @@ export function computeModifiers(cov: CoverageReport): BlobModifiers {
const speedMul = 1 + 0.6 * buffStrength(red)
const grip = buffStrength(green) // 0..1
const waterproof = blue >= ACTIVATE
+ // ORANGE = BOUNCE (GDD §6): rubberized, jumps higher. `jumpMul` was already
+ // plumbed end-to-end (controller multiplies take-off speed by it, BuffSystem
+ // copies it) but nothing ever drove it — orange is the colour that does.
+ const jumpMul = 1 + BOUNCE_JUMP * buffStrength(cov.byColor.orange)
// ---- scale (MEGA vs MINI) ----
// net grow/shrink strength; purple and pink oppose so a mixed blob nets out.
@@ -147,7 +155,7 @@ export function computeModifiers(cov: CoverageReport): BlobModifiers {
return {
speedMul,
- jumpMul: 1,
+ jumpMul,
massMul,
grip,
size,
diff --git a/src/paint/hud.ts b/src/paint/hud.ts
index ba04fa6..883f428 100644
--- a/src/paint/hud.ts
+++ b/src/paint/hud.ts
@@ -103,6 +103,7 @@ export class PaintHUD {
private buffText(cov: CoverageReport, mods: BlobModifiers): string {
const parts: string[] = []
if (cov.byColor.red >= ACTIVATE) parts.push(`BURN ×${mods.speedMul.toFixed(2)}`)
+ if (cov.byColor.orange >= ACTIVATE) parts.push(`BOUNCE ×${mods.jumpMul.toFixed(2)}`)
if (cov.byColor.green >= ACTIVATE) parts.push(`GRIP ${mods.grip.toFixed(2)}`)
if (cov.byColor.blue >= ACTIVATE) parts.push(`SLICK waterproof`)
if (cov.byColor.purple >= ACTIVATE) parts.push(`MEGA ×${mods.size.toFixed(2)}`)
diff --git a/src/paint/index.ts b/src/paint/index.ts
index 51a27c8..bc98fc3 100644
--- a/src/paint/index.ts
+++ b/src/paint/index.ts
@@ -23,6 +23,7 @@ export {
dominantColor,
ACTIVATE,
SUPER,
+ BOUNCE_JUMP,
} from './coverage-math'
export interface PaintEventTarget {