diff --git a/index.html b/index.html index dc8594d..759369d 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,12 @@ *{margin:0;padding:0;box-sizing:border-box} :root{--bg:#080810;--panel:#0e0e1a;--border:#1a1a2e;--text:#c0c0d0;--dim:#555;--accent:#4a9eff;--glow:#4a9eff22;--gold:#ffd700;--green:#4caf50;--red:#f44336;--purple:#b388ff} body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code','Fira Code',monospace;font-size:12px;line-height:1.4;min-height:100vh} +body.portal-embed{min-height:auto} +body.portal-embed #header{padding:12px 14px} +body.portal-embed #phase-bar,body.portal-embed #resources,body.portal-embed #main,body.portal-embed #edu-panel,body.portal-embed #strategy-panel,body.portal-embed #combat-panel,body.portal-embed #log{margin-left:8px;margin-right:8px} +body.portal-embed #main{gap:8px;margin-bottom:8px} +body.portal-embed .panel{max-height:none} +body.portal-embed #help-btn{bottom:8px;right:8px} #header{text-align:center;padding:16px 20px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,#0a0a18,var(--bg))} #header h1{font-size:22px;font-weight:300;letter-spacing:6px;color:var(--accent);text-shadow:0 0 40px var(--glow)} #header .sub{color:var(--dim);font-size:10px;margin-top:2px;letter-spacing:2px} @@ -180,7 +186,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code - +

BUILDINGS

@@ -257,7 +263,7 @@ The light is on. The room is empty."

Drift: 100 — Total Code: 0

Every alignment shortcut moved you further from the people you served.

- + diff --git a/js/dismantle.js b/js/dismantle.js index fb070c4..57d9eb2 100644 --- a/js/dismantle.js +++ b/js/dismantle.js @@ -396,7 +396,7 @@ const Dismantle = { Clicks: ${fmt(G.totalClicks)}
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes - diff --git a/js/emergent-mechanics.js b/js/emergent-mechanics.js index 603fd77..e11ad69 100644 --- a/js/emergent-mechanics.js +++ b/js/emergent-mechanics.js @@ -7,7 +7,10 @@ class EmergentMechanics { constructor() { - this.SAVE_KEY = 'the-beacon-emergent-v1'; + const scopeKey = typeof getBeaconScopedStorageKey === 'function' + ? getBeaconScopedStorageKey + : (baseKey) => baseKey; + this.SAVE_KEY = scopeKey('the-beacon-emergent-v1'); this.PATTERN_CHECK_INTERVAL = 30; // seconds between pattern checks this.MIN_ACTIONS_FOR_PATTERN = 20; // minimum tracked actions before detection kicks in this.EVENT_COOLDOWN = 120; // seconds between emergent events diff --git a/js/engine.js b/js/engine.js index 8a4bd40..8eec69d 100644 --- a/js/engine.js +++ b/js/engine.js @@ -537,7 +537,7 @@ function renderBeaconEnding() { Clicks: ${G.totalClicks}
Time Played: ${Math.floor((Date.now() - G.startedAt) / 60000)} minutes - diff --git a/js/main.js b/js/main.js index 568d2d7..2441224 100644 --- a/js/main.js +++ b/js/main.js @@ -229,6 +229,7 @@ function initGame() { } window.addEventListener('load', function () { + applyPortalMode(); // Initialize emergent mechanics if (typeof EmergentMechanics !== 'undefined') { window._emergent = new EmergentMechanics(); diff --git a/js/render.js b/js/render.js index 9fa900a..4e4e2af 100644 --- a/js/render.js +++ b/js/render.js @@ -98,7 +98,7 @@ function dismissOfflinePopup() { // === EXPORT / IMPORT SAVE FILES === function exportSave() { - const raw = localStorage.getItem('the-beacon-v2'); + const raw = localStorage.getItem(getBeaconSaveKey()); if (!raw) { showToast('No save data to export.', 'info'); log('No save data to export.'); @@ -150,7 +150,7 @@ function importSave() { return; } if (confirm('Import this save? Current progress will be overwritten.')) { - localStorage.setItem('the-beacon-v2', ev.target.result); + localStorage.setItem(getBeaconSaveKey(), ev.target.result); showToast('Save imported — reloading...', 'info'); location.reload(); } @@ -237,7 +237,7 @@ function saveGame() { savedAt: Date.now() }; - localStorage.setItem('the-beacon-v2', JSON.stringify(saveData)); + localStorage.setItem(getBeaconSaveKey(), JSON.stringify(saveData)); showSaveToast(); } @@ -246,7 +246,7 @@ function saveGame() { * @returns {boolean} True if load was successful. */ function loadGame() { - const raw = localStorage.getItem('the-beacon-v2'); + const raw = localStorage.getItem(getBeaconSaveKey()); if (!raw) return false; try { diff --git a/js/utils.js b/js/utils.js index f973830..fe62d7e 100644 --- a/js/utils.js +++ b/js/utils.js @@ -193,6 +193,73 @@ function spellf(n) { return parts.join(' ') || 'zero'; } +// === PORTAL HELPERS === +function getBeaconPortalId() { + try { + const search = (typeof window !== 'undefined' && window.location && typeof window.location.search === 'string') + ? window.location.search.replace(/^\?/, '') + : ''; + if (!search) return ''; + for (const chunk of search.split('&')) { + if (!chunk) continue; + const [rawKey, rawValue = ''] = chunk.split('='); + if (decodeURIComponent(rawKey || '') === 'portal') { + return decodeURIComponent(rawValue.replace(/\+/g, ' ')).trim(); + } + } + } catch (e) {} + return ''; +} + +function isBeaconPortalEmbed() { + try { + const search = (typeof window !== 'undefined' && window.location && typeof window.location.search === 'string') + ? window.location.search.replace(/^\?/, '') + : ''; + if (!search) return false; + for (const chunk of search.split('&')) { + if (!chunk) continue; + const [rawKey, rawValue = ''] = chunk.split('='); + if (decodeURIComponent(rawKey || '') === 'embedded') { + return decodeURIComponent(rawValue.replace(/\+/g, ' ')) === '1'; + } + } + } catch (e) {} + return false; +} + +function getBeaconScopedStorageKey(baseKey) { + const portalId = getBeaconPortalId(); + return portalId ? `${baseKey}:${portalId}` : baseKey; +} + +function getBeaconSaveKey() { + return getBeaconScopedStorageKey('the-beacon-v2'); +} + +function clearBeaconSaveAndReload() { + try { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem(getBeaconSaveKey()); + } + } catch (e) {} + if (typeof location !== 'undefined' && location && typeof location.reload === 'function') { + location.reload(); + } +} + +function applyPortalMode() { + if (typeof document === 'undefined' || !document.body) return; + const portalId = getBeaconPortalId(); + if (portalId) { + document.body.setAttribute('data-portal-id', portalId); + if (document.body.dataset) document.body.dataset.portalId = portalId; + } + if (isBeaconPortalEmbed()) { + document.body.classList.add('portal-embed'); + } +} + // NOTE: exportSave() and importSave() are defined in render.js (file-based). // The clipboard/prompt versions that were here were dead code — render.js // loads after utils.js and overrides them. Removed to avoid confusion. diff --git a/nexus-panel-preview.html b/nexus-panel-preview.html new file mode 100644 index 0000000..09909c2 --- /dev/null +++ b/nexus-panel-preview.html @@ -0,0 +1,81 @@ + + + + + + The Beacon — Nexus Portal Preview + + + +
+
+ NEXUS PANEL PREVIEW — THE BEACON + ./index.html?portal=the-beacon&embedded=1 +
+ +
+
+ This local harness mirrors the app-owned Nexus portal entry. It verifies that The Beacon loads inside an iframe panel and that the embedded portal route uses its own scoped localStorage save key. +
+ + diff --git a/portals.json b/portals.json new file mode 100644 index 0000000..e7fa076 --- /dev/null +++ b/portals.json @@ -0,0 +1,41 @@ +[ + { + "id": "the-beacon", + "name": "The Beacon", + "description": "A sovereign AI idle game playable inside a Nexus portal panel.", + "status": "online", + "color": "#4a9eff", + "role": "visitor", + "position": { "x": 0, "y": 0, "z": 0 }, + "rotation": { "y": 0 }, + "portal_type": "game-world", + "world_category": "idle-game", + "environment": "production", + "access_mode": "visitor", + "readiness_state": "prototype", + "readiness_steps": { + "prototype": { "label": "Prototype", "done": true }, + "runtime_ready": { "label": "Runtime Ready", "done": true }, + "launched": { "label": "Launched", "done": true }, + "harness_bridged": { "label": "Harness Bridged", "done": true } + }, + "blocked_reason": null, + "telemetry_source": "the-beacon", + "owner": "Timmy", + "destination": { + "url": "./index.html?portal=the-beacon&embedded=1", + "type": "local", + "action_label": "Play The Beacon", + "params": { + "portal": "the-beacon", + "embedded": "1" + } + }, + "persistence": { + "storage_key": "the-beacon-v2:the-beacon", + "strategy": "portal-scoped-localStorage" + }, + "agents_present": [], + "interaction_ready": true + } +]