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);