Satellite ground and real building footprints
CI/CD / test (push) Successful in 18s
CI/CD / publish (push) Successful in 22s
CI/CD / deploy (push) Successful in 6s

- The ground is Esri World Imagery: ~1 m/pixel tiles around the city
  zone, the factory and 300 m either side of the drive between them,
  coarser tiles for the whole region. Tiles are hosted with the game
  (public/imagery, scripts/fetch-imagery.mjs), stream in near the camera,
  are cached by the service worker as visited, and replace the drawn
  roads and land areas
- ~141,500 Microsoft building footprints (ODbL) join OSM's ~4,000 in a
  compact delta-encoded file (public/data/buildings.bin, built by
  scripts/build-buildings.mjs). Heights are estimated from footprint
  area and land use, with industrial halls outside the ring road.
  Buildings are built in 500 m blocks as the camera approaches
- Caddy compresses the buildings file and caches map data for a week;
  the build targets ES2022 for top-level await
- Tests for decoding, factory clearance, machine reachability and tile
  placement

Imagery attribution: Esri, Maxar, Earthstar Geographics, and the GIS
User Community. Commercial use requires a suitable Esri licence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 09:30:02 +03:30
co-authored by Claude Opus 5.5
parent 5cea802b97
commit 9b289f76b0
1012 changed files with 529 additions and 41 deletions
+14 -2
View File
@@ -10,21 +10,23 @@ const listFiles = dir => readdirSync(dir).flatMap(name => {
// Emits sw.js listing every built file, so the installed game works offline. The cache name is a
// hash of that list: a new build gets a new cache and the page offers to reload into it.
// Satellite tiles (16 MB) are not precached; they are kept as the player passes them.
function offlineServiceWorker() {
return {
name: 'offline-service-worker',
apply: 'build',
generateBundle(_, bundle) {
const files = [...Object.keys(bundle), ...listFiles('public').map(f => relative('public', f).replaceAll('\\', '/'))]
.filter(f => f !== 'sw.js' && !f.endsWith('.map'));
.filter(f => f !== 'sw.js' && !f.endsWith('.map') && !(f.startsWith('imagery/') && f.endsWith('.jpg')));
const version = createHash('sha1').update(files.join('\n')).digest('hex').slice(0, 12);
this.emitFile({
type: 'asset', fileName: 'sw.js', source: `// Generated by vite.config.js — do not edit.
const CACHE = 'robot-market-${version}';
const TILES = 'robot-market-imagery';
const FILES = ${JSON.stringify(['./', ...files.map(f => `./${f}`)])};
self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(FILES))));
self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys =>
Promise.all(keys.filter(k => k.startsWith('robot-market-') && k !== CACHE).map(k => caches.delete(k)))).then(() => self.clients.claim())));
Promise.all(keys.filter(k => k.startsWith('robot-market-') && k !== CACHE && k !== TILES).map(k => caches.delete(k)))).then(() => self.clients.claim())));
self.addEventListener('message', event => event.data === 'skip-waiting' && self.skipWaiting());
self.addEventListener('fetch', event => {
const request = event.request;
@@ -34,6 +36,14 @@ self.addEventListener('fetch', event => {
event.respondWith(fetch(request).catch(() => caches.match('./')));
return;
}
// Satellite tiles: cache first, and keep each one fetched so visited areas work offline.
if (new URL(request.url).pathname.includes('/imagery/')) {
event.respondWith(caches.open(TILES).then(cache => cache.match(request).then(hit => hit ?? fetch(request).then(response => {
if (response.ok) cache.put(request, response.clone());
return response;
}))));
return;
}
event.respondWith(caches.match(request, {ignoreSearch: true}).then(hit => hit ?? fetch(request)));
});
`
@@ -46,6 +56,8 @@ export default defineConfig({
// Relative assets also load under a GitHub Pages repository path.
base: './',
plugins: [offlineServiceWorker()],
// Top-level await loads the building and imagery files before the world is built.
build: {target: 'es2022'},
server: {
host: true,
allowedHosts: [