SOURCE / PINNED RELEASE
Made of little things.
City Breaker
- Release
- 94d2b8664b10…
- Author-recorded commit
- df23103cbcc1…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-cf8b858653e
Archive hash verified: eeeed725668c1ece…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Save games go through the shell's napplet-scoped storage (NAP storage), not
// browser localStorage, which napplets may not use. Without the optional storage
// domain the campaign still runs; it just cannot be resumed after reload.
import { STORAGE_DOMAIN, storage } from '@napplet/sdk';
import { runtimeHasDomain } from '../domain-availability.js';
import type { GameState } from './state';
const KEY = 'city-breaker-save-v1';
export function canSave(): boolean {
return runtimeHasDomain(STORAGE_DOMAIN);
}
function withTimeout<T>(p: Promise<T>, ms = 4000): Promise<T> {
return Promise.race([p, new Promise<T>((_, rej) => setTimeout(() => rej(new Error('storage timeout')), ms))]);
}
export async function saveGame(s: GameState): Promise<boolean> {
if (!canSave()) return false;
const trimmed: GameState = { ...s, raidHeat: s.raidHeat.slice(-600), news: s.news.slice(0, 60) };
const json = JSON.stringify(trimmed, (_k, v) => (typeof v === 'number' && !Number.isInteger(v) ? Math.round(v * 10000) / 10000 : v));
try {
await withTimeout(storage.setItem(KEY, json));
return true;
} catch {
return false;
}
}
export async function loadGame(): Promise<GameState | null> {
if (!canSave()) return null;
try {
const raw = await withTimeout(storage.getItem(KEY));
if (!raw) return null;
const s = JSON.parse(raw) as GameState;
return s && s.version === 1 ? s : null;
} catch {
return null;
}
}
export async function clearSave(): Promise<void> {
if (!canSave()) return;
try {
await withTimeout(storage.removeItem(KEY));
} catch {
/* ignore */
}
}
