diff --git a/index.html b/index.html
index dc8594d..e5df22b 100644
--- a/index.html
+++ b/index.html
@@ -262,6 +262,7 @@ The light is on. The room is empty."
+
diff --git a/js/engine.js b/js/engine.js
index 8a4bd40..f09a6d8 100644
--- a/js/engine.js
+++ b/js/engine.js
@@ -230,6 +230,10 @@ function tick() {
G.lastEventAt = G.tick;
}
+ if (typeof StateExport !== 'undefined' && StateExport && typeof StateExport.onTickBoundary === 'function') {
+ StateExport.onTickBoundary(G);
+ }
+
// Emergent mechanics: track resource state and check for emergent events
if (typeof EmergentMechanics !== 'undefined' && window._emergent) {
if (Math.floor(G.tick * 10) % 100 === 0) { // every ~10 seconds
diff --git a/js/state-export.js b/js/state-export.js
new file mode 100644
index 0000000..5a62ec5
--- /dev/null
+++ b/js/state-export.js
@@ -0,0 +1,118 @@
+(function (global) {
+ const STORE_KEY = 'compounding-intelligence:beacon-state';
+ const MAX_SNAPSHOTS = 300;
+
+ function _safeNumber(value, fallback = 0) {
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
+ }
+
+ function _tickKey(value) {
+ if (typeof value !== 'number' || !Number.isFinite(value)) return '0';
+ return value.toFixed(3);
+ }
+
+ function _resolveSink(explicitSink) {
+ if (explicitSink) return explicitSink;
+ return global.CompoundingIntelligence || null;
+ }
+
+ function _resolveStorage(explicitStorage) {
+ if (explicitStorage) return explicitStorage;
+ return typeof global.localStorage !== 'undefined' ? global.localStorage : null;
+ }
+
+ function buildSnapshot(gameState = {}) {
+ return {
+ source: 'the-beacon',
+ kind: 'idle_game_state',
+ timestamp: new Date().toISOString(),
+ tick: _safeNumber(gameState.tick, 0),
+ phase: _safeNumber(gameState.phase, 1),
+ trust: _safeNumber(gameState.trust, 0),
+ resources: {
+ code: _safeNumber(gameState.code, 0),
+ compute: _safeNumber(gameState.compute, 0),
+ knowledge: _safeNumber(gameState.knowledge, 0),
+ users: _safeNumber(gameState.users, 0),
+ impact: _safeNumber(gameState.impact, 0),
+ ops: _safeNumber(gameState.ops, 0),
+ },
+ project_progress: {
+ active: Array.isArray(gameState.activeProjects) ? [...gameState.activeProjects] : [],
+ completed: Array.isArray(gameState.completedProjects) ? [...gameState.completedProjects] : [],
+ active_count: Array.isArray(gameState.activeProjects) ? gameState.activeProjects.length : 0,
+ completed_count: Array.isArray(gameState.completedProjects) ? gameState.completedProjects.length : 0,
+ },
+ };
+ }
+
+ function readStore({ storage, storeKey = STORE_KEY } = {}) {
+ const resolved = _resolveStorage(storage);
+ if (!resolved) return [];
+
+ try {
+ const raw = resolved.getItem(storeKey);
+ if (!raw) return [];
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (_) {
+ return [];
+ }
+ }
+
+ function writeStore(entries, { storage, storeKey = STORE_KEY } = {}) {
+ const resolved = _resolveStorage(storage);
+ if (!resolved) return false;
+ resolved.setItem(storeKey, JSON.stringify(entries));
+ return true;
+ }
+
+ function writeSnapshot(snapshot, { storage, storeKey = STORE_KEY, sink } = {}) {
+ const entries = readStore({ storage, storeKey });
+ entries.push(snapshot);
+ while (entries.length > MAX_SNAPSHOTS) entries.shift();
+ writeStore(entries, { storage, storeKey });
+
+ const resolvedSink = _resolveSink(sink);
+ if (resolvedSink && typeof resolvedSink.ingestSnapshot === 'function') {
+ resolvedSink.ingestSnapshot(snapshot);
+ }
+
+ if (typeof global.dispatchEvent === 'function' && typeof global.CustomEvent === 'function') {
+ global.dispatchEvent(new global.CustomEvent('compounding-intelligence:state-export', { detail: snapshot }));
+ }
+
+ return snapshot;
+ }
+
+ function onTickBoundary(gameState, options = {}) {
+ const snapshot = buildSnapshot(gameState);
+ const key = _tickKey(snapshot.tick);
+ if (onTickBoundary._lastTickKey === key) return null;
+ onTickBoundary._lastTickKey = key;
+ return writeSnapshot(snapshot, options);
+ }
+
+ function resetTickBoundary() {
+ onTickBoundary._lastTickKey = null;
+ }
+
+ resetTickBoundary();
+
+ const api = {
+ STORE_KEY,
+ MAX_SNAPSHOTS,
+ buildSnapshot,
+ readStore,
+ writeStore,
+ writeSnapshot,
+ onTickBoundary,
+ resetTickBoundary,
+ };
+
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = api;
+ }
+
+ global.StateExport = api;
+})(typeof window !== 'undefined' ? window : globalThis);