Fix sidebar init, client-side first, coastline rework
- Fix: move terrainPickerUI declaration before createToolbar to avoid "can't access lexical declaration before initialization" error - Hex size is now fixed per map (not adjustable at runtime) - Client-side first: localStorage for persistence, no server needed for editing. Added Export/Import JSON buttons in sidebar. - Removed server dependency from main.ts init flow - Coastline rework: routes edge-to-edge like road/river (bezier), fills one side with water color (the side away from hex center). No longer draws along hex edges — it's a proper dividing curve. - Simplified map-settings to just grid toggle + opacity slider Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
181
src/main.ts
181
src/main.ts
@@ -1,6 +1,6 @@
|
||||
import './style/main.css';
|
||||
import { initMap } from './map/map-init.js';
|
||||
import { HexOverlayLayer } from './map/hex-layer.js';
|
||||
import { createHexLayer } from './map/hex-layer.js';
|
||||
import { attachHexInteraction, type HexClickEvent } from './map/hex-interaction.js';
|
||||
import { HexMap } from '../core/hex-map.js';
|
||||
import type { AxialCoord, TerrainType } from '../core/types.js';
|
||||
@@ -15,23 +15,23 @@ import {
|
||||
enforceEdgeConstraints,
|
||||
applyConstraintActions,
|
||||
} from '../core/edge-connectivity.js';
|
||||
import * as api from './data/api-client.js';
|
||||
|
||||
const STORAGE_KEY = 'hexifyer_map';
|
||||
|
||||
// --- State ---
|
||||
const hexMap = new HexMap();
|
||||
let currentMode: ToolMode = 'select';
|
||||
let selectedTerrain: TerrainType | null = null;
|
||||
let selectedHex: AxialCoord | null = null;
|
||||
let hexSize = 48;
|
||||
const hexSize = 48; // Fixed per map — set once
|
||||
const origin = { x: 0, y: 0 };
|
||||
let currentMapId: number | null = null;
|
||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// --- Init Map ---
|
||||
const map = initMap('map');
|
||||
|
||||
// --- Hex Layer ---
|
||||
let hexLayer = new HexOverlayLayer({
|
||||
const hexLayer = createHexLayer({
|
||||
hexSize,
|
||||
hexMap,
|
||||
origin,
|
||||
@@ -44,63 +44,83 @@ hexLayer.addTo(map);
|
||||
const sidebarEl = document.getElementById('sidebar')!;
|
||||
const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl);
|
||||
|
||||
const terrainPickerUI = createTerrainPicker(terrainPicker, (terrain) => {
|
||||
selectedTerrain = terrain;
|
||||
});
|
||||
|
||||
createToolbar(toolbar, (mode) => {
|
||||
currentMode = mode;
|
||||
terrainPickerUI.setMode(mode);
|
||||
});
|
||||
|
||||
const terrainPickerUI = createTerrainPicker(terrainPicker, (terrain) => {
|
||||
selectedTerrain = terrain;
|
||||
});
|
||||
|
||||
const hexInspectorUI = createHexInspector(hexInspector);
|
||||
|
||||
createMapSettings(settings, { hexSize, showGrid: true, opacity: 0.7 }, (s) => {
|
||||
if (s.hexSize !== hexSize) {
|
||||
hexSize = s.hexSize;
|
||||
rebuildHexLayer(s.showGrid, s.opacity);
|
||||
} else {
|
||||
hexLayer.setShowGrid(s.showGrid);
|
||||
hexLayer.setHexOpacity(s.opacity);
|
||||
}
|
||||
createMapSettings(settings, { showGrid: true, opacity: 0.7 }, (s) => {
|
||||
hexLayer.setShowGrid(s.showGrid);
|
||||
hexLayer.setHexOpacity(s.opacity);
|
||||
});
|
||||
|
||||
// --- Rebuild hex layer (when hex size changes) ---
|
||||
function rebuildHexLayer(showGrid: boolean, opacity: number) {
|
||||
map.removeLayer(hexLayer);
|
||||
hexLayer = new HexOverlayLayer({
|
||||
hexSize,
|
||||
hexMap,
|
||||
origin,
|
||||
showGrid,
|
||||
opacity,
|
||||
});
|
||||
hexLayer.addTo(map);
|
||||
reattachInteraction();
|
||||
}
|
||||
|
||||
// --- Auto-save (debounced) ---
|
||||
// --- Auto-save to localStorage (debounced) ---
|
||||
function scheduleSave() {
|
||||
if (!currentMapId) return;
|
||||
if (saveTimeout) clearTimeout(saveTimeout);
|
||||
saveTimeout = setTimeout(async () => {
|
||||
if (!currentMapId || !hexMap.dirty) return;
|
||||
saveTimeout = setTimeout(() => {
|
||||
if (!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 })),
|
||||
})));
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
hexMap.markClean();
|
||||
console.log(`[save] Saved ${data.hexes.length} hexes`);
|
||||
} catch (err) {
|
||||
console.error('[save] Failed:', err);
|
||||
console.error('[save] localStorage failed:', err);
|
||||
}
|
||||
}, 1000);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// --- File export/import ---
|
||||
function exportMap() {
|
||||
const data = hexMap.serialize();
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'hexmap.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function importMap() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string);
|
||||
hexMap.clear();
|
||||
for (const hex of data.hexes) {
|
||||
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
|
||||
base: hex.base,
|
||||
features: hex.features,
|
||||
});
|
||||
}
|
||||
hexMap.markClean();
|
||||
hexLayer.redraw();
|
||||
scheduleSave();
|
||||
} catch (err) {
|
||||
console.error('[import] Failed:', err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
// Expose to sidebar buttons
|
||||
(window as any).__hexifyer = { exportMap, importMap };
|
||||
|
||||
// --- Interaction handlers ---
|
||||
|
||||
function handleSelect(event: HexClickEvent) {
|
||||
@@ -142,54 +162,33 @@ function handleFeature(event: HexClickEvent) {
|
||||
}
|
||||
|
||||
// --- Hex Interaction ---
|
||||
let detachInteraction: (() => void) | null = null;
|
||||
attachHexInteraction(
|
||||
map, hexSize, origin,
|
||||
(event) => {
|
||||
if (currentMode === 'select') handleSelect(event);
|
||||
else if (currentMode === 'paint') handlePaint(event);
|
||||
else if (currentMode === 'feature') handleFeature(event);
|
||||
},
|
||||
(event) => {
|
||||
if (currentMode === 'paint') handlePaint(event);
|
||||
},
|
||||
);
|
||||
|
||||
function reattachInteraction() {
|
||||
detachInteraction?.();
|
||||
detachInteraction = attachHexInteraction(
|
||||
map, hexSize, origin,
|
||||
(event) => {
|
||||
if (currentMode === 'select') handleSelect(event);
|
||||
else if (currentMode === 'paint') handlePaint(event);
|
||||
else if (currentMode === 'feature') handleFeature(event);
|
||||
},
|
||||
(event) => {
|
||||
if (currentMode === 'paint') handlePaint(event);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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 from localStorage ---
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
for (const hex of data.hexes) {
|
||||
hexMap.setTerrain({ q: hex.q, r: hex.r }, {
|
||||
base: hex.base,
|
||||
features: hex.features,
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
hexMap.markClean();
|
||||
hexLayer.redraw();
|
||||
console.log(`[init] Loaded ${data.hexes.length} hexes from localStorage`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[init] Failed to load from localStorage:', err);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user