feat(paint): ORANGE = BOUNCE — plug in the buff that was wired but dead

`jumpMul` has been plumbed end-to-end since Lane A — the controller multiplies
take-off speed by it, BuffSystem copies it every step — but computeModifiers
hardcoded `jumpMul: 1`, so a finished mechanic sat unplugged and 2 of the 7
palette colours (orange, yellow) had neither an effect nor a single source in
any course. Orange now drives it: jumpMul 1.0 -> 1.5 at full strength
(BOUNCE_JUMP, GDD §6 "rubberized"), same buffStrength curve as every other buff.

Paint source: orange bubbles on Breakfast Rush's high shelf, right before the
gap-jump lip — the one place in the game where jump height IS the problem, so
the buff has an obvious purpose instead of being decoration. Deliberately
opt-in and deliberately costly: they sit inside the GREEN grip zone, so
grabbing orange dilutes your green. Route for the jump or route for the grip.

Tuned during verification: the field uses impulse 1, not the default 3 —
at full strength the pops stacked and launched the blob 3.3u above the shelf
right at the lip, throwing off a jump this course is finely tuned around.

Verified end-to-end: orange 61% -> jumpMul 1.424 (matches buffStrength exactly),
take-off 7.10 -> 10.30, jump height 1.95 -> 3.87 (nearly double); HUD reads
"BOUNCE ×1.41" in orange; rolling the shelf pops 4 bubbles per pass. New test
assertions cover the 19%/45%/70% curve AND pin it to BOUNCE_JUMP, so a
regression to a constant 1 fails loudly instead of silently removing the buff
(53 assertions, 10 suites green).

Palette now 6/7 usable. Yellow ZAP is the remaining gap and is real design work
(triggering machinery at range), not a one-liner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 23:08:51 +10:00
parent ed1c3af17a
commit 2777a580c9
5 changed files with 44 additions and 1 deletions

View File

@ -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,

View File

@ -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 }))

View File

@ -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,

View File

@ -103,6 +103,7 @@ export class PaintHUD {
private buffText(cov: CoverageReport, mods: BlobModifiers): string {
const parts: string[] = []
if (cov.byColor.red >= ACTIVATE) parts.push(`<b style="color:${PALETTE.red}">BURN</b> ×${mods.speedMul.toFixed(2)}`)
if (cov.byColor.orange >= ACTIVATE) parts.push(`<b style="color:${PALETTE.orange}">BOUNCE</b> ×${mods.jumpMul.toFixed(2)}`)
if (cov.byColor.green >= ACTIVATE) parts.push(`<b style="color:${PALETTE.green}">GRIP</b> ${mods.grip.toFixed(2)}`)
if (cov.byColor.blue >= ACTIVATE) parts.push(`<b style="color:${PALETTE.blue}">SLICK</b> waterproof`)
if (cov.byColor.purple >= ACTIVATE) parts.push(`<b style="color:${PALETTE.purple}">MEGA</b> ×${mods.size.toFixed(2)}`)

View File

@ -23,6 +23,7 @@ export {
dominantColor,
ACTIVATE,
SUPER,
BOUNCE_JUMP,
} from './coverage-math'
export interface PaintEventTarget {