diff --git a/database.js b/database.js index bdf260c..c893e82 100644 --- a/database.js +++ b/database.js @@ -286,6 +286,11 @@ class HikeMapDB { this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN reveal_radius INTEGER DEFAULT 800`); } catch (e) { /* Column already exists */ } + // Migration: Add map_theme column to store user's custom map theme + try { + this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN map_theme TEXT`); + } catch (e) { /* Column already exists */ } + // Migration: Add animation overrides to monster_types try { this.db.exec(`ALTER TABLE monster_types ADD COLUMN attack_animation TEXT DEFAULT 'attack'`); @@ -811,6 +816,31 @@ class HikeMapDB { return stmt.run(iconId, userId); } + // Get user's map theme + getMapTheme(userId) { + const stmt = this.db.prepare(`SELECT map_theme FROM rpg_stats WHERE user_id = ?`); + const result = stmt.get(userId); + if (result && result.map_theme) { + try { + return JSON.parse(result.map_theme); + } catch (e) { + return null; + } + } + return null; + } + + // Set user's map theme + setMapTheme(userId, theme) { + const stmt = this.db.prepare(` + UPDATE rpg_stats SET + map_theme = ?, + updated_at = datetime('now') + WHERE user_id = ? + `); + return stmt.run(JSON.stringify(theme), userId); + } + // Check if user can set home base (once per day) canSetHomeBase(userId) { const stmt = this.db.prepare(` diff --git a/index.html b/index.html index 9cc6a85..40b30cf 100644 --- a/index.html +++ b/index.html @@ -2632,7 +2632,64 @@ min-height: 180px; } .player-side { - flex: 0 0 140px; + flex: 1; + max-width: 200px; + } + /* Player entry - mirrors monster-entry exactly */ + .player-entry { + background: rgba(0, 0, 0, 0.4); + border: 2px solid #4285f4; + border-radius: 10px; + padding: 10px; + } + .player-entry-header { + display: flex; + align-items: center; + margin-bottom: 6px; + } + .player-entry-icon { + width: var(--combat-icon-size); + height: var(--combat-icon-size); + object-fit: contain; + } + .player-entry .sprite-container { + position: relative; + width: var(--combat-icon-size); + height: var(--combat-icon-size); + margin-right: 10px; + flex-shrink: 0; + } + .player-entry-name { + font-size: 12px; + font-weight: bold; + flex: 1; + } + .player-entry-hp { + margin-top: 4px; + } + .player-entry-hp .hp-bar { + height: 10px; + } + .player-entry-hp .stat-text { + font-size: 10px; + margin-top: 2px; + } + .player-entry-mp { + margin-top: 2px; + } + .player-entry-mp .mp-bar { + height: 8px; + } + .player-entry-mp .stat-text { + font-size: 9px; + margin-top: 1px; + color: #4ecdc4; + } + .player-entry .status-overlay img { + width: var(--combat-status-size); + height: var(--combat-status-size); + top: 0; + left: 0; } .monster-side { flex: 1; @@ -2814,21 +2871,6 @@ top: -4px; left: -4px; } - .player-side .sprite-container { - width: 80px; - height: 80px; - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 8px; - } - .player-side .combatant-icon { - font-size: 48px; - } - .player-side .status-overlay img { - width: 28px; - height: 28px; - } .monster-entry .status-overlay img { width: var(--combat-status-size); height: var(--combat-status-size); @@ -3007,15 +3049,15 @@ /* Home Base Marker */ .home-base-marker { - width: 50px; - height: 50px; + width: 80px; + height: 80px; display: flex; align-items: center; justify-content: center; } .home-base-marker img { - width: 50px; - height: 50px; + width: 80px; + height: 80px; object-fit: contain; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); } @@ -3689,25 +3731,24 @@
⚡ Your Turn
-
-
-
🏃
-
-
-
Trail Runner
-
-
-
HP
+
+
+
+
+ Player +
+
+ Trail Runner +
+
+
HP: 100/100
-
-
MP
+
+
MP: 50/50
-
- HP: 100/100 | MP: 50/50 -
VS
@@ -4375,118 +4416,173 @@ // ===================== // FANTASY MAP STYLE // ===================== - // Default fantasy colors (can be customized via Theme Editor) - const defaultMapColors = { + // Default theme (colors + visibility toggles) + const defaultMapTheme = { land: '#1a1a2e', water: '#0f3460', roads: '#e94560', buildings: '#16213e', - parks: '#1b4332' + parks: '#1b4332', + buildings3d: true, + showRoads: true, + showBuildings: true, + showParks: true, + showWater: true, + showRoadLabels: true, + showPlaceLabels: true }; - // Load active theme from localStorage (set by Theme Editor) - let mapColors = { ...defaultMapColors }; - try { - const savedTheme = localStorage.getItem('hikemap_active_theme'); - if (savedTheme) { - const parsed = JSON.parse(savedTheme); - mapColors = { ...defaultMapColors, ...parsed }; - console.log('Loaded custom map theme:', mapColors); + // Current map theme (will be loaded from server after login) + let mapTheme = { ...defaultMapTheme }; + + // Legacy: also support mapColors for backwards compatibility + let mapColors = mapTheme; + + // Load global theme from server (public endpoint, no auth needed) + async function loadMapThemeFromServer() { + try { + const response = await fetch('/api/map-theme'); + + if (response.ok) { + const data = await response.json(); + if (data.theme) { + console.log('Loaded map theme from server:', data.theme); + return data.theme; + } + } + } catch (err) { + console.error('Error loading map theme:', err); } - } catch (e) { - console.log('Using default map colors'); + return null; } - // Build the fantasy map style - function buildFantasyStyle(colors, use3dBuildings = true) { - return { - version: 8, - name: 'HikeMap Fantasy', - sources: { - 'openmaptiles': { - type: 'vector', - url: 'https://tiles.openfreemap.org/planet' - } - }, - glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', - layers: [ - // Background - { id: 'background', type: 'background', paint: { 'background-color': colors.land } }, - // Parks + // Apply a new theme to the map + function applyMapTheme(theme) { + mapTheme = { ...defaultMapTheme, ...theme }; + mapColors = mapTheme; // Keep backwards compat + if (map) { + map.setStyle(buildFantasyStyle(mapTheme)); + } + } + + // Build the fantasy map style with visibility toggles + function buildFantasyStyle(theme) { + const layers = [ + // Background (always shown) + { id: 'background', type: 'background', paint: { 'background-color': theme.land } } + ]; + + // Parks + if (theme.showParks !== false) { + layers.push( { id: 'park', type: 'fill', source: 'openmaptiles', 'source-layer': 'park', - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.7 } }, - // Landcover - wood + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.7 } }, { id: 'landcover_wood', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover', filter: ['==', ['get', 'class'], 'wood'], - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.5 } }, - // Landcover - grass + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.5 } }, { id: 'landcover_grass', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover', filter: ['==', ['get', 'class'], 'grass'], - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.4 } }, - // Water + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.4 } } + ); + } + + // Water + if (theme.showWater !== false) { + layers.push( { id: 'water', type: 'fill', source: 'openmaptiles', 'source-layer': 'water', - paint: { 'fill-color': colors.water } }, - // Waterways + paint: { 'fill-color': theme.water } }, { id: 'waterway', type: 'line', source: 'openmaptiles', 'source-layer': 'waterway', - paint: { 'line-color': colors.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } }, - // Buildings (3D or 2D) - use3dBuildings ? { + paint: { 'line-color': theme.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } } + ); + } + + // Buildings + if (theme.showBuildings !== false) { + if (theme.buildings3d !== false) { + layers.push({ id: 'buildings-3d', type: 'fill-extrusion', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, paint: { - 'fill-extrusion-color': colors.buildings, + 'fill-extrusion-color': theme.buildings, 'fill-extrusion-height': ['*', 2, ['get', 'render_height']], 'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']], 'fill-extrusion-opacity': 0.85 } - } : { + }); + } else { + layers.push({ id: 'buildings', type: 'fill', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, - paint: { 'fill-color': colors.buildings, 'fill-opacity': 0.8 } - }, - // Roads - service/track + paint: { 'fill-color': theme.buildings, 'fill-opacity': 0.8 } + }); + } + } + + // Roads + if (theme.showRoads !== false) { + layers.push( { id: 'roads-service', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['service', 'track'], true, false], - paint: { 'line-color': colors.roads, 'line-width': 1, 'line-opacity': 0.4 } }, - // Roads - path/pedestrian + paint: { 'line-color': theme.roads, 'line-width': 1, 'line-opacity': 0.4 } }, { id: 'roads-path', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false], - paint: { 'line-color': colors.roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } }, - // Roads - minor + paint: { 'line-color': theme.roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } }, { id: 'roads-minor', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['==', ['get', 'class'], 'minor'], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } }, - // Roads - secondary/tertiary + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } }, { id: 'roads-secondary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } }, - // Roads - primary/trunk + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } }, { id: 'roads-primary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } }, - // Roads - motorway + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } }, { id: 'roads-motorway', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['==', ['get', 'class'], 'motorway'], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } }, - // Rail - { id: 'rail', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', - filter: ['==', ['get', 'class'], 'rail'], - paint: { 'line-color': '#888888', 'line-width': 2, 'line-dasharray': [3, 3] } }, - // Boundaries - { id: 'boundary', type: 'line', source: 'openmaptiles', 'source-layer': 'boundary', - filter: ['==', ['get', 'admin_level'], 2], - paint: { 'line-color': '#888888', 'line-width': 1, 'line-dasharray': [4, 2] } }, - // Road labels - { id: 'road-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', minzoom: 14, - layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': 10, 'symbol-placement': 'line', 'text-font': ['Noto Sans Regular'] }, - paint: { 'text-color': '#ffffff', 'text-halo-color': colors.land, 'text-halo-width': 1 } }, - // Place labels - { id: 'place-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'place', - layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 16], 'text-font': ['Noto Sans Bold'] }, - paint: { 'text-color': '#ffffff', 'text-halo-color': colors.land, 'text-halo-width': 2 } } - ] + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } } + ); + } + + // Rail and boundaries (always show) + layers.push( + { id: 'rail', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', + filter: ['==', ['get', 'class'], 'rail'], + paint: { 'line-color': '#888888', 'line-width': 2, 'line-dasharray': [3, 3] } }, + { id: 'boundary', type: 'line', source: 'openmaptiles', 'source-layer': 'boundary', + filter: ['==', ['get', 'admin_level'], 2], + paint: { 'line-color': '#888888', 'line-width': 1, 'line-dasharray': [4, 2] } } + ); + + // Road labels + if (theme.showRoadLabels !== false) { + layers.push({ + id: 'road-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', minzoom: 14, + layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': 10, 'symbol-placement': 'line', 'text-font': ['Noto Sans Regular'] }, + paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 1 } + }); + } + + // Place labels + if (theme.showPlaceLabels !== false) { + layers.push({ + id: 'place-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'place', + layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 16], 'text-font': ['Noto Sans Bold'] }, + paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 2 } + }); + } + + return { + version: 8, + name: 'HikeMap Fantasy', + sources: { + 'openmaptiles': { + type: 'vector', + url: 'https://tiles.openfreemap.org/planet' + } + }, + glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', + layers: layers }; } @@ -4495,7 +4591,7 @@ // ===================== const map = new maplibregl.Map({ container: 'map', - style: buildFantasyStyle(mapColors, true), + style: buildFantasyStyle(mapTheme), center: [-97.84, 30.49], // Note: [lng, lat] for MapLibre zoom: 13, maxZoom: 22, @@ -4506,7 +4602,15 @@ }); // Add navigation controls - map.addControl(new maplibregl.NavigationControl(), 'top-right'); + map.addControl(new maplibregl.NavigationControl({ showZoom: false }), 'top-left'); + + // Load global theme from server on startup + (async function loadGlobalTheme() { + const savedTheme = await loadMapThemeFromServer(); + if (savedTheme) { + applyMapTheme(savedTheme); + } + })(); // ===================== // COORDINATE HELPER FUNCTIONS @@ -6639,9 +6743,9 @@ if (!gpsMarker) { const el = document.createElement('div'); el.className = 'custom-div-icon gps-marker-icon'; - el.innerHTML = ''; - el.style.width = '36px'; - el.style.height = '36px'; + el.innerHTML = ''; + el.style.width = '80px'; + el.style.height = '80px'; gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' }) .setLngLat([lng, lat]) .addTo(map); @@ -6650,7 +6754,7 @@ // Update rotation if we have heading if (currentHeading !== null) { const el = gpsMarker.getElement(); - const icon = el.querySelector('.mdi-navigation'); + const icon = el.querySelector('.player-marker-img'); if (icon) { icon.style.transform = 'rotate(' + currentHeading + 'deg)'; } @@ -12829,9 +12933,9 @@ // Recreate the marker const el = document.createElement('div'); el.className = 'custom-div-icon gps-marker-icon'; - el.innerHTML = ''; - el.style.width = '36px'; - el.style.height = '36px'; + el.innerHTML = ''; + el.style.width = '80px'; + el.style.height = '80px'; gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' }) .setLngLat([lng, lat]) .addTo(map); diff --git a/mapgameimgs/cacheicons/cacheIcon100-01.png b/mapgameimgs/cacheicons/cacheIcon100-01.png new file mode 100755 index 0000000..a66e5c5 Binary files /dev/null and b/mapgameimgs/cacheicons/cacheIcon100-01.png differ diff --git a/mapgameimgs/cacheicons/cacheIcon100-01_shadow.png b/mapgameimgs/cacheicons/cacheIcon100-01_shadow.png new file mode 100755 index 0000000..f7e2bbf Binary files /dev/null and b/mapgameimgs/cacheicons/cacheIcon100-01_shadow.png differ diff --git a/mapgameimgs/player/runner.png b/mapgameimgs/player/runner.png new file mode 100755 index 0000000..06bc550 Binary files /dev/null and b/mapgameimgs/player/runner.png differ diff --git a/mapgameimgs/skills/basic_attack.png b/mapgameimgs/skills/basic_attack.png new file mode 100644 index 0000000..8ee46e5 Binary files /dev/null and b/mapgameimgs/skills/basic_attack.png differ diff --git a/mapgameimgs/skills/defend.png b/mapgameimgs/skills/defend.png new file mode 100644 index 0000000..97fc9c4 Binary files /dev/null and b/mapgameimgs/skills/defend.png differ diff --git a/mapgameimgs/skills/double_attack.png b/mapgameimgs/skills/double_attack.png new file mode 100644 index 0000000..81cb139 Binary files /dev/null and b/mapgameimgs/skills/double_attack.png differ diff --git a/mapgameimgs/skills/focus.png b/mapgameimgs/skills/focus.png new file mode 100644 index 0000000..97fc9c4 Binary files /dev/null and b/mapgameimgs/skills/focus.png differ diff --git a/mapgameimgs/skills/full_restore.png b/mapgameimgs/skills/full_restore.png new file mode 100644 index 0000000..97fc9c4 Binary files /dev/null and b/mapgameimgs/skills/full_restore.png differ diff --git a/mapgameimgs/skills/heal.png b/mapgameimgs/skills/heal.png new file mode 100644 index 0000000..8ec280e Binary files /dev/null and b/mapgameimgs/skills/heal.png differ diff --git a/mapgameimgs/skills/heavy_blow.png b/mapgameimgs/skills/heavy_blow.png new file mode 100644 index 0000000..bde437f Binary files /dev/null and b/mapgameimgs/skills/heavy_blow.png differ diff --git a/mapgameimgs/skills/power_strike.png b/mapgameimgs/skills/power_strike.png new file mode 100644 index 0000000..bde437f Binary files /dev/null and b/mapgameimgs/skills/power_strike.png differ diff --git a/mapgameimgs/skills/quick_heal.png b/mapgameimgs/skills/quick_heal.png new file mode 100644 index 0000000..8ec280e Binary files /dev/null and b/mapgameimgs/skills/quick_heal.png differ diff --git a/mapgameimgs/skills/quick_strike.png b/mapgameimgs/skills/quick_strike.png new file mode 100644 index 0000000..bde437f Binary files /dev/null and b/mapgameimgs/skills/quick_strike.png differ diff --git a/mapgameimgs/skills/second_wind.png b/mapgameimgs/skills/second_wind.png new file mode 100644 index 0000000..97fc9c4 Binary files /dev/null and b/mapgameimgs/skills/second_wind.png differ diff --git a/mapgameimgs/skills/triple_strike.png b/mapgameimgs/skills/triple_strike.png new file mode 100644 index 0000000..bde437f Binary files /dev/null and b/mapgameimgs/skills/triple_strike.png differ diff --git a/mapgameimgs/skills/whirlwind.png b/mapgameimgs/skills/whirlwind.png new file mode 100644 index 0000000..bde437f Binary files /dev/null and b/mapgameimgs/skills/whirlwind.png differ diff --git a/maplibre-test.html b/maplibre-test.html index 350b0ea..509e2e9 100644 --- a/maplibre-test.html +++ b/maplibre-test.html @@ -27,7 +27,7 @@ border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 1000; - width: 300px; + width: 320px; max-height: 90vh; overflow-y: auto; } @@ -90,7 +90,7 @@ display: flex; align-items: center; gap: 8px; - margin-top: 10px; + margin-bottom: 6px; } .checkbox-row label { @@ -98,6 +98,11 @@ cursor: pointer; } + .checkbox-row input[type="checkbox"] { + width: 16px; + height: 16px; + } + .slider-row { display: flex; align-items: center; @@ -158,31 +163,12 @@ background: #c5372b; } - .btn-secondary { - background: #666; - color: white; - } - - .btn-secondary:hover { - background: #555; - } - .btn-block { display: block; width: 100%; margin-bottom: 8px; } - .btn-group { - display: flex; - gap: 8px; - margin-top: 10px; - } - - .btn-group .btn { - flex: 1; - } - .theme-name-input { width: 100%; padding: 8px 12px; @@ -198,7 +184,7 @@ } .saved-themes { - max-height: 200px; + max-height: 150px; overflow-y: auto; margin-top: 10px; } @@ -311,9 +297,7 @@ border: 1px solid rgba(0,0,0,0.2); } - .success-msg { - background: #d4edda; - color: #155724; + .success-msg, .error-msg { padding: 8px 12px; border-radius: 4px; font-size: 12px; @@ -321,11 +305,36 @@ display: none; } + .success-msg { + background: #d4edda; + color: #155724; + } + + .error-msg { + background: #f8d7da; + color: #721c24; + } + .info-text { font-size: 11px; color: #666; margin-top: 8px; } + + .login-prompt { + background: #fff3cd; + color: #856404; + padding: 10px; + border-radius: 4px; + font-size: 12px; + margin-bottom: 10px; + } + + .toggle-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px; + } @@ -334,7 +343,12 @@

