Skip to content

Edvins Antonovs

Rebuilding Clash of Clans’ building system in React

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.

coc build system


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 localStorage so it survives page refreshes.

Show clear UI indicators for production rate, capacity, uncollected gold, upgrade costs, and upgrade timers.


Breaking down the code

  1. State structure - We use a building object to track level, lastCollectedAt, and upgradedUntil. Gold is tracked separately.
  2. Gold generation logic - We compute gold lazily with calculateGold(), based on time since last collection and capped by the mine’s level.
  3. 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 a setInterval inside useEffect.
  4. Storage and persistence - The state is saved to localStorage on 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:

GoldMine.tsx
1import { useEffect, useState } from "react";
2
3type Building = {
4 level: number;
5 lastCollectedAt: number;
6 upgradedUntil: number | null;
7};
8
9const MAX_LEVEL = 5;
10
11const GOLD_PER_MINUTE = [0, 50, 100, 250, 500, 1000];
12const UPGRADE_DURATIONS = [0, 10, 30, 60, 120, 360]; // seconds
13const UPGRADE_COSTS = [0, 5, 10, 25, 50, 100]; // gold
14const MAX_CAPACITY = [0, 500, 1000, 2500, 5000, 10000];
15
16const STORAGE_KEY = "goldMineData";
17
18function getNow() {
19 return Date.now();
20}
21
22function calculateGold(building: Building): number {
23 const now = getNow();
24 const level = Math.min(building.level, MAX_LEVEL);
25 const rate = GOLD_PER_MINUTE[level] / 60000; // gold per ms
26
27 const from = building.lastCollectedAt;
28 const to = building.upgradedUntil
29 ? Math.min(now, building.upgradedUntil)
30 : now;
31
32 const duration = Math.max(to - from, 0);
33 const rawAmount = Math.floor(rate * duration);
34 return Math.min(rawAmount, MAX_CAPACITY[level]);
35}
36
37function loadData(): { building: Building; gold: number } {
38 const raw = localStorage.getItem(STORAGE_KEY);
39 const defaultData = {
40 building: {
41 level: 1,
42 lastCollectedAt: getNow(),
43 upgradedUntil: null,
44 },
45 gold: 0,
46 };
47
48 if (!raw) return defaultData;
49
50 try {
51 const parsed = JSON.parse(raw);
52 return {
53 building: {
54 level: Math.min(parsed.building?.level ?? 1, MAX_LEVEL),
55 lastCollectedAt: parsed.building?.lastCollectedAt ?? getNow(),
56 upgradedUntil: parsed.building?.upgradedUntil ?? null,
57 },
58 gold: parsed.gold ?? 0,
59 };
60 } catch {
61 return defaultData;
62 }
63}
64
65function formatDuration(ms: number) {
66 const sec = Math.max(0, Math.floor(ms / 1000));
67 const min = Math.floor(sec / 60);
68 const remSec = sec % 60;
69 return `${min}m ${remSec}s`;
70}
71
72export default function GoldMine() {
73 const [building, setBuilding] = useState<Building>(() => loadData().building);
74 const [gold, setGold] = useState(() => loadData().gold);
75 const [generated, setGenerated] = useState(0);
76 const [timeLeft, setTimeLeft] = useState("");
77
78 useEffect(() => {
79 localStorage.setItem(STORAGE_KEY, JSON.stringify({ building, gold }));
80 }, [building, gold]);
81
82 useEffect(() => {
83 function tick() {
84 const now = getNow();
85
86 if (building.upgradedUntil && building.upgradedUntil <= now) {
87 setBuilding((b) => ({
88 level: b.level + 1,
89 lastCollectedAt: now,
90 upgradedUntil: null,
91 }));
92 setTimeLeft("");
93 setGenerated(0);
94 return;
95 }
96
97 setGenerated(calculateGold(building));
98
99 if (building.upgradedUntil) {
100 const remaining = building.upgradedUntil - now;
101 setTimeLeft(formatDuration(remaining));
102 } else {
103 setTimeLeft("");
104 }
105 }
106
107 tick();
108 const interval = setInterval(tick, 1000);
109 return () => clearInterval(interval);
110 }, [building]);
111
112 function collect() {
113 const gain = calculateGold(building);
114 setGold((g) => g + gain);
115 setBuilding((b) => ({ ...b, lastCollectedAt: getNow() }));
116 setGenerated(0);
117 }
118
119 function upgrade() {
120 if (building.level >= MAX_LEVEL || building.upgradedUntil) return;
121
122 const nextLevel = building.level + 1;
123 const cost = UPGRADE_COSTS[nextLevel];
124 if (gold < cost) return;
125
126 const now = getNow();
127 const duration = UPGRADE_DURATIONS[building.level] * 1000;
128 const finishAt = now + duration;
129
130 setGold((g) => g - cost);
131 setBuilding((b) => ({
132 ...b,
133 upgradedUntil: finishAt,
134 }));
135 setTimeLeft(formatDuration(duration));
136 }
137
138 const isUpgrading = !!(
139 building.upgradedUntil && building.upgradedUntil > getNow()
140 );
141 const nextLevel = building.level + 1;
142 const upgradeCost = UPGRADE_COSTS[nextLevel] ?? Infinity;
143 const canUpgrade =
144 !isUpgrading && building.level < MAX_LEVEL && gold >= upgradeCost;
145
146 const level = Math.min(building.level, MAX_LEVEL);
147
148 return (
149 <div>
150 <h2>Gold Mine (Level {building.level})</h2>
151 <p>Generates: {GOLD_PER_MINUTE[building.level]} gold/min</p>
152
153 {isUpgrading ? (
154 <p>Upgrading{timeLeft ? `... (${timeLeft} left)` : "..."}</p>
155 ) : (
156 <p>
157 Gold stored: {generated} / {MAX_CAPACITY[building.level]}
158 </p>
159 )}
160
161 <p>Gold: {gold}</p>
162
163 <div>
164 <button onClick={collect} disabled={generated === 0 || isUpgrading}>
165 Collect
166 </button>
167
168 <button onClick={upgrade} disabled={!canUpgrade}>
169 {building.level >= MAX_LEVEL
170 ? "Upgrade (max level)"
171 : `Upgrade (${upgradeCost} gold)`}
172 </button>
173 </div>
174 </div>
175 );
176}

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_LEVEL when loading state. Prevents manual localStorage edits 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 upgradedUntil to 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.


Playable demo

Gold Mine (Level 1)

Generates: 50 gold/min

Gold stored: 0 / 500

Gold: 0


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
© 2025 by Edvins Antonovs. All rights reserved.