games / launchpad
I built a mobile web-based vertical platformer, Launchpad.
The game has a startup journey metaphor. You pilot a rocket upward, bouncing off platforms. Normal platforms decay and break after two bounces, showing a startup pitfall message when they shatter. Boost platforms are branded with the IVP logo and give extra bounce force. Miss a platform and fall — game over.
Architecture
The architecture is the same as Duck Duck Bay: a Go game engine compiled to WebAssembly, a Go HTTP server for static assets and templates, and vanilla HTML, CSS, and JavaScript on the client.
Physics
All physics constants live in Go:
const (
Gravity = 0.3
BounceForce = -12.0
BoostForce = -18.0
MoveForce = 0.8
MaxVelocityX = 8.0
Friction = 0.98
)
Gravity pulls the rocket down each tick.
Bouncing off a platform sets vertical velocity to BounceForce (upward).
Boost platforms use a stronger BoostForce.
Horizontal movement applies MoveForce impulses capped at MaxVelocityX,
with Friction applied each frame so the rocket drifts and decelerates.
The rocket bounces off screen edges rather than wrapping:
if g.Rocket.X < 0 {
g.Rocket.X = 0
g.Rocket.VX = -g.Rocket.VX
} else if g.Rocket.X > CanvasWidth-g.Rocket.Width {
g.Rocket.X = CanvasWidth - g.Rocket.Width
g.Rocket.VX = -g.Rocket.VX
}
Platform decay
Normal platforms break after two bounces. The first bounce is normal. The second bounce still propels the rocket upward, but the platform shatters and a pitfall message appears:
p.BounceCount++
if p.BounceCount >= 2 {
p.Broken = true
g.SoundEvents = append(g.SoundEvents, SoundBreak)
g.PitfallMsg = PitfallMessages[g.rng.IntN(len(PitfallMessages))]
g.PitfallTimer = 120 // ~2 seconds at 60fps
}
Boost platforms never decay, rewarding the player for landing on them.
Pitfall messages
The pitfall messages are startup and VC-themed:
var PitfallMessages = []string{
"Surprise AWS bill",
"Claude did it in one prompt",
"Competitor raised $100M",
"Runway down to 3 months",
"Series A fell through",
"Roasted on Hacker News",
"It's always DNS",
"CAC > LTV",
// ... 40+ more
}
Platform reachability
Every platform must be reachable from the previous one. The game constrains horizontal distance between consecutive platforms:
const MaxHorizontalGap = 250
prevX := g.Platforms[len(g.Platforms)-1].X
minX := prevX - MaxHorizontalGap
maxX := prevX + MaxHorizontalGap
The max bounce height from the physics is
v²/(2g) = 12²/(2×0.3) = 240px,
and vertical gaps are 60–100px,
so the player always has a reachable path upward.
Background gradient
The background color shifts as altitude increases, transitioning from earth green to sky blue to dark atmosphere to black space:
func calculateBackgroundColor(altitude int) string {
if altitude < 1000 {
t := float64(altitude) / 1000
return lerpColor(0x3d5c3d, 0x87ceeb, t) // Earth → Sky
} else if altitude < 5000 {
t := float64(altitude-1000) / 4000
return lerpColor(0x87ceeb, 0x1a1a4e, t) // Sky → Atmosphere
} else if altitude < 10000 {
t := float64(altitude-5000) / 5000
return lerpColor(0x1a1a4e, 0x0a0a1a, t) // Atmosphere → Space
}
return "#0a0a1a"
}
Stars fade in above altitude 4000 using a parallax scroll at 10% of the camera speed.
Synthesized audio
All sound is generated from oscillators and noise via the Web Audio API. No audio files are loaded.
The game emits sound event strings from Go ("bounce", "boost", "break", etc.)
and JavaScript maps them to synthesizer calls:
- Bounce: square wave with a quick pitch bend down (Ms. Pac-Man "waka" inspired)
- Boost: sawtooth wave sweeping upward
- Break: square wave pitch drop layered with a noise burst
- Game over: three descending chromatic square wave notes
- Milestone: ascending two-tone triangle wave chime
Above altitude 4000, a background drone fades in — two slightly detuned sine waves (55 Hz and 55.5 Hz) creating a slow beating effect. The drone volume and pitch shift upward in deep space:
const droneVolume = Math.min(0.06, ((altitude - 4000) / 6000) * 0.06);
Fixed timestep
The game loop uses a fixed timestep to ensure consistent physics regardless of the display's refresh rate:
const FIXED_DT = 1000 / 60; // 60 ticks per second
function gameLoop(currentTime) {
const deltaTime = Math.min(currentTime - lastTime, 100);
lastTime = currentTime;
accumulator += deltaTime;
while (accumulator >= FIXED_DT) {
tick();
accumulator -= FIXED_DT;
}
const state = JSON.parse(getState());
render(state);
requestAnimationFrame(gameLoop);
}
On a 120 Hz display, two render frames share one physics tick.
On a 60 Hz display, each render frame runs one tick.
The deltaTime cap at 100ms prevents a spiral of death
if the browser tab loses focus and accumulates a large delta.
Play
Play the game at launchpad.ivp.com.