I just shipped Stage 1 of digi-boss, an idle/clicker business sim where you start as a solo coder at a terminal and work your way up to CEO. Chapter 1 (Coder) is free in browser right now.
This is a devlog about the technical decisions that made it possible to ship in vanilla JS with no framework, no build step, and no bundler.
The concept
The game has three stages: Coder, Founder, and CEO.
Stage 1 is a tap/idle loop. You click the terminal to write code and earn revenue. Buy upgrades (better IDE, linting tools, AI autocomplete, Cloud Code) and your tap value multiplies. Unlock idle income, hire your first coder, and the terminal stops being your problem.
Stage 2 (Founder) is in development. You'll manage an emoji-based team, respond to business events, and watch the passive income flow. Stage 3 is full CEO mode: department panels, board events, and an IPO prestige.
Why vanilla JS
I've built enough React projects to know that for a game, the component model creates more friction than it removes. The game state changes constantly (every 100ms when idle income ticks) and React's diffing overhead isn't free.
The entire game runs from index.html. No npm run dev, no webpack config, no TypeScript compilation. You open the file. It works. On itch.io it plays instantly in the browser.
The ES modules structure keeps the code organized:
game/
state.js -- save/load, formatMoney
upgrades.js -- UPGRADES, HIRES, EFFECT_DEFS
events.js -- event trigger logic
engine.js -- coordinator, tick handler
renderer.js -- state -> DOM sync
tick.worker.js -- Web Worker idle tick
No imports from node_modules. No .jsx. Just JavaScript.
The Web Worker idle engine
Browser idle games almost universally cheat the background tick. When the tab loses focus, setInterval throttles to 1 second or worse. Most games either lie ("you earned 5h of offline progress!") or silently lose ticks.
I wanted accurate idle income. The solution is a Web Worker:
// tick.worker.js
let interval = null;
self.onmessage = ({ data }) => {
if (data.type === 'start') {
interval = setInterval(() => {
self.postMessage({ type: 'tick', ts: Date.now() });
}, 100);
}
if (data.type === 'stop') clearInterval(interval);
};
Web Workers aren't throttled the same way as the main thread. The tick runs at 100ms intervals and posts back to the main thread. The engine receives it, runs the income calculation, and updates state. When the tab comes back into view, the DOM sync catches up.
If you leave the game open for an hour and come back, your idle income is accurate. Not approximately accurate. Actually accurate.
The two events
The events are the part I'm most interested to see land with a developer audience.
Debug Mode fires randomly during normal play. Scrambled red words appear inline in the terminal code feed:
const fnu mainLoop() { <- "fnu" is scrambled
return setatTimeout(tick, 100); <- "setatTimeout" is scrambled
You tap the terminal to pulse the display, then click each scrambled word to fix it. All bugs resolved means earning resumes. It's a mini-game inside the idle loop.
The Ollena upgrade enables auto-debug: words unscramble themselves over time. You buy automation for the annoying interruption. That's the whole joke.
Setup Wizards is a fake ncurses-style Linux installer overlay. It fires when you buy certain upgrades. It asks three questions about your system configuration. Pick the wrong answer on any question and it sends you back to Question 1.
The game gives you no real choice. Some answers are wrong, some aren't, and the interface is deliberately obtuse. Every developer has clicked through a Linux setup wizard at some point. That's all this is.
The upgrade effect system
The cleanest architecture decision was centralizing all upgrade effects in a single EFFECT_DEFS map:
const EFFECT_DEFS = {
tap_multiplier: {
apply: (effect, state) => { state.tapMultiplier += effect.value; },
actual: (effect, state) => `${state.tapMultiplier.toFixed(1)}x tap value`,
vague: (effect) => 'Increases tap value'
},
idle_rate: {
apply: (effect, state) => { state.idleRate += effect.value; },
actual: (effect, state) => `+$${formatMoney(effect.value)}/sec`,
vague: (effect) => 'Generates passive income'
},
// ...
};
Each effect owns three things: how it mutates state (apply), the precise description shown after purchase (actual), and the teaser shown before purchase (vague). Both the apply function and the upgrade modal loop over this single map. Adding a new effect type is one entry, no changes elsewhere.
What's live
Stage 1 is complete. The full Coder chapter is free to play in browser at shoogar.itch.io/digiboss. Pay-what-you-want if you want to support Stage 2.
Stage 2 (Founder) is what I'm building next. I'll post a devlog about the architecture decisions when the time comes.
Curious whether the Debug Mode and Setup Wizard events land with developers specifically. If you play it, let me know in the comments.