diff --git a/src/components/system/TPS.vue b/src/components/system/TPS.vue
index ca96f10..7626b3e 100644
--- a/src/components/system/TPS.vue
+++ b/src/components/system/TPS.vue
@@ -1,18 +1,18 @@
 <template>
-	<div class="tpsDisplay">
+	<div class="tpsDisplay" v-if="tps !== 'NaN'">
 		TPS: {{ tps }}
 	</div>
 </template>
 
 <script>
-import Decimal, { format } from '../../util/bignum';
+import Decimal, { formatWhole } from '../../util/bignum';
+import { player } from '../../store/proxies';
 
 export default {
 	name: 'TPS',
 	computed: {
 		tps() {
-			const lastTenTicks = this.$store.state.lastTenTicks;
-			return format(Decimal.div(lastTenTicks.length, lastTenTicks.reduce((acc, curr) => acc + curr, 0)))
+			return formatWhole(Decimal.div(player.lastTenTicks.length, player.lastTenTicks.reduce((acc, curr) => acc + curr, 0)))
 		}
 	}
 };
diff --git a/src/data/mod.js b/src/data/mod.js
index c23bb03..5fe86a4 100644
--- a/src/data/mod.js
+++ b/src/data/mod.js
@@ -1,4 +1,4 @@
-import { hasMilestone, hasUpgrade, inChallenge, getBuyableAmount } from '../util/features';
+import { hasUpgrade, upgradeEffect } from '../util/features';
 import { layers } from '../store/layers';
 import { player } from '../store/proxies';
 import Decimal from '../util/bignum';
