diff --git a/index.html b/index.html index b899255..d994f40 100644 --- a/index.html +++ b/index.html @@ -2542,16 +2542,14 @@ background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); border: 2px solid #e94560; color: white; - padding: 8px 6px; + padding: 8px 10px; border-radius: 10px; cursor: pointer; transition: all 0.2s; - text-align: center; display: flex; - flex-direction: column; + flex-direction: row; align-items: center; - justify-content: center; - min-height: 70px; + gap: 8px; } .skill-btn:hover:not(:disabled) { background: linear-gradient(135deg, #e94560 0%, #c73e54 100%); @@ -2564,16 +2562,26 @@ transform: none; } .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 { - 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 { font-weight: bold; - font-size: 11px; - margin-bottom: 2px; + font-size: 12px; line-height: 1.2; } .skill-btn .skill-cost { @@ -3318,6 +3326,173 @@ 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; + } + @@ -4183,15 +4358,333 @@ maxNativeZoom: 19 }); + // CartoDB free tile layers + const cartoPositron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoPositronLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoVoyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoVoyagerLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoDark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + subdomains: 'abcd', + maxZoom: 22, + maxNativeZoom: 20 + }); + + const cartoDarkLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap © CARTO', + 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 © memomaps.de CC-BY-SA, map data © OpenStreetMap', + maxZoom: 22, + maxNativeZoom: 18 + }); + // Add street map by default 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 = { - "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 += ``; + }); + + // Build overlay layer options + let overlayOptionsHtml = ''; + Object.keys(baseMaps).forEach(name => { + overlayOptionsHtml += ``; + }); + + // 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 => + `` + ).join(''); + + container.innerHTML = ` +
🗺️
+
+
+
+
Base Layer
+ ${layerOptionsHtml} +
+
+
Layer Blend
+
+ + +
+
+ + +
+
+ Opacity + + 70% +
+
+
+
CSS Filters
+
+ Brightness + + 100% +
+
+ Contrast + + 100% +
+
+ Saturation + + 100% +
+
+ Grayscale + + 0% +
+
+ Hue Rotate + + +
+
+ +
+ +
+
+ `; + + // 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 @@ -14760,16 +15253,55 @@ if (baseSkill.type === 'utility') return; 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; + // 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'); btn.className = 'skill-btn'; btn.dataset.skillId = skillId; btn.innerHTML = ` ${iconHtml} - ${displayName} - ${mpCost > 0 ? mpCost + ' MP' : 'Free'} + + ${displayName} + ${costLine} + `; btn.onclick = () => executePlayerSkill(skillId); skillsContainer.appendChild(btn); @@ -14783,9 +15315,11 @@ btn.dataset.skillId = 'admin_banish'; btn.style.borderColor = '#ff6b35'; btn.innerHTML = ` - ${adminSkill.icon} - ${adminSkill.name} - Admin + ${adminSkill.icon} + + ${adminSkill.name} + Admin + `; btn.onclick = () => executePlayerSkill('admin_banish'); skillsContainer.appendChild(btn);