Browse Source

Add global map theme system, player icon, and combat UI improvements

- Add global map theme stored in game_settings (admin-only to set)
- Add map theme editor with visibility toggles for map elements
- Replace player GPS marker and combat icon with runner.png
- Restructure player combat UI to mirror monster entry layout
- Increase player and home base marker sizes (80px vs 70px monsters)
- Move navigation control to top-left, remove zoom buttons
- Add skill icons and cache icons

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
HikeMap User 4 weeks ago
parent
commit
96217eb39a
  1. 30
      database.js
  2. 336
      index.html
  3. BIN
      mapgameimgs/cacheicons/cacheIcon100-01.png
  4. BIN
      mapgameimgs/cacheicons/cacheIcon100-01_shadow.png
  5. BIN
      mapgameimgs/player/runner.png
  6. BIN
      mapgameimgs/skills/basic_attack.png
  7. BIN
      mapgameimgs/skills/defend.png
  8. BIN
      mapgameimgs/skills/double_attack.png
  9. BIN
      mapgameimgs/skills/focus.png
  10. BIN
      mapgameimgs/skills/full_restore.png
  11. BIN
      mapgameimgs/skills/heal.png
  12. BIN
      mapgameimgs/skills/heavy_blow.png
  13. BIN
      mapgameimgs/skills/power_strike.png
  14. BIN
      mapgameimgs/skills/quick_heal.png
  15. BIN
      mapgameimgs/skills/quick_strike.png
  16. BIN
      mapgameimgs/skills/second_wind.png
  17. BIN
      mapgameimgs/skills/triple_strike.png
  18. BIN
      mapgameimgs/skills/whirlwind.png
  19. 383
      maplibre-test.html
  20. 29
      server.js

30
database.js