Map Theme Editor

+ +
+
Color Palette
@@ -364,8 +378,37 @@ #1b4332
+
-
+
+
Map Elements
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -441,7 +484,7 @@
Apply to Game
-

This will save the current colors as the active theme used in HikeMap.

+

Saves to your account. Refresh HikeMap to see changes.

@@ -482,7 +525,16 @@ } }; - // Load saved themes from localStorage + // Check if user is logged in + function getToken() { + return localStorage.getItem('accessToken'); + } + + function isLoggedIn() { + return !!getToken(); + } + + // Load saved themes from localStorage (local storage for theme library) function loadSavedThemes() { const themes = JSON.parse(localStorage.getItem('hikemap_themes') || '{}'); return themes; @@ -493,28 +545,40 @@ localStorage.setItem('hikemap_themes', JSON.stringify(themes)); } - // Get current colors from inputs - function getCurrentColors() { + // Get current theme from inputs + function getCurrentTheme() { return { land: document.getElementById('colorLand').value, water: document.getElementById('colorWater').value, roads: document.getElementById('colorRoads').value, buildings: document.getElementById('colorBuildings').value, parks: document.getElementById('colorParks').value, - buildings3d: document.getElementById('buildings3d').checked + buildings3d: document.getElementById('buildings3d').checked, + showRoads: document.getElementById('showRoads').checked, + showBuildings: document.getElementById('showBuildings').checked, + showParks: document.getElementById('showParks').checked, + showWater: document.getElementById('showWater').checked, + showRoadLabels: document.getElementById('showRoadLabels').checked, + showPlaceLabels: document.getElementById('showPlaceLabels').checked }; } - // Set colors to inputs - function setColors(colors) { - document.getElementById('colorLand').value = colors.land; - document.getElementById('colorWater').value = colors.water; - document.getElementById('colorRoads').value = colors.roads; - document.getElementById('colorBuildings').value = colors.buildings; - document.getElementById('colorParks').value = colors.parks; - if (colors.buildings3d !== undefined) { - document.getElementById('buildings3d').checked = colors.buildings3d; - } + // Set theme to inputs + function setTheme(theme) { + if (theme.land) document.getElementById('colorLand').value = theme.land; + if (theme.water) document.getElementById('colorWater').value = theme.water; + if (theme.roads) document.getElementById('colorRoads').value = theme.roads; + if (theme.buildings) document.getElementById('colorBuildings').value = theme.buildings; + if (theme.parks) document.getElementById('colorParks').value = theme.parks; + + document.getElementById('buildings3d').checked = theme.buildings3d !== false; + document.getElementById('showRoads').checked = theme.showRoads !== false; + document.getElementById('showBuildings').checked = theme.showBuildings !== false; + document.getElementById('showParks').checked = theme.showParks !== false; + document.getElementById('showWater').checked = theme.showWater !== false; + document.getElementById('showRoadLabels').checked = theme.showRoadLabels !== false; + document.getElementById('showPlaceLabels').checked = theme.showPlaceLabels !== false; + updateHexDisplays(); applyStyle(); } @@ -563,9 +627,8 @@ const themeName = item.dataset.theme; const themes = loadSavedThemes(); if (themes[themeName]) { - setColors(themes[themeName]); + setTheme(themes[themeName]); document.getElementById('themeName').value = themeName; - // Update active state container.querySelectorAll('.theme-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); } @@ -591,91 +654,196 @@ // Show success message function showSuccess(msg) { const el = document.getElementById('successMsg'); + document.getElementById('errorMsg').style.display = 'none'; el.textContent = msg; el.style.display = 'block'; setTimeout(() => { el.style.display = 'none'; }, 3000); } + // Show error message + function showError(msg) { + const el = document.getElementById('errorMsg'); + document.getElementById('successMsg').style.display = 'none'; + el.textContent = msg; + el.style.display = 'block'; + setTimeout(() => { el.style.display = 'none'; }, 5000); + } + // Build and apply the map style function applyStyle() { - const colors = getCurrentColors(); + const theme = getCurrentTheme(); - const style = { - version: 8, - name: 'HikeMap Theme', - sources: { - 'openmaptiles': { - type: 'vector', - url: 'https://tiles.openfreemap.org/planet' - } - }, - glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', - layers: [ - { id: 'background', type: 'background', paint: { 'background-color': colors.land } }, + const layers = [ + { id: 'background', type: 'background', paint: { 'background-color': theme.land } } + ]; + + // Parks + if (theme.showParks) { + layers.push( { id: 'park', type: 'fill', source: 'openmaptiles', 'source-layer': 'park', - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.7 } }, + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.7 } }, { id: 'landcover_wood', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover', filter: ['==', ['get', 'class'], 'wood'], - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.5 } }, + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.5 } }, { id: 'landcover_grass', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover', filter: ['==', ['get', 'class'], 'grass'], - paint: { 'fill-color': colors.parks, 'fill-opacity': 0.4 } }, + paint: { 'fill-color': theme.parks, 'fill-opacity': 0.4 } } + ); + } + + // Water + if (theme.showWater) { + layers.push( { id: 'water', type: 'fill', source: 'openmaptiles', 'source-layer': 'water', - paint: { 'fill-color': colors.water } }, + paint: { 'fill-color': theme.water } }, { id: 'waterway', type: 'line', source: 'openmaptiles', 'source-layer': 'waterway', - paint: { 'line-color': colors.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } }, - colors.buildings3d ? { + paint: { 'line-color': theme.water, 'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1, 14, 3] } } + ); + } + + // Buildings + if (theme.showBuildings) { + if (theme.buildings3d) { + layers.push({ id: 'buildings-3d', type: 'fill-extrusion', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, paint: { - 'fill-extrusion-color': colors.buildings, + 'fill-extrusion-color': theme.buildings, 'fill-extrusion-height': ['*', 2, ['get', 'render_height']], 'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']], 'fill-extrusion-opacity': 0.85 } - } : { + }); + } else { + layers.push({ id: 'buildings', type: 'fill', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, - paint: { 'fill-color': colors.buildings, 'fill-opacity': 0.8 } - }, + paint: { 'fill-color': theme.buildings, 'fill-opacity': 0.8 } + }); + } + } + + // Roads + if (theme.showRoads) { + layers.push( { id: 'roads-service', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['service', 'track'], true, false], - paint: { 'line-color': colors.roads, 'line-width': 1, 'line-opacity': 0.4 } }, + paint: { 'line-color': theme.roads, 'line-width': 1, 'line-opacity': 0.4 } }, { id: 'roads-path', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false], - paint: { 'line-color': colors.roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } }, + paint: { 'line-color': theme.roads, 'line-width': 1, 'line-dasharray': [2, 1], 'line-opacity': 0.6 } }, { id: 'roads-minor', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['==', ['get', 'class'], 'minor'], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } }, + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 13, 1, 20, 10], 'line-opacity': 0.8 } }, { id: 'roads-secondary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } }, + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 8, 0.5, 20, 13] } }, { id: 'roads-primary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } }, + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 0.5, 20, 18] } }, { id: 'roads-motorway', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', filter: ['==', ['get', 'class'], 'motorway'], layout: { 'line-cap': 'round', 'line-join': 'round' }, - paint: { 'line-color': colors.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } }, - { id: 'rail', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', - filter: ['==', ['get', 'class'], 'rail'], - paint: { 'line-color': '#888888', 'line-width': 2, 'line-dasharray': [3, 3] } }, - { id: 'boundary', type: 'line', source: 'openmaptiles', 'source-layer': 'boundary', - filter: ['==', ['get', 'admin_level'], 2], - paint: { 'line-color': '#888888', 'line-width': 1, 'line-dasharray': [4, 2] } }, - { id: 'road-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', minzoom: 14, - layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': 10, 'symbol-placement': 'line', 'text-font': ['Noto Sans Regular'] }, - paint: { 'text-color': '#ffffff', 'text-halo-color': colors.land, 'text-halo-width': 1 } }, - { id: 'place-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'place', - layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 16], 'text-font': ['Noto Sans Bold'] }, - paint: { 'text-color': '#ffffff', 'text-halo-color': colors.land, 'text-halo-width': 2 } } - ] + paint: { 'line-color': theme.roads, 'line-width': ['interpolate', ['exponential', 1.2], ['zoom'], 5, 1, 20, 20] } } + ); + } + + // Rail and boundary (always show) + layers.push( + { id: 'rail', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation', + filter: ['==', ['get', 'class'], 'rail'], + paint: { 'line-color': '#888888', 'line-width': 2, 'line-dasharray': [3, 3] } }, + { id: 'boundary', type: 'line', source: 'openmaptiles', 'source-layer': 'boundary', + filter: ['==', ['get', 'admin_level'], 2], + paint: { 'line-color': '#888888', 'line-width': 1, 'line-dasharray': [4, 2] } } + ); + + // Road labels + if (theme.showRoadLabels) { + layers.push({ + id: 'road-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'transportation_name', minzoom: 14, + layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': 10, 'symbol-placement': 'line', 'text-font': ['Noto Sans Regular'] }, + paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 1 } + }); + } + + // Place labels + if (theme.showPlaceLabels) { + layers.push({ + id: 'place-labels', type: 'symbol', source: 'openmaptiles', 'source-layer': 'place', + layout: { 'text-field': ['coalesce', ['get', 'name_en'], ['get', 'name']], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 12, 14, 16], 'text-font': ['Noto Sans Bold'] }, + paint: { 'text-color': '#ffffff', 'text-halo-color': theme.land, 'text-halo-width': 2 } + }); + } + + const style = { + version: 8, + name: 'HikeMap Theme', + sources: { + 'openmaptiles': { + type: 'vector', + url: 'https://tiles.openfreemap.org/planet' + } + }, + glyphs: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf', + layers: layers }; map.setStyle(style); } + // Load active theme from server (public endpoint) + async function loadActiveTheme() { + try { + const response = await fetch('/api/map-theme'); + + if (response.ok) { + const data = await response.json(); + return data.theme; + } + } catch (err) { + console.error('Error loading theme from server:', err); + } + + return null; + } + + // Save active theme to server (admin only) + async function saveActiveTheme(theme) { + const token = getToken(); + if (!token) { + showError('Please log in as admin to HikeMap first'); + return false; + } + + try { + const response = await fetch('/api/admin/map-theme', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ theme }) + }); + + if (response.ok) { + return true; + } else if (response.status === 403) { + showError('Admin access required to set game theme'); + return false; + } else { + const err = await response.json(); + showError(err.error || 'Failed to save theme'); + return false; + } + } catch (err) { + console.error('Error saving theme to server:', err); + showError('Failed to save theme to server'); + return false; + } + } + // Initialize map const map = new maplibregl.Map({ container: 'map', @@ -689,18 +857,16 @@ map.addControl(new maplibregl.NavigationControl()); // Apply initial style after map loads - map.on('load', () => { - applyStyle(); - renderSavedThemes(); - - // Load active game theme if exists - const activeTheme = localStorage.getItem('hikemap_active_theme'); - if (activeTheme) { - try { - const colors = JSON.parse(activeTheme); - setColors(colors); - } catch (e) {} + map.on('load', async () => { + // Try to load active theme from server + const serverTheme = await loadActiveTheme(); + if (serverTheme) { + setTheme(serverTheme); + } else { + applyStyle(); } + + renderSavedThemes(); }); // Color input handlers @@ -711,19 +877,22 @@ }); }); - document.getElementById('buildings3d').addEventListener('change', applyStyle); + // Toggle handlers + ['buildings3d', 'showRoads', 'showBuildings', 'showParks', 'showWater', 'showRoadLabels', 'showPlaceLabels'].forEach(id => { + document.getElementById(id).addEventListener('change', applyStyle); + }); // Preset handlers document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', () => { const presetName = btn.dataset.preset; if (presets[presetName]) { - setColors({ ...presets[presetName], buildings3d: true }); + setTheme({ ...presets[presetName], buildings3d: true, showRoads: true, showBuildings: true, showParks: true, showWater: true, showRoadLabels: true, showPlaceLabels: true }); } }); }); - // Save theme + // Save theme to local library document.getElementById('saveTheme').addEventListener('click', () => { const name = document.getElementById('themeName').value.trim(); if (!name) { @@ -731,17 +900,19 @@ return; } const themes = loadSavedThemes(); - themes[name] = getCurrentColors(); + themes[name] = getCurrentTheme(); saveSavedThemes(themes); renderSavedThemes(); - showSuccess(`Saved "${name}"`); + showSuccess(`Saved "${name}" to library`); }); - // Apply to game - document.getElementById('applyToGame').addEventListener('click', () => { - const colors = getCurrentColors(); - localStorage.setItem('hikemap_active_theme', JSON.stringify(colors)); - showSuccess('Theme applied to game! Refresh HikeMap to see changes.'); + // Apply to game (save to server) + document.getElementById('applyToGame').addEventListener('click', async () => { + const theme = getCurrentTheme(); + const success = await saveActiveTheme(theme); + if (success) { + showSuccess('Theme applied! Refresh HikeMap to see changes.'); + } }); // Camera controls diff --git a/server.js b/server.js index feb01a2..1a1d5da 100644 --- a/server.js +++ b/server.js @@ -1145,6 +1145,35 @@ app.put('/api/user/home-base/icon', authenticateToken, (req, res) => { } }); +// Get global map theme (public - no auth required) +app.get('/api/map-theme', (req, res) => { + try { + const themeJson = db.getSetting('mapTheme'); + const theme = themeJson ? JSON.parse(themeJson) : null; + res.json({ theme }); + } catch (err) { + console.error('Get map theme error:', err); + res.status(500).json({ error: 'Failed to get map theme' }); + } +}); + +// Set global map theme (admin only) +app.put('/api/admin/map-theme', adminOnly, (req, res) => { + try { + const { theme } = req.body; + + if (!theme) { + return res.status(400).json({ error: 'Theme data is required' }); + } + + db.setSetting('mapTheme', JSON.stringify(theme)); + res.json({ success: true }); + } catch (err) { + console.error('Set map theme error:', err); + res.status(500).json({ error: 'Failed to set map theme' }); + } +}); + // Handle player death app.post('/api/user/death', authenticateToken, (req, res) => { try {