+
+
+
-
-
- 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
+
+ Admin login required to save global game theme.
+
+
+
+
-
+
+
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 {