@@ -44,53 +44,27 @@ const main = {
 
 export const initialLayers = [ main, f, c, a, g, h, spook ];
 
-export function update(delta) {
-	let gain = new Decimal(3.19)
-	if (hasMilestone("p",0))gain=gain.plus(0.01)
-	if (hasMilestone("p",4)){
-		if (hasUpgrade("p",12))gain=gain.plus(0.1)
-		if (hasUpgrade("p",13))gain=gain.plus(0.1)
-		if (hasUpgrade("p",14))gain=gain.plus(0.1)
-		if (hasUpgrade("p",21))gain=gain.plus(0.1)
-		if (hasUpgrade("p",22))gain=gain.plus(0.1)
-		if (hasUpgrade("p",23))gain=gain.plus(0.1)
-		if (hasUpgrade("p",31))gain=gain.plus(0.1)
-		if (hasUpgrade("p",32))gain=gain.plus(0.1)
-		if (hasUpgrade("p",33))gain=gain.plus(0.1)
-	}
-	if (hasUpgrade("p",11))gain=gain.plus(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",12))gain=gain.times(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",13))gain=gain.pow(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-	if (hasUpgrade("p",14))gain=gain.tetrate(hasUpgrade("p",34)?(new Decimal(1).plus(layers.p.upgrades[34].effect)):1)
-
-	if (hasUpgrade("p",71)) gain=gain.plus(1.1)
-	if (hasUpgrade("p",72)) gain=gain.times(1.1)
-	if (hasUpgrade("p",73)) gain=gain.pow(1.1)
-	if (hasUpgrade("p",74)) gain=gain.tetrate(1.1)
-	if (hasMilestone("p",5)&&!inChallenge("p",22)){
-		let asdf = (hasUpgrade("p",132)?player.p.gp.plus(1).pow(new Decimal(1).div(2)):hasUpgrade("p",101)?player.p.gp.plus(1).pow(new Decimal(1).div(3)):hasUpgrade("p",93)?player.p.gp.plus(1).pow(0.2):player.p.gp.plus(1).log10())
-		gain=gain.plus(asdf)
-		if (hasUpgrade("p",213))gain=gain.mul(asdf.plus(1))
-    }
-	if (hasUpgrade("p",104)) gain=gain.times(player.p.points.plus(1).pow(0.5))
-	if (hasUpgrade("p",142))gain=gain.times(5)
-	if (player.i.unlocked)gain=gain.times(player.i.points.plus(1).pow(hasUpgrade("p",235)?6.9420:1))
-	if (inChallenge("p",11)||inChallenge("p",21))gain=new Decimal(10).pow(gain.log10().pow(0.75))
-	if (inChallenge("p",12)||inChallenge("p",21))gain=gain.pow(new Decimal(1).sub(new Decimal(1).div(getBuyableAmount("p",11).plus(1))))
-	if (hasUpgrade("p",211))gain=gain.times(getBuyableAmount("p",21).plus(1))
-	if (hasMilestone("p",13))gain=gain.times(layers.p.buyables[31].effect)
-	if (hasMilestone("p",13))gain=gain.pow(layers.p.buyables[42].effect)
-	gain.times(delta);
-}
-
 export function getStartingData() {
 	return {
 		points: new Decimal(10),
 	}
 }
 
-export function hasWon() {
-	return false;
+export const getters = {
+	hasWon() {
+		return false
+	},
+	pointGain() {
+		if(!hasUpgrade("c", 11))
+			return new Decimal(0);
+		let gain = new Decimal(1)
+		if (hasUpgrade("c", 12)) gain = gain.times(upgradeEffect("c", 12))
+		return gain;
+	}
+};
+
+/* eslint-disable-next-line no-unused-vars */
+export function update(delta) {
 }
 
 /* eslint-disable-next-line no-unused-vars */
diff --git a/src/data/modInfo.json b/src/data/modInfo.json
index 53d4469..e006906 100644
--- a/src/data/modInfo.json
+++ b/src/data/modInfo.json
@@ -16,5 +16,6 @@
 	"logo": null,
 	"initialTabs": [ "main", "c" ],
 
-	"maxTickLength": 3600
+	"maxTickLength": 3600,
+	"offlineLimit": 1
 }
diff --git a/src/main.js b/src/main.js
index 7fca680..f93c405 100644
--- a/src/main.js
+++ b/src/main.js
@@ -3,6 +3,7 @@ import App from './App';
 import store from './store';
 import { addLayer} from './store/layers';
 import { setVue } from './util/vue';
+import { startGameLoop } from './store/game';
 import './components/index';
 
 // Setup
@@ -21,5 +22,5 @@ requestAnimationFrame(async () => {
 	setVue(vue);
 	vue.$mount('#app');
 
-	// Start game loop
+	startGameLoop();
 });
diff --git a/src/store/game.js b/src/store/game.js
new file mode 100644
index 0000000..b05c434
--- /dev/null
+++ b/src/store/game.js
@@ -0,0 +1,117 @@
+import { update as modUpdate } from '../data/mod';
+import Decimal from '../util/bignum';
+import modInfo from '../data/modInfo.json';
+import store from './index';
+import { layers } from './layers';
+import { player } from './proxies';
+
+function updatePopups(/* diff */) {
+	// TODO
+}
+
+function updateParticles(/* diff */) {
+	// TODO
+}
+
+function update() {
+	let now = Date.now();
+	let diff = (now - player.time) / 1e3;
+	player.time = now;
+	let trueDiff = diff;
+
+	// Always update UI
+	updatePopups(trueDiff);
+	updateParticles(trueDiff);
+	player.lastTenTicks.push(trueDiff);
+	if (player.lastTenTicks.length > 10) {
+		player.lastTenTicks = player.lastTenTicks.slice(1);
+	}
+
+	// Stop here if the game is paused on the win screen
+	if (store.getters.hasWon && !player.keepGoing) {
+		return;
+	}
+
+	diff = new Decimal(diff).max(0);
+
+	// Add offline time if any
+	if (player.offTime != undefined) {
+		if (player.offTime.remain > modInfo.offlineLimit * 3600) {
+			player.offTime.remain = modInfo.offlineLimit * 3600;
+		}
+		if (player.offTime.remain > 0) {
+			let offlineDiff = Math.max(player.offTime.remain / 10, diff);
+			player.offTime.remain -= offlineDiff;
+			diff = diff.add(offlineDiff);
+		}
+		if (!player.offlineProd || player.offTime.remain <= 0) {
+			player.offTime = undefined;
+		}
+	}
+
+	// Cap at max tick length
+	diff = Decimal.min(diff, modInfo.maxTickLength);
+
+	// Apply dev speed
+	if (player.devSpeed != undefined) {
+		diff = diff.times(player.devSpeed);
+	}
+
+	// Update
+	if (diff.eq(0)) {
+		return;
+	}
+	player.timePlayed = player.timePlayed.add(diff);
+	if (player.points != undefined) {
+		player.points = player.points.add(Decimal.times(store.getters.pointGain, diff));
+	}
+	modUpdate(diff);
+	// Update each active layer
+	const activeLayers = Object.keys(layers).filter(layer => !layers[layer].deactivated);
+	activeLayers.forEach(layer => {
+		if (player[layer].resetTime != undefined) {
+			player[layer].resetTime = player[layer].resetTime.add(diff);
+		}
+		if (layers[layer].passiveGeneration) {
+			player[layer].points =
+				player[layer].points.add(Decimal.times(layers[layer].resetGain, layers[layer].passiveGeneration).times(diff));
+		}
+		layers[layer].update?.(diff);
+	});
+	// Automate each active layer
+	activeLayers.forEach(layer => {
+		if (layers[layer].autoReset && layers[layer].canReset) {
+			layers[layer].reset();
+		}
+		layers[layer].automate?.();
+		if (layers[layer].upgrades && layers[layer].autoUpgrade) {
+			Object.values(layers[layer].upgrades).forEach(upgrade => upgrade.buy());
+		}
+	});
+	// Check each active layer for newly unlocked achievements or milestones
+	activeLayers.forEach(layer => {
+		if (layers[layer].milestones) {
+			Object.values(layers[layer].milestones).forEach(milestone => {
+				if (milestone.unlocked !== false && !milestone.earned && milestone.done) {
+					player[layer].milestones.push(milestone.id);
+					milestone.onComplete?.();
+					// TODO popup notification
+					player[layer].lastMilestone = milestone.id;
+				}
+			});
+		}
+		if (layers[layer].achievements) {
+			Object.values(layers[layer].achievements).forEach(achievement => {
+				if (achievement.unlocked !== false && !achievement.earned && achievement.done) {
+					player[layer].achievements.push(achievement.id);
+					achievement.onComplete?.();
+					// TODO popup notification
+				}
+			});
+		}
+	});
+}
+
+export function startGameLoop() {
+	setInterval(update, 50);
+}
diff --git a/src/store/index.js b/src/store/index.js
index cdb944c..6a71852 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,9 +1,11 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 import { getInitialStore } from '../util/load';
+import { getters } from '../data/mod';
 
 Vue.use(Vuex);
 
 export default new Vuex.Store({
-	state: getInitialStore()
+	state: getInitialStore(),
+	getters
 });
diff --git a/src/store/layers.js b/src/store/layers.js
index 605c035..9423e5a 100644
--- a/src/store/layers.js
+++ b/src/store/layers.js
@@ -34,6 +34,9 @@ export function addLayer(layer) {
 	if (layer.onClick != undefined) {
 		layer.onClick.forceCached = false;
 	}
+	if (layer.update != undefined) {
+		layer.update.forceCached = false;
+	}
 
 	const getters = {};
 
@@ -55,7 +58,7 @@ export function addLayer(layer) {
 		for (let id in layer.upgrades) {
 			if (isPlainObject(layer.upgrades[id])) {
 				layer.upgrades[id].bought = function() {
-					return !this.deactivated && player[layer.id].upgrades.some(upgrade => upgrade == id);
+					return !layer.deactivated && player[layer.id].upgrades.some(upgrade => upgrade == id);
 				}
 				if (layer.upgrades[id].canAfford == undefined) {
 					layer.upgrades[id].canAfford = function() {
@@ -130,7 +133,7 @@ export function addLayer(layer) {
 		for (let id in layer.achievements) {
 			if (isPlainObject(layer.achievements[id])) {
 				layer.achievements[id].earned = function() {
-					return !this.deactivated && player[layer.id].achievements.some(achievement => achievement == id);
+					return !layer.deactivated && player[layer.id].achievements.some(achievement => achievement == id);
 				}
 			}
 		}
@@ -142,13 +145,13 @@ export function addLayer(layer) {
 		for (let id in layer.challenges) {
 			if (isPlainObject(layer.challenges[id])) {
 				layer.challenges[id].completed = function() {
-					return !this.deactivated && !!player[layer.id].challenges[id];
+					return !layer.deactivated && !!player[layer.id].challenges[id];
 				}
 				layer.challenges[id].completions = function() {
 					return player[layer.id].challenges[id];
 				}
 				layer.challenges[id].maxed = function() {
-					return !this.deactivated && Decimal.gte(player[layer.id].challenges[id], this.completionLimit);
+					return !layer.deactivated && Decimal.gte(player[layer.id].challenges[id], this.completionLimit);
 				}
 				if (layer.challenges[id].mark == undefined) {
 					layer.challenges[id].mark = function() {
@@ -156,7 +159,7 @@ export function addLayer(layer) {
 					}
 				}
 				layer.challenges[id].active = function() {
-					return !this.deactivated && player[layer.id].activeChallenge === id;
+					return !layer.deactivated && player[layer.id].activeChallenge === id;
 				}
 				if (layer.challenges[id].canComplete == undefined) {
 					layer.challenges[id].canComplete = function() {
@@ -227,7 +230,7 @@ export function addLayer(layer) {
 					player[layer.id].buyables[id] = amount;
 				}
 				layer.buyables[id].canBuy = function() {
-					return !this.deactivated && this.unlocked !== false && this.canAfford !== false &&
+					return !layer.deactivated && this.unlocked !== false && this.canAfford !== false &&
 						Decimal.lt(player[layer.id].buyables[id], this.purchaseLimit);
 				}
 				if (layer.buyables[id].purchaseLimit == undefined) {
@@ -292,7 +295,7 @@ export function addLayer(layer) {
 					}
 				}
 				layer.milestones[id].earned = function() {
-					return !this.deactivated && player[layer.id].milestones.some(milestone => milestone == id);
+					return !layer.deactivated && player[layer.id].milestones.some(milestone => milestone == id);
 				}
 			}
 		}
@@ -572,6 +575,12 @@ export const defaultLayerProperties = {
 			return this.canReset && this.resetGain.gte(player[this.layer].points.div(10));
 		}
 		return false;
+	},
+	reset(force = false) {
+		console.warn("Not yet implemented!", force);
+	},
+	resetData(keep = []) {
+		console.warn("Not yet implemented!", keep);
 	}
 };
 const gridProperties = [ 'upgrades', 'achievements', 'challenges', 'buyables', 'clickables' ];
diff --git a/src/store/proxies.js b/src/store/proxies.js
index 5436fb9..8e8ee5c 100644
--- a/src/store/proxies.js
+++ b/src/store/proxies.js
@@ -16,7 +16,7 @@ export const tmp = new Proxy({}, {
 
 const playerHandler = {
 	get(target, key) {
-		if (key == 'isProxy') {
+		if (key === 'isProxy') {
 			return true;
 		}
 
@@ -35,6 +35,17 @@ const playerHandler = {
 	},
 	set(target, property, value) {
 		Vue.set(target, property, value);
+		if (property === 'points') {
+			if (target.best != undefined) {
+				target.best = Decimal.max(target.best, value);
+			}
+			if (target.total != undefined) {
+				const diff = Decimal.sub(value, target.points);
+				if (diff.gt(0)) {
+					target.total = target.total.add(diff);
+				}
+			}
+		}
 		return true;
 	}
 };
@@ -80,7 +91,7 @@ function travel(callback, object, objectProxy, getters, prefix) {
 function getHandler(prefix) {
 	return {
 		get(target, key, receiver) {
-			if (key == 'isProxy') {
+			if (key === 'isProxy') {
 				return true;
 			}
 
@@ -119,7 +130,7 @@ function getHandler(prefix) {
 function getGridHandler(prefix) {
 	return {
 		get(target, key, receiver) {
-			if (key == 'isProxy') {
+			if (key === 'isProxy') {
 				return true;
 			}
 
@@ -157,7 +168,7 @@ function getGridHandler(prefix) {
 function getCellHandler(id) {
 	return {
 		get(target, key, receiver) {
-			if (key == 'isProxy') {
+			if (key === 'isProxy') {
 				return true;
 			}
 
diff --git a/src/util/layers.js b/src/util/layers.js
index 91b48d1..c91f2a7 100644
--- a/src/util/layers.js
+++ b/src/util/layers.js
@@ -1,14 +1,13 @@
 import Decimal from './bignum';
 import { isPlainObject } from './common';
+import { layers } from '../store/layers';
 
-// TODO make layer.reset(force = false)
 export function resetLayer(layer, force = false) {
-	console.warn("Not yet implemented!", layer, force);
+	layers[layer].reset(force);
 }
 
-// TODO make layer.resetData(keep = [])
 export function resetLayerData(layer, keep = []) {
-	console.warn("Not yet implemented!", layer, keep);
+	layers[layer].resetData(keep);
 }
 
 export function cache(func) {
diff --git a/src/util/load.js b/src/util/load.js
index 58ee98d..9faf04d 100644
--- a/src/util/load.js
+++ b/src/util/load.js
@@ -1,6 +1,7 @@
 import modInfo from '../data/modInfo';
 import { getStartingData, initialLayers } from '../data/mod';
 import { getStartingBuyables, getStartingClickables, getStartingChallenges } from './layers';
+import Decimal from './bignum';
 
 export function getInitialStore() {
 	return {
@@ -8,7 +9,7 @@ export function getInitialStore() {
 		time: Date.now(),
 		autosave: true,
 		offlineProd: true,
-		timePlayed: 0,
+		timePlayed: new Decimal(0),
 		keepGoing: false,
 		hasNaN: false,
 		lastTenTicks: [],