I’ve always enjoyed recreating game mechanics and logic in a UI. There’s something satisfying about breaking down how games work - how gold is calculated, how upgrades are timed - and then rebuilding those systems from scratch. This time, I looked at the Clash of Clans's building system. It’s a simple loop: passive resource generation, timed upgrades, storage caps. I rebuilt it in React using just localStorage and clean logic. No backend, just the fun bits.
How the Clash of Clans building system works
At its core, the building system in Clash of Clans revolves around two main things: resource generation over time and building upgrades that improve that generation.
- Resource generation happens continuously at a fixed rate per building level, but with a maximum storage capacity to prevent infinite accumulation.
 - Max capacity is a critical mechanic - once the storage cap is reached, the building stops accumulating until the player collects resources.
 - Upgrades take time to complete and cost resources. During an upgrade, resource production continues at the current level, but you can’t start another upgrade until the current one finishes.
 - The game tracks only minimal state: the building’s level, the timestamp of the last resource collection, and the upgrade completion timestamp (if upgrading).
 
When a player collects resources, the game calculates how much has accumulated since the last collection - capped at the max capacity. When upgrading, it sets a timer and disables further upgrades until it completes.

Why this system is so smart
This system is elegant because it’s extremely efficient and cheap to implement at scale. The server doesn’t have to update or store resource counts every second for every player - it just records timestamps and levels. The resource amounts are computed on demand using simple arithmetic, saving massive amounts of processing and storage.
It also creates an engaging loop for players: you return to collect resources, spend them on upgrades, and wait for your building to improve, all while the game state is easy to manage and sync.
Building a React demo
Let’s implement a simplified Gold Mine building in React to preview this mechanic. It will:
- Generate gold continuously based on building level.
 - Enforce a max capacity limit on uncollected gold.
 - Allow upgrades that cost gold and take time.
 - Display a live countdown timer during upgrades.
 - Persist progress in 
