TLDR: My first leveling formula hit Level 12 in 11 weeks. That's not a progression system — it's a countdown clock.
the build
I'm building a fitness app for dads who want to stay strong well into their 70s.
One of the things I'm most excited about is the 12-stage 8-bit character arc — a scrawny pixel-art dad who transforms, stage by stage, into a jacked version of himself as you complete workouts.
Every level = a new sprite. The visual feedback is the whole point.
So I wired up a leveling system.
the formula that seemed fine
First pass was dead simple:
level = 1 + floor(completedWorkouts / 3)
Every 3 workouts, you level up. Clean. Understandable. Felt fair.
Except I ran the numbers.
At 3 workouts a week, you hit Level 12 in about 11 weeks.
Eleven. Weeks.
That's not a progression system — that's a sprint that ends before most people have even built the habit. You max out the character before the app has had a chance to change your life.
what I needed instead
The insight is embarrassingly obvious once you see it: real progress is NOT linear.
The jump from "complete beginner" to "you've been at this a month" is HUGE. The jump from "one year in" to "two years in" is a different kind of huge — same word, different shape. Early levels should feel fast and hooky. Late levels should represent real time on earth.
That's exponential. Not uniform difficulty. Exponential.
the fix
I scrapped WORKOUTS_PER_STAGE entirely and replaced it with a hand-tuned threshold array in data.js:
LEVEL_REQ = [0, 3, 5, 9, 15, 26, 44, 74, 125, 215, 365, 620]
Each entry is the cumulative finished workouts to reach that level.
L3 = 5 workouts. (Achievable in your first two weeks — on purpose.)
L12 = 620 workouts. At 3 per week, that's roughly 4 years.
(Interesting thing — the new curve is actually EASIER at the start. L3 used to require 6 workouts; now it's 5. Exponential doesn't mean "harder everywhere." It means hook fast, stretch late. Don't assume otherwise.)
One gotcha I hit: at max level, xpIntoStage() was trying to read LEVEL_REQ[12] — which is undefined — and the XP bar was happily displaying "NaN workouts to level up."
Added an explicit atMax early-return before the index lookup. Without it, the math quietly breaks in the place where you most want it to feel magical.
why it matters to me
The whole reason I'm building this app is summed up in my own training goal: strength I can carry into my 70s.
Not a sprint. Not ego lifting. Slow, sustainable, built for the long haul — reps over load, durability over drama.
The exponential curve is just that philosophy expressed in code.
A 4-year climb to max level isn't a punishment. It mirrors what real transformation actually takes. If you're building any kind of progression mechanic — workouts, skills, streaks, anything — ask what maxing out means. If the answer is "about 11 weeks," you've built a math problem, not a motivation system.