Phase 3: Express backend, SQLite persistence, auto-save
- server/db.ts: sql.js with migration system (hex_maps, hexes, hex_features) - server/routes/maps.ts: CRUD for hex maps - server/routes/hexes.ts: Bulk hex upsert, region load, sparse storage - server/index.ts: Express 5, CORS, tile serving, SPA fallback - src/data/api-client.ts: Frontend HTTP client for all API endpoints - src/main.ts: Auto-save with 1s debounce, load map state on startup - Port 3002 (Kiepenkerl uses 3001) - Graceful fallback when API unavailable (works without server too) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
64
src/main.ts
64
src/main.ts
@@ -15,6 +15,7 @@ import {
|
||||
enforceEdgeConstraints,
|
||||
applyConstraintActions,
|
||||
} from '../core/edge-connectivity.js';
|
||||
import * as api from './data/api-client.js';
|
||||
|
||||
// --- State ---
|
||||
const hexMap = new HexMap();
|
||||
@@ -23,6 +24,8 @@ let selectedTerrain: TerrainType | null = null;
|
||||
let selectedHex: AxialCoord | null = null;
|
||||
let hexSize = 48;
|
||||
const origin = { x: 0, y: 0 };
|
||||
let currentMapId: number | null = null;
|
||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// --- Init Map ---
|
||||
const map = initMap('map');
|
||||
@@ -41,7 +44,7 @@ hexLayer.addTo(map);
|
||||
const sidebarEl = document.getElementById('sidebar')!;
|
||||
const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl);
|
||||
|
||||
const toolbarUI = createToolbar(toolbar, (mode) => {
|
||||
createToolbar(toolbar, (mode) => {
|
||||
currentMode = mode;
|
||||
terrainPickerUI.setMode(mode);
|
||||
});
|
||||
@@ -76,6 +79,28 @@ function rebuildHexLayer(showGrid: boolean, opacity: number) {
|
||||
reattachInteraction();
|
||||
}
|
||||
|
||||
// --- Auto-save (debounced) ---
|
||||
function scheduleSave() {
|
||||
if (!currentMapId) return;
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(async () => {
|
||||
if (!currentMapId || !hexMap.dirty) return;
|
||||
try {
|
||||
const data = hexMap.serialize();
|
||||
await api.saveHexes(currentMapId, data.hexes.map(h => ({
|
||||
q: h.q,
|
||||
r: h.r,
|
||||
base: h.base,
|
||||
features: h.features.map(f => ({ terrainId: f.terrainId, edgeMask: f.edgeMask })),
|
||||
})));
|
||||
hexMap.markClean();
|
||||
console.log(`[save] Saved ${data.hexes.length} hexes`);
|
||||
} catch (err) {
|
||||
console.error('[save] Failed:', err);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// --- Interaction handlers ---
|
||||
|
||||
function handleSelect(event: HexClickEvent) {
|
||||
@@ -90,6 +115,7 @@ function handlePaint(event: HexClickEvent) {
|
||||
hexLayer.redraw();
|
||||
selectedHex = event.coord;
|
||||
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
function handleFeature(event: HexClickEvent) {
|
||||
@@ -112,6 +138,7 @@ function handleFeature(event: HexClickEvent) {
|
||||
hexLayer.setSelectedHex(selectedHex);
|
||||
hexLayer.redraw();
|
||||
hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex));
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
// --- Hex Interaction ---
|
||||
@@ -121,13 +148,11 @@ function reattachInteraction() {
|
||||
detachInteraction?.();
|
||||
detachInteraction = attachHexInteraction(
|
||||
map, hexSize, origin,
|
||||
// Click handler
|
||||
(event) => {
|
||||
if (currentMode === 'select') handleSelect(event);
|
||||
else if (currentMode === 'paint') handlePaint(event);
|
||||
else if (currentMode === 'feature') handleFeature(event);
|
||||
},
|
||||
// Drag-paint handler (only active in paint mode)
|
||||
(event) => {
|
||||
if (currentMode === 'paint') handlePaint(event);
|
||||
},
|
||||
@@ -135,3 +160,36 @@ function reattachInteraction() {
|
||||
}
|
||||
|
||||
reattachInteraction();
|
||||
|
||||
// --- Load or create map from API ---
|
||||
async function init() {
|
||||
try {
|
||||
const maps = await api.listMaps();
|
||||
if (maps.length > 0) {
|
||||
currentMapId = maps[0].id;
|
||||
console.log(`[init] Loading map "${maps[0].name}" (id: ${currentMapId})`);
|
||||
} else {
|
||||
const result = await api.createMap({ name: 'Default Map' });
|
||||
currentMapId = result.id;
|
||||
console.log(`[init] Created new map (id: ${currentMapId})`);
|
||||
}
|
||||
|
||||
// Load existing hexes
|
||||
const hexes = await api.loadHexes(currentMapId);
|
||||
if (hexes.length > 0) {
|
||||
for (const hex of hexes) {
|
||||
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
|
||||
base: hex.base,
|
||||
features: hex.features,
|
||||
});
|
||||
}
|
||||
hexMap.markClean();
|
||||
hexLayer.redraw();
|
||||
console.log(`[init] Loaded ${hexes.length} hexes`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[init] API not available, running without persistence:', err);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user