itch.io is community of indie game creators and players

Devlogs

Patch 1.1

Game 22
A browser game made in HTML5

Why this exists

I'm a developer who works by vibecoding — I describe what I want, let AI write most of the code, and spend my own attention on the decisions rather than the typing. Game22 is where I test whether that actually holds up on something real.

Not a to-do app. An idle ARPG with build depth: a stats engine with tagged modifiers, a deterministic simulation core, offline progress replayed by the real combat code, procedural graphics with no external assets, three languages. The kind of project where a wrong decision doesn't crash — it quietly ruins the balance three weeks later.

So the interesting question was never "can AI write this code". It was: can you keep a project like this honest when you didn't type most of it?

The answer I've landed on is: only if you refuse to trust anything you haven't measured. Almost every real defect below looked like "hmm, that build feels weak" and turned out to be a one-line structural mistake. Not one of them was found by reading code.

That's what this devlog is. Not a diary of features shipped — a list of things that turned out not to be what they looked like.

The balance harness paid for itself on its very first run

The first thing it surfaced looked like "this build is underpowered" and was structural: character life grew linearly against exponential monster damage. Those curves cross around level 40 — the classic way to break an ARPG. 1,126 deaths in two simulated hours.

The same run hid a second one: exponential mana cost against linear mana. Expensive spells became unplayable around level 20. In the report it read as "Fireball does no damage", when damage had nothing to do with it.

Each of those is about one line of code. You cannot find them by looking.

Five defects that all looked like "this build is weak"

The balance milestone produced a whole series, and every one was invisible in the source:

  1. Resistances were impossible to stack. All five shared an exclusion tag with armour, so exactly one defensive affix could land per item. The 75% cap across five damage types was unreachable by construction.
  2. physicalResist never reached combat. The stat rolled on affixes, was granted by a tree branch — and got dropped when the runtime was assembled. Every build played with 0% against physical.
  3. The tier ratchet only moved down. Climbing required two clears in a row, which is unreachable for a build that dies at all: 696 tier-1 clears without a single promotion.
  4. Paying life per hit had no cap. The cost scaled with attack speed; the damage bonus didn't. The blood build was dealing 250 damage per second to itself against 208 from monsters.
  5. Past the monster level cap, difficulty stopped growing entirely. The design promised the opposite, but there was nothing left to grow with: a build that broke tier 25 coasted to tier 2156.

Separately, the "Overwhelm" keystone turned out to be a trap rather than a choice: the same build reached T25 with it and T45 with "Retaliation" while dealing three times less damage. Attack speed in the endgame buys leech and ignites, not just damage — the keystone traded something useful for something that wasn't.

Half-translated is worse than untranslated

English and Spanish sat at 50%, and that wasn't "half done" — it was worse. The translated half was the UI and screens; the untranslated half was abilities, affixes, tree nodes and monsters. Which is exactly the content that appears in lists, side by side: players saw "Frost spike" sitting directly above "Огненный шар" in the same column.

Hence the rule: the unit of translation is a key group, not a key. An empty string is obvious at a glance. Mixed languages look like finished work and survive until someone complains.

Four bugs weren't caught by anything — not the build, not the coverage report, because the string is present and the type checks out:

  • ◆ БОСС ◆ was drawn on the canvas as a literal and stayed Russian in every language;
  • same for the "s" on the surge timer and "/s" on the character sheet;
  • the return card stored a pre-formatted string '12.0 ч', so the time unit froze at whatever language was active on load and sat there in the middle of an English UI;
  • four behavioural affixes were named after themselves rather than their effect ("◆ of the feast"), even though the item card prints them alone on one line and says nothing else about them.

You don't fix that class of bug by proofreading. You fix it by making Cyrillic string literals in the presentation layers fail a test.

Some colors are decoration, some are meaning

The UI moved from cold blue-grey to warm charcoal. That move exposed something nobody had planned for: the hero silhouette was painted with the UI accent color. The accent became brass — and the player blended into the brown monsters.

