openvibers/openvibe.games Commits
Shared network chrome on every page, SEO, open-voice verbiage
Changes
@@@ -3,7 +3,32 @@<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />-- <title>Scraplandia Map Editor</title>++ <title>Scraplandia Map Editor — OpenVibe.Games</title>++ <meta++ name="description"++ content="Collaborative map editor for Scraplandia on OpenVibe.Games."++ />++ <!-- An admin tool: not for search engines. -->++ <meta name="robots" content="noindex, nofollow" />++ <link rel="canonical" href="https://openvibe.games/editor" />++ <meta property="og:type" content="website" />++ <meta property="og:site_name" content="OpenVibe.Games" />++ <meta property="og:title" content="Scraplandia Map Editor" />++ <meta++ property="og:description"++ content="Collaborative map editor for Scraplandia on OpenVibe.Games."++ />++ <meta property="og:url" content="https://openvibe.games/editor" />++ <meta property="og:image" content="https://openvibe.games/og-image.png" />++ <meta name="twitter:card" content="summary_large_image" />++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->++ <script src="https://openvibe.network/shared/theme-loader.js"></script>++ <script src="https://openvibe.network/shared/navbar.js" defer></script>++ <script src="https://openvibe.network/shared/footer.js" defer></script>++ <script>++ window.__ovgChrome = { page: 'editor', collapsible: true, toggleFromStart: true }++ </script>++ <script src="/chrome.js" defer></script></head><body><!--@@@ -15,39 +40,47 @@What stays here is what must exist before any script runs (the canvas)and the overlays that are not part of the workspace grid.-->-- <canvas id="game"></canvas>-- <div id="shell"></div>++ <div id="navbar-mount"></div>++ <div id="stage">++ <canvas id="game"></canvas>++ <div id="shell"></div>-- <!-- Revision conflict. Nothing here merges, and it says so. -->-- <div id="conflict" class="overlay" style="display: none">-- <div class="overlay-card">-- <h2>⚠ Newer map saved by another admin</h2>-- <div id="conflict-summary" class="hint"></div>-- <div class="overlay-actions">-- <button id="conflict-load" class="mini" type="button">Discard mine · load theirs</button>-- <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>-- <button id="conflict-export" class="mini" type="button">💾 Download my local copy</button>-- </div>-- <div class="hint">-- "Keep mine" only dismisses this panel — Save stays refused until you reload.++ <!-- Revision conflict. Nothing here merges, and it says so. -->++ <div id="conflict" class="overlay" style="display: none">++ <div class="overlay-card">++ <h2>⚠ Newer map saved by another admin</h2>++ <div id="conflict-summary" class="hint"></div>++ <div class="overlay-actions">++ <button id="conflict-load" class="mini" type="button">++ Discard mine · load theirs++ </button>++ <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>++ <button id="conflict-export" class="mini" type="button">++ 💾 Download my local copy++ </button>++ </div>++ <div class="hint">++ "Keep mine" only dismisses this panel — Save stays refused until you reload.++ </div></div></div>-- </div>-- <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->-- <div id="settings" class="overlay" style="display: none">-- <div class="overlay-card">-- <h2>⌨ Keyboard shortcuts</h2>-- <div id="keys-list" class="keys-list"></div>-- <div class="overlay-actions">-- <button id="keys-reset" class="mini" type="button">Reset to defaults</button>-- <button id="settings-close" class="mini" type="button">Close</button>++ <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->++ <div id="settings" class="overlay" style="display: none">++ <div class="overlay-card">++ <h2>⌨ Keyboard shortcuts</h2>++ <div id="keys-list" class="keys-list"></div>++ <div class="overlay-actions">++ <button id="keys-reset" class="mini" type="button">Reset to defaults</button>++ <button id="settings-close" class="mini" type="button">Close</button>++ </div></div></div>-- </div>-- <!-- Co-editor name tags, positioned over the viewport. -->-- <div id="peers"></div>++ <!-- Co-editor name tags, positioned over the viewport. -->++ <div id="peers"></div>++ </div>++ <footer id="ov-footer" class="ovf"></footer><script type="module" src="/src/editor/main.ts"></script></body>@@@ -3,28 +3,93 @@<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" />-- <title>OpenVibe.Games — free browser games on the OpenVibe network</title>++ <title>OpenVibe.Games — browser games on the OpenVibe network</title><metaname="description"-- content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — free, in your browser, no download."++ content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download."/>++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->++ <script src="https://openvibe.network/shared/theme-loader.js"></script>++ <script src="https://openvibe.network/shared/navbar.js" defer></script>++ <script src="https://openvibe.network/shared/footer.js" defer></script>++ <link rel="canonical" href="https://openvibe.games/" />++ <meta name="robots" content="index, follow" />++ <meta property="og:type" content="website" />++ <meta property="og:site_name" content="OpenVibe.Games" />++ <meta property="og:title" content="OpenVibe.Games — browser games on the OpenVibe network" />++ <meta++ property="og:description"++ content="Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download. One OpenVibe account across the whole network."++ />++ <meta property="og:url" content="https://openvibe.games/" />++ <meta property="og:image" content="https://openvibe.games/og-image.png" />++ <meta property="og:image:width" content="1200" />++ <meta property="og:image:height" content="630" />++ <meta name="twitter:card" content="summary_large_image" />++ <meta name="twitter:title" content="OpenVibe.Games — browser games on the OpenVibe network" />++ <meta++ name="twitter:description"++ content="Play Scraplandia — multiplayer survival-building in your browser, no download."++ />++ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />++ <script type="application/ld+json">++ {++ "@context": "https://schema.org",++ "@graph": [++ {++ "@type": "WebSite",++ "@id": "https://openvibe.games/#website",++ "name": "OpenVibe.Games",++ "url": "https://openvibe.games/",++ "description": "Browser games on the OpenVibe network — multiplayer, in your browser, no download.",++ "publisher": {++ "@type": "Organization",++ "name": "OpenVibe",++ "url": "https://openvibe.network/"++ }++ },++ {++ "@type": "VideoGame",++ "@id": "https://play.openvibe.games/#game",++ "name": "Scraplandia",++ "url": "https://play.openvibe.games/",++ "image": "https://openvibe.games/og-image.png",++ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",++ "genre": ["Survival", "Sandbox", "Building"],++ "playMode": "MultiPlayer",++ "gamePlatform": "Web browser",++ "applicationCategory": "Game",++ "operatingSystem": "Any",++ "isPartOf": { "@id": "https://openvibe.games/#website" },++ "publisher": {++ "@type": "Organization",++ "name": "OpenVibe",++ "url": "https://openvibe.network/"++ }++ }++ ]++ }++ </script><style>* {margin: 0;box-sizing: border-box;}++ /* Page palette. The shared theme-loader sets --accent, --accent-light, --accent-dark,++ --accent-rgb, --bg-primary, --text-primary… on <html>, so the portal follows the++ signed-in user's theme; the values here are the fallbacks before it runs. */:root {-- --accent: #8b5cf6;-- --accent-light: #a78bfa;-- --accent-dark: #6d28d9;++ --accent: #3b82f6;++ --accent-light: #60a5fa;++ --accent-dark: #1d4ed8;--secondary: #22d3ee;-- --bg: #131318;-- --bg-raised: #1a1a22;-- --bg-card: rgba(26, 26, 34, 0.88);-- --border: #2a2a36;-- --text: #e8e8f0;-- --text-dim: #a0a0b2;-- --text-faint: #6a6a7a;++ --bg: var(--bg-primary, #0a0f1c);++ --bg-raised: var(--bg-secondary, #101828);++ --bg-card: var(--bg-card-page, rgba(19, 28, 46, 0.88));++ --border: #1f2d47;++ --text: var(--text-primary, #e6edf7);++ --text-dim: var(--text-secondary, #96a7c2);++ --text-faint: var(--text-muted, #7386a3);}html,body {@@@ -45,9 +110,13 @@inset: 0;z-index: -2;background:-- radial-gradient(1100px 520px at 50% -8%, rgba(139, 92, 246, 0.22), transparent 68%),++ radial-gradient(++ 1100px 520px at 50% -8%,++ rgba(var(--accent-rgb, 59, 130, 246), 0.22),++ transparent 68%++ ),radial-gradient(800px 420px at 88% 28%, rgba(34, 211, 238, 0.07), transparent 60%),-- linear-gradient(180deg, #16121f 0%, var(--bg) 46%);++ linear-gradient(180deg, var(--bg-raised) 0%, var(--bg) 46%);}.stars {position: fixed;@@@ -56,59 +125,6 @@opacity: 0.7;}-- /* ── nav ─────────────────────────────────────────────────────── */-- nav {-- display: flex;-- align-items: center;-- gap: 20px;-- padding: 16px clamp(16px, 4vw, 40px);-- max-width: 1200px;-- margin: 0 auto;-- }-- .brand {-- font-weight: 900;-- font-size: 19px;-- letter-spacing: 0.02em;-- text-decoration: none;-- color: var(--text);-- white-space: nowrap;-- }-- .brand b {-- background: linear-gradient(120deg, var(--accent-light), var(--accent));-- -webkit-background-clip: text;-- background-clip: text;-- color: transparent;-- }-- .nav-links {-- margin-left: auto;-- display: flex;-- align-items: center;-- gap: 6px;-- flex-wrap: wrap;-- }-- .nav-links a {-- color: var(--text-dim);-- text-decoration: none;-- font-size: 14px;-- padding: 8px 12px;-- border-radius: 8px;-- transition:-- color 0.15s,-- background 0.15s;-- }-- .nav-links a:hover {-- color: var(--text);-- background: rgba(139, 92, 246, 0.12);-- }-- .nav-links a.signin {-- color: #fff;-- background: linear-gradient(120deg, var(--accent), var(--accent-dark));-- font-weight: 700;-- }-- .nav-links a.signin:hover {-- background: linear-gradient(120deg, var(--accent-light), var(--accent));-- }--/* ── hero ────────────────────────────────────────────────────── */header {text-align: center;@@@ -122,11 +138,16 @@animation: rise 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both;}.hero-title b {-- background: linear-gradient(120deg, var(--accent-light) 10%, var(--accent) 55%, var(--secondary) 110%);++ background: linear-gradient(++ 120deg,++ var(--accent-light) 10%,++ var(--accent) 55%,++ var(--secondary) 110%++ );-webkit-background-clip: text;background-clip: text;color: transparent;-- filter: drop-shadow(0 6px 34px rgba(139, 92, 246, 0.35));++ filter: drop-shadow(0 6px 34px rgba(var(--accent-rgb, 59, 130, 246), 0.35));}.hero-sub {margin: 18px auto 0;@@@ -171,7 +192,7 @@.game-card:hover {transform: translateY(-5px);border-color: var(--accent);-- box-shadow: 0 14px 44px rgba(139, 92, 246, 0.18);++ box-shadow: 0 14px 44px rgba(var(--accent-rgb, 59, 130, 246), 0.18);}.game-card.featured {grid-column: 1 / -1;@@@ -190,17 +211,21 @@align-items: center;justify-content: center;background:-- radial-gradient(420px 200px at 30% 20%, rgba(139, 92, 246, 0.35), transparent 70%),++ radial-gradient(++ 420px 200px at 30% 20%,++ rgba(var(--accent-rgb, 59, 130, 246), 0.35),++ transparent 70%++ ),radial-gradient(380px 220px at 75% 85%, rgba(34, 211, 238, 0.18), transparent 70%),-- linear-gradient(160deg, #221a33, #141420 70%);++ linear-gradient(160deg, var(--bg-raised), var(--bg) 70%);}.shot::after {content: '';position: absolute;inset: 0;background-image:-- linear-gradient(rgba(167, 139, 250, 0.06) 1px, transparent 1px),-- linear-gradient(90deg, rgba(167, 139, 250, 0.06) 1px, transparent 1px);++ linear-gradient(rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px),++ linear-gradient(90deg, rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px);background-size: 34px 34px;mask-image: radial-gradient(closest-side, #000, transparent);-webkit-mask-image: radial-gradient(closest-side, #000, transparent);@@@ -231,8 +256,8 @@}.badge.soon {color: var(--accent-light);-- background: rgba(139, 92, 246, 0.14);-- border: 1px solid rgba(139, 92, 246, 0.35);++ background: rgba(var(--accent-rgb, 59, 130, 246), 0.14);++ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.35);}.game-body h2 {font-size: clamp(26px, 3.4vw, 38px);@@@ -273,7 +298,7 @@text-decoration: none;color: #fff;background: linear-gradient(120deg, var(--accent), var(--accent-dark));-- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);++ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);transition:transform 0.15s ease,box-shadow 0.15s ease;@@@ -285,10 +310,10 @@@keyframes pulse {0%,100% {-- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);++ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);}50% {-- box-shadow: 0 8px 48px rgba(139, 92, 246, 0.7);++ box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);}}.game-card.soon {@@@ -309,8 +334,12 @@padding: 0 20px;}.account-inner {-- background: linear-gradient(160deg, rgba(139, 92, 246, 0.12), rgba(26, 26, 34, 0.9) 55%);-- border: 1px solid rgba(139, 92, 246, 0.3);++ background: linear-gradient(++ 160deg,++ rgba(var(--accent-rgb, 59, 130, 246), 0.12),++ var(--bg-card) 55%++ );++ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.3);border-radius: 20px;padding: clamp(26px, 5vw, 44px);text-align: center;@@@ -348,27 +377,14 @@color: var(--text-faint);}-- /* ── footer ──────────────────────────────────────────────────── */-- footer {-- border-top: 1px solid var(--border);-- padding: 30px 20px 40px;-- text-align: center;-- color: var(--text-faint);-- font-size: 13px;-- line-height: 2;-- }-- footer a {-- color: var(--text-dim);-- text-decoration: none;-- margin: 0 10px;-- }-- footer a:hover {-- color: var(--accent-light);++ /* ── shared chrome ───────────────────────────────────────────── */++ #navbar-mount {++ position: relative;++ z-index: 5;}-- .foot-brand {-- font-weight: 800;-- color: var(--text-dim);-- letter-spacing: 0.06em;++ #ov-footer {++ max-width: 1200px;++ margin: 0 auto;}</style></head>@@@ -376,24 +392,13 @@<div class="backdrop"></div><canvas class="stars" id="stars"></canvas>-- <nav>-- <a class="brand" href="/">Open<b>Vibe</b>.Games</a>-- <div class="nav-links">-- <a href="https://openvibe.live" target="_blank" rel="noopener">openvibe.live</a>-- <a href="https://openvibe.tools" target="_blank" rel="noopener">openvibe.tools</a>-- <a-- class="signin"-- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"-- >Sign in</a-- >-- </div>-- </nav>++ <div id="navbar-mount"></div><header><h1 class="hero-title">Open<b>Vibe.Games</b></h1><p class="hero-sub">-- Free multiplayer games that run in your browser. No installs, no launchers — one OpenVibe-- account across the whole network.++ Multiplayer games that run in your browser. No installs, no launchers — one OpenVibe account++ across the whole network.</p></header>@@@ -416,7 +421,7 @@><span>PvP wilds</span><span>bottle caps</span></div><a class="play-cta" href="https://play.openvibe.games" data-dev-href="/play"-- >▶ PLAY FREE — NO DOWNLOAD</a++ >▶ PLAY — NO DOWNLOAD</a></div></div>@@@ -427,7 +432,9 @@<div class="game-body"><span class="badge soon">COMING SOON</span><h3>Vibe Tables</h3>-- <p>Casual card and board nights with your stream chat — join from a link, no account walls.</p>++ <p>++ Casual card and board nights with your stream chat — join from a link, no account walls.++ </p></div></div><div class="game-card soon">@@@ -453,31 +460,22 @@<h2>One character as a guest. <b>Three</b> with an OpenVibe account.</h2><p>Jump into any game instantly as a guest — your character is tied to this browser and-- connection. Sign in with your free <b>OpenVibe</b> account to unlock three character-- slots, keep progress safe across devices and IP changes, and carry one identity across++ connection. Sign in with your <b>OpenVibe</b> account to unlock three character slots,++ keep progress safe across devices and IP changes, and carry one identity acrossopenvibe.live, openvibe.tools and every game here.</p>-- <a-- class="signin"-- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"-- >Sign in with OpenVibe</a-- >++ <a class="signin" href="/auth/login?next=%2F">Sign in with OpenVibe</a><div class="fine">One account. All of OpenVibe.</div></div></section>-- <footer>-- <div>-- <a href="https://openvibe.network">openvibe.network</a>-- <a href="https://openvibe.live">openvibe.live</a>-- <a href="https://openvibe.tools">openvibe.tools</a>-- <a href="https://openvibe.media">openvibe.media</a>-- <a href="https://openvibe.community">openvibe.community</a>-- </div>-- <div class="foot-brand">OPENVIBE.GAMES</div>-- <div>Part of the OpenVibe network · One Account. All of OpenVibe.</div>-- </footer>++ <!-- Shared cross-site footer (openvibe.network/shared/footer.js): network links, legal, signed-in row. -->++ <footer id="ov-footer" class="ovf"></footer>++ <script>++ window.__ovgChrome = { page: 'portal' }++ </script>++ <script src="/chrome.js" defer></script><script>// Dev convenience: on localhost the play. vhost doesn't exist, so PLAY// buttons fall back to the same-origin /play route (Vite alias / server).@@@ -506,7 +504,11 @@ctx.clearRect(0, 0, c.width, c.height)for (const [x, y, r, tint] of stars) {ctx.globalAlpha = 0.25 + Math.random() * 0.5-- ctx.fillStyle = tint ? '#a78bfa' : '#ffffff'++ ctx.fillStyle = tint++ ? getComputedStyle(document.documentElement)++ .getPropertyValue('--accent-light')++ .trim() || '#60a5fa'++ : '#ffffff'ctx.beginPath()ctx.arc(x * c.width, y * c.height, r, 0, 7)ctx.fill()@@@ -3,12 +3,79 @@<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />-- <title>Scraplandia</title>++ <title>Scraplandia — OpenVibe.Games</title>++ <meta++ name="description"++ content="Play Scraplandia: scrappy multiplayer survival-building in and around Scrap City, in your browser, no download. Part of the OpenVibe network."++ />++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->++ <script src="https://openvibe.network/shared/theme-loader.js"></script>++ <script src="https://openvibe.network/shared/navbar.js" defer></script>++ <script src="https://openvibe.network/shared/footer.js" defer></script>++ <link rel="canonical" href="https://play.openvibe.games/" />++ <meta name="robots" content="index, follow" />++ <meta property="og:type" content="website" />++ <meta property="og:site_name" content="OpenVibe.Games" />++ <meta property="og:title" content="Scraplandia — play in your browser" />++ <meta++ property="og:description"++ content="Scrappy multiplayer survival-building in and around Scrap City. Physgun anything, weld it to anything else, haul your salvage home. No download."++ />++ <meta property="og:url" content="https://play.openvibe.games/" />++ <meta property="og:image" content="https://openvibe.games/og-image.png" />++ <meta property="og:image:width" content="1200" />++ <meta property="og:image:height" content="630" />++ <meta name="twitter:card" content="summary_large_image" />++ <meta name="twitter:title" content="Scraplandia — play in your browser" />++ <meta++ name="twitter:description"++ content="Multiplayer survival-building on the OpenVibe network. In your browser, no download."++ />++ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />++ <script type="application/ld+json">++ {++ "@context": "https://schema.org",++ "@type": "VideoGame",++ "@id": "https://play.openvibe.games/#game",++ "name": "Scraplandia",++ "url": "https://play.openvibe.games/",++ "image": "https://openvibe.games/og-image.png",++ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",++ "genre": ["Survival", "Sandbox", "Building"],++ "playMode": "MultiPlayer",++ "gamePlatform": "Web browser",++ "applicationCategory": "Game",++ "operatingSystem": "Any",++ "isPartOf": {++ "@type": "WebSite",++ "name": "OpenVibe.Games",++ "url": "https://openvibe.games/"++ },++ "publisher": {++ "@type": "Organization",++ "name": "OpenVibe",++ "url": "https://openvibe.network/"++ }++ }++ </script><link rel="stylesheet" href="/src/ui/styles.css" />++ <script>++ window.__ovgChrome = { page: 'play', title: 'Scraplandia', collapsible: true }++ </script>++ <script src="/chrome.js" defer></script></head><body>-- <canvas id="game"></canvas>-- <div id="ui"></div>++ <!--++ The shared navbar mounts at the top; the game fills the stage below it.++ Once the player is in the world (the `ovg:playing` event) the chrome++ folds away and a small floating button brings it back — see chrome.js.++ -->++ <div id="navbar-mount"></div>++ <div id="stage">++ <canvas id="game"></canvas>++ <div id="ui"></div>++ </div>++ <footer id="ov-footer" class="ovf"></footer><script type="module" src="/src/main.ts"></script></body></html>nnew file mode 100644@@@ -0,0 +1,153 @@++/**++ * OpenVibe.Games — the shared network chrome (navbar + footer) on every page.++ *++ * The navbar and footer themselves come from openvibe.network/shared/*.js,++ * loaded by each page's <head>; this file only tells them who we are: the++ * site's links, where sign-in/sign-out live, and the session the game keeps++ * in the `ovg_sso` cookie / localStorage entry (set by /auth/callback).++ *++ * Pages configure it with `window.__ovgChrome = { page, title, collapsible }`++ * before this script runs:++ * page 'portal' | 'play' | 'editor'++ * title recorded to the signed-in user's network history (play only)++ * collapsible the chrome folds away once the game starts (the `ovg:playing`++ * event) and a small floating button brings it back.++ * toggleFromStart show that button immediately (the editor), not only after++ * `ovg:playing`.++ */++;(function () {++ 'use strict'++ var NETWORK = 'https://openvibe.network'++ var cfg = window.__ovgChrome || {}++ var local = ['localhost', '127.0.0.1'].indexOf(location.hostname) !== -1++ var onPlayHost = location.hostname.indexOf('play.') === 0++ var PORTAL = local || !onPlayHost ? '/' : 'https://openvibe.games/'++ var PLAY = local ? '/play' : onPlayHost ? '/' : 'https://play.openvibe.games/'++ var EDITOR = local || !onPlayHost ? '/editor' : 'https://openvibe.games/editor'++++ function cookie(name) {++ var m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'))++ try {++ return m ? decodeURIComponent(m[1]) : null++ } catch (e) {++ return m ? m[1] : null++ }++ }++ /** The openvibe.network access token this site holds, if any. */++ function sessionToken() {++ var t = null++ try {++ t = localStorage.getItem('ovg_sso')++ } catch (e) {++ /* storage unavailable */++ }++ return t || cookie('ovg_sso') || undefined++ }++++ var here = location.pathname + location.search++ var links = [++ { label: 'Games', href: PORTAL },++ { label: 'Scraplandia', href: PLAY },++ ]++ var footerLinks = [++ {++ heading: 'Games',++ items: [++ { label: 'All games', href: PORTAL },++ { label: 'Play Scraplandia', href: PLAY },++ { label: 'Map editor', href: EDITOR },++ ],++ },++ {++ heading: 'Account',++ items: [++ { label: 'Sign in', href: '/auth/login?next=' + encodeURIComponent(here) },++ { label: 'Sign out', href: '/auth/logout?next=' + encodeURIComponent(location.pathname) },++ { label: 'Themes', href: NETWORK + '/themes' },++ ],++ },++ ]++++ function initNavbar() {++ var opts = {++ service: 'games',++ apiBase: NETWORK,++ token: sessionToken(),++ // Same-origin fallback: resolves the ovg_sso cookie server-side when the++ // token is not readable here (or the Network rejects it client-side).++ sessionUrl: '/auth/me',++ loginUrl: '/auth/login?next=' + encodeURIComponent(here),++ onLogout: function () {++ location.href = '/auth/logout?next=' + encodeURIComponent(location.pathname)++ },++ links: links,++ menu: { after: [{ label: 'Map editor', href: EDITOR, icon: 'fa-map' }] },++ // One prompt=none attempt per tab when the browser says it has an OpenVibe account.++ silentLogin: '/auth/login?silent=1&next={url}',++ }++ if (cfg.page === 'play') opts.history = { type: 'game', title: cfg.title || 'Scraplandia' }++ window.OpenVibeNavbar.init(opts)++ }++++ function initFooter() {++ if (!document.getElementById('ov-footer')) return++ window.OpenVibeFooter.init({++ service: 'games',++ variant: cfg.page === 'portal' ? 'full' : 'compact',++ links: footerLinks,++ mount: '#ov-footer',++ })++ }++++ // ── Collapsible chrome for the game / editor pages ─────────────────────++ // The canvas is sized by CSS below the navbar, so showing or hiding the++ // chrome is a class flip plus a resize event for the engine.++ function setupCollapse() {++ var stage = document.getElementById('stage')++ if (!stage) return++ var btn = document.createElement('button')++ btn.id = 'ovg-chrome-toggle'++ btn.type = 'button'++ btn.setAttribute('aria-label', 'Show or hide the OpenVibe navigation')++ btn.innerHTML = '<span class="ovg-chrome-toggle-mark">☰</span> OpenVibe'++ // The game page reveals it once play starts; the editor has it from the start.++ btn.hidden = !cfg.toggleFromStart++ function apply(hidden) {++ document.body.classList.toggle('ovg-chrome-hidden', hidden)++ btn.classList.toggle('is-hidden', hidden)++ btn.title = hidden ? 'Show the OpenVibe navigation' : 'Hide the OpenVibe navigation'++ window.dispatchEvent(new Event('resize'))++ }++ btn.addEventListener('click', function () {++ apply(!document.body.classList.contains('ovg-chrome-hidden'))++ })++ stage.appendChild(btn)++ window.addEventListener('ovg:playing', function () {++ btn.hidden = false++ apply(true)++ })++ }++++ var tries = 0++ function boot() {++ if (!window.OpenVibeNavbar || !window.OpenVibeFooter) {++ // The shared scripts are deferred; if the Network is unreachable the++ // page simply runs without its chrome.++ if (++tries < 60) setTimeout(boot, 100)++ return++ }++ try {++ initNavbar()++ } catch (e) {++ /* navbar optional */++ }++ try {++ initFooter()++ } catch (e) {++ /* footer optional */++ }++ }++ if (cfg.collapsible) setupCollapse()++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot)++ else boot()++})()nnew file mode 100644BBinary files /dev/null and b/apps/client/public/og-image.png differ@@@ -519,6 +519,61 @@ body {sans-serif;}++/* Page frame: shared navbar, the editor stage, a one-line footer. The stage++ is the positioning root for the canvas, the workspace grid and the dialogs. */++body {++ display: flex;++ flex-direction: column;++}++#navbar-mount {++ flex: none;++ position: relative;++ z-index: 30;++}++#stage {++ position: relative;++ flex: 1 1 auto;++ min-height: 0;++}++#ov-footer {++ flex: none;++}++#ov-footer.ovf {++ margin-top: 0 !important;++ padding-top: 6px !important;++ padding-bottom: 6px !important;++}++body.ovg-chrome-hidden #navbar-mount,++body.ovg-chrome-hidden #ov-footer {++ display: none;++}++#ovg-chrome-toggle {++ position: absolute;++ right: 10px;++ bottom: 30px;++ z-index: 40;++ display: inline-flex;++ align-items: center;++ gap: 6px;++ padding: 4px 10px;++ border-radius: 999px;++ border: 1px solid var(--line, #2a3340);++ background: var(--panel-bg, rgba(10, 14, 18, 0.8));++ color: var(--dim, #78828e);++ font:++ 600 11px/1 system-ui,++ sans-serif;++ cursor: pointer;++ opacity: 0.6;++}++#ovg-chrome-toggle:hover {++ opacity: 1;++ color: var(--ink, #d5dbe2);++}++#ovg-chrome-toggle[hidden] {++ display: none;++}++#game {position: absolute;inset: 0;@@@ -152,6 +152,9 @@ async function start(): Promise<void> {}const slot = choice.slotsaveName(name)++ // The player is heading into the world: the page folds the network chrome++ // away so the canvas gets the whole viewport (see public/chrome.js).++ window.dispatchEvent(new CustomEvent('ovg:playing'))const havok = await havokPromiseconst physics = createHavokWorldForScene(scene, havok)@@@ -21,6 +21,28 @@ input {-webkit-user-select: text;}++/* ── Page frame ────────────────────────────────────────────────────++ Shared navbar on top, the game stage filling the rest, a one-line++ footer below. Once the player is in the world the chrome folds away++ (body.ovg-chrome-hidden, see public/chrome.js) and the stage takes the++ whole viewport; the engine resizes off the canvas's CSS box. */++body {++ display: flex;++ flex-direction: column;++}++++#navbar-mount {++ flex: none;++ position: relative;++ z-index: 30;++}++++#stage {++ position: relative;++ flex: 1 1 auto;++ min-height: 0;++}++#game {width: 100%;height: 100%;@@@ -29,11 +51,55 @@ input {}#ui {-- position: fixed;++ position: absolute;inset: 0;pointer-events: none;}++#ov-footer {++ flex: none;++}++#ov-footer.ovf {++ margin-top: 0 !important;++ padding-top: 8px !important;++ padding-bottom: 8px !important;++}++++body.ovg-chrome-hidden #navbar-mount,++body.ovg-chrome-hidden #ov-footer {++ display: none;++}++++/* Floating "bring the chrome back" pill (chrome.js adds it once the game starts). */++#ovg-chrome-toggle {++ position: absolute;++ top: 10px;++ left: 10px;++ z-index: 40;++ display: inline-flex;++ align-items: center;++ gap: 6px;++ padding: 5px 11px;++ border-radius: 999px;++ border: 1px solid var(--border, #2a3340);++ background: var(--bg-secondary, rgba(10, 14, 18, 0.8));++ color: var(--text-secondary, #b0b0b8);++ font:++ 600 12px/1 system-ui,++ sans-serif;++ cursor: pointer;++ opacity: 0.55;++ transition: opacity 0.15s;++}++#ovg-chrome-toggle:hover,++#ovg-chrome-toggle:focus-visible {++ opacity: 1;++ color: var(--text-primary, #e6e6e6);++}++#ovg-chrome-toggle[hidden] {++ display: none;++}++.crosshair {position: absolute;left: 50%;
@@ -3,7 +3,32 @@
@@ -3,7 +3,32 @@
<head>
<head>
<meta charset="UTF-8" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Scraplandia Map Editor</title>
+ <title>Scraplandia Map Editor — OpenVibe.Games</title>
+ <meta
+ name="description"
+ content="Collaborative map editor for Scraplandia on OpenVibe.Games."
+ />
+ <!-- An admin tool: not for search engines. -->
+ <meta name="robots" content="noindex, nofollow" />
+ <link rel="canonical" href="https://openvibe.games/editor" />
+ <meta property="og:type" content="website" />
+ <meta property="og:site_name" content="OpenVibe.Games" />
+ <meta property="og:title" content="Scraplandia Map Editor" />
+ <meta
+ property="og:description"
+ content="Collaborative map editor for Scraplandia on OpenVibe.Games."
+ />
+ <meta property="og:url" content="https://openvibe.games/editor" />
+ <meta property="og:image" content="https://openvibe.games/og-image.png" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
+ <script src="https://openvibe.network/shared/theme-loader.js"></script>
+ <script src="https://openvibe.network/shared/navbar.js" defer></script>
+ <script src="https://openvibe.network/shared/footer.js" defer></script>
+ <script>
+ window.__ovgChrome = { page: 'editor', collapsible: true, toggleFromStart: true }
+ </script>
+ <script src="/chrome.js" defer></script>
</head>
</head>
<body>
<body>
<!--
<!--
@@ -15,39 +40,47 @@
@@ -15,39 +40,47 @@
What stays here is what must exist before any script runs (the canvas)
What stays here is what must exist before any script runs (the canvas)
and the overlays that are not part of the workspace grid.
and the overlays that are not part of the workspace grid.
-->
-->
- <canvas id="game"></canvas>
- <div id="shell"></div>
+ <div id="navbar-mount"></div>
+ <div id="stage">
+ <canvas id="game"></canvas>
+ <div id="shell"></div>
- <!-- Revision conflict. Nothing here merges, and it says so. -->
- <div id="conflict" class="overlay" style="display: none">
- <div class="overlay-card">
- <h2>⚠ Newer map saved by another admin</h2>
- <div id="conflict-summary" class="hint"></div>
- <div class="overlay-actions">
- <button id="conflict-load" class="mini" type="button">Discard mine · load theirs</button>
- <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>
- <button id="conflict-export" class="mini" type="button">💾 Download my local copy</button>
- </div>
- <div class="hint">
- "Keep mine" only dismisses this panel — Save stays refused until you reload.
+ <!-- Revision conflict. Nothing here merges, and it says so. -->
+ <div id="conflict" class="overlay" style="display: none">
+ <div class="overlay-card">
+ <h2>⚠ Newer map saved by another admin</h2>
+ <div id="conflict-summary" class="hint"></div>
+ <div class="overlay-actions">
+ <button id="conflict-load" class="mini" type="button">
+ Discard mine · load theirs
+ </button>
+ <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>
+ <button id="conflict-export" class="mini" type="button">
+ 💾 Download my local copy
+ </button>
+ </div>
+ <div class="hint">
+ "Keep mine" only dismisses this panel — Save stays refused until you reload.
+ </div>
</div>
</div>
</div>
</div>
- </div>
- <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->
- <div id="settings" class="overlay" style="display: none">
- <div class="overlay-card">
- <h2>⌨ Keyboard shortcuts</h2>
- <div id="keys-list" class="keys-list"></div>
- <div class="overlay-actions">
- <button id="keys-reset" class="mini" type="button">Reset to defaults</button>
- <button id="settings-close" class="mini" type="button">Close</button>
+ <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->
+ <div id="settings" class="overlay" style="display: none">
+ <div class="overlay-card">
+ <h2>⌨ Keyboard shortcuts</h2>
+ <div id="keys-list" class="keys-list"></div>
+ <div class="overlay-actions">
+ <button id="keys-reset" class="mini" type="button">Reset to defaults</button>
+ <button id="settings-close" class="mini" type="button">Close</button>
+ </div>
</div>
</div>
</div>
</div>
- </div>
- <!-- Co-editor name tags, positioned over the viewport. -->
- <div id="peers"></div>
+ <!-- Co-editor name tags, positioned over the viewport. -->
+ <div id="peers"></div>
+ </div>
+ <footer id="ov-footer" class="ovf"></footer>
<script type="module" src="/src/editor/main.ts"></script>
<script type="module" src="/src/editor/main.ts"></script>
</body>
</body>
@@ -3,28 +3,93 @@
@@ -3,28 +3,93 @@
<head>
<head>
<meta charset="UTF-8" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>OpenVibe.Games — free browser games on the OpenVibe network</title>
+ <title>OpenVibe.Games — browser games on the OpenVibe network</title>
<meta
<meta
name="description"
name="description"
- content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — free, in your browser, no download."
+ content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download."
/>
/>
+ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
+ <script src="https://openvibe.network/shared/theme-loader.js"></script>
+ <script src="https://openvibe.network/shared/navbar.js" defer></script>
+ <script src="https://openvibe.network/shared/footer.js" defer></script>
+ <link rel="canonical" href="https://openvibe.games/" />
+ <meta name="robots" content="index, follow" />
+ <meta property="og:type" content="website" />
+ <meta property="og:site_name" content="OpenVibe.Games" />
+ <meta property="og:title" content="OpenVibe.Games — browser games on the OpenVibe network" />
+ <meta
+ property="og:description"
+ content="Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download. One OpenVibe account across the whole network."
+ />
+ <meta property="og:url" content="https://openvibe.games/" />
+ <meta property="og:image" content="https://openvibe.games/og-image.png" />
+ <meta property="og:image:width" content="1200" />
+ <meta property="og:image:height" content="630" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:title" content="OpenVibe.Games — browser games on the OpenVibe network" />
+ <meta
+ name="twitter:description"
+ content="Play Scraplandia — multiplayer survival-building in your browser, no download."
+ />
+ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />
+ <script type="application/ld+json">
+ {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "WebSite",
+ "@id": "https://openvibe.games/#website",
+ "name": "OpenVibe.Games",
+ "url": "https://openvibe.games/",
+ "description": "Browser games on the OpenVibe network — multiplayer, in your browser, no download.",
+ "publisher": {
+ "@type": "Organization",
+ "name": "OpenVibe",
+ "url": "https://openvibe.network/"
+ }
+ },
+ {
+ "@type": "VideoGame",
+ "@id": "https://play.openvibe.games/#game",
+ "name": "Scraplandia",
+ "url": "https://play.openvibe.games/",
+ "image": "https://openvibe.games/og-image.png",
+ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",
+ "genre": ["Survival", "Sandbox", "Building"],
+ "playMode": "MultiPlayer",
+ "gamePlatform": "Web browser",
+ "applicationCategory": "Game",
+ "operatingSystem": "Any",
+ "isPartOf": { "@id": "https://openvibe.games/#website" },
+ "publisher": {
+ "@type": "Organization",
+ "name": "OpenVibe",
+ "url": "https://openvibe.network/"
+ }
+ }
+ ]
+ }
+ </script>
<style>
<style>
* {
* {
margin: 0;
margin: 0;
box-sizing: border-box;
box-sizing: border-box;
}
}
+ /* Page palette. The shared theme-loader sets --accent, --accent-light, --accent-dark,
+ --accent-rgb, --bg-primary, --text-primary… on <html>, so the portal follows the
+ signed-in user's theme; the values here are the fallbacks before it runs. */
:root {
:root {
- --accent: #8b5cf6;
- --accent-light: #a78bfa;
- --accent-dark: #6d28d9;
+ --accent: #3b82f6;
+ --accent-light: #60a5fa;
+ --accent-dark: #1d4ed8;
--secondary: #22d3ee;
--secondary: #22d3ee;
- --bg: #131318;
- --bg-raised: #1a1a22;
- --bg-card: rgba(26, 26, 34, 0.88);
- --border: #2a2a36;
- --text: #e8e8f0;
- --text-dim: #a0a0b2;
- --text-faint: #6a6a7a;
+ --bg: var(--bg-primary, #0a0f1c);
+ --bg-raised: var(--bg-secondary, #101828);
+ --bg-card: var(--bg-card-page, rgba(19, 28, 46, 0.88));
+ --border: #1f2d47;
+ --text: var(--text-primary, #e6edf7);
+ --text-dim: var(--text-secondary, #96a7c2);
+ --text-faint: var(--text-muted, #7386a3);
}
}
html,
html,
body {
body {
@@ -45,9 +110,13 @@
@@ -45,9 +110,13 @@
inset: 0;
inset: 0;
z-index: -2;
z-index: -2;
background:
background:
- radial-gradient(1100px 520px at 50% -8%, rgba(139, 92, 246, 0.22), transparent 68%),
+ radial-gradient(
+ 1100px 520px at 50% -8%,
+ rgba(var(--accent-rgb, 59, 130, 246), 0.22),
+ transparent 68%
+ ),
radial-gradient(800px 420px at 88% 28%, rgba(34, 211, 238, 0.07), transparent 60%),
radial-gradient(800px 420px at 88% 28%, rgba(34, 211, 238, 0.07), transparent 60%),
- linear-gradient(180deg, #16121f 0%, var(--bg) 46%);
+ linear-gradient(180deg, var(--bg-raised) 0%, var(--bg) 46%);
}
}
.stars {
.stars {
position: fixed;
position: fixed;
@@ -56,59 +125,6 @@
@@ -56,59 +125,6 @@
opacity: 0.7;
opacity: 0.7;
}
}
- /* ── nav ─────────────────────────────────────────────────────── */
- nav {
- display: flex;
- align-items: center;
- gap: 20px;
- padding: 16px clamp(16px, 4vw, 40px);
- max-width: 1200px;
- margin: 0 auto;
- }
- .brand {
- font-weight: 900;
- font-size: 19px;
- letter-spacing: 0.02em;
- text-decoration: none;
- color: var(--text);
- white-space: nowrap;
- }
- .brand b {
- background: linear-gradient(120deg, var(--accent-light), var(--accent));
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- }
- .nav-links {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
- }
- .nav-links a {
- color: var(--text-dim);
- text-decoration: none;
- font-size: 14px;
- padding: 8px 12px;
- border-radius: 8px;
- transition:
- color 0.15s,
- background 0.15s;
- }
- .nav-links a:hover {
- color: var(--text);
- background: rgba(139, 92, 246, 0.12);
- }
- .nav-links a.signin {
- color: #fff;
- background: linear-gradient(120deg, var(--accent), var(--accent-dark));
- font-weight: 700;
- }
- .nav-links a.signin:hover {
- background: linear-gradient(120deg, var(--accent-light), var(--accent));
- }
-
/* ── hero ────────────────────────────────────────────────────── */
/* ── hero ────────────────────────────────────────────────────── */
header {
header {
text-align: center;
text-align: center;
@@ -122,11 +138,16 @@
@@ -122,11 +138,16 @@
animation: rise 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both;
animation: rise 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
}
.hero-title b {
.hero-title b {
- background: linear-gradient(120deg, var(--accent-light) 10%, var(--accent) 55%, var(--secondary) 110%);
+ background: linear-gradient(
+ 120deg,
+ var(--accent-light) 10%,
+ var(--accent) 55%,
+ var(--secondary) 110%
+ );
-webkit-background-clip: text;
-webkit-background-clip: text;
background-clip: text;
background-clip: text;
color: transparent;
color: transparent;
- filter: drop-shadow(0 6px 34px rgba(139, 92, 246, 0.35));
+ filter: drop-shadow(0 6px 34px rgba(var(--accent-rgb, 59, 130, 246), 0.35));
}
}
.hero-sub {
.hero-sub {
margin: 18px auto 0;
margin: 18px auto 0;
@@ -171,7 +192,7 @@
@@ -171,7 +192,7 @@
.game-card:hover {
.game-card:hover {
transform: translateY(-5px);
transform: translateY(-5px);
border-color: var(--accent);
border-color: var(--accent);
- box-shadow: 0 14px 44px rgba(139, 92, 246, 0.18);
+ box-shadow: 0 14px 44px rgba(var(--accent-rgb, 59, 130, 246), 0.18);
}
}
.game-card.featured {
.game-card.featured {
grid-column: 1 / -1;
grid-column: 1 / -1;
@@ -190,17 +211,21 @@
@@ -190,17 +211,21 @@
align-items: center;
align-items: center;
justify-content: center;
justify-content: center;
background:
background:
- radial-gradient(420px 200px at 30% 20%, rgba(139, 92, 246, 0.35), transparent 70%),
+ radial-gradient(
+ 420px 200px at 30% 20%,
+ rgba(var(--accent-rgb, 59, 130, 246), 0.35),
+ transparent 70%
+ ),
radial-gradient(380px 220px at 75% 85%, rgba(34, 211, 238, 0.18), transparent 70%),
radial-gradient(380px 220px at 75% 85%, rgba(34, 211, 238, 0.18), transparent 70%),
- linear-gradient(160deg, #221a33, #141420 70%);
+ linear-gradient(160deg, var(--bg-raised), var(--bg) 70%);
}
}
.shot::after {
.shot::after {
content: '';
content: '';
position: absolute;
position: absolute;
inset: 0;
inset: 0;
background-image:
background-image:
- linear-gradient(rgba(167, 139, 250, 0.06) 1px, transparent 1px),
- linear-gradient(90deg, rgba(167, 139, 250, 0.06) 1px, transparent 1px);
+ linear-gradient(rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px);
background-size: 34px 34px;
background-size: 34px 34px;
mask-image: radial-gradient(closest-side, #000, transparent);
mask-image: radial-gradient(closest-side, #000, transparent);
-webkit-mask-image: radial-gradient(closest-side, #000, transparent);
-webkit-mask-image: radial-gradient(closest-side, #000, transparent);
@@ -231,8 +256,8 @@
@@ -231,8 +256,8 @@
}
}
.badge.soon {
.badge.soon {
color: var(--accent-light);
color: var(--accent-light);
- background: rgba(139, 92, 246, 0.14);
- border: 1px solid rgba(139, 92, 246, 0.35);
+ background: rgba(var(--accent-rgb, 59, 130, 246), 0.14);
+ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.35);
}
}
.game-body h2 {
.game-body h2 {
font-size: clamp(26px, 3.4vw, 38px);
font-size: clamp(26px, 3.4vw, 38px);
@@ -273,7 +298,7 @@
@@ -273,7 +298,7 @@
text-decoration: none;
text-decoration: none;
color: #fff;
color: #fff;
background: linear-gradient(120deg, var(--accent), var(--accent-dark));
background: linear-gradient(120deg, var(--accent), var(--accent-dark));
- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);
+ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);
transition:
transition:
transform 0.15s ease,
transform 0.15s ease,
box-shadow 0.15s ease;
box-shadow 0.15s ease;
@@ -285,10 +310,10 @@
@@ -285,10 +310,10 @@
@keyframes pulse {
@keyframes pulse {
0%,
0%,
100% {
100% {
- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);
+ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);
}
}
50% {
50% {
- box-shadow: 0 8px 48px rgba(139, 92, 246, 0.7);
+ box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);
}
}
}
}
.game-card.soon {
.game-card.soon {
@@ -309,8 +334,12 @@
@@ -309,8 +334,12 @@
padding: 0 20px;
padding: 0 20px;
}
}
.account-inner {
.account-inner {
- background: linear-gradient(160deg, rgba(139, 92, 246, 0.12), rgba(26, 26, 34, 0.9) 55%);
- border: 1px solid rgba(139, 92, 246, 0.3);
+ background: linear-gradient(
+ 160deg,
+ rgba(var(--accent-rgb, 59, 130, 246), 0.12),
+ var(--bg-card) 55%
+ );
+ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.3);
border-radius: 20px;
border-radius: 20px;
padding: clamp(26px, 5vw, 44px);
padding: clamp(26px, 5vw, 44px);
text-align: center;
text-align: center;
@@ -348,27 +377,14 @@
@@ -348,27 +377,14 @@
color: var(--text-faint);
color: var(--text-faint);
}
}
- /* ── footer ──────────────────────────────────────────────────── */
- footer {
- border-top: 1px solid var(--border);
- padding: 30px 20px 40px;
- text-align: center;
- color: var(--text-faint);
- font-size: 13px;
- line-height: 2;
- }
- footer a {
- color: var(--text-dim);
- text-decoration: none;
- margin: 0 10px;
- }
- footer a:hover {
- color: var(--accent-light);
+ /* ── shared chrome ───────────────────────────────────────────── */
+ #navbar-mount {
+ position: relative;
+ z-index: 5;
}
}
- .foot-brand {
- font-weight: 800;
- color: var(--text-dim);
- letter-spacing: 0.06em;
+ #ov-footer {
+ max-width: 1200px;
+ margin: 0 auto;
}
}
</style>
</style>
</head>
</head>
@@ -376,24 +392,13 @@
@@ -376,24 +392,13 @@
<div class="backdrop"></div>
<div class="backdrop"></div>
<canvas class="stars" id="stars"></canvas>
<canvas class="stars" id="stars"></canvas>
- <nav>
- <a class="brand" href="/">Open<b>Vibe</b>.Games</a>
- <div class="nav-links">
- <a href="https://openvibe.live" target="_blank" rel="noopener">openvibe.live</a>
- <a href="https://openvibe.tools" target="_blank" rel="noopener">openvibe.tools</a>
- <a
- class="signin"
- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"
- >Sign in</a
- >
- </div>
- </nav>
+ <div id="navbar-mount"></div>
<header>
<header>
<h1 class="hero-title">Open<b>Vibe.Games</b></h1>
<h1 class="hero-title">Open<b>Vibe.Games</b></h1>
<p class="hero-sub">
<p class="hero-sub">
- Free multiplayer games that run in your browser. No installs, no launchers — one OpenVibe
- account across the whole network.
+ Multiplayer games that run in your browser. No installs, no launchers — one OpenVibe account
+ across the whole network.
</p>
</p>
</header>
</header>
@@ -416,7 +421,7 @@
@@ -416,7 +421,7 @@
><span>PvP wilds</span><span>bottle caps</span>
><span>PvP wilds</span><span>bottle caps</span>
</div>
</div>
<a class="play-cta" href="https://play.openvibe.games" data-dev-href="/play"
<a class="play-cta" href="https://play.openvibe.games" data-dev-href="/play"
- >▶ PLAY FREE — NO DOWNLOAD</a
+ >▶ PLAY — NO DOWNLOAD</a
>
>
</div>
</div>
</div>
</div>
@@ -427,7 +432,9 @@
@@ -427,7 +432,9 @@
<div class="game-body">
<div class="game-body">
<span class="badge soon">COMING SOON</span>
<span class="badge soon">COMING SOON</span>
<h3>Vibe Tables</h3>
<h3>Vibe Tables</h3>
- <p>Casual card and board nights with your stream chat — join from a link, no account walls.</p>
+ <p>
+ Casual card and board nights with your stream chat — join from a link, no account walls.
+ </p>
</div>
</div>
</div>
</div>
<div class="game-card soon">
<div class="game-card soon">
@@ -453,31 +460,22 @@
@@ -453,31 +460,22 @@
<h2>One character as a guest. <b>Three</b> with an OpenVibe account.</h2>
<h2>One character as a guest. <b>Three</b> with an OpenVibe account.</h2>
<p>
<p>
Jump into any game instantly as a guest — your character is tied to this browser and
Jump into any game instantly as a guest — your character is tied to this browser and
- connection. Sign in with your free <b>OpenVibe</b> account to unlock three character
- slots, keep progress safe across devices and IP changes, and carry one identity across
+ connection. Sign in with your <b>OpenVibe</b> account to unlock three character slots,
+ keep progress safe across devices and IP changes, and carry one identity across
openvibe.live, openvibe.tools and every game here.
openvibe.live, openvibe.tools and every game here.
</p>
</p>
- <a
- class="signin"
- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"
- >Sign in with OpenVibe</a
- >
+ <a class="signin" href="/auth/login?next=%2F">Sign in with OpenVibe</a>
<div class="fine">One account. All of OpenVibe.</div>
<div class="fine">One account. All of OpenVibe.</div>
</div>
</div>
</section>
</section>
- <footer>
- <div>
- <a href="https://openvibe.network">openvibe.network</a>
- <a href="https://openvibe.live">openvibe.live</a>
- <a href="https://openvibe.tools">openvibe.tools</a>
- <a href="https://openvibe.media">openvibe.media</a>
- <a href="https://openvibe.community">openvibe.community</a>
- </div>
- <div class="foot-brand">OPENVIBE.GAMES</div>
- <div>Part of the OpenVibe network · One Account. All of OpenVibe.</div>
- </footer>
+ <!-- Shared cross-site footer (openvibe.network/shared/footer.js): network links, legal, signed-in row. -->
+ <footer id="ov-footer" class="ovf"></footer>
+ <script>
+ window.__ovgChrome = { page: 'portal' }
+ </script>
+ <script src="/chrome.js" defer></script>
<script>
<script>
// Dev convenience: on localhost the play. vhost doesn't exist, so PLAY
// Dev convenience: on localhost the play. vhost doesn't exist, so PLAY
// buttons fall back to the same-origin /play route (Vite alias / server).
// buttons fall back to the same-origin /play route (Vite alias / server).
@@ -506,7 +504,11 @@
@@ -506,7 +504,11 @@
ctx.clearRect(0, 0, c.width, c.height)
ctx.clearRect(0, 0, c.width, c.height)
for (const [x, y, r, tint] of stars) {
for (const [x, y, r, tint] of stars) {
ctx.globalAlpha = 0.25 + Math.random() * 0.5
ctx.globalAlpha = 0.25 + Math.random() * 0.5
- ctx.fillStyle = tint ? '#a78bfa' : '#ffffff'
+ ctx.fillStyle = tint
+ ? getComputedStyle(document.documentElement)
+ .getPropertyValue('--accent-light')
+ .trim() || '#60a5fa'
+ : '#ffffff'
ctx.beginPath()
ctx.beginPath()
ctx.arc(x * c.width, y * c.height, r, 0, 7)
ctx.arc(x * c.width, y * c.height, r, 0, 7)
ctx.fill()
ctx.fill()
@@ -3,12 +3,79 @@
@@ -3,12 +3,79 @@
<head>
<head>
<meta charset="utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
- <title>Scraplandia</title>
+ <title>Scraplandia — OpenVibe.Games</title>
+ <meta
+ name="description"
+ content="Play Scraplandia: scrappy multiplayer survival-building in and around Scrap City, in your browser, no download. Part of the OpenVibe network."
+ />
+ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
+ <script src="https://openvibe.network/shared/theme-loader.js"></script>
+ <script src="https://openvibe.network/shared/navbar.js" defer></script>
+ <script src="https://openvibe.network/shared/footer.js" defer></script>
+ <link rel="canonical" href="https://play.openvibe.games/" />
+ <meta name="robots" content="index, follow" />
+ <meta property="og:type" content="website" />
+ <meta property="og:site_name" content="OpenVibe.Games" />
+ <meta property="og:title" content="Scraplandia — play in your browser" />
+ <meta
+ property="og:description"
+ content="Scrappy multiplayer survival-building in and around Scrap City. Physgun anything, weld it to anything else, haul your salvage home. No download."
+ />
+ <meta property="og:url" content="https://play.openvibe.games/" />
+ <meta property="og:image" content="https://openvibe.games/og-image.png" />
+ <meta property="og:image:width" content="1200" />
+ <meta property="og:image:height" content="630" />
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:title" content="Scraplandia — play in your browser" />
+ <meta
+ name="twitter:description"
+ content="Multiplayer survival-building on the OpenVibe network. In your browser, no download."
+ />
+ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />
+ <script type="application/ld+json">
+ {
+ "@context": "https://schema.org",
+ "@type": "VideoGame",
+ "@id": "https://play.openvibe.games/#game",
+ "name": "Scraplandia",
+ "url": "https://play.openvibe.games/",
+ "image": "https://openvibe.games/og-image.png",
+ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",
+ "genre": ["Survival", "Sandbox", "Building"],
+ "playMode": "MultiPlayer",
+ "gamePlatform": "Web browser",
+ "applicationCategory": "Game",
+ "operatingSystem": "Any",
+ "isPartOf": {
+ "@type": "WebSite",
+ "name": "OpenVibe.Games",
+ "url": "https://openvibe.games/"
+ },
+ "publisher": {
+ "@type": "Organization",
+ "name": "OpenVibe",
+ "url": "https://openvibe.network/"
+ }
+ }
+ </script>
<link rel="stylesheet" href="/src/ui/styles.css" />
<link rel="stylesheet" href="/src/ui/styles.css" />
+ <script>
+ window.__ovgChrome = { page: 'play', title: 'Scraplandia', collapsible: true }
+ </script>
+ <script src="/chrome.js" defer></script>
</head>
</head>
<body>
<body>
- <canvas id="game"></canvas>
- <div id="ui"></div>
+ <!--
+ The shared navbar mounts at the top; the game fills the stage below it.
+ Once the player is in the world (the `ovg:playing` event) the chrome
+ folds away and a small floating button brings it back — see chrome.js.
+ -->
+ <div id="navbar-mount"></div>
+ <div id="stage">
+ <canvas id="game"></canvas>
+ <div id="ui"></div>
+ </div>
+ <footer id="ov-footer" class="ovf"></footer>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</body>
</html>
</html>
new file mode 100644
new file mode 100644
@@ -0,0 +1,153 @@
@@ -0,0 +1,153 @@
+/**
+ * OpenVibe.Games — the shared network chrome (navbar + footer) on every page.
+ *
+ * The navbar and footer themselves come from openvibe.network/shared/*.js,
+ * loaded by each page's <head>; this file only tells them who we are: the
+ * site's links, where sign-in/sign-out live, and the session the game keeps
+ * in the `ovg_sso` cookie / localStorage entry (set by /auth/callback).
+ *
+ * Pages configure it with `window.__ovgChrome = { page, title, collapsible }`
+ * before this script runs:
+ * page 'portal' | 'play' | 'editor'
+ * title recorded to the signed-in user's network history (play only)
+ * collapsible the chrome folds away once the game starts (the `ovg:playing`
+ * event) and a small floating button brings it back.
+ * toggleFromStart show that button immediately (the editor), not only after
+ * `ovg:playing`.
+ */
+;(function () {
+ 'use strict'
+ var NETWORK = 'https://openvibe.network'
+ var cfg = window.__ovgChrome || {}
+ var local = ['localhost', '127.0.0.1'].indexOf(location.hostname) !== -1
+ var onPlayHost = location.hostname.indexOf('play.') === 0
+ var PORTAL = local || !onPlayHost ? '/' : 'https://openvibe.games/'
+ var PLAY = local ? '/play' : onPlayHost ? '/' : 'https://play.openvibe.games/'
+ var EDITOR = local || !onPlayHost ? '/editor' : 'https://openvibe.games/editor'
+
+ function cookie(name) {
+ var m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'))
+ try {
+ return m ? decodeURIComponent(m[1]) : null
+ } catch (e) {
+ return m ? m[1] : null
+ }
+ }
+ /** The openvibe.network access token this site holds, if any. */
+ function sessionToken() {
+ var t = null
+ try {
+ t = localStorage.getItem('ovg_sso')
+ } catch (e) {
+ /* storage unavailable */
+ }
+ return t || cookie('ovg_sso') || undefined
+ }
+
+ var here = location.pathname + location.search
+ var links = [
+ { label: 'Games', href: PORTAL },
+ { label: 'Scraplandia', href: PLAY },
+ ]
+ var footerLinks = [
+ {
+ heading: 'Games',
+ items: [
+ { label: 'All games', href: PORTAL },
+ { label: 'Play Scraplandia', href: PLAY },
+ { label: 'Map editor', href: EDITOR },
+ ],
+ },
+ {
+ heading: 'Account',
+ items: [
+ { label: 'Sign in', href: '/auth/login?next=' + encodeURIComponent(here) },
+ { label: 'Sign out', href: '/auth/logout?next=' + encodeURIComponent(location.pathname) },
+ { label: 'Themes', href: NETWORK + '/themes' },
+ ],
+ },
+ ]
+
+ function initNavbar() {
+ var opts = {
+ service: 'games',
+ apiBase: NETWORK,
+ token: sessionToken(),
+ // Same-origin fallback: resolves the ovg_sso cookie server-side when the
+ // token is not readable here (or the Network rejects it client-side).
+ sessionUrl: '/auth/me',
+ loginUrl: '/auth/login?next=' + encodeURIComponent(here),
+ onLogout: function () {
+ location.href = '/auth/logout?next=' + encodeURIComponent(location.pathname)
+ },
+ links: links,
+ menu: { after: [{ label: 'Map editor', href: EDITOR, icon: 'fa-map' }] },
+ // One prompt=none attempt per tab when the browser says it has an OpenVibe account.
+ silentLogin: '/auth/login?silent=1&next={url}',
+ }
+ if (cfg.page === 'play') opts.history = { type: 'game', title: cfg.title || 'Scraplandia' }
+ window.OpenVibeNavbar.init(opts)
+ }
+
+ function initFooter() {
+ if (!document.getElementById('ov-footer')) return
+ window.OpenVibeFooter.init({
+ service: 'games',
+ variant: cfg.page === 'portal' ? 'full' : 'compact',
+ links: footerLinks,
+ mount: '#ov-footer',
+ })
+ }
+
+ // ── Collapsible chrome for the game / editor pages ─────────────────────
+ // The canvas is sized by CSS below the navbar, so showing or hiding the
+ // chrome is a class flip plus a resize event for the engine.
+ function setupCollapse() {
+ var stage = document.getElementById('stage')
+ if (!stage) return
+ var btn = document.createElement('button')
+ btn.id = 'ovg-chrome-toggle'
+ btn.type = 'button'
+ btn.setAttribute('aria-label', 'Show or hide the OpenVibe navigation')
+ btn.innerHTML = '<span class="ovg-chrome-toggle-mark">☰</span> OpenVibe'
+ // The game page reveals it once play starts; the editor has it from the start.
+ btn.hidden = !cfg.toggleFromStart
+ function apply(hidden) {
+ document.body.classList.toggle('ovg-chrome-hidden', hidden)
+ btn.classList.toggle('is-hidden', hidden)
+ btn.title = hidden ? 'Show the OpenVibe navigation' : 'Hide the OpenVibe navigation'
+ window.dispatchEvent(new Event('resize'))
+ }
+ btn.addEventListener('click', function () {
+ apply(!document.body.classList.contains('ovg-chrome-hidden'))
+ })
+ stage.appendChild(btn)
+ window.addEventListener('ovg:playing', function () {
+ btn.hidden = false
+ apply(true)
+ })
+ }
+
+ var tries = 0
+ function boot() {
+ if (!window.OpenVibeNavbar || !window.OpenVibeFooter) {
+ // The shared scripts are deferred; if the Network is unreachable the
+ // page simply runs without its chrome.
+ if (++tries < 60) setTimeout(boot, 100)
+ return
+ }
+ try {
+ initNavbar()
+ } catch (e) {
+ /* navbar optional */
+ }
+ try {
+ initFooter()
+ } catch (e) {
+ /* footer optional */
+ }
+ }
+ if (cfg.collapsible) setupCollapse()
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot)
+ else boot()
+})()
new file mode 100644
new file mode 100644
Binary files /dev/null and b/apps/client/public/og-image.png differ
Binary files /dev/null and b/apps/client/public/og-image.png differ
@@ -519,6 +519,61 @@ body {
@@ -519,6 +519,61 @@ body {
sans-serif;
sans-serif;
}
}
+/* Page frame: shared navbar, the editor stage, a one-line footer. The stage
+ is the positioning root for the canvas, the workspace grid and the dialogs. */
+body {
+ display: flex;
+ flex-direction: column;
+}
+#navbar-mount {
+ flex: none;
+ position: relative;
+ z-index: 30;
+}
+#stage {
+ position: relative;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+#ov-footer {
+ flex: none;
+}
+#ov-footer.ovf {
+ margin-top: 0 !important;
+ padding-top: 6px !important;
+ padding-bottom: 6px !important;
+}
+body.ovg-chrome-hidden #navbar-mount,
+body.ovg-chrome-hidden #ov-footer {
+ display: none;
+}
+#ovg-chrome-toggle {
+ position: absolute;
+ right: 10px;
+ bottom: 30px;
+ z-index: 40;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ border: 1px solid var(--line, #2a3340);
+ background: var(--panel-bg, rgba(10, 14, 18, 0.8));
+ color: var(--dim, #78828e);
+ font:
+ 600 11px/1 system-ui,
+ sans-serif;
+ cursor: pointer;
+ opacity: 0.6;
+}
+#ovg-chrome-toggle:hover {
+ opacity: 1;
+ color: var(--ink, #d5dbe2);
+}
+#ovg-chrome-toggle[hidden] {
+ display: none;
+}
+
#game {
#game {
position: absolute;
position: absolute;
inset: 0;
inset: 0;
@@ -152,6 +152,9 @@ async function start(): Promise<void> {
@@ -152,6 +152,9 @@ async function start(): Promise<void> {
}
}
const slot = choice.slot
const slot = choice.slot
saveName(name)
saveName(name)
+ // The player is heading into the world: the page folds the network chrome
+ // away so the canvas gets the whole viewport (see public/chrome.js).
+ window.dispatchEvent(new CustomEvent('ovg:playing'))
const havok = await havokPromise
const havok = await havokPromise
const physics = createHavokWorldForScene(scene, havok)
const physics = createHavokWorldForScene(scene, havok)
@@ -21,6 +21,28 @@ input {
@@ -21,6 +21,28 @@ input {
-webkit-user-select: text;
-webkit-user-select: text;
}
}
+/* ── Page frame ────────────────────────────────────────────────────
+ Shared navbar on top, the game stage filling the rest, a one-line
+ footer below. Once the player is in the world the chrome folds away
+ (body.ovg-chrome-hidden, see public/chrome.js) and the stage takes the
+ whole viewport; the engine resizes off the canvas's CSS box. */
+body {
+ display: flex;
+ flex-direction: column;
+}
+
+#navbar-mount {
+ flex: none;
+ position: relative;
+ z-index: 30;
+}
+
+#stage {
+ position: relative;
+ flex: 1 1 auto;
+ min-height: 0;
+}
+
#game {
#game {
width: 100%;
width: 100%;
height: 100%;
height: 100%;
@@ -29,11 +51,55 @@ input {
@@ -29,11 +51,55 @@ input {
}
}
#ui {
#ui {
- position: fixed;
+ position: absolute;
inset: 0;
inset: 0;
pointer-events: none;
pointer-events: none;
}
}
+#ov-footer {
+ flex: none;
+}
+#ov-footer.ovf {
+ margin-top: 0 !important;
+ padding-top: 8px !important;
+ padding-bottom: 8px !important;
+}
+
+body.ovg-chrome-hidden #navbar-mount,
+body.ovg-chrome-hidden #ov-footer {
+ display: none;
+}
+
+/* Floating "bring the chrome back" pill (chrome.js adds it once the game starts). */
+#ovg-chrome-toggle {
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ z-index: 40;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 11px;
+ border-radius: 999px;
+ border: 1px solid var(--border, #2a3340);
+ background: var(--bg-secondary, rgba(10, 14, 18, 0.8));
+ color: var(--text-secondary, #b0b0b8);
+ font:
+ 600 12px/1 system-ui,
+ sans-serif;
+ cursor: pointer;
+ opacity: 0.55;
+ transition: opacity 0.15s;
+}
+#ovg-chrome-toggle:hover,
+#ovg-chrome-toggle:focus-visible {
+ opacity: 1;
+ color: var(--text-primary, #e6e6e6);
+}
+#ovg-chrome-toggle[hidden] {
+ display: none;
+}
+
.crosshair {
.crosshair {
position: absolute;
position: absolute;
left: 50%;
left: 50%;