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.
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.
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.
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.
Let’s implement a simplified Gold Mine building in React to preview this mechanic. It will:
localStorage
so it survives page refreshes.Show clear UI indicators for production rate, capacity, uncollected gold, upgrade costs, and upgrade timers.
level
, lastCollectedAt
, and upgradedUntil
. Gold is tracked separately.calculateGold()
, based on time since last collection and capped by the mine’s level.Date.now()
every second via a setInterval
inside useEffect
.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:
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]; // seconds13const UPGRADE_COSTS = [0, 5, 10, 25, 50, 100]; // gold14const 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 ms26
27 const from = building.lastCollectedAt;28 const to = building.upgradedUntil29 ? 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 Collect166 </button>167
168 <button onClick={upgrade} disabled={!canUpgrade}>169 {building.level >= MAX_LEVEL170 ? "Upgrade (max level)"171 : `Upgrade (${upgradeCost} gold)`}172 </button>173 </div>174 </div>175 );176}
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:
1..MAX_LEVEL
when loading state. Prevents manual localStorage
edits from breaking logic or granting extra gold.Math.min(generated, MAX_CAPACITY[level])
so edited timestamps don’t yield infinite gold.upgradedUntil
to be in the future and not unreasonably large (e.g. > 1 day from now).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.
Generates: 50 gold/min
Gold stored: 0 / 500
Gold: 0
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:
Sign up to get updates when I write something new. No spam ever.
Subscribe to my Newsletter