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.
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',
}),
],
});
