Back to Black Site
SOURCE / PINNED RELEASE

Made of little things.

Black Site

Release
666bc7c74ebd…
Author-recorded commit
05e95af79676…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391

Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

scripts/desktop.js
// Builds the desktop copy of Black Site from this project: one self-contained HTML file that opens straight from disk.
// Saves go to the browser's localStorage under the same keys as before, so old progress carries over.
// The napplet-only parts (accounts, NAP-KEYS) find no shell here and fall back to local behaviour.
// Usage: bun scripts/desktop.js [output path]   (default: desktop/black-site.html)
const fs = require('fs');
const path = require('path');

const app = path.resolve(__dirname, '..');
const out = path.resolve(process.argv[2] || path.join(app, 'desktop', 'black-site.html'));
const read = p => fs.readFileSync(path.join(app, p), 'utf8');

// the same module vite.config.ts builds as `virtual:black-site-game`
const GAME_FILES = ['data', 'art', 'audio', 'game', 'ui'];
const readJson = p => JSON.parse(read(p));
const maps = readJson('src/game/maps/index.json').campaign.map(id => readJson(`src/game/maps/${id}.json`));
const keyboard = path.join(app, 'src', 'game-keyboard.ts').replace(/\\/g, '/');
const game = `import { installGameKeyboard } from ${JSON.stringify(keyboard)};
export function bootGame() {
const MAPS = ${JSON.stringify(maps)};
${GAME_FILES.map(n => `// ---- src/game/${n}.js\n${read(`src/game/${n}.js`)}`).join('\n')}
${read('src/game-bridge.js')}
}
`;
const entry = `import { bootGame } from 'virtual:black-site-game';
globalThis.BLACKSITE_STORE = {
  get(k, d) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : d; } catch (e) { return d; } },
  set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) { /* storage unavailable */ } },
};
// Help & How to Play: styles.css hides the menu's controls because the napplet moves them to a
// Help screen (src/current-user.ts). Do the same here, minus the account sections. Mount before bootGame.
const menu = document.querySelector('#menu .col'), hint = menu && menu.querySelector('.hint');
if (hint) {
  const screen = document.createElement('section');
  screen.id = 'howToPlay'; screen.className = 'screen account-screen';
  screen.innerHTML = '<div class="lbar"><h2>HELP &amp; HOW TO PLAY</h2><button type="button" data-go="menu">BACK</button></div>';
  screen.append(hint);
  document.body.append(screen);
  const button = document.createElement('button');
  button.type = 'button'; button.id = 'helpButton'; button.textContent = 'HELP & HOW TO PLAY';
  button.addEventListener('click', () => { for (const s of document.querySelectorAll('.screen')) s.classList.toggle('on', s === screen); });
  menu.append(button);
}
bootGame();
`;

(async () => {
  const result = await Bun.build({
    entrypoints: ['desktop-entry.js'],
    target: 'browser',
    minify: false,
    plugins: [{
      name: 'black-site-desktop',
      setup(build) {
        build.onResolve({ filter: /^(desktop-entry\.js|virtual:black-site-game)$/ }, a => ({ path: a.path, namespace: 'bs' }));
        build.onLoad({ filter: /.*/, namespace: 'bs' }, a => ({ contents: a.path === 'desktop-entry.js' ? entry : game, loader: 'js', resolveDir: app }));
      },
    }],
  });
  if (!result.success) { for (const log of result.logs) console.error(log); process.exit(1); }
  const js = (await result.outputs[0].text()).replace(/<\/script/gi, '<\\/script');
  const css = read('src/game/game.css') + '\n' + read('src/styles.css');
  const body = read('index.html').match(/<body>([\s\S]*?)<script type="module"/)[1].trimEnd();
  const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Black Site</title>
<style>
${css}
</style>
</head>
<body>
${body}
<script type="module">
${js}
</script>
</body>
</html>
`;
  fs.mkdirSync(path.dirname(out), { recursive: true });
  fs.writeFileSync(out, html);
  console.log(`wrote ${out} (${(fs.statSync(out).size / 1024).toFixed(0)} KB)`);
})();