The first fix gave the hero blue steel. That landed 45 RGB units from the magic rarity color, and in a pack of blue monsters the player got lost again. The final answer is a turquoise no rarity uses, 95 units from its nearest neighbour.

Which produced my favourite test in the project: it checks distinguishability, not inequality. The first version of that test compared strings and would have passed both bugs.

The bag that only ate weapons

Found on a release screenshot: all forty items in the bag looked identical. The icons weren't the problem. addToInventory evicted the weakest item by comparing scores across slots — while the docstring on the scoring function says, in as many words, that its values are only comparable within a slot.

The score is based on average weapon damage, which grows exponentially with item level; every other slot uses a flat constant. By the endgame that's 77,686 against 40. Weapons evicted everything. Measured result: forty weapons out of forty.

The function was being used outside its own contract, and the contract was written directly above it.

Cutting the loot flood: my first answer was wrong

Too many items were dropping — close to 300,000 over 120 hours. The obvious move: cut the base drop chance. I cut it 45%.

Measured across three seeds, campaign pace became 11.8, 18.6 and 19.5 hours against a 6-hour tolerance. Not one unlucky seed — a systematic regression. The campaign runs on gear, and you can't take a third of the gear away from it. Softening the cut made it worse.

The excess was somewhere else entirely. The campaign drops 705 items in two hours — that's not a flood. The 300,000 accumulate in rifts, where quantity is multiplied by a per-tier bonus. Cutting that instead doesn't touch the campaign at all.

The result beat the original on all three measures at once: half the items, campaign pace 0.7–3.6h against the previous 0.8–4.6, and 10 of 10 builds reaching T30+ against 9.

What the release check caught

Two things, each of which would have sunk the launch or made it worse:

  • The build was broken in a subdirectory. Vite emits absolute /assets/… paths by default, and itch.io unpacks into /html/12345/. Players would have seen a blank page. One line fixes it (base: './'), but you can only notice by opening the built index.html and looking.
  • Mobile layout was broken. The two-column grid handed the sidebar its 260px minimum out of 375, leaving the scene a strip the width of a finger. The combat — the entire reason to open the game — was unreadable.

Decisions that were mine to make, not the code's

Several forks weren't technical, and the right move was to ask rather than pick:

  • Rewarding goals. The code carried a reasoned "no rewards, deliberately": a reward turns goals into a chore list. The concern is right but aimed slightly off — a chore list is what you get when the reward must be collected. Here it's granted automatically, including while the tab is closed.
  • Selling versus salvaging. Facts settled this one, not taste: there is no money in the game at all — no currency, no vendor, no prices — while crafting currency already exists and already drives the endgame. Selling would have meant inventing an economy for the sake of one button.
  • Art direction. "Too plain and too light" admits several readings, and different readings mean completely different work. Cheaper to ask.

What the game deliberately doesn't have

  • Per-ability skill trees. Four loadout slots and behavioural affixes cover that role.
  • Multiple classes. One class, but deep: one unit of content work yields maximum variety, and no archetype ships half-finished.
  • Positional combat. A pack, timers, formulas. Coordinates wouldn't add any decision the player actually makes.
  • PoE-grade crafting. It would drag its own economy along behind it.

What I actually learned about vibecoding

It moves fast and it lies confidently. Both halves matter. The code arrives working and plausible; the structural mistake is invisible inside it and stays invisible until something measures it.

So the harness isn't a nice-to-have here, it's the whole safety net. Every number change gets a 120-hour simulation run compared against a stored baseline. Every class of bug that survived the build, the tests and the coverage reports got a new test written for that class — mixed-language groups, colliding entity names, Cyrillic literals in the render layer, hero color versus rarity colors.

The rule I ended up with: if a mistake can't be seen by the build, the tests or the reports, then the job isn't done until something can see it. Everything above is a receipt for that rule.

Update 1.1 — the patch two strangers wrote for me

