定居点
类型
全部
🏘️ 村庄
🏰 城镇
🏯 城堡
🏘 城堡村
⛺ 藏身处
筛选:
所属阵营
坐标
繁荣度
驻军
🗺️ 瓦片加载中…
// ============================================================ // CONFIG — 瓦片目录(相对于网站根目录) // 切割后目录结构:tiles/z/x.png (扁平化,按行优先) // ============================================================ const TILE_DIR = '/tiles'; // 世界坐标范围(与 settlements_data.json 一致) const WORLD = { minX: 76.517, maxX: 782.63, // posX minY: 62.205, maxY: 610.56, // posY }; // 瓦片层级定义(扁平化结构:z/x.png) // z1: 2 tiles (1x2), z2: 4 tiles (2x2), z3: 8 tiles (2x4), z4: 16 tiles (4x4) // z5: 32 tiles (4x8), z6: 64 tiles (8x8) const MIN_ZOOM = 1; const MAX_ZOOM = 6; // 每层网格尺寸(宽 x 高) const GRID_SIZES = { 1: { w: 2, h: 1 }, // 2 tiles 2: { w: 16, h: 18 }, // 252 tiles 3: { w: 32, h: 34 }, // 972 tiles 4: { w: 62, h: 69 }, // 3816 tiles 5: { w: 6, h: 6 }, // 32 tiles 6: { w: 8, h: 8 }, // 64 tiles }; // 初始缩放级别(z2 = 显示 1/4 面积) const INIT_ZOOM = 2; // 地图世界边界(Leaflet CRS.Simple) const WORLD_BOUNDS = L.latLngBounds( [WORLD.minY, WORLD.minX], [WORLD.maxY, WORLD.maxX] ); // ============================================================ // MAP INIT (ImageOverlay - pixel coordinate system) // ============================================================ // Image: 5108w x 3807h pixels // Settlements: world coords (posX=76-783, posY=62-611) // Leaflet CRS.Simple: pixel coords directly (1 unit = 1 pixel) // Image y-axis DOWN, Leaflet y-axis UP → flip y // // Map bounds: [[south_lat, west_lng], [north_lat, east_lng]] // = [[0, 0], [3807, 5108]] in pixel coords // // Settlements: convert world → pixel // pixel_x = world_x * (5108/783) ≈ world_x * 6.52 // pixel_y = IMG_H - world_y * (3807/548) = 3807 - world_y * 6.95 const IMG_W = 5108, IMG_H = 3807; const PX_PER_WORLD_X = IMG_W / 783.0; // ≈ 6.52 const PX_PER_WORLD_Y = IMG_H / 548.0; // ≈ 6.95 function worldToPixel(worldX, worldY) { // Leaflet [lat=y, lng=x], image y=0 at top const px = worldX * PX_PER_WORLD_X; const py = IMG_H - worldY * PX_PER_WORLD_Y; return [py, px]; // [lat, lng] } // Map bounds in pixel coords (CRS.Simple: [lat=y, lng=x]) const MAP_BOUNDS = [[0, 0], [IMG_H, IMG_W]]; // [[south,west], [north,east]] // 地图配置 const map = L.map('map', { crs: L.CRS.Simple, minZoom: -1, maxZoom: 4, zoomSnap: 1, zoomDelta: 1, wheelPxPerZoomLevel: 100, attributionControl: false, maxBoundsViscosity: 1.0, zoomControl: true, }); // ImageOverlay 显示底图 const imageOverlay = L.imageOverlay('/map/map.jpg', [[0, 0], [IMG_H, IMG_W]], { opacity: 1.0, interactive: false, }).addTo(map); // 设置视图和边界 map.fitBounds([[0, 0], [IMG_H, IMG_W]]); map.setMaxBounds([[-50, -50], [IMG_H + 50, IMG_W + 50]]); // 加载状态 imageOverlay.on('loading', () => document.getElementById('loading').classList.add('show')); imageOverlay.on('load', () => document.getElementById('loading').classList.remove('show')); // ============================================================ // DATA // ============================================================ let settlements = []; let currentFilter = 'all'; let currentFav = new Set(JSON.parse(localStorage.getItem('xzg_fav') || '[]')); let currentSettlement = null; async function loadData() { try { const resp = await fetch('/settlements_data.json'); const data = await resp.json(); settlements = data.settlements || data; initMarkers(); updateFavCount(); } catch (e) { console.error('加载 settlements_data.json 失败:', e); } } // ============================================================ // MARKERS // ============================================================ const markers = {}; const TYPE_ICONS = { town: '🏰', castle: '🏛️', castle_village: '🏘️', village: '🏠', hideout: '⛺', other: '📍', }; const TYPE_COLORS = { town: '#FFD700', castle: '#C0C0C0', castle_village: '#BDB76B', village: '#9ACD32', hideout: '#FF6347', other: '#FFFFFF', }; function markerIcon(type, isFav) { const icon = TYPE_ICONS[type] || '📍'; const glow = isFav ? 'filter:drop-shadow(0 0 6px #FFD700);' : ''; return L.divIcon({ html: `
${icon}
`, className: 'settlement-marker', iconSize: [24, 24], iconAnchor: [12, 12], }); } function initMarkers() { settlements.forEach(s => { const isFav = currentFav.has(s.id); // Convert world coords to pixel coords for Leaflet const [lat, lng] = worldToPixel(s.posX, s.posY); const m = L.marker([lat, lng], { icon: markerIcon(s.type, isFav), }); m.settlementId = s.id; m.on('click', () => openTooltip(s)); markers[s.id] = m; }); applyFilter(); } function applyFilter() { Object.values(markers).forEach(m => { const s = settlements.find(x => x.id === m.settlementId); if (!s) return; const show = currentFilter === 'all' || s.type === currentFilter; show ? m.addTo(map) : m.remove(); }); } // ============================================================ // TOOLTIP // ============================================================ function openTooltip(s) { currentSettlement = s; document.getElementById('tooltip-name').textContent = s.name; document.getElementById('tooltip-name-cn').textContent = s.nameCN || '—'; document.getElementById('tooltip-type').textContent = (TYPE_ICONS[s.type] || '') + ' ' + (s.type || ''); document.getElementById('tt-kingdom').textContent = s.kingdom || '—'; const [lat, lng] = worldToPixel(s.posX, s.posY); document.getElementById('tt-coord').textContent = `${lat.toFixed(0)}, ${lng.toFixed(0)} (px)`; document.getElementById('tt-prosperity').textContent = s.prosperity != null ? `${s.prosperity} (金/回合)` : '—'; document.getElementById('tt-garrison').textContent = s.garrison != null ? `${s.garrison} 人` : '—'; const favBtn = document.getElementById('tt-fav-btn'); const isFav = currentFav.has(s.id); favBtn.classList.toggle('active', isFav); favBtn.textContent = isFav ? '⭐ 已收藏' : '⭐ 收藏'; const tooltip = document.getElementById('tooltip'); const mapEl = document.getElementById('map').getBoundingClientRect(); let left = Math.min(mapEl.left + mapEl.width * 0.5 + 10, window.innerWidth - 260); let top = Math.min(mapEl.top + mapEl.height * 0.5 + 10, window.innerHeight - 250); left = Math.max(10, left); top = Math.max(10, top); tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; tooltip.classList.add('show'); map.flyTo([s.posY, s.posX], Math.max(map.getZoom(), 2), { duration: 0.5 }); } function closeTooltip() { document.getElementById('tooltip').classList.remove('show'); currentSettlement = null; } map.on('click', e => { if (!e.originalEvent.target.closest('#tooltip')) closeTooltip(); }); // ============================================================ // FAVORITES // ============================================================ function toggleFavCurrent() { if (!currentSettlement) return; const id = currentSettlement.id; if (currentFav.has(id)) currentFav.delete(id); else currentFav.add(id); localStorage.setItem('xzg_fav', JSON.stringify([...currentFav])); updateFavCount(); const favBtn = document.getElementById('tt-fav-btn'); const isFav = currentFav.has(id); favBtn.classList.toggle('active', isFav); favBtn.textContent = isFav ? '⭐ 已收藏' : '⭐ 收藏'; if (markers[id]) markers[id].setIcon(markerIcon(currentSettlement.type, isFav)); } function toggleFavorites() { const panel = document.getElementById('favorites-panel'); const btn = document.getElementById('favorites-btn'); if (panel.classList.contains('show')) { panel.classList.remove('show'); btn.classList.remove('active'); } else { renderFavorites(); panel.classList.add('show'); btn.classList.add('active'); } } function renderFavorites() { const panel = document.getElementById('favorites-panel'); if (currentFav.size === 0) { panel.innerHTML = '
暂无收藏
'; return; } panel.innerHTML = [...currentFav].map(id => { const s = settlements.find(x => x.id === id); if (!s) return ''; return `
${TYPE_ICONS[s.type]||'📍'} ${s.name}
${s.nameCN || '—'}
${s.kingdom}
`; }).join(''); } function jumpTo(id) { const s = settlements.find(x => x.id === id); if (!s) return; closeTooltip(); setTimeout(() => { map.flyTo([s.posY, s.posX], Math.max(map.getZoom(), 2), { duration: 0.6 }); openTooltip(s); }, 100); } function updateFavCount() { document.getElementById('fav-count').textContent = currentFav.size; } // ============================================================ // FILTER // ============================================================ function setFilter(type) { currentFilter = type; document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.dataset.filter === type) ); applyFilter(); } // ============================================================ // SEARCH // ============================================================ const searchBox = document.getElementById('search-box'); const searchResults = document.getElementById('search-results'); searchBox.addEventListener('input', () => { const q = searchBox.value.trim().toLowerCase(); if (!q) { searchResults.classList.remove('show'); return; } const results = settlements.filter(s => s.name.toLowerCase().includes(q) || (s.nameCN || '').toLowerCase().includes(q) || (s.kingdom || '').toLowerCase().includes(q) ).slice(0, 20); if (!results.length) { searchResults.innerHTML = '
无结果
'; } else { searchResults.innerHTML = results.map(s => `
${TYPE_ICONS[s.type]||'📍'} ${s.name}
${s.nameCN || ''}
${s.kingdom} · ${s.type}
`).join(''); } searchResults.classList.add('show'); }); searchBox.addEventListener('blur', () => setTimeout(() => searchResults.classList.remove('show'), 200)); searchBox.addEventListener('keydown', e => { if (e.key === 'Escape') { searchResults.classList.remove('show'); searchBox.value = ''; closeTooltip(); } }); // ============================================================ // GAME LINK // ============================================================ function openInGame() { if (!currentSettlement) return; alert(`游戏内定位坐标:X=${currentSettlement.posX.toFixed(2)}, Y=${currentSettlement.posY.toFixed(2)}`); } // ============================================================ // COORD DISPLAY // ============================================================ map.on('mousemove', e => { document.getElementById('coord-display').textContent = `X: ${e.latlng.lng.toFixed(1)} Y: ${e.latlng.lat.toFixed(1)}`; }); // ============================================================ // BOOT // ============================================================ loadData();