Skip to content

For developers

JavaScript Incremental Game Tutorial: 12 Steps From a Button to Prestige, Checked Against Real Source Code

A JavaScript incremental game tutorial in 12 steps: delta-time loop, scaling costs, saves, offline progress and prestige, matched to real open-source games.

You can build a working incremental game in one HTML file and one JavaScript file. The parts that matter are a single state object, a loop that uses real elapsed time, costs that grow by a fixed ratio, number formatting, an autosave, and a reset that keeps something. Most beginner tutorials stop at a click button and an auto-clicker; this one covers the pieces that decide whether your game survives a background tab, a closed laptop and a second week of play.

Each step points to how a real open-source game does it, mainly The Modding Tree (Acamaeda's MIT-licensed engine, whose game loop and save code we read for this guide), Antimatter Dimensions and A Dark Room. Code is kept short and written inline so you can type it straight into a file.

01

Step 1: Put the whole game in one state object: best for making saving and resetting trivial later

Platform: Plain JavaScript · no libraries

Start with a function that returns a fresh game, for example: function newGame() { return { gold: 0, miners: 0, lastTick: Date.now() } } and then let game = newGame(). Every number the player can change lives in that object, and nothing else. Buttons call functions that change game; a render function reads it.

This is exactly how The Modding Tree works: its player object holds all progress, and a getStartPlayer function rebuilds the default. Because everything is in one place, saving becomes JSON.stringify(game), loading becomes Object.assign(newGame(), saved), and a prestige reset becomes replacing parts of game with newGame() values.

Watch out for: Do not keep progress in DOM text or scattered globals. The first time you add a save, anything outside the state object will silently reset on reload.

02

Step 2: Run the loop on real elapsed time: best for games that must keep counting in background tabs

Platform: setInterval plus Date.now()

Write the loop so it measures time instead of counting calls: setInterval(tick, 50), where tick computes const now = Date.now(); const dt = (now - game.lastTick) / 1000; game.lastTick = now; and then adds production times dt, as in game.gold += game.miners * 1 * dt.

The Modding Tree does the same thing: its loop runs every 50 milliseconds, computes diff from Date.now() minus the last saved time, and passes diff into every layer's production. It also guards against bad values by setting diff to 0 if it is NaN or negative, which protects saves when a player changes the system clock backwards.

Watch out for: Chrome checks timers in hidden tabs once per second, and once a page has been hidden and silent for over five minutes it can drop chained timers to once per minute. A loop that adds a fixed amount per call will fall far behind; a delta-time loop will not.

03

Step 3: Render less often than you simulate: best for keeping a game with hundreds of numbers smooth

Platform: requestAnimationFrame or a slow interval

Keep two functions: tick changes state, render writes state to the page. Call render from requestAnimationFrame or a separate interval of about 100 milliseconds, and only touch elements whose text actually changed. Writing textContent for every number 20 times a second is the most common reason a hobby incremental feels sluggish.

Larger games use a UI framework for this. The Antimatter Dimensions source is a Vue 2 app where each panel is a component that redraws from game state. If you are heading that way, our engine guide compares the options.

Watch out for: requestAnimationFrame stops in background tabs, so never put production inside it. Simulation goes in the interval, drawing goes in the frame callback.

04

Step 4: Make costs grow by a fixed ratio: best for a first economy that already feels like the genre

Platform: Math.pow · the Cookie Clicker formula

The standard cost curve is exponential: cost = Math.ceil(base * Math.pow(1.15, owned)). The Cookie Clicker wiki gives exactly this formula for Cookie Clicker's buildings: base cost times 1.15 to the power of the number owned, rounded up. Production per building stays linear, so each purchase is a little worse value than the last, and the next tier of building becomes the better deal. That tension is the core loop.

Give each generator a base cost and a base rate, then store only the count owned. A buy function checks game.gold >= cost, subtracts, and increments the count. Start with ratios between 1.07 and 1.15 and tune by playing; our cost formula guide goes deeper.

Watch out for: Floating-point rounding can make a cost display as 100 while the true value is 100.0000001, so the button looks affordable and fails. Round costs up once, when you compute them, and compare against the rounded value.

05

Step 5: Add buy 10 and buy max with one formula: best for removing click fatigue in the mid game

Platform: Geometric series · Math.log

With ratio r, the cost of the next n items is base * r^owned * (r^n - 1) / (r - 1). The largest n you can afford is Math.floor(Math.log(money * (r - 1) / (base * Math.pow(r, owned)) + 1) / Math.log(r)). Two lines give you buy 10, buy 100 and buy max without a loop.

Almost every long-running incremental ships a buy-max button early, because buying a hundred buildings one click at a time is not a decision. Antimatter Dimensions goes further and turns bulk buying into automated autobuyers you unlock, which is a reward in itself.

Watch out for: These formulas assume one fixed ratio. If costs jump at thresholds (a common way to add softcaps), compute buy max piecewise or fall back to a capped loop.

06

Step 6: Format numbers before they get long: best for players who will see a million by minute ten

Platform: Intl.NumberFormat or a suffix table

Write one format(n) function on day one and use it everywhere. A simple version: below 1,000 show the number with toFixed; above that, use either suffixes (K, M, B, T) or scientific notation via n.toExponential(2). The built-in new Intl.NumberFormat('en', { notation: 'compact' }) gives K, M, B and T for free, but it runs out of suffixes in the trillions.

Most of the genre offers a notation setting, because players who reach 1e50 prefer scientific to invented suffixes. The Modding Tree keeps all its formatting in one NumberFormating.js file, which is a good model: one module, one place to add notations.

Watch out for: Number.MAX_VALUE is about 1.8e308. Past that a JavaScript number becomes Infinity and your save breaks. If your design goes there, switch to a library before launch; see our big number libraries guide.

07

Step 7: Autosave to localStorage: best for never losing a player's progress

Platform: localStorage · JSON

localStorage only stores strings, so save with localStorage.setItem('mygame', JSON.stringify(game)) and load with Object.assign(newGame(), JSON.parse(localStorage.getItem('mygame'))). Merging into newGame() means fields you add in later versions get default values instead of undefined.

The Modding Tree autosaves every 5 seconds on an interval and saves again in window.onbeforeunload. It also stores a version number and calls a fixOldSave hook when an older save loads, so the developer can migrate data. Copy all three habits: frequent autosave, save on close, versioned saves.

Watch out for: Saves are per browser and per origin. A player who clears site data, switches browser or plays on a different domain (your itch.io page versus GitHub Pages) starts over, which is why step 8 matters.

08

Step 8: Offer export and import strings: best for players moving between devices and sites

Platform: btoa and atob · clipboard

An export is the save as a copyable string: btoa(JSON.stringify(game)). Import is the reverse, inside a try block, followed by the same merge as loading. Put both buttons in an options tab with a text box.

This is a genre convention, not a nice-to-have. The Modding Tree's save code builds its export the same way, with btoa over the JSON. Players paste save strings into Reddit and Discord when they report bugs, which also makes debugging far easier for you.

Watch out for: btoa fails on characters outside Latin-1, such as an emoji in a player-chosen name. The Modding Tree wraps the JSON with encodeURIComponent and unescape before btoa for that reason; do the same or use a library like lz-string, which Profectus depends on.

09

Step 9: Pay out offline progress on load: best for players who close the tab and come back tomorrow

Platform: Timestamp difference · a cap

Because the state stores lastTick, the time away is simply (Date.now() - game.lastTick) / 1000 when the game loads. The cheapest implementation passes that into one tick. That is exact for constant production, and wrong once production changes during the absence.

The Modding Tree's approach is worth copying: on load it adds the time away to an offline bank, caps the bank at a developer-set number of hours (modInfo.offlineLimit), then spends about a tenth of what remains on each tick, so the player watches the game fast-forward. Our offline progress guide compares this with formula and simulation approaches.

Watch out for: Players can move their system clock forward to fake time away. For a single-player browser game that is usually acceptable, but always clamp negative or absurd values and never let one huge tick skip unlock checks.

10

Step 10: Reveal the game one piece at a time: best for first-time players who would bounce off a full UI

Platform: Browser, Mobile · free

Hide every button, resource and tab until it is useful, and reveal it with a flag in state such as game.unlocked.smithy = true. Check unlock conditions in the tick, and have render show or hide elements based on those flags.

A Dark Room is the reference for this technique. It opens on a dark room and one action, and the interface grows as the story does; the source on GitHub (MPL-2.0) shows how each area is added only when its condition is met. Unlock flags also double as a free save-compatible record of progress.

Watch out for: Store unlocks as flags rather than recomputing them from current resources, or a purchase that drops a resource below the threshold will make a button vanish again.

11

Step 11: Add a prestige reset that keeps something: best for turning a 20-minute toy into a game people return to

Platform: Browser · free

Prestige is a function that computes a reward from the current run, adds it to a kept currency, then resets everything else: const gain = Math.floor(Math.sqrt(game.gold / 1e6)); game.prestige += gain; Object.assign(game, newGame(), { prestige: game.prestige }). The kept currency then multiplies production, so the next run goes faster.

Jacorb's The Prestige Tree is built entirely from this idea, with each layer resetting the ones below it, and The Modding Tree generalises it into a reset function per layer. Show the player how much they would gain before they confirm, and make the first reset arrive within the first session. Our prestige system examples cover many variations.

Watch out for: Square-root and log rewards keep prestige gains from exploding, but if the first reset gives too little, players feel punished. Playtest the first prestige more than any other moment.

12

Step 12: Publish a playable build: best for getting your first real players this week

Platform: GitHub Pages, itch.io HTML5, galaxy.click

A plain JavaScript game needs no build. Push it to a GitHub repository and enable GitHub Pages, or zip it and upload to itch.io: the zip must contain a file called index.html, should stay under 1,000 files and 500MB extracted, and file names are case sensitive. galaxy.click is where much of the incremental community browses new web games.

Then post it where incremental players look, and list it on IncrementalGames.org through the free developer form so it appears in the catalog. Our where to post your incremental game guide covers each venue and its rules.

Watch out for: Moving hosts changes the origin, and localStorage saves do not follow. Pick your permanent home before you announce, or tell players to export their saves before the move.

How to choose what to build first

Twelve steps is a lot for one sitting. Order them by what blocks play.

01

Day one: steps 1, 2, 4 and 6

A state object, a delta-time loop, one scaling generator and a format function are a playable game. Everything else can wait, but these four are painful to retrofit, especially real elapsed time and formatting.

02

Before anyone else plays: steps 7, 8 and 9

The first outside player will reload the page, close the laptop or switch browsers. Autosave, export strings and offline catch-up make that a non-event. Without them, your first feedback will be about lost progress instead of your design.

03

Before you call it a game: steps 10 and 11

Reveals give the first five minutes shape and prestige gives the second week a reason. If your game is meant to grow past 1e308 or into a big UI, decide on a big number library or an engine or framework now, not after launch.

Write the first tick tonight

Open an empty folder, create index.html and game.js, and get one number going up on real elapsed time with an autosave. Then add the second generator and see whether buying it feels like a choice. If it does, you have an incremental game; everything else is content.

When it is playable, submit it to IncrementalGames.org for free so players can find it in the catalog. If you want extra visibility around launch, the optional Get featured placement adds a labelled Featured card on the home page, game pages and best-of lists from $30 a week.

List your game free →

Common questions

Can you make an incremental game with just HTML and JavaScript?

Yes. A Dark Room, Trimps and Kittens Game are all browser games written in HTML and JavaScript without a game engine, and The Modding Tree is plain JavaScript too. You only need a text editor and a browser.

How do idle games keep running when the tab is closed?

They do not run. They save a timestamp, and when the game opens again they compute the time away and award the production for it, usually with a cap. That is why the loop must use real elapsed time rather than counting ticks.

How do you save progress in a JavaScript idle game?

Convert the state object to JSON and store it with localStorage.setItem, on an autosave interval and in a beforeunload handler. The Modding Tree autosaves every 5 seconds and also on page close. Add an export string so players can back up saves.

What is the cost formula for clicker game upgrades?

Most use base cost times a ratio to the power of the number owned. Cookie Clicker's buildings use a ratio of 1.15, according to the Cookie Clicker wiki, rounded up to the next integer.

When do I need a big number library in JavaScript?

When any value can pass about 1.8e308, the largest JavaScript number. Below that, normal numbers are faster and simpler. Antimatter Dimensions uses break_infinity.js and The Modding Tree ships break_eternity.js for this reason.

Sources