Two players left reviews on itch.io. Both were right, and neither was about balance. They were about the interface hiding half the game.

The first one ended with: "I see exactly one thing I can do. I can pick the skills for my build." He was at zone 35. He was not exaggerating — he was describing the build exactly as shipped.

The bug that made a player think the game had no features

The shell laid itself out with min-height: 100vh instead of a height. A CSS grid whose container height is indefinite sizes its 1fr row to content, not to the window — so the row grew to fit the sidebar, and the footer began 169 pixels below the fold. That footer holds Boost, Character, Inventory and Tree. Everything a player does with their hands, except swapping skills.

It broke on any window shorter than about 910 pixels, which includes the embed size my own publishing guide recommends. In fullscreen the page doesn't scroll, so the footer wasn't merely awkward to reach — it was unreachable.

The second review found the same class of failure one level down: the item card in the inventory was an absolutely-positioned tooltip, and the Equip button underneath was pushed clear by a hand-tuned marginTop: 220. A rare item with a comparison draws 250 pixels tall. The button was still clickable. It was just invisible, and an invisible button does not exist.

Both are the same lesson in different clothes: a layout mistake is a content mistake. Twelve uniques that change rules, a crafting economy, a passive tree — all of it was in the build the whole time, and none of it was reachable.

Fixed

  • Fullscreen dropped the action bar off-screen. Boost, Character, Inventory and Tree were unreachable at any window under ~910px tall.
  • The item card covered the Equip button, so found gear looked unequippable.
  • The bag squeezed forty items into two visible rows behind a cascade of two scrollbars caused by its own overflow: auto.
  • Unique items showed the base name and zero properties. They worked perfectly in combat; the view layer simply never asked the definition for anything.
  • Reforging changed an item's id, so a reforged item became a different item to the rest of the code — it vanished from the player's selection and was silently swapped in the slot.
  • Auto-craft applied value-rerolls blind, including when they made the item worse.
  • Current resources weren't clamped after a rebuild, so an aura could leave you showing 35/14 mana.

Added

  • Crafting you do yourself. There was no crafting UI at all — no command, no screen. The only crafter in the game was a function in the tick loop. Now: a wallet, eight currencies, and a click that applies one to the item you picked.
  • Four currencies built around a decision, not a button. Scouring strips the item to a bare base; annulment removes a random affix; imprint protects one affix through a reforge and burns away with it; tuning rerolls values inside their tiers. The three risky ones are deliberately withheld from the automation — it picks the safe move, it doesn't make a decision.
  • Twenty-one new affixes (38 → 59), and uniques now roll their own on top of fixed properties. While the set was entirely fixed, a six-affix rare beat any unique on every stat by level 40 — a rule nobody can pay for is a rule nobody buys.
  • Six auras that reserve mana. Mana had been a stat the game computes, displays and never asks about. Reservation makes maximum mana a budget, and regeneration now runs off the unreserved pool, so holding half your mana costs you half your recovery.
  • Skill numbers on the build screen — damage by type, DPS with crit and targets, cooldown, cost, level and tags.
  • A "What hits you" panel and damage types in the combat log. The game computed the damage type in combat and threw it away. A player wrote that he balanced resistances and never felt tankier; of course he didn't — nothing told him which type was killing him. It's usually physical, and his physical resistance was 12%.
  • Goals that teach. The campaign used to ask you to reach zone N. You reach zone N by waiting. Now it asks you to spend tree points, salvage items, craft. A player finished half the campaign without opening the passive tree once and was right to: the game had never once pointed at it.