localStorageso it survives page refreshes. 
Show clear UI indicators for production rate, capacity, uncollected gold, upgrade costs, and upgrade timers.
Breaking down the code
- State structure - We use a building object to track 
level,lastCollectedAt, andupgradedUntil. Gold is tracked separately. - Gold generation logic - We compute gold lazily with 
calculateGold(), based on time since last collection and capped by the mine’s level. - Upgrade system - When you upgrade, we deduct gold, set an upgrade finish timestamp, and block further upgrades until it completes. We check 
Date.now()every second via asetIntervalinsideuseEffect. - Storage and persistence - The state is saved to 
localStorageon every change. On page load, we hydrate from saved values and resume any ongoing upgrade or gold generation. 
Here’s the full React component code:
import React, { useState, useEffect } from 'react';
const MAX_LEVEL = 5;
const GOLD_PER_MINUTE = [0, 50, 100, 250, 500, 1000];
const UPGRADE_DURATIONS = [0, 10, 30, 60, 120, 360]; // seconds
const UPGRADE_COSTS = [0, 5, 10, 25, 50, 100]; // gold
const MAX_CAPACITY = [0, 500, 1000, 2500, 5000, 10000];
const STORAGE_KEY = 'goldMineData';
function getNow() {
  return Date.now();
}
function calculateGold(building) {
  const now = getNow();
  const level = Math.min(building.level, MAX_LEVEL);
  const rate = GOLD_PER_MINUTE[level] / 60000; // gold per ms
  const from = building.lastCollectedAt;
  const to = building.upgradedUntil ? Math.min(now, building.upgradedUntil) : now;
  const duration = Math.max(to - from, 0);
  const rawAmount = Math.floor(rate * duration);
  return Math.min(rawAmount, MAX_CAPACITY[level]);
}
function loadData() {
  const defaultData = {
    building: {
      level: 1,
      lastCollectedAt: getNow(),
      upgradedUntil: null,
    },
    gold: 0,
  };
  if (typeof window === 'undefined') {
    return defaultData;
  }
  const raw = localStorage.getItem(STORAGE_KEY);
  if (!raw) return defaultData;
  try {
    const parsed = JSON.parse(raw);
    return {
      building: {
        level: Math.min(parsed.building?.level ?? 1, MAX_LEVEL),
        lastCollectedAt: parsed.building?.lastCollectedAt ?? getNow(),
        upgradedUntil: parsed.building?.upgradedUntil ?? null,
      },
      gold: parsed.gold ?? 0,
    };
  } catch {
    return defaultData;
  }
}
function formatDuration(ms) {
  const sec = Math.max(0, Math.floor(ms / 1000));
  const min = Math.floor(sec / 60);
  const remSec = sec % 60;
  return `${min}m ${remSec}s`;
}
export default function GoldMineDemo() {
  const [mounted, setMounted] = useState(false);
  const [building, setBuilding] = useState<{
    level: number;
    lastCollectedAt: number;
    upgradedUntil: number | null;
  }>({
    level: 1,
    lastCollectedAt: getNow(),
    upgradedUntil: null,
  });
  const [gold, setGold] = useState(0);
  const [generated, setGenerated] = useState(0);
  const [timeLeft, setTimeLeft] = useState('');
  useEffect(() => {
    const data = loadData();
    setBuilding(data.building);
    setGold(data.gold);
    setMounted(true);
  }, []);
  useEffect(() => {
    if (!mounted) return;
    if (typeof window !== 'undefined') {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({ building, gold }));
    }
  }, [building, gold, mounted]);
  useEffect(() => {
    if (!mounted) return;
    function tick() {
      const now = getNow();
      if (building.upgradedUntil && building.upgradedUntil <= now) {
        setBuilding((b) => ({
          level: b.level + 1,
          lastCollectedAt: now,
          upgradedUntil: null,
        }));
        setTimeLeft('');
        setGenerated(0);
        return;
      }
      setGenerated(calculateGold(building));
      if (building.upgradedUntil) {
        const remaining = building.upgradedUntil - now;
        setTimeLeft(formatDuration(remaining));
      } else {
        setTimeLeft('');
      }
    }
    tick();
    const interval = setInterval(tick, 1000);
    return () => clearInterval(interval);
  }, [building, mounted]);
  function collect() {
    const gain = calculateGold(building);
    setGold((g) => g + gain);
    setBuilding((b) => ({ ...b, lastCollectedAt: getNow() }));
    setGenerated(0);
  }
  function upgrade() {
    if (building.level >= MAX_LEVEL || building.upgradedUntil) return;
    const nextLevel = building.level + 1;
    const cost = UPGRADE_COSTS[nextLevel];
    if (gold < cost) return;
    const now = getNow();
    const duration = UPGRADE_DURATIONS[building.level] * 1000;
    const finishAt = now + duration;
    setGold((g) => g - cost);
    setBuilding((b) => ({
      ...b,
      upgradedUntil: finishAt as number,
    }));
    setTimeLeft(formatDuration(duration));
  }
  const isUpgrading = !!(building.upgradedUntil && building.upgradedUntil > getNow());
  const nextLevel = building.level + 1;
  const upgradeCost = UPGRADE_COSTS[nextLevel] ?? Infinity;
  const canUpgrade = !isUpgrading && building.level < MAX_LEVEL && gold >= upgradeCost;
  if (!mounted) {
    return <div className="p-6 bg-slate-900 text-white rounded-xl text-center">Loading...</div>;
  }
  return (
    <div className="py-1 px-6 mt-6 mb-6 bg-slate-900 text-white rounded-xl">
      <h2 className="text-xl font-bold mb-4">Gold Mine (Level {building.level})</h2>
      <p className="text-xs opacity-60 mb-4">
        Generates {GOLD_PER_MINUTE[building.level]} gold/min • Max capacity{' '}
        {MAX_CAPACITY[building.level]}
      </p>
      {isUpgrading ? (
        <p className="text-yellow-400 mb-3">
          ⏳ Upgrading {timeLeft ? `(${timeLeft} left)` : '...'}
        </p>
      ) : (
        <p className="mb-3 text-green-400">
          💰 Gold stored: {generated} / {MAX_CAPACITY[building.level]}
        </p>
      )}
      <p className="text-sm opacity-75 mt-0">Total gold:</p>
      <p className="text-4xl font-semibold mb-6">{gold}</p>
      <div className="flex gap-3">
        <button
          onClick={collect}
          disabled={generated= 0 || isUpgrading}
          className={`px-4 py-2 rounded-lg transition ${
            generated= 0 || isUpgrading
              ? 'bg-gray-700 cursor-not-allowed opacity-50'
              : 'bg-green-600 hover:bg-green-500'
          }`}
        >
          Collect
        </button>
        <button
          onClick={upgrade}
          disabled={!canUpgrade}
          className={`px-4 py-2 rounded-lg transition ${
            !canUpgrade
              ? 'bg-gray-700 cursor-not-allowed opacity-50'
              : 'bg-indigo-600 hover:bg-indigo-500'
          }`}
        >
          {building.level >= MAX_LEVEL ? 'Upgrade (max)' : `Upgrade (${upgradeCost} gold)`}
        </button>
      </div>
      <p className="text-xs text-slate-400 mt-6">
        💡 Collect gold before upgrading. Upgrades increase production rate and capacity.
      </p>
    </div>
  );
}
Playable demo
Preventing cheating or manipulation
Since we’re running purely on the client, some basic anti-cheat thinking goes a long way. This isn’t about stopping pros with dev tools - but we can deter casual manipulation.
Here’s how you can harden it a bit:
- Sanitise level bounds - Clamp level to 
1..MAX_LEVELwhen loading state. Prevents manuallocalStorageedits from breaking logic or granting extra gold. - Cap max gold collection - Always use 
Math.min(generated, MAX_CAPACITY[level])so edited timestamps don’t yield infinite gold. - Upgrade time sanity check - When resuming, validate 
upgradedUntilto be in the future and not unreasonably large (e.g. > 1 day from now). - Use server time (optional) - If connected to a backend, sync upgrade timestamps with server time instead of 
Date.now(), which is client-editable. 
This setup is good enough for casual demos and front-end-only games. For real multiplayer games, you’d move these checks to a backend.
Wrapping up
This demo captures the essence of Clash of Clans' economy loop using nothing but React and localStorage. The key trick is deferring calculations until needed, using timestamps to simulate "what would have happened" since the last visit.
It’s a great technique for building scalable idle mechanics, time-based upgrades, and progress that continues offline - all with minimal state and no timers running in the background.
You can use the same model for:
- Idle games (resource mines, clickers)
 - Tamagotchi-style simulations
 - Habit apps with unlock timers
 - Cooldowns in productivity tools