筛选:
🗺️ 瓦片加载中…
// ============================================================
// 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();