Changed

  • Monsters hit harder, loot is several times rarer, uniques dropped from 1.2% to 0.35% of drops.
  • Levels no longer buy invulnerability. Player life used to outgrow monster damage, handing out a 2× cushion by level 60 for nothing but elapsed time. It's 1.19× now; the rest you earn from gear and the tree.
  • Rifts got their own difficulty curve. Both halves of the game had been growing on one shared exponent, so there was no way to make rifts harder without turning the campaign into a grind. Campaign teaches; challenge lives in rifts.
  • Dying in a rift burns experience, not just the rift.
  • Resistances weigh double in the affix pool. Widening the pool had quietly diluted them — three autopsied builds each had a hole at 0% and nothing above 46% against a 75% cap.
  • Saves reset. Balance changed enough that an old character can't clear the campaign under the new rules. The save format grew an epoch alongside its version: a version describes the shape of the data and is healed by a migration, an epoch describes the rules of the game and can't be healed at all. Foreign-epoch saves are refused with an explanation in three languages rather than wiped in silence.
    The one that got away
    One build still takes 22 hours where the target is twelve. Its autopsy is unambiguous and unfixable by tuning curves: chain lightning deals 68 damage per second to a single target against fireball's 259, and bosses gate every tenth zone. It also spends one of its four slots on a passive that grants mana — which, until auras existed, was a slot wired to nothing at all. That's the honest state of it. The systems are reachable now, the numbers are measured, and one build is still wrong in a way that needs a design decision rather than a constant.
    The passive tree wasn't a tree
    I asked myself the same question a player would: is the passive tree interesting? Then I measured it instead of answering.
    That's not a tree. It's six straight corridors radiating from a hub. Inside a branch there was no choice at all: reaching a keystone meant taking those exact eleven nodes in that exact order. Between branches there were no links whatsoever — getting from Might to Flame meant starting over from the centre.
    Thirty points buys two keystones and six leftovers. So the entire "tree" reduced to pick 2 branches out of 6 — fifteen combinations, and nothing to decide after that. The telling detail: a reviewer had praised this system. He'd noticed that the last node of one path removed attack speed, unspecced it, and felt his character speed up. Good feedback, he said. But look at what he actually did — he removed the keystone at the end of a corridor and kept eleven nodes of forced path he may never have wanted. What it took to make it a graph Crossings between adjacent branches at the two notable depths. The shape of a crossing is small · junction · small, not a single link — a direct notable-to-notable connection would cost one point, and the optimal play would instantly become "walk the rim and collect every notable". Three nodes per crossing makes the full circuit cost 36 points against a budget of 30, and it yields no keystone at all. Measured, not assumed. Junction notables that pay off only next to both neighbours: ailment damage on attacks between Might and Flame, life leech from crits between Blood and Precision. A node whose value depends on the rest of your choices is what depth actually means. More nodes is not depth. A fork inside every branch. After the first notable the branch splits in two and rejoins at the keystone: one side offence, the other survivability or speed. "I want that keystone" stopped meaning "I take these exact eleven nodes." Paragon grants tree points. The tree used to close forever at level 60 — while the endgame runs for a hundred hours and paragon reaches 120. All that new depth would have been spent in the first five.
    The campaign timings came out identical to the run before the rework, which is exactly right for a structural change: it added options without changing what the reference builds do.
    The refactor that had to come first pathToKeystone — used by both the balance harness and the autopsy tool — walked up connects[0] to the root. That assumed every node has exactly one parent, which was true right up until the first crossing existed. It became a breadth-first search, and the question it answers is now the interesting one: what does it actually cost to reach a keystone when you're allowed to turn. I also deleted a test that asserted the tree has "between 60 and 90 nodes". It broke on the rework instead of guarding it — a magic number pretending to be an invariant. What replaced it measures the things that matter: the player must take under 40% of the tree, and there must be forks. Those hold no matter how the generator is rewritten next. One decision I deliberately did not make A full respec is free, and players say so in reviews — "good to be able to respec at anytime, experiment for free". So the new currency that removes a single node buys precision, not capability: it saves you from rebuilding fifty nodes over one mistake deep in a branch, and nothing more. Making it a real economic decision would require pricing the full respec, which would take away the thing people liked. I wrote that trade-off into the ADR rather than quietly resolving it.

Files

  • game22-web.zip 145 kB
    3 days ago
Leave a comment