Two Numbers and a Straight Line
Towers in td-survivors level up from 1 to 100, and every level has its own damage, range, fire rate, and a pile of behavior-specific stats. That's a hundred stat blocks per tower, times a dozen towers. Today I stopped pretending I wanted to author all of that, and settled on a scaling model I'm a little smug about: describe only the two endpoints, level1 and level100, and let a straight line draw the other ninety-eight. This is the same "make the data tiny" instinct that put the waves on a diet last week, pointed at tower stats this time.
The tower JSON is now just its start and finish
A tower type declares two stat blocks and nothing in between:
{
"id": "gun",
"level1": { "damage": 30, "range": 4, "interval": 0.55 },
"level100": { "damage": 200, "range": 10, "interval": 0.2 }
}
Level 50 isn't written down anywhere. It's computed: interpolate each field by how far along the 1-to-100 track you are. The whole model is one pure function:
const t = (level - 1) / (maxLevel - 1) // 0 at lvl 1, 1 at lvl 100
out[key] = a + (b - a) * t // a = level1, b = level100
That's it. That's the scaling system. Want a tower's damage to climb steeply? Push its level100 damage up. Want range to barely move? Author it nearly identical in both endpoints. To keep a stat perfectly flat across all 100 levels, write the same number in both blocks and the line has zero slope. The behavior falls straight out of the two numbers you can actually see, instead of hiding inside a growth formula you have to simulate in your head.
There's exactly one wrinkle. Some stats are counts, not magnitudes: the tesla's chain jumps, the ricochet's bounces, the spike trap's max traps. A tower with "3.7 chain targets" is nonsense, so those fields (named in scaling.json's intFields) get rounded to whole numbers after interpolation. Everything else stays a float.
The system I deleted was cleverer, and that was the problem
The old model was a single multiplier. Every level multiplied the base stats by 1 + 0.05 * (level - 1), so each tower needed only one stat block. Elegant on paper. Miserable in practice, because not every stat wants to go up. Fire interval should shrink as the tower levels, not grow, so it needed an inverseFields list to get divided instead of multiplied. Ratios and angles and counts shouldn't move at all, so they needed a fixedFields list to be skipped. And any tower that didn't fit the one-size-multiplier at all got a scaleMode: "linear" escape hatch with... a second stat block. So I had two scaling code paths, three config lists classifying every field, and towers split across two systems depending on whether the uniform multiplier happened to suit them.
The linear model makes all of that evaporate. Interval shrinking? Just author level100 interval lower than level1; the same interpolation runs downhill. A stat that shouldn't move? Same value both ends. There is no "up vs down vs fixed" taxonomy anymore because direction is just which endpoint is bigger. scaling.json went from a perLevel rate plus inverseFields plus fixedFields to two lines: maxLevel and the tiny intFields list. resolveTowerLevel, scaleMode, and both classification arrays are gone, and every tower runs through the one code path.
The lesson, again: the clever compressed representation (one number, grown by a formula) cost more than the dumb explicit one (two numbers, connected by a line), because the dumb one lets me see and directly set the thing I actually care about, which is where the tower ends up at max level. Same trap the waves fell into. Fewer moving parts, and the parts that remain are the ones a human tuning the game actually reaches for.
Upgrades are next
The obvious next move: the per-run upgrade cards (damage, fire rate, slow, pickup radius) still level up the old way, on their own bespoke logic. They're begging for the same treatment, a level1/levelMax pair and one straight line between them, so the whole game speaks a single scaling language. That's the plan for next time, assuming the straight line doesn't develop opinions about wanting to be a curve.