games / launchpad
I built a mobile web-based vertical platformer game, Launchpad.
The game models a startup journey. You pilot a rocket upward and bounce off platforms. Normal platforms break after two bounces and show a startup pitfall message. Boost platforms carry the IVP logo and add bounce force. If you miss a platform and fall, the game ends.
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. A bounce sets vertical
velocity to BounceForce, or BoostForce on a boost platform.
Friction applies each frame, so the rocket drifts and decelerates.
The rocket bounces off screen edges:
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
A normal platform breaks on its second bounce. That bounce still propels the rocket upward:
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 break.
Pitfall messages
The pitfall messages use startup and venture capital themes:
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
The game limits the horizontal gap between consecutive platforms, so every platform is reachable:
const MaxHorizontalGap = 250
prevX := g.Platforms[len(g.Platforms)-1].X
minX := prevX - MaxHorizontalGap
maxX := prevX + MaxHorizontalGap
The maximum bounce height is v²/(2g) = 12²/(2×0.3) = 240px.
Vertical gaps stay between 60px and 100px.
Background gradient
The background color shifts with altitude:
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 and scroll at 10% of the camera speed.
Synthesized audio
The Web Audio API generates all sound from oscillators and noise.
Go emits sound event strings ("bounce", "boost", "break")
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 drone of two detuned sine waves (55 Hz and 55.5 Hz) fades in. Its volume and pitch rise with altitude:
const droneVolume = Math.min(0.06, ((altitude - 4000) / 6000) * 0.06);
Fixed timestep
The game loop uses a fixed timestep, so physics do not depend on the display 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 tick.
The 100ms cap on deltaTime stops a burst of ticks after the tab
loses focus.
Play
Play the game at launchpad.ivp.com.