@ -286,6 +286,11 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN reveal_radius INTEGER DEFAULT 800`); this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN reveal_radius INTEGER DEFAULT 800`);
} catch (e) { /* Column already exists */ } } 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 // Migration: Add animation overrides to monster_types
try { try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN attack_animation TEXT DEFAULT 'attack'`); 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); 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) // Check if user can set home base (once per day)
canSetHomeBase(userId) { canSetHomeBase(userId) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`

336
index.html

@ -2632,7 +2632,64 @@
min-height: 180px; min-height: 180px;
} }
.player-side { .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 { .monster-side {
flex: 1; flex: 1;
@ -2814,21 +2871,6 @@
top: -4px; top: -4px;
left: -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 { .monster-entry .status-overlay img {
width: var(--combat-status-size); width: var(--combat-status-size);
height: var(--combat-status-size); height: var(--combat-status-size);
@ -3007,15 +3049,15 @@
/* Home Base Marker */ /* Home Base Marker */
.home-base-marker { .home-base-marker {
width: 50px;
height: 50px;
width: 80px;
height: 80px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.home-base-marker img { .home-base-marker img {
width: 50px;
height: 50px;
width: 80px;
height: 80px;
object-fit: contain; object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5)); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
} }
@ -3689,25 +3731,24 @@
<div id="turnIndicator" class="turn-indicator player-turn">⚡ Your Turn</div> <div id="turnIndicator" class="turn-indicator player-turn">⚡ Your Turn</div>
<div class="combat-arena"> <div class="combat-arena">
<div class="combatant player-side">
<div class="sprite-container">
<div class="combatant-icon" id="playerCombatIcon">🏃</div>
<div class="status-overlay" id="playerStatusOverlay"></div>
</div>
<div class="combatant-name" id="playerCombatName">Trail Runner</div>
<div class="stat-bars">
<div class="stat-bar-container">
<div class="stat-bar-label">HP</div>
<div class="player-side">
<div class="player-entry">
<div class="player-entry-header">
<div class="sprite-container">
<img class="player-entry-icon" id="playerCombatIcon" src="mapgameimgs/player/runner.png" alt="Player">
<div class="status-overlay" id="playerStatusOverlay"></div>
</div>
<span class="player-entry-name" id="playerCombatName">Trail Runner</span>
</div>
<div class="player-entry-hp">
<div class="hp-bar"><div class="hp-fill" id="playerHpBar" style="width: 100%;"></div></div> <div class="hp-bar"><div class="hp-fill" id="playerHpBar" style="width: 100%;"></div></div>
<div class="stat-text">HP: <span id="playerHpText">100/100</span></div>
</div> </div>
<div class="stat-bar-container">
<div class="stat-bar-label">MP</div>
<div class="player-entry-mp">
<div class="mp-bar"><div class="mp-fill" id="playerMpBar" style="width: 100%;"></div></div> <div class="mp-bar"><div class="mp-fill" id="playerMpBar" style="width: 100%;"></div></div>
<div class="stat-text">MP: <span id="playerMpText">50/50</span></div>
</div> </div>
</div> </div>
<div class="stat-text">
HP: <span id="playerHpText">100/100</span> | MP: <span id="playerMpText">50/50</span>
</div>
</div> </div>
<div class="combat-vs">VS</div> <div class="combat-vs">VS</div>
@ -4375,118 +4416,173 @@
// ===================== // =====================
// FANTASY MAP STYLE // FANTASY MAP STYLE
// ===================== // =====================
// Default fantasy colors (can be customized via Theme Editor)
const defaultMapColors = {
// Default theme (colors + visibility toggles)
const defaultMapTheme = {
land: '#1a1a2e', land: '#1a1a2e',
water: '#0f3460', water: '#0f3460',
roads: '#e94560', roads: '#e94560',
buildings: '#16213e', 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', { 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', { id: 'landcover_wood', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'wood'], 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', { id: 'landcover_grass', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'grass'], 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', { 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', { 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, id: 'buildings-3d', type: 'fill-extrusion', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13,
paint: { paint: {
'fill-extrusion-color': colors.buildings,
'fill-extrusion-color': theme.buildings,
'fill-extrusion-height': ['*', 2, ['get', 'render_height']], 'fill-extrusion-height': ['*', 2, ['get', 'render_height']],
'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']], 'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']],
'fill-extrusion-opacity': 0.85 'fill-extrusion-opacity': 0.85
} }
} : {
});
} else {
layers.push({
id: 'buildings', type: 'fill', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, 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', { id: 'roads-service', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['service', 'track'], true, false], 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', { id: 'roads-path', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false], 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', { id: 'roads-minor', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'minor'], filter: ['==', ['get', 'class'], 'minor'],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-secondary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false], filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-primary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false], filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-motorway', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'motorway'], filter: ['==', ['get', 'class'], 'motorway'],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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({ const map = new maplibregl.Map({
container: 'map', container: 'map',
style: buildFantasyStyle(mapColors, true),
style: buildFantasyStyle(mapTheme),
center: [-97.84, 30.49], // Note: [lng, lat] for MapLibre center: [-97.84, 30.49], // Note: [lng, lat] for MapLibre
zoom: 13, zoom: 13,
maxZoom: 22, maxZoom: 22,
@ -4506,7 +4602,15 @@
}); });
// Add navigation controls // 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 // COORDINATE HELPER FUNCTIONS
@ -6639,9 +6743,9 @@
if (!gpsMarker) { if (!gpsMarker) {
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'custom-div-icon gps-marker-icon'; el.className = 'custom-div-icon gps-marker-icon';
el.innerHTML = '<i class="mdi mdi-navigation" style="color: #4285f4; font-size: 36px; transform: rotate(' + (currentHeading || 0) + 'deg);"></i>';
el.style.width = '36px';
el.style.height = '36px';
el.innerHTML = '<img src="mapgameimgs/player/runner.png" class="player-marker-img" style="width: 80px; height: 80px; transform: rotate(' + (currentHeading || 0) + 'deg);">';
el.style.width = '80px';
el.style.height = '80px';
gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' }) gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
.setLngLat([lng, lat]) .setLngLat([lng, lat])
.addTo(map); .addTo(map);
@ -6650,7 +6754,7 @@
// Update rotation if we have heading // Update rotation if we have heading
if (currentHeading !== null) { if (currentHeading !== null) {
const el = gpsMarker.getElement(); const el = gpsMarker.getElement();
const icon = el.querySelector('.mdi-navigation');
const icon = el.querySelector('.player-marker-img');
if (icon) { if (icon) {
icon.style.transform = 'rotate(' + currentHeading + 'deg)'; icon.style.transform = 'rotate(' + currentHeading + 'deg)';
} }
@ -12829,9 +12933,9 @@
// Recreate the marker // Recreate the marker
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'custom-div-icon gps-marker-icon'; el.className = 'custom-div-icon gps-marker-icon';
el.innerHTML = '<i class="mdi mdi-navigation" style="color: #4285f4; font-size: 36px; transform: rotate(' + (currentHeading || 0) + 'deg);"></i>';
el.style.width = '36px';
el.style.height = '36px';
el.innerHTML = '<img src="mapgameimgs/player/runner.png" class="player-marker-img" style="width: 80px; height: 80px; transform: rotate(' + (currentHeading || 0) + 'deg);">';
el.style.width = '80px';
el.style.height = '80px';
gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' }) gpsMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
.setLngLat([lng, lat]) .setLngLat([lng, lat])
.addTo(map); .addTo(map);

BIN
mapgameimgs/cacheicons/cacheIcon100-01.png

After

Width: 100  |  Height: 100  |  Size: 7.7 KiB

BIN
mapgameimgs/cacheicons/cacheIcon100-01_shadow.png

After

Width: 100  |  Height: 100  |  Size: 2.4 KiB

BIN
mapgameimgs/player/runner.png

After

Width: 100  |  Height: 100  |  Size: 12 KiB

BIN
mapgameimgs/skills/basic_attack.png

After

Width: 50  |  Height: 50  |  Size: 3.6 KiB

BIN
mapgameimgs/skills/defend.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/double_attack.png

After

Width: 50  |  Height: 50  |  Size: 4.3 KiB

BIN
mapgameimgs/skills/focus.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/full_restore.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/heal.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/heavy_blow.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/power_strike.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/quick_heal.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/quick_strike.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/second_wind.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/triple_strike.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgameimgs/skills/whirlwind.png

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

383
maplibre-test.html

@ -27,7 +27,7 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.3); box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 1000; z-index: 1000;
width: 300px;
width: 320px;
max-height: 90vh; max-height: 90vh;
overflow-y: auto; overflow-y: auto;
} }
@ -90,7 +90,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-top: 10px;
margin-bottom: 6px;
} }
.checkbox-row label { .checkbox-row label {
@ -98,6 +98,11 @@
cursor: pointer; cursor: pointer;
} }
.checkbox-row input[type="checkbox"] {
width: 16px;
height: 16px;
}
.slider-row { .slider-row {
display: flex; display: flex;
align-items: center; align-items: center;
@ -158,31 +163,12 @@
background: #c5372b; background: #c5372b;
} }
.btn-secondary {
background: #666;
color: white;
}
.btn-secondary:hover {
background: #555;
}
.btn-block { .btn-block {
display: block; display: block;
width: 100%; width: 100%;
margin-bottom: 8px; margin-bottom: 8px;
} }
.btn-group {
display: flex;
gap: 8px;
margin-top: 10px;
}
.btn-group .btn {
flex: 1;
}
.theme-name-input { .theme-name-input {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
@ -198,7 +184,7 @@
} }
.saved-themes { .saved-themes {
max-height: 200px;
max-height: 150px;
overflow-y: auto; overflow-y: auto;
margin-top: 10px; margin-top: 10px;
} }
@ -311,9 +297,7 @@
border: 1px solid rgba(0,0,0,0.2); border: 1px solid rgba(0,0,0,0.2);
} }
.success-msg {
background: #d4edda;
color: #155724;
.success-msg, .error-msg {
padding: 8px 12px; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
@ -321,11 +305,36 @@
display: none; display: none;
} }
.success-msg {
background: #d4edda;
color: #155724;
}
.error-msg {
background: #f8d7da;
color: #721c24;
}
.info-text { .info-text {
font-size: 11px; font-size: 11px;
color: #666; color: #666;
margin-top: 8px; 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;
}
</style> </style>
</head> </head>
<body> <body>
@ -334,7 +343,12 @@
<div class="control-panel"> <div class="control-panel">
<h3>Map Theme Editor</h3> <h3>Map Theme Editor</h3>
<div id="loginPrompt" class="login-prompt" style="display:none;">
Admin login required to save global game theme.
</div>
<div id="successMsg" class="success-msg"></div> <div id="successMsg" class="success-msg"></div>
<div id="errorMsg" class="error-msg"></div>
<div class="section"> <div class="section">
<div class="section-title">Color Palette</div> <div class="section-title">Color Palette</div>
@ -364,8 +378,37 @@
<input type="color" id="colorParks" value="#1b4332"> <input type="color" id="colorParks" value="#1b4332">
<span class="hex-value" id="hexParks">#1b4332</span> <span class="hex-value" id="hexParks">#1b4332</span>
</div> </div>
</div>
<div class="checkbox-row">
<div class="section">
<div class="section-title">Map Elements</div>
<div class="toggle-grid">
<div class="checkbox-row">
<input type="checkbox" id="showRoads" checked>
<label for="showRoads">Roads</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="showBuildings" checked>
<label for="showBuildings">Buildings</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="showParks" checked>
<label for="showParks">Parks</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="showWater" checked>
<label for="showWater">Water</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="showRoadLabels" checked>
<label for="showRoadLabels">Road Names</label>
</div>
<div class="checkbox-row">
<input type="checkbox" id="showPlaceLabels" checked>
<label for="showPlaceLabels">Place Names</label>
</div>
</div>
<div class="checkbox-row" style="margin-top:8px;">
<input type="checkbox" id="buildings3d" checked> <input type="checkbox" id="buildings3d" checked>
<label for="buildings3d">3D Buildings (200% height)</label> <label for="buildings3d">3D Buildings (200% height)</label>
</div> </div>
@ -441,7 +484,7 @@
<div class="section"> <div class="section">
<div class="section-title">Apply to Game</div> <div class="section-title">Apply to Game</div>
<button class="btn btn-primary btn-block" id="applyToGame">Set as Active Game Theme</button> <button class="btn btn-primary btn-block" id="applyToGame">Set as Active Game Theme</button>
<p class="info-text">This will save the current colors as the active theme used in HikeMap.</p>
<p class="info-text">Saves to your account. Refresh HikeMap to see changes.</p>
</div> </div>
<div class="section"> <div class="section">
@ -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() { function loadSavedThemes() {
const themes = JSON.parse(localStorage.getItem('hikemap_themes') || '{}'); const themes = JSON.parse(localStorage.getItem('hikemap_themes') || '{}');
return themes; return themes;
@ -493,28 +545,40 @@
localStorage.setItem('hikemap_themes', JSON.stringify(themes)); localStorage.setItem('hikemap_themes', JSON.stringify(themes));
} }
// Get current colors from inputs
function getCurrentColors() {
// Get current theme from inputs
function getCurrentTheme() {
return { return {
land: document.getElementById('colorLand').value, land: document.getElementById('colorLand').value,
water: document.getElementById('colorWater').value, water: document.getElementById('colorWater').value,
roads: document.getElementById('colorRoads').value, roads: document.getElementById('colorRoads').value,
buildings: document.getElementById('colorBuildings').value, buildings: document.getElementById('colorBuildings').value,
parks: document.getElementById('colorParks').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(); updateHexDisplays();
applyStyle(); applyStyle();
} }
@ -563,9 +627,8 @@
const themeName = item.dataset.theme; const themeName = item.dataset.theme;
const themes = loadSavedThemes(); const themes = loadSavedThemes();
if (themes[themeName]) { if (themes[themeName]) {
setColors(themes[themeName]);
setTheme(themes[themeName]);
document.getElementById('themeName').value = themeName; document.getElementById('themeName').value = themeName;
// Update active state
container.querySelectorAll('.theme-item').forEach(i => i.classList.remove('active')); container.querySelectorAll('.theme-item').forEach(i => i.classList.remove('active'));
item.classList.add('active'); item.classList.add('active');
} }
@ -591,91 +654,196 @@
// Show success message // Show success message
function showSuccess(msg) { function showSuccess(msg) {
const el = document.getElementById('successMsg'); const el = document.getElementById('successMsg');
document.getElementById('errorMsg').style.display = 'none';
el.textContent = msg; el.textContent = msg;
el.style.display = 'block'; el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 3000); 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 // Build and apply the map style
function applyStyle() { 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', { 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', { id: 'landcover_wood', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'wood'], 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', { id: 'landcover_grass', type: 'fill', source: 'openmaptiles', 'source-layer': 'landcover',
filter: ['==', ['get', 'class'], 'grass'], 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', { 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', { 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, id: 'buildings-3d', type: 'fill-extrusion', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13,
paint: { paint: {
'fill-extrusion-color': colors.buildings,
'fill-extrusion-color': theme.buildings,
'fill-extrusion-height': ['*', 2, ['get', 'render_height']], 'fill-extrusion-height': ['*', 2, ['get', 'render_height']],
'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']], 'fill-extrusion-base': ['*', 2, ['get', 'render_min_height']],
'fill-extrusion-opacity': 0.85 'fill-extrusion-opacity': 0.85
} }
} : {
});
} else {
layers.push({
id: 'buildings', type: 'fill', source: 'openmaptiles', 'source-layer': 'building', minzoom: 13, 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', { id: 'roads-service', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['service', 'track'], true, false], 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', { id: 'roads-path', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['path', 'pedestrian'], true, false], 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', { id: 'roads-minor', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'minor'], filter: ['==', ['get', 'class'], 'minor'],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-secondary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false], filter: ['match', ['get', 'class'], ['secondary', 'tertiary'], true, false],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-primary', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false], filter: ['match', ['get', 'class'], ['primary', 'trunk'], true, false],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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', { id: 'roads-motorway', type: 'line', source: 'openmaptiles', 'source-layer': 'transportation',
filter: ['==', ['get', 'class'], 'motorway'], filter: ['==', ['get', 'class'], 'motorway'],
layout: { 'line-cap': 'round', 'line-join': 'round' }, 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); 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 // Initialize map
const map = new maplibregl.Map({ const map = new maplibregl.Map({
container: 'map', container: 'map',
@ -689,18 +857,16 @@
map.addControl(new maplibregl.NavigationControl()); map.addControl(new maplibregl.NavigationControl());
// Apply initial style after map loads // 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 // 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 // Preset handlers
document.querySelectorAll('.preset-btn').forEach(btn => { document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const presetName = btn.dataset.preset; const presetName = btn.dataset.preset;
if (presets[presetName]) { 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', () => { document.getElementById('saveTheme').addEventListener('click', () => {
const name = document.getElementById('themeName').value.trim(); const name = document.getElementById('themeName').value.trim();
if (!name) { if (!name) {
@ -731,17 +900,19 @@
return; return;
} }
const themes = loadSavedThemes(); const themes = loadSavedThemes();
themes[name] = getCurrentColors();
themes[name] = getCurrentTheme();
saveSavedThemes(themes); saveSavedThemes(themes);
renderSavedThemes(); 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 // Camera controls

29
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 // Handle player death
app.post('/api/user/death', authenticateToken, (req, res) => { app.post('/api/user/death', authenticateToken, (req, res) => {
try { try {

Loading…
Cancel
Save