secco.tech
...
A game developer types at a laptop in a cozy pixel-art workspace, surrounded by idle RPG design notes and a giant spider enemy sketch, with a fantasy world under construction in the background.

Idle Game Devlog — Week 1

8 min readYgor Secco
Building in PublicNext.jsIdle GamesDevlogGaming

In my last post I mentioned I'd started building an idle game — a character that hunts on its own — to get the old grind-loop feeling back. This is the first proper devlog. Week one went almost entirely into architecture and tooling, so that's what I'll walk through: how the thing is put together, why it's put together that way, and the parts still held up by placeholder numbers.

A brain and a body

The decision that shaped everything else was splitting the brain from the body.

The brain is the simulation: pure TypeScript that owns every rule — combat, loot, experience, inventory, progression — and knows nothing about rendering. The body is the 3D layer that draws whatever the brain reports. The renderer is deliberately powerless: it can show a hit, but it can't decide one, and it never awards experience, currency, or a drop. Those only ever come out of the simulation.

Two properties fall out of that, and both matter more than they sound. First, the brain is deterministic — it runs on a seeded pseudo-random generator whose seed is threaded through the simulation state, so the same seed and the same inputs reproduce the exact same fight. That's what makes combat unit-testable instead of something I eyeball and hope about. Second, because the body is only a subscriber, I can rewrite the visuals — or eventually lift the brain out of the browser and onto a server — without touching a single rule.

Concretely: the 3D viewport is Three.js driven through React Three Fiber, so scenes are described as React components. Its render loop advances the simulation on a fixed timestep — one small, constant slice of time per step — so game logic ticks at a steady rate regardless of frame rate. A pair of in-memory stores (Zustand) hold the read-model the interface renders from: your bags, your equipped gear, your health. The brain sits underneath all of it and never reaches up.

Inside the tick

Each simulation step is a small, ordered decision list, run per entity. Survival comes first — refresh buffs, drink a potion if health crosses a threshold — then offense: a priority skill if it's off cooldown, otherwise a basic attack. Targeting is nearest-live-monster; the character closes distance (a run, then a walk into melee range) and swings on a cooldown.

Resolution is all rolls against stats. A hit is a ratio of attack rating to the target's defense rating, clamped so nothing is ever a guaranteed hit or a guaranteed miss. Damage is a roll inside the weapon's range minus armor, floored at one so every hit lands something, then scaled by the skill's multiplier. A kill flips the monster onto a respawn timer and emits a burst of events — experience gained, currency rolled from the monster's range, and a single pass over its drop table. Crucially, nothing mutates the UI directly: the kernel emits events, and the stores and renderer react to them. That indirection is what keeps rewards authoritative and the renderer honest.

Everything is data

None of those numbers live in code. Monsters, drop tables, items, skills, maps, shop stock — all of it is versioned content, loaded and validated against schemas (Zod, at the boundary) before it ever reaches the simulation. A malformed definition fails loudly at load instead of quietly breaking a fight an hour later.

The payoff is that balancing is a data problem, not a code problem. I can retune a drop rate or a monster's stat block without recompiling any logic, and the kernel stays pure. It also lets me be honest about the current values: most of them are placeholders. The machine is real; the balance isn't, yet.

Talking to the simulation without corrupting it

There's a tension in putting a React UI on top of an authoritative simulation. The UI needs to cause things — allocate a stat point, buy an item, refine a piece of gear — without being allowed to change game state itself. If any component could reach in and mutate the character, "the simulation is authoritative" would be a lie.

So the UI never writes game state. It posts intents across a command bridge — "allocate one point here", "buy this" — and the simulation applies them on its own terms, then publishes a fresh snapshot back to the stores for React to render. Business rules stay out of the components entirely; the interface is a keyboard and a screen, not a decision-maker. It's more ceremony than calling a setter, but it's the line between a UI and a cheat menu — and it's the same seam I'll later split across the network.

Loot, inventory, and gear

Drops run on weighted tables — each monster has one, each entry has its own drop chance and rolls its own item level, so the same kill can hand you filler or something worth equipping. Picked-up items flow into a grid inventory that places them with a deterministic first-fit: a small bin-packing pass that drops each item into the first open footprint that fits, multi-cell items included. If nothing fits, the drop is genuinely lost — and that miss is counted, so capacity actually means something.

Gear sits in named equipment slots with stat requirements. Equipping a two-handed weapon evicts the off-hand; a swap with nowhere to put the displaced item is refused rather than silently eating it. Whatever you equip is reflected on the skinned 3D character — the weapon in hand is the weapon in your bag.

The world, and a room to tune it

Most of the raw hours went into the world itself. I didn't want asset-store stand-ins; I wanted the specific, era-accurate look of the game I remembered. The catch: the original assets were locked in old, proprietary, partly-obfuscated formats that were never meant to be reopened. So I wrote decoders for them — working out the layouts, undoing the scrambling — and a pipeline that translates the models, their skeletal animations, the terrain, and the textures into something a modern engine can load. The real world geometry gets baked down into the arena each fight runs in, and the town is stood up from the authentic map data rather than hand-placed.

Getting assets in is only half the problem, though. The other half is making them look right in motion — and that's slow, fiddly work if the only way to see a change is to boot a whole combat session and wait for the right moment. So I built a sandbox: a separate, dev-only route that reuses the exact rendering engine the game runs on, minus the game. No simulation, no combat — just a controlled room where I can load a single model, scrub through its animation clips, hand-adjust poses, and dial in visual effects in isolation, then copy the tuned values back into the content data.

Building it on top of the engine I already had is the whole point: the sandbox and the real game render through identical code, so what looks right in the room looks right in a fight — no "works in the tool, wrong in the game" gap. It turned animation and VFX work from a guess-and-reboot slog into a tight iteration loop, and most of the weapon-specific attack cycles and effects came together there.

Persistence, and who the server trusts

Progress lives in Postgres, through TypeORM with hand-written migrations. A character row carries the scalar progress you'd expect plus two JSON snapshots — one for inventory and equipped gear, one for the combat configuration — alongside the current location. The client checkpoints every few seconds during a hunt and force-saves on the moments that matter; saves are scoped to a cookie-based account, one character each, and clamp incoming values into sane bounds.

Here's the honest part. Today the simulation runs entirely in the browser, which means the client decides what died, what dropped, and how much currency you earned — and the server simply records it. Clamping aside, it does not re-derive or verify any of it. For a pre-alpha that's the right trade: you can't secure a game whose rules change every day. But it's a temporary posture, and closing it is the headline of next week.

Next week

Three things:

  • GUI polish. The HUD is functional but rough. It needs to feel deliberate, not developer-placed.
  • Drop animations. Loot currently just appears in your bag. It should fall, settle, and read as a reward — the pickup is half the dopamine.
  • Server-side idempotency for combat and drops. The real work: giving the server a way to accept the client's claimed outcomes without trusting them blindly — deduplicating repeated or replayed events, reconciling them against elapsed time, and refusing to double-count. I don't have the design settled yet; choosing the approach is the task. It's the first concrete step from "a game that plays itself" to "a game you can't quietly rewrite from the console."

That's week one: a deterministic simulation, a renderer that only reports, a stack of data-driven content behind schemas, and a world I had to decode to get at. It's fragile and mostly placeholder — but the architecture was the part worth getting right first, because everything after this leans on it.

More next week.

Related Posts