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.

vite.config.ts
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';
import { defineConfig, type Plugin } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';

// The game is plain JS written as classic scripts that share top-level names. This plugin joins
// them, in load order, into one function body so they keep sharing scope inside a single module,
// with the JSON maps from src/game/maps injected first as `MAPS`.
// napplet.json is the single source for the listing text the manifest embeds.
const { description } = JSON.parse(readFileSync('napplet.json', 'utf8')) as { description: string };
const GAME_FILES = ['data', 'art', 'audio', 'game', 'ui'];
const MAPS_DIR = 'src/game/maps';
// the built-in maps (spec/map-format.md), in campaign order
function readMaps(watch: (path: string) => void): unknown[] {
  const readJson = (path: string) => {
    watch(path);
    return JSON.parse(readFileSync(path, 'utf8'));
  };
  const { campaign } = readJson(`${MAPS_DIR}/index.json`) as { campaign: string[] };
  return campaign.map((id) => readJson(`${MAPS_DIR}/${id}.json`));
}
function blackSiteGame(): Plugin {
  const id = 'virtual:black-site-game';
  const resolved = `\0${id}`;
  return {
    name: 'black-site-game',
    resolveId(source) {
      return source === id ? resolved : source === 'virtual:black-site-catalog' ? '\0black-site-catalog' : null;
    },
    load(source) {
      if (source === '\0black-site-catalog') {
        this.addWatchFile('src/game/data.js');
        const catalog = runInNewContext(`const MAPS = ${JSON.stringify(readMaps((path) => this.addWatchFile(path)))};\n` + readFileSync('src/game/data.js', 'utf8') +
          '\n({weapons: Object.keys(WEAPONS), throwables: THROWABLE_IDS, armors: ARMOUR_IDS, maxWeaponLevel: MAX_WEAPON_LEVEL, maxArmorLevel: MAX_ARMOUR_LEVEL, primary: PRIMARY_IDS, secondary: SECONDARY_IDS, characters: Object.keys(CHARACTERS), difficulties: Object.keys(DIFFICULTIES), maps: MAPS.map(map => map.id)})');
        return `export const armoryCatalog = ${JSON.stringify(catalog)};`;
      }
      if (source !== resolved) return null;
      const maps = readMaps((path) => this.addWatchFile(path));
      const parts = GAME_FILES.map((name) => {
        const path = `src/game/${name}.js`;
        this.addWatchFile(path);
        return `// ---- ${path}\n${readFileSync(path, 'utf8')}`;
      });
      this.addWatchFile('src/game-bridge.js');
      return `import { installGameKeyboard } from '/src/game-keyboard.ts';\nexport function bootGame() {\nconst MAPS = ${JSON.stringify(maps)};\n${parts.join('\n')}\n${readFileSync('src/game-bridge.js', 'utf8')}\n}\n`;
    },
  };
}

export default defineConfig({
  build: {
    // Vite's module-preload polyfill calls `fetch`; one inlined entry needs no
    // preload graph, and NIP-5D napplet code has no ambient network authority.
    modulePreload: { polyfill: false },
  },
  plugins: [
    blackSiteGame(),
    // Produce one self-contained `/index.html` for NIP-5D `srcdoc` loading,
    // soyLI publishes its raw HTML hash using standalone NIP-5D manifests.
    nip5aManifest({
      nappletType: 'black-site',
      description,
      artifactMode: 'single-file',
    }),
  ],
});