DEV Community

Cover image for Designing an idle RPG economy in vanilla JS: lessons from Snow317.io
SnowRSPS
SnowRSPS

Posted on

Designing an idle RPG economy in vanilla JS: lessons from Snow317.io

Snow317.io is a small 8-bit clicker RPG we're building in plain JavaScript: no framework, no engine, one page. You click ATTACK, monsters drop coins, and coins buy weapons, armour and pets. Pets deal damage for you. It's a spin-off of SnowRSPS, a custom game server we've run since 2012.

The code is easy. The economy is the hard part. Here's what we've learned so far.

1. Pick your growth curves before you write content

Every incremental game comes down to two curves:

  • Income growth: how fast coins per second rise
  • Cost growth: how fast prices rise

If costs grow faster than income, the player hits a wall. If income grows faster, the game is "solved" in an afternoon. The usual pattern is exponential costs and multiplicative income:

const cost = (base, owned, rate = 1.15) =>
  Math.floor(base * Math.pow(rate, owned));
Enter fullscreen mode Exit fullscreen mode

Tune rate per item category. Pets (passive DPS) should get steeper curves than weapons (which only help when you click), or idle play makes clicking pointless.

2. Tiny numbers need a plan

Auto Attack in Snow317.io unlocks at 0.0001 coins, so the first purchase is practically free. That's deliberate: it teaches "the game plays itself" within seconds. But tiny decimals in plain JS floats mean display bugs, like 0.30000000000000004.

What helped us:

  • Keep the internal value raw, and format only at render time.
  • Use one formatter for every number on screen (decimals at the start, suffixes like K/M later).
const fmt = n => n < 1 ? n.toFixed(4)
  : n < 1e3 ? n.toFixed(1)
  : Intl.NumberFormat('en', { notation: 'compact' }).format(n);
Enter fullscreen mode Exit fullscreen mode

3. Idle needs a manual counterweight

If pets do everything, the player stops coming back. We kept a few reasons to click:

  • Critical hits on manual attacks
  • A Heal button, because HP matters and the monster hits back
  • Waves and Cycles as visible milestones

Rule of thumb: active play should earn noticeably more per minute than idle, but idle should never earn zero.

4. Resets are part of the economy

Our reset costs you your coins and half your pets. A reset is an economic sink: it removes value so prices make sense again. If it's too soft, nothing matters. If it's too harsh, players quit. We're still tuning it, and it's the number we argue about most.

5. Save to localStorage, defensively

No accounts means localStorage is the save file:

function save(state) {
  try { localStorage.setItem('s317', JSON.stringify({ v: 1, ...state })); }
  catch (e) { /* quota or private mode: fail quietly */ }
}
function load() {
  try {
    const s = JSON.parse(localStorage.getItem('s317'));
    return s && s.v === 1 ? s : null;
  } catch { return null; }
}
Enter fullscreen mode Exit fullscreen mode

Version your save format from day one. Your economy will change, and old saves need a migration path, not a crash.

6. Drop tables are an economy too

Each monster in Snow317.io has its own drops popup. Showing players the odds makes the grind feel fair, which is a lesson we took straight from running the full server: players accept grind when they can see the math.

What's next

Music, a gentler early cost curve, and original art. If you've shipped an incremental game, how did you decide your cost multiplier? Was it spreadsheets, or vibes and playtesting?

Try the work-in-progress build at snowrsps.com/io (free, no sign-up). If you like the custom-loot flavour, the full SnowRSPS server is at snowrsps.com.

Top comments (0)