import './style/main.css'; import { initMap } from './map/map-init.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'; import { createSidebar } from './ui/sidebar.js'; import { createToolbar, type ToolMode } from './ui/toolbar.js'; import { createTerrainPicker } from './ui/terrain-picker.js'; import { createHexInspector, type FeatureRotateEvent } from './ui/hex-inspector.js'; import { createMapSettings } from './ui/map-settings.js'; import type { SelectedPattern } from './ui/tile-palette.js'; import { enforceEdgeConstraints, applyConstraintActions, } from '../core/edge-connectivity.js'; const STORAGE_KEY = 'hexifyer_map'; // --- State --- const hexMap = new HexMap(); let currentMode: ToolMode = 'select'; let selectedAreaTerrain: TerrainType | null = null; let selectedPattern: SelectedPattern | null = null; let selectedHex: AxialCoord | null = null; const hexSize = 48; const origin = { x: 0, y: 0 }; let saveTimeout: ReturnType | null = null; // --- Init Map --- const map = initMap('map'); // --- Hex Layer --- const hexLayer = createHexLayer({ hexSize, hexMap, origin, showGrid: true, opacity: 0.7, }); hexLayer.addTo(map); // --- Sidebar --- const sidebarEl = document.getElementById('sidebar')!; const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl); const terrainPickerUI = createTerrainPicker( terrainPicker, (terrain) => { selectedAreaTerrain = terrain; }, (pattern) => { selectedPattern = pattern; }, ); createToolbar(toolbar, (mode) => { currentMode = mode; terrainPickerUI.setMode(mode); }); const hexInspectorUI = createHexInspector(hexInspector, (event: FeatureRotateEvent) => { if (event.newMask === 0) { hexMap.removeFeature(event.coord, event.terrainId); } else { hexMap.setFeature(event.coord, event.terrainId, event.newMask); // Re-enforce constraints after rotation const actions = enforceEdgeConstraints(hexMap, event.coord, event.terrainId, event.newMask); applyConstraintActions(hexMap, actions); } hexLayer.redraw(); hexInspectorUI.update(event.coord, hexMap.getTerrain(event.coord)); scheduleSave(); }); createMapSettings(settings, { showGrid: true, opacity: 0.7 }, (s) => { hexLayer.setShowGrid(s.showGrid); hexLayer.setHexOpacity(s.opacity); }); // --- Auto-save to localStorage --- function scheduleSave() { if (saveTimeout) clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { if (!hexMap.dirty) return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(hexMap.serialize())); hexMap.markClean(); } catch (err) { console.error('[save] localStorage failed:', err); } }, 500); } // --- File export/import --- function exportMap() { const json = JSON.stringify(hexMap.serialize(), 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(); } (window as any).__hexifyer = { exportMap, importMap }; // --- Interaction handlers --- function handleSelect(event: HexClickEvent) { selectedHex = event.coord; hexLayer.setSelectedHex(selectedHex); hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex)); } function handlePaint(event: HexClickEvent) { if (!selectedAreaTerrain) return; hexMap.setBase(event.coord, selectedAreaTerrain.id); hexLayer.redraw(); selectedHex = event.coord; hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex)); scheduleSave(); } function handleFeaturePlacement(event: HexClickEvent) { if (!selectedPattern) return; const coord = event.coord; // Place the selected pattern on this hex hexMap.setFeature(coord, selectedPattern.terrainId, selectedPattern.mask); // Enforce edge constraints on neighbors const actions = enforceEdgeConstraints(hexMap, coord, selectedPattern.terrainId, selectedPattern.mask); applyConstraintActions(hexMap, actions); selectedHex = coord; hexLayer.setSelectedHex(selectedHex); hexLayer.redraw(); hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex)); scheduleSave(); } // --- Hex Interaction --- attachHexInteraction( map, hexSize, origin, (event) => { if (currentMode === 'select') handleSelect(event); else if (currentMode === 'paint') handlePaint(event); else if (currentMode === 'feature') handleFeaturePlacement(event); }, (event) => { if (currentMode === 'paint') handlePaint(event); }, ); // --- Keyboard rotation (Q/E) --- document.addEventListener('keydown', (e) => { if (currentMode !== 'feature') return; if (e.key === 'q' || e.key === 'Q') { terrainPickerUI.rotateCCW(); } else if (e.key === 'e' || e.key === 'E') { terrainPickerUI.rotateCW(); } }); // --- Mouse wheel rotation --- document.getElementById('map')?.addEventListener('wheel', (e) => { if (currentMode !== 'feature' || !selectedPattern) return; e.preventDefault(); if (e.deltaY > 0) { terrainPickerUI.rotateCW(); } else { terrainPickerUI.rotateCCW(); } }, { passive: false }); // --- 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, }); } hexMap.markClean(); hexLayer.redraw(); } } catch (err) { console.warn('[init] Failed to load from localStorage:', err); }