The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
107 lines
5.4 KiB
JavaScript
107 lines
5.4 KiB
JavaScript
// Saves in IndexedDB, world files (export / import) and the blueprint library.
|
|
// ---------- Saves (IndexedDB) ----------
|
|
const DB = {
|
|
db: null,
|
|
open() {
|
|
if (this.db) return Promise.resolve(this.db);
|
|
return new Promise((res, rej) => {
|
|
try {
|
|
const r = indexedDB.open('deepgrain', 1);
|
|
r.onupgradeneeded = () => r.result.createObjectStore('saves');
|
|
r.onsuccess = () => { this.db = r.result; res(this.db); };
|
|
r.onerror = () => rej(r.error);
|
|
} catch (e) { rej(e); }
|
|
});
|
|
},
|
|
async op(mode, fn) {
|
|
const db = await this.open();
|
|
return new Promise((res, rej) => { const tx = db.transaction('saves', mode); const st = tx.objectStore('saves'); const rq = fn(st); rq.onsuccess = () => res(rq.result); rq.onerror = () => rej(rq.error); });
|
|
},
|
|
put(k, v) { return this.op('readwrite', s => s.put(v, k)); },
|
|
get(k) { return this.op('readonly', s => s.get(k)); },
|
|
del(k) { return this.op('readwrite', s => s.delete(k)); },
|
|
};
|
|
async function saveWorld(slot, quiet) {
|
|
try {
|
|
const s = serializeWorld();
|
|
await DB.put(slot, { meta: s.meta, buffers: s.buffers, savedAt: Date.now(), playTime: GAME.playTime });
|
|
if (!quiet) toast(slot === 'auto' ? 'Autosaved.' : 'World saved.');
|
|
return true;
|
|
} catch (e) {
|
|
if (!quiet) toast('Could not save. Browser storage is unavailable in this view. Use Export world file instead.');
|
|
return false;
|
|
}
|
|
}
|
|
async function newestSave() {
|
|
try {
|
|
const a = await DB.get('main'), b = await DB.get('auto');
|
|
if (a && b) return a.savedAt >= b.savedAt ? a : b;
|
|
return a || b || null;
|
|
} catch (e) { return null; }
|
|
}
|
|
function b64FromBytes(u8) { let s = ''; const CH = 0x8000; for (let i = 0; i < u8.length; i += CH) s += String.fromCharCode.apply(null, u8.subarray(i, i + CH)); return btoa(s); }
|
|
function bytesFromB64(b64) { const s = atob(b64); const u8 = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i); return u8; }
|
|
async function gz(u8, inflate) {
|
|
if (typeof CompressionStream === 'undefined') return null;
|
|
const st = new Blob([u8]).stream().pipeThrough(inflate ? new DecompressionStream('gzip') : new CompressionStream('gzip'));
|
|
return new Uint8Array(await new Response(st).arrayBuffer());
|
|
}
|
|
async function exportWorld() {
|
|
const s = serializeWorld();
|
|
const out = { format: 'deepgrain-world', v: 1, meta: s.meta, buffers: {} };
|
|
for (const k in s.buffers) {
|
|
const arr = s.buffers[k], bytes = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
const z = await gz(bytes, false);
|
|
out.buffers[k] = { type: arr.constructor.name, gz: !!z, b64: b64FromBytes(z || bytes) };
|
|
}
|
|
const text = JSON.stringify(out);
|
|
let dl = null;
|
|
try { dl = window.claude && window.claude.use ? await window.claude.use('downloads') : null; } catch (e) { dl = null; }
|
|
if (!dl) {
|
|
// an ordinary browser download (the downloads hook above only exists inside claude.ai)
|
|
try {
|
|
const url = URL.createObjectURL(new Blob([text], { type: 'application/json' }));
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = `deepgrain-${worldSeed}.json`;
|
|
document.body.appendChild(a); a.click(); a.remove();
|
|
setTimeout(() => URL.revokeObjectURL(url), 60000);
|
|
toast('World file saved.');
|
|
} catch (e) { toast('Saving files is not available in this view.'); }
|
|
return;
|
|
}
|
|
try { await dl.save({ filename: `deepgrain-${worldSeed}.json`, data: new Blob([text], { type: 'application/json' }) }); toast('World file saved.'); }
|
|
catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); }
|
|
}
|
|
async function importWorldFile(file) {
|
|
try {
|
|
const obj = JSON.parse(await file.text());
|
|
if (obj.format !== 'deepgrain-world') throw new Error('Not a Deepgrain world file.');
|
|
const bufs = {};
|
|
const T = { Uint8Array, Uint16Array, Int16Array, Float32Array, Int32Array };
|
|
for (const k in obj.buffers) {
|
|
const b = obj.buffers[k]; let bytes = bytesFromB64(b.b64);
|
|
if (b.gz) bytes = await gz(bytes, true);
|
|
const C = T[b.type] || Uint8Array;
|
|
bufs[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
|
}
|
|
startFromSave({ meta: obj.meta, buffers: bufs });
|
|
toast('World imported.');
|
|
} catch (e) { toast('That file could not be loaded: ' + (e.message || 'unknown error')); }
|
|
}
|
|
|
|
// ---------- Blueprint library ----------
|
|
function libLoad() { try { return JSON.parse(localStorage.getItem('deepgrain.blueprints') || '[]'); } catch (e) { return []; } }
|
|
function libSave(list) { try { localStorage.setItem('deepgrain.blueprints', JSON.stringify(list)); return true; } catch (e) { toast('Blueprints cannot be stored in this view. Copy the blueprint code instead.'); return false; } }
|
|
function bpEncode(bp) { return 'DGBP2:' + btoa(unescape(encodeURIComponent(JSON.stringify(bp)))); }
|
|
function bpValid(bp) { return !!(bp && bp.v === 2 && Array.isArray(bp.blocks) && bp.bw > 0 && bp.bh > 0); }
|
|
function bpDecode(code) {
|
|
const s = code.trim();
|
|
if (s.startsWith('DGBP1:')) throw new Error('That blueprint is from an older version of the game, before the block grid. It cannot be used.');
|
|
if (!s.startsWith('DGBP2:')) throw new Error('Blueprint codes start with DGBP2:');
|
|
let bp;
|
|
try { bp = JSON.parse(decodeURIComponent(escape(atob(s.slice(6))))); } catch (e) { throw new Error('That code is damaged.'); }
|
|
if (!bpValid(bp)) throw new Error('That code is damaged.');
|
|
bp.machines = bp.machines || []; bp.molds = bp.molds || [];
|
|
return bp;
|
|
}
|