|
|
@ -2542,16 +2542,14 @@ |
|
|
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); |
|
|
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); |
|
|
border: 2px solid #e94560; |
|
|
border: 2px solid #e94560; |
|
|
color: white; |
|
|
color: white; |
|
|
padding: 8px 6px; |
|
|
|
|
|
|
|
|
padding: 8px 10px; |
|
|
border-radius: 10px; |
|
|
border-radius: 10px; |
|
|
cursor: pointer; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
transition: all 0.2s; |
|
|
text-align: center; |
|
|
|
|
|
display: flex; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
|
|
|
|
|
|
flex-direction: row; |
|
|
align-items: center; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
|
|
|
min-height: 70px; |
|
|
|
|
|
|
|
|
gap: 8px; |
|
|
} |
|
|
} |
|
|
.skill-btn:hover:not(:disabled) { |
|
|
.skill-btn:hover:not(:disabled) { |
|
|
background: linear-gradient(135deg, #e94560 0%, #c73e54 100%); |
|
|
background: linear-gradient(135deg, #e94560 0%, #c73e54 100%); |
|
|
@ -2564,16 +2562,26 @@ |
|
|
transform: none; |
|
|
transform: none; |
|
|
} |
|
|
} |
|
|
.skill-btn .skill-icon-wrapper { |
|
|
.skill-btn .skill-icon-wrapper { |
|
|
margin-bottom: 3px; |
|
|
|
|
|
|
|
|
flex-shrink: 0; |
|
|
|
|
|
width: 32px; |
|
|
|
|
|
height: 32px; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
justify-content: center; |
|
|
} |
|
|
} |
|
|
.skill-btn .skill-icon-wrapper img { |
|
|
.skill-btn .skill-icon-wrapper img { |
|
|
width: 28px; |
|
|
|
|
|
height: 28px; |
|
|
|
|
|
|
|
|
width: 32px; |
|
|
|
|
|
height: 32px; |
|
|
|
|
|
} |
|
|
|
|
|
.skill-btn .skill-info { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
align-items: flex-start; |
|
|
|
|
|
min-width: 0; |
|
|
} |
|
|
} |
|
|
.skill-btn .skill-name { |
|
|
.skill-btn .skill-name { |
|
|
font-weight: bold; |
|
|
font-weight: bold; |
|
|
font-size: 11px; |
|
|
|
|
|
margin-bottom: 2px; |
|
|
|
|
|
|
|
|
font-size: 12px; |
|
|
line-height: 1.2; |
|
|
line-height: 1.2; |
|
|
} |
|
|
} |
|
|
.skill-btn .skill-cost { |
|
|
.skill-btn .skill-cost { |
|
|
@ -3318,6 +3326,173 @@ |
|
|
to { transform: translateX(100%); opacity: 0; } |
|
|
to { transform: translateX(100%); opacity: 0; } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Custom Layer & Filter Control */ |
|
|
|
|
|
.layer-filter-control { |
|
|
|
|
|
background: white; |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.3); |
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
|
|
|
font-size: 13px; |
|
|
|
|
|
max-height: 80vh; |
|
|
|
|
|
overflow-y: auto; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-toggle { |
|
|
|
|
|
width: 36px; |
|
|
|
|
|
height: 36px; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
font-size: 20px; |
|
|
|
|
|
background: white; |
|
|
|
|
|
border-radius: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-toggle:hover { |
|
|
|
|
|
background: #f0f0f0; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-panel { |
|
|
|
|
|
display: none; |
|
|
|
|
|
padding: 12px; |
|
|
|
|
|
min-width: 220px; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-control.expanded .layer-filter-toggle { |
|
|
|
|
|
display: none; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-control.expanded .layer-filter-panel { |
|
|
|
|
|
display: block; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section { |
|
|
|
|
|
margin-bottom: 12px; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section:last-child { |
|
|
|
|
|
margin-bottom: 0; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section-title { |
|
|
|
|
|
font-weight: 600; |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
text-transform: uppercase; |
|
|
|
|
|
color: #666; |
|
|
|
|
|
margin-bottom: 8px; |
|
|
|
|
|
padding-bottom: 4px; |
|
|
|
|
|
border-bottom: 1px solid #eee; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section label { |
|
|
|
|
|
display: block; |
|
|
|
|
|
padding: 4px 0; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section label:hover { |
|
|
|
|
|
background: #f5f5f5; |
|
|
|
|
|
margin: 0 -8px; |
|
|
|
|
|
padding: 4px 8px; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-section input[type="radio"] { |
|
|
|
|
|
margin-right: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-row { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
margin-bottom: 8px; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-row:last-child { |
|
|
|
|
|
margin-bottom: 0; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-label { |
|
|
|
|
|
width: 70px; |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
color: #444; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-row input[type="range"] { |
|
|
|
|
|
flex: 1; |
|
|
|
|
|
height: 4px; |
|
|
|
|
|
-webkit-appearance: none; |
|
|
|
|
|
background: #ddd; |
|
|
|
|
|
border-radius: 2px; |
|
|
|
|
|
outline: none; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-row input[type="range"]::-webkit-slider-thumb { |
|
|
|
|
|
-webkit-appearance: none; |
|
|
|
|
|
width: 14px; |
|
|
|
|
|
height: 14px; |
|
|
|
|
|
background: #4CAF50; |
|
|
|
|
|
border-radius: 50%; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-slider-value { |
|
|
|
|
|
width: 36px; |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
color: #666; |
|
|
|
|
|
text-align: right; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-checkbox-row { |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 8px; |
|
|
|
|
|
margin-top: 8px; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-checkbox-row label { |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
color: #444; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
gap: 4px; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-reset-btn { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
margin-top: 10px; |
|
|
|
|
|
padding: 6px 12px; |
|
|
|
|
|
background: #f0f0f0; |
|
|
|
|
|
border: 1px solid #ddd; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
} |
|
|
|
|
|
.filter-reset-btn:hover { |
|
|
|
|
|
background: #e0e0e0; |
|
|
|
|
|
} |
|
|
|
|
|
.blend-section select { |
|
|
|
|
|
width: 100%; |
|
|
|
|
|
padding: 6px 8px; |
|
|
|
|
|
border: 1px solid #ddd; |
|
|
|
|
|
border-radius: 4px; |
|
|
|
|
|
font-size: 12px; |
|
|
|
|
|
margin-bottom: 8px; |
|
|
|
|
|
background: white; |
|
|
|
|
|
} |
|
|
|
|
|
.blend-section label { |
|
|
|
|
|
display: block; |
|
|
|
|
|
font-size: 11px; |
|
|
|
|
|
color: #444; |
|
|
|
|
|
margin-bottom: 4px; |
|
|
|
|
|
} |
|
|
|
|
|
.blend-section .blend-row { |
|
|
|
|
|
margin-bottom: 10px; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-close { |
|
|
|
|
|
position: absolute; |
|
|
|
|
|
top: 8px; |
|
|
|
|
|
right: 8px; |
|
|
|
|
|
width: 20px; |
|
|
|
|
|
height: 20px; |
|
|
|
|
|
display: flex; |
|
|
|
|
|
align-items: center; |
|
|
|
|
|
justify-content: center; |
|
|
|
|
|
cursor: pointer; |
|
|
|
|
|
font-size: 14px; |
|
|
|
|
|
color: #999; |
|
|
|
|
|
border-radius: 50%; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-close:hover { |
|
|
|
|
|
background: #f0f0f0; |
|
|
|
|
|
color: #333; |
|
|
|
|
|
} |
|
|
|
|
|
.layer-filter-panel { |
|
|
|
|
|
position: relative; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
</style> |
|
|
</style> |
|
|
<!-- Monster Animation Definitions --> |
|
|
<!-- Monster Animation Definitions --> |
|
|
<script src="/animations.js"></script> |
|
|
<script src="/animations.js"></script> |
|
|
@ -4183,15 +4358,333 @@ |
|
|
maxNativeZoom: 19 |
|
|
maxNativeZoom: 19 |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// CartoDB free tile layers |
|
|
|
|
|
const cartoPositron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const cartoPositronLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const cartoVoyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const cartoVoyagerLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const cartoDark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const cartoDarkLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { |
|
|
|
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/attributions">CARTO</a>', |
|
|
|
|
|
subdomains: 'abcd', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 20 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// ÖPNVKarte (Public Transport Map) |
|
|
|
|
|
const opnvKarte = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { |
|
|
|
|
|
attribution: 'Map © <a href="https://memomaps.de/">memomaps.de</a> CC-BY-SA, map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>', |
|
|
|
|
|
maxZoom: 22, |
|
|
|
|
|
maxNativeZoom: 18 |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
// Add street map by default |
|
|
// Add street map by default |
|
|
streetMap.addTo(map); |
|
|
streetMap.addTo(map); |
|
|
|
|
|
|
|
|
// Layer control |
|
|
|
|
|
|
|
|
// Tile layer configurations (for creating overlay instances) |
|
|
|
|
|
const tileConfigs = { |
|
|
|
|
|
"OSM Street": { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', options: { subdomains: 'abc', maxZoom: 22, maxNativeZoom: 19 } }, |
|
|
|
|
|
"Positron": { url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Positron (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Voyager": { url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Voyager (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Dark": { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Dark (No Labels)": { url: 'https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', options: { subdomains: 'abcd', maxZoom: 22, maxNativeZoom: 20 } }, |
|
|
|
|
|
"Satellite": { url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', options: { maxZoom: 22, maxNativeZoom: 19 } }, |
|
|
|
|
|
"Transport (ÖPNV)": { url: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', options: { maxZoom: 22, maxNativeZoom: 18 } } |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Layer instances for base layer switching |
|
|
const baseMaps = { |
|
|
const baseMaps = { |
|
|
"Street Map": streetMap, |
|
|
|
|
|
"Satellite": satellite |
|
|
|
|
|
|
|
|
"OSM Street": streetMap, |
|
|
|
|
|
"Positron": cartoPositronLabels, |
|
|
|
|
|
"Positron (No Labels)": cartoPositron, |
|
|
|
|
|
"Voyager": cartoVoyagerLabels, |
|
|
|
|
|
"Voyager (No Labels)": cartoVoyager, |
|
|
|
|
|
"Dark": cartoDarkLabels, |
|
|
|
|
|
"Dark (No Labels)": cartoDark, |
|
|
|
|
|
"Satellite": satellite, |
|
|
|
|
|
"Transport (ÖPNV)": opnvKarte |
|
|
}; |
|
|
}; |
|
|
L.control.layers(baseMaps, null, { position: 'bottomleft' }).addTo(map); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Custom Layer & Filter Control |
|
|
|
|
|
const LayerFilterControl = L.Control.extend({ |
|
|
|
|
|
options: { position: 'bottomleft' }, |
|
|
|
|
|
|
|
|
|
|
|
onAdd: function(map) { |
|
|
|
|
|
this._map = map; |
|
|
|
|
|
this._currentLayer = streetMap; |
|
|
|
|
|
this._overlayLayer = null; |
|
|
|
|
|
this._filters = { |
|
|
|
|
|
brightness: 100, |
|
|
|
|
|
contrast: 100, |
|
|
|
|
|
saturate: 100, |
|
|
|
|
|
grayscale: 0, |
|
|
|
|
|
hueRotate: 0, |
|
|
|
|
|
invert: false |
|
|
|
|
|
}; |
|
|
|
|
|
this._blend = { |
|
|
|
|
|
overlay: 'none', |
|
|
|
|
|
opacity: 70, |
|
|
|
|
|
mode: 'overlay' |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const container = L.DomUtil.create('div', 'layer-filter-control'); |
|
|
|
|
|
|
|
|
|
|
|
// Build layer radio buttons HTML |
|
|
|
|
|
let layerOptionsHtml = ''; |
|
|
|
|
|
Object.keys(baseMaps).forEach((name, i) => { |
|
|
|
|
|
const checked = i === 0 ? 'checked' : ''; |
|
|
|
|
|
layerOptionsHtml += `<label><input type="radio" name="baseLayer" value="${name}" ${checked}>${name}</label>`; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Build overlay layer options |
|
|
|
|
|
let overlayOptionsHtml = '<option value="none">None</option>'; |
|
|
|
|
|
Object.keys(baseMaps).forEach(name => { |
|
|
|
|
|
overlayOptionsHtml += `<option value="${name}">${name}</option>`; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Blend modes |
|
|
|
|
|
const blendModes = ['normal', 'multiply', 'screen', 'overlay', 'darken', 'lighten', |
|
|
|
|
|
'color-dodge', 'color-burn', 'hard-light', 'soft-light', |
|
|
|
|
|
'difference', 'exclusion', 'hue', 'saturation', 'color', 'luminosity']; |
|
|
|
|
|
let blendModeOptionsHtml = blendModes.map(m => |
|
|
|
|
|
`<option value="${m}" ${m === 'overlay' ? 'selected' : ''}>${m}</option>` |
|
|
|
|
|
).join(''); |
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = ` |
|
|
|
|
|
<div class="layer-filter-toggle" title="Map Layers & Filters">🗺️</div> |
|
|
|
|
|
<div class="layer-filter-panel"> |
|
|
|
|
|
<div class="layer-filter-close">✕</div> |
|
|
|
|
|
<div class="layer-filter-section"> |
|
|
|
|
|
<div class="layer-filter-section-title">Base Layer</div> |
|
|
|
|
|
${layerOptionsHtml} |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="layer-filter-section blend-section"> |
|
|
|
|
|
<div class="layer-filter-section-title">Layer Blend</div> |
|
|
|
|
|
<div class="blend-row"> |
|
|
|
|
|
<label>Overlay Layer</label> |
|
|
|
|
|
<select id="overlayLayer">${overlayOptionsHtml}</select> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="blend-row"> |
|
|
|
|
|
<label>Blend Mode</label> |
|
|
|
|
|
<select id="blendMode">${blendModeOptionsHtml}</select> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Opacity</span> |
|
|
|
|
|
<input type="range" id="overlayOpacity" min="0" max="100" value="70"> |
|
|
|
|
|
<span class="filter-slider-value" id="overlayOpacityVal">70%</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="layer-filter-section"> |
|
|
|
|
|
<div class="layer-filter-section-title">CSS Filters</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Brightness</span> |
|
|
|
|
|
<input type="range" id="filterBrightness" min="0" max="200" value="100"> |
|
|
|
|
|
<span class="filter-slider-value" id="filterBrightnessVal">100%</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Contrast</span> |
|
|
|
|
|
<input type="range" id="filterContrast" min="0" max="200" value="100"> |
|
|
|
|
|
<span class="filter-slider-value" id="filterContrastVal">100%</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Saturation</span> |
|
|
|
|
|
<input type="range" id="filterSaturate" min="0" max="200" value="100"> |
|
|
|
|
|
<span class="filter-slider-value" id="filterSaturateVal">100%</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Grayscale</span> |
|
|
|
|
|
<input type="range" id="filterGrayscale" min="0" max="100" value="0"> |
|
|
|
|
|
<span class="filter-slider-value" id="filterGrayscaleVal">0%</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-slider-row"> |
|
|
|
|
|
<span class="filter-slider-label">Hue Rotate</span> |
|
|
|
|
|
<input type="range" id="filterHueRotate" min="0" max="360" value="0"> |
|
|
|
|
|
<span class="filter-slider-value" id="filterHueRotateVal">0°</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="filter-checkbox-row"> |
|
|
|
|
|
<label><input type="checkbox" id="filterInvert"> Invert Colors</label> |
|
|
|
|
|
</div> |
|
|
|
|
|
<button class="filter-reset-btn" id="filterReset">Reset All</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
`; |
|
|
|
|
|
|
|
|
|
|
|
// Prevent map interactions |
|
|
|
|
|
L.DomEvent.disableClickPropagation(container); |
|
|
|
|
|
L.DomEvent.disableScrollPropagation(container); |
|
|
|
|
|
|
|
|
|
|
|
// Toggle panel |
|
|
|
|
|
const toggle = container.querySelector('.layer-filter-toggle'); |
|
|
|
|
|
const closeBtn = container.querySelector('.layer-filter-close'); |
|
|
|
|
|
toggle.addEventListener('click', () => container.classList.add('expanded')); |
|
|
|
|
|
closeBtn.addEventListener('click', () => container.classList.remove('expanded')); |
|
|
|
|
|
|
|
|
|
|
|
// Layer switching |
|
|
|
|
|
container.querySelectorAll('input[name="baseLayer"]').forEach(radio => { |
|
|
|
|
|
radio.addEventListener('change', (e) => { |
|
|
|
|
|
this._map.removeLayer(this._currentLayer); |
|
|
|
|
|
this._currentLayer = baseMaps[e.target.value]; |
|
|
|
|
|
this._currentLayer.addTo(this._map); |
|
|
|
|
|
this._applyFilters(); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Filter sliders |
|
|
|
|
|
const sliderConfig = [ |
|
|
|
|
|
{ id: 'filterBrightness', key: 'brightness', unit: '%' }, |
|
|
|
|
|
{ id: 'filterContrast', key: 'contrast', unit: '%' }, |
|
|
|
|
|
{ id: 'filterSaturate', key: 'saturate', unit: '%' }, |
|
|
|
|
|
{ id: 'filterGrayscale', key: 'grayscale', unit: '%' }, |
|
|
|
|
|
{ id: 'filterHueRotate', key: 'hueRotate', unit: '°' } |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
sliderConfig.forEach(cfg => { |
|
|
|
|
|
const slider = container.querySelector(`#${cfg.id}`); |
|
|
|
|
|
const valueEl = container.querySelector(`#${cfg.id}Val`); |
|
|
|
|
|
slider.addEventListener('input', (e) => { |
|
|
|
|
|
this._filters[cfg.key] = parseInt(e.target.value); |
|
|
|
|
|
valueEl.textContent = e.target.value + cfg.unit; |
|
|
|
|
|
this._applyFilters(); |
|
|
|
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Invert checkbox |
|
|
|
|
|
container.querySelector('#filterInvert').addEventListener('change', (e) => { |
|
|
|
|
|
this._filters.invert = e.target.checked; |
|
|
|
|
|
this._applyFilters(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Overlay layer selection |
|
|
|
|
|
container.querySelector('#overlayLayer').addEventListener('change', (e) => { |
|
|
|
|
|
this._blend.overlay = e.target.value; |
|
|
|
|
|
this._updateOverlayLayer(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Blend mode selection |
|
|
|
|
|
container.querySelector('#blendMode').addEventListener('change', (e) => { |
|
|
|
|
|
this._blend.mode = e.target.value; |
|
|
|
|
|
this._applyBlendMode(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Overlay opacity slider |
|
|
|
|
|
const opacitySlider = container.querySelector('#overlayOpacity'); |
|
|
|
|
|
const opacityVal = container.querySelector('#overlayOpacityVal'); |
|
|
|
|
|
opacitySlider.addEventListener('input', (e) => { |
|
|
|
|
|
this._blend.opacity = parseInt(e.target.value); |
|
|
|
|
|
opacityVal.textContent = e.target.value + '%'; |
|
|
|
|
|
this._applyBlendMode(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Reset button |
|
|
|
|
|
container.querySelector('#filterReset').addEventListener('click', () => { |
|
|
|
|
|
// Reset filters |
|
|
|
|
|
this._filters = { brightness: 100, contrast: 100, saturate: 100, grayscale: 0, hueRotate: 0, invert: false }; |
|
|
|
|
|
container.querySelector('#filterBrightness').value = 100; |
|
|
|
|
|
container.querySelector('#filterBrightnessVal').textContent = '100%'; |
|
|
|
|
|
container.querySelector('#filterContrast').value = 100; |
|
|
|
|
|
container.querySelector('#filterContrastVal').textContent = '100%'; |
|
|
|
|
|
container.querySelector('#filterSaturate').value = 100; |
|
|
|
|
|
container.querySelector('#filterSaturateVal').textContent = '100%'; |
|
|
|
|
|
container.querySelector('#filterGrayscale').value = 0; |
|
|
|
|
|
container.querySelector('#filterGrayscaleVal').textContent = '0%'; |
|
|
|
|
|
container.querySelector('#filterHueRotate').value = 0; |
|
|
|
|
|
container.querySelector('#filterHueRotateVal').textContent = '0°'; |
|
|
|
|
|
container.querySelector('#filterInvert').checked = false; |
|
|
|
|
|
this._applyFilters(); |
|
|
|
|
|
|
|
|
|
|
|
// Reset blend |
|
|
|
|
|
this._blend = { overlay: 'none', opacity: 70, mode: 'overlay' }; |
|
|
|
|
|
container.querySelector('#overlayLayer').value = 'none'; |
|
|
|
|
|
container.querySelector('#blendMode').value = 'overlay'; |
|
|
|
|
|
container.querySelector('#overlayOpacity').value = 70; |
|
|
|
|
|
container.querySelector('#overlayOpacityVal').textContent = '70%'; |
|
|
|
|
|
this._updateOverlayLayer(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
this._container = container; |
|
|
|
|
|
return container; |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
_updateOverlayLayer: function() { |
|
|
|
|
|
// Remove existing overlay |
|
|
|
|
|
if (this._overlayLayer) { |
|
|
|
|
|
this._map.removeLayer(this._overlayLayer); |
|
|
|
|
|
this._overlayLayer = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Add new overlay if selected |
|
|
|
|
|
if (this._blend.overlay !== 'none') { |
|
|
|
|
|
const config = tileConfigs[this._blend.overlay]; |
|
|
|
|
|
if (config) { |
|
|
|
|
|
// Create a new tile layer instance for overlay |
|
|
|
|
|
this._overlayLayer = L.tileLayer(config.url, { |
|
|
|
|
|
...config.options, |
|
|
|
|
|
className: 'overlay-tile-layer' |
|
|
|
|
|
}); |
|
|
|
|
|
this._overlayLayer.addTo(this._map); |
|
|
|
|
|
// Apply blend mode after a short delay to ensure container exists |
|
|
|
|
|
setTimeout(() => this._applyBlendMode(), 100); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
_applyBlendMode: function() { |
|
|
|
|
|
if (this._overlayLayer) { |
|
|
|
|
|
const overlayContainer = this._overlayLayer.getContainer(); |
|
|
|
|
|
if (overlayContainer) { |
|
|
|
|
|
overlayContainer.style.opacity = this._blend.opacity / 100; |
|
|
|
|
|
overlayContainer.style.mixBlendMode = this._blend.mode; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
_applyFilters: function() { |
|
|
|
|
|
const tilePane = this._map.getPanes().tilePane; |
|
|
|
|
|
const f = this._filters; |
|
|
|
|
|
tilePane.style.filter = ` |
|
|
|
|
|
brightness(${f.brightness}%) |
|
|
|
|
|
contrast(${f.contrast}%) |
|
|
|
|
|
saturate(${f.saturate}%) |
|
|
|
|
|
grayscale(${f.grayscale}%) |
|
|
|
|
|
hue-rotate(${f.hueRotate}deg) |
|
|
|
|
|
${f.invert ? 'invert(1)' : ''} |
|
|
|
|
|
`.trim(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
new LayerFilterControl().addTo(map); |
|
|
|
|
|
|
|
|
// ===================== |
|
|
// ===================== |
|
|
// FOG OF WAR SYSTEM |
|
|
// FOG OF WAR SYSTEM |
|
|
@ -14760,16 +15253,55 @@ |
|
|
if (baseSkill.type === 'utility') return; |
|
|
if (baseSkill.type === 'utility') return; |
|
|
|
|
|
|
|
|
const displayName = skillInfo?.displayName || hardcodedSkill?.name || dbSkill?.name || skillId; |
|
|
const displayName = skillInfo?.displayName || hardcodedSkill?.name || dbSkill?.name || skillId; |
|
|
const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 28); |
|
|
|
|
|
|
|
|
const iconHtml = renderSkillIcon(skillId, 'class', playerStats.class, 32); |
|
|
const mpCost = hardcodedSkill?.mpCost || dbSkill?.mpCost || 0; |
|
|
const mpCost = hardcodedSkill?.mpCost || dbSkill?.mpCost || 0; |
|
|
|
|
|
|
|
|
|
|
|
// Calculate skill effect for display |
|
|
|
|
|
const skill = hardcodedSkill || dbSkill; |
|
|
|
|
|
let effectText = ''; |
|
|
|
|
|
if (skill.type === 'damage') { |
|
|
|
|
|
let dmg = 0; |
|
|
|
|
|
if (skill.calculate) { |
|
|
|
|
|
dmg = skill.calculate(playerStats.atk); |
|
|
|
|
|
} else if (skill.basePower) { |
|
|
|
|
|
dmg = Math.floor(playerStats.atk * (skill.basePower / 100)); |
|
|
|
|
|
} |
|
|
|
|
|
const hits = skill.hits || skill.hitCount || 1; |
|
|
|
|
|
effectText = dmg > 0 ? (hits > 1 ? `${hits}×${dmg}` : `${dmg} dmg`) : ''; |
|
|
|
|
|
} else if (skill.type === 'heal') { |
|
|
|
|
|
let heal = 0; |
|
|
|
|
|
if (skill.calculate) { |
|
|
|
|
|
heal = skill.calculate(playerStats.maxHp); |
|
|
|
|
|
} else if (skill.basePower) { |
|
|
|
|
|
heal = Math.floor(playerStats.maxHp * (skill.basePower / 100)); |
|
|
|
|
|
} |
|
|
|
|
|
effectText = heal > 0 ? `+${heal} HP` : ''; |
|
|
|
|
|
} else if (skill.type === 'restore') { |
|
|
|
|
|
let restore = 0; |
|
|
|
|
|
if (skill.calculate) { |
|
|
|
|
|
restore = skill.calculate(playerStats.maxMp); |
|
|
|
|
|
} else if (skill.basePower) { |
|
|
|
|
|
restore = Math.floor(playerStats.maxMp * (skill.basePower / 100)); |
|
|
|
|
|
} |
|
|
|
|
|
effectText = restore > 0 ? `+${restore} MP` : ''; |
|
|
|
|
|
} else if (skill.type === 'buff') { |
|
|
|
|
|
effectText = skill.effect === 'dodge' ? 'Dodge' : (skill.effect || 'Buff'); |
|
|
|
|
|
} else if (skill.type === 'status') { |
|
|
|
|
|
effectText = skill.effect || 'Status'; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const mpText = mpCost > 0 ? `${mpCost} MP` : 'Free'; |
|
|
|
|
|
const costLine = effectText ? `${mpText} • ${effectText}` : mpText; |
|
|
|
|
|
|
|
|
const btn = document.createElement('button'); |
|
|
const btn = document.createElement('button'); |
|
|
btn.className = 'skill-btn'; |
|
|
btn.className = 'skill-btn'; |
|
|
btn.dataset.skillId = skillId; |
|
|
btn.dataset.skillId = skillId; |
|
|
btn.innerHTML = ` |
|
|
btn.innerHTML = ` |
|
|
<span class="skill-icon-wrapper">${iconHtml}</span> |
|
|
<span class="skill-icon-wrapper">${iconHtml}</span> |
|
|
<span class="skill-name">${displayName}</span> |
|
|
|
|
|
<span class="skill-cost ${mpCost === 0 ? 'free' : ''}">${mpCost > 0 ? mpCost + ' MP' : 'Free'}</span> |
|
|
|
|
|
|
|
|
<span class="skill-info"> |
|
|
|
|
|
<span class="skill-name">${displayName}</span> |
|
|
|
|
|
<span class="skill-cost ${mpCost === 0 ? 'free' : ''}">${costLine}</span> |
|
|
|
|
|
</span> |
|
|
`; |
|
|
`; |
|
|
btn.onclick = () => executePlayerSkill(skillId); |
|
|
btn.onclick = () => executePlayerSkill(skillId); |
|
|
skillsContainer.appendChild(btn); |
|
|
skillsContainer.appendChild(btn); |
|
|
@ -14783,9 +15315,11 @@ |
|
|
btn.dataset.skillId = 'admin_banish'; |
|
|
btn.dataset.skillId = 'admin_banish'; |
|
|
btn.style.borderColor = '#ff6b35'; |
|
|
btn.style.borderColor = '#ff6b35'; |
|
|
btn.innerHTML = ` |
|
|
btn.innerHTML = ` |
|
|
<span class="skill-icon-wrapper" style="font-size: 24px;">${adminSkill.icon}</span> |
|
|
|
|
|
<span class="skill-name">${adminSkill.name}</span> |
|
|
|
|
|
<span class="skill-cost free">Admin</span> |
|
|
|
|
|
|
|
|
<span class="skill-icon-wrapper" style="font-size: 28px;">${adminSkill.icon}</span> |
|
|
|
|
|
<span class="skill-info"> |
|
|
|
|
|
<span class="skill-name">${adminSkill.name}</span> |
|
|
|
|
|
<span class="skill-cost free">Admin</span> |
|
|
|
|
|
</span> |
|
|
`; |
|
|
`; |
|
|
btn.onclick = () => executePlayerSkill('admin_banish'); |
|
|
btn.onclick = () => executePlayerSkill('admin_banish'); |
|
|
skillsContainer.appendChild(btn); |
|
|
skillsContainer.appendChild(btn); |
|
|
|