Devlog 6: Adding Idle Mechanics Without Ruining the Balance

7 min read
game-design mechanics engagement

Devlog 6: Adding Idle Mechanics Without Ruining the Balance

The core clicking is fun, but I wanted to add more depth. Idle games have great retention because there's always something to optimize. But I had a problem: permanent upgrades would ruin Tuggy's accessibility.

If someone discovers a matchup after thousands of people have been playing for days, they'd be so far behind that participation would feel pointless. I needed the depth of an idle game without the grind.

The Design Constraint

Here's what I committed to:

  • Only temporary upgrades (no permanent progression)
  • Each matchup is independent (no cross-matchup progress)
  • Everyone starts equal when they first open a matchup

This keeps the barrier to entry low while still giving engaged players something to optimize.

How Idle Voting Works

I added a currency called "tugs" (yes, it's on-brand). Every click earns you tugs. You can spend tugs on temporary upgrades that boost your clicking power or give you passive income.

interface MatchupSession {
  tugs: number;                 // Currency from clicking
  tugs_per_second: number;      // Passive income rate
  current_side: 'a' | 'b' | null;
  active_upgrades: TempUpgrade[];
}

Once you have some tugs per second, the game runs in the background:

setInterval(() => {
  const idleEarnings = tugs_per_second * (interval / 1000);
  tugs += idleEarnings;

  if (current_side) {
    castVote(current_side, Math.floor(idleEarnings));
  }
}, 1000);

So you can leave the tab open and your chosen side keeps getting votes passively. It's not as powerful as active clicking, but it adds up.

The loop is: click to earn tugs → buy TPS upgrades → earn passive tugs → buy more powerful upgrades. But everything expires after 30-120 seconds, so there's no permanent advantage.

The Upgrade System

I added three types of temporary power-ups:

interface TempUpgrade {
  type: 'click_multiplier' | 'idle_tugs' | 'speed_boost';
  multiplier: number;
  duration: number;      // 30-120 seconds
  cost: number;          // in tugs
}
  1. Click Multiplier: Makes each click worth 2x-5x votes
  2. Idle Boost: Increases your tugs per second
  3. Speed Boost: Lets you click faster

They all expire after 30-120 seconds. So you can't just grind your way to dominance. You have to keep earning tugs and reactivating upgrades.

Preventing Exploits

I originally let people stack the same upgrade multiple times, and of course someone immediately figured out they could spam-buy click multipliers and get like 50x votes per click. Oops.

Now I prevent duplicate active upgrades:

const hasActiveUpgradeOfType = active_upgrades.some(
  u => u.type === upgrade.type && !isExpired(u)
);

if (hasActiveUpgradeOfType) {
  return { error: 'Upgrade already active' };
}

I also added dynamic pricing that increases with each purchase to prevent people from just buying everything immediately:

const purchaseCount = getPurchaseCount(upgradeType);
const finalCost = baseCost * Math.pow(1.5, purchaseCount);

Session Persistence

One nice detail: sessions save to localStorage so you don't lose progress if you close the tab:

localStorage.setItem(`tuggy_session_${matchupId}`, JSON.stringify({
  tugs,
  tugs_per_second,
  current_side,
  active_upgrades,
  last_activity: Date.now()
}));

Each matchup has its own session, so switching between matchups works correctly.

Does It Actually Work?

In testing, people who use the upgrade system definitely have more impact than pure clickers. But it's not overwhelming. A new player can still contribute meaningfully even on a matchup that's been running for hours.

The key is that upgrades expire. So even power users have to keep playing to maintain their advantage. There's no "I played for 10 hours yesterday so now I'm permanently stronger" effect.

I'm pretty happy with how this turned out. It adds strategic depth without breaking the core accessibility promise.

Next up: figuring out anonymous user tracking so people can play without signing up.