
МЕДИА
ПРАВИЛА
БАЗА ДАННЫХ
РЕГИСТРАЦИЯ
КРАФТ
СКАЧАТЬ
}
}
}
if (currentLocation.respawn && currentLocation.respawn.p && Array.isArray(currentLocation.respawn.p)) {
currentLocation.respawn.p[0] = (currentLocation.respawn.p[0] || 0) + offsetX;
currentLocation.respawn.p[1] = (currentLocation.respawn.p[1] || 0) + offsetY;
currentLocation.respawn.p[2] = (currentLocation.respawn.p[2] || 0) + offsetZ;
}
if (currentLocation) {
currentLocation.objects = currentObjects;
}
originalObjects = [...currentObjects];
isModified = false;
renderTable();
updateStatus(`✅ Сдвиг применен: X ${offsetX > 0 ? '+' : ''}${offsetX}, Y ${offsetY > 0 ? '+' : ''}${offsetY}, Z ${offsetZ > 0 ? '+' : ''}${offsetZ} (${movedCount} объектов сдвинуто)`, 'success');
offsetXInput.value = 0;
offsetYInput.value = 0;
offsetZInput.value = 0;
}
}
function getRegularObjects() {
return currentObjects.filter(obj => obj.n !== 'group');
}
// Функция для получения ключа сравнения дубликатов (БЕЗ УЧЕТА МАТЕРИАЛА)
function getDuplicateKey(obj) {
const key = {
name: obj.n || '',
position: JSON.stringify(obj.p || [0, 0, 0]),
rotation: obj.r !== undefined ? JSON.stringify(obj.r) : '',
size: obj.s !== undefined ? JSON.stringify(obj.s) : ''
// material НЕ учитываем!
};
return JSON.stringify(key);
}
function isDuplicate(obj, index, regularObjects) {
const key = getDuplicateKey(obj);
for (let i = 0; i < index; i++) {
if (getDuplicateKey(regularObjects[i]) === key) {
return true;
}
}
return false;
}
function getAllDuplicates() {
const regularObjects = getRegularObjects();
const dups = [];
for (let i = 0; i < regularObjects.length; i++) {
if (isDuplicate(regularObjects[i], i, regularObjects)) {
const originalIdx = currentObjects.findIndex(o => o === regularObjects[i]);
dups.push(originalIdx);
}
}
return dups;
}
function deleteAllDuplicates() {
const regularObjects = getRegularObjects();
const dupsIndices = [];
for (let i = 0; i < regularObjects.length; i++) {
if (isDuplicate(regularObjects[i], i, regularObjects)) {
const originalIdx = currentObjects.findIndex(o => o === regularObjects[i]);
dupsIndices.push(originalIdx);
}
}
if (dupsIndices.length === 0) {
updateStatus('ℹ️ Дубликатов не найдено', 'info');
return;
}
if (confirm(`Найдено ${dupsIndices.length} дубликатов. Удалить? Останется по одному экземпляру каждого уникального объекта.\n\nДубликаты определяются по: названию, координатам, повороту, размеру (материал НЕ учитывается).`)) {
dupsIndices.sort((a, b) => b - a).forEach(i => currentObjects.splice(i, 1));
if (currentLocation) currentLocation.objects = currentObjects;
originalObjects = [...currentObjects];
isModified = false;
renderTable();
updateStatus(`��️ Удалено ${dupsIndices.length} дубликатов`, 'warning');
}
}
function getSortKey(obj, field) {
if (field === 'name') return obj.n || '';
if (field === 'x') return obj.p?.[0] || 0;
if (field === 'y') return obj.p?.[1] || 0;
if (field === 'z') return obj.p?.[2] || 0;
return '';
}
function sortObjects(objects, field, order) {
if (!field) return [...objects];
return [...objects].sort((a, b) => {
let av = getSortKey(a, field);
let bv = getSortKey(b, field);
if (typeof av === 'number' && typeof bv === 'number') {
return order === 'asc' ? av - bv : bv - av;
}
av = String(av);
bv = String(bv);
return order === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
});
}
function filterObjects(objects, term) {
if (!term) return objects;
const t = term.toLowerCase();
return objects.filter(o => o.n && o.n.toLowerCase().includes(t));
}
function updateModifiedStatus() {
isModified = JSON.stringify(currentObjects) !== JSON.stringify(originalObjects);
const stats = document.getElementById('stats');
if (stats && isModified) {
if (!stats.querySelector('.modified-badge')) {
const badge = document.createElement('div');
badge.className = 'stat-badge modified-badge';
badge.style.backgroundColor = '#ff9800';
badge.innerHTML = '✏️ Есть изменения';
stats.appendChild(badge);
}
} else if (stats && !isModified) {
const badge = stats.querySelector('.modified-badge');
if (badge) badge.remove();
}
}
function openUrl(url) {
if (url && url.startsWith('http')) window.open(url, '_blank');
}
function scrollToTop() {
if (tableScrollContainer) {
tableScrollContainer.scrollTop = 0;
}
}
function scrollToBottom() {
if (tableScrollContainer) {
tableScrollContainer.scrollTop = tableScrollContainer.scrollHeight;
}
}
function updateContainerHeight() {
if (tableScrollContainer) {
const objectsSection = document.querySelector('.objects-section');
if (objectsSection) {
const headerHeight = document.querySelector('.objects-header')?.clientHeight || 60;
const statsHeight = document.getElementById('stats')?.clientHeight || 50;
const availableHeight = objectsSection.clientHeight - headerHeight - statsHeight - 20;
if (availableHeight > 100) {
tableScrollContainer.style.height = availableHeight + 'px';
} else {
tableScrollContainer.style.height = '300px';
}
}
}
}
function renderTable() {
if (!currentObjects) return;
const objectsWithoutGroups = currentObjects.filter(obj => obj.n !== 'group');
const filtered = filterObjects(objectsWithoutGroups, searchTerm);
const sorted = sortObjects(filtered, currentSort.field, currentSort.order);
const tbody = document.getElementById('objectsTableBody');
tbody.innerHTML = '';
if (sorted.length === 0) {
const row = tbody.insertRow();
const cell = row.insertCell(0);
cell.colSpan = 10;
cell.className = 'empty-state';
cell.textContent = searchTerm ? '❌ Ничего не найдено' : '�� Нет объектов';
updateStatsDisplay();
return;
}
const fragment = document.createDocumentFragment();
const regularObjectsList = getRegularObjects();
sorted.forEach((obj, idx) => {
const isSpecial = specialTypes.includes(obj.n);
const objIndexInRegular = regularObjectsList.findIndex(o => o === obj);
const dup = objIndexInRegular !== -1 ? isDuplicate(obj, objIndexInRegular, regularObjectsList) : false;
const row = document.createElement('tr');
if (dup) row.classList.add('duplicate');
const nameCell = document.createElement('td');
if (isSpecial) {
const span = document.createElement('span');
let cls = '';
if (obj.n === 'teleport') cls = 'type-teleport';
else if (obj.n === 'trigger') cls = 'type-trigger';
else if (obj.n === 'Photo') cls = 'type-photo';
else if (obj.n === 'Rotator') cls = 'type-rotator';
else if (obj.n === 'TextS') cls = 'type-text';
span.className = `object-type ${cls}`;
span.textContent = obj.n;
nameCell.appendChild(span);
} else {
nameCell.textContent = obj.n || '-';
}
row.appendChild(nameCell);
const xCell = document.createElement('td');
xCell.textContent = obj.p ? obj.p[0]?.toFixed(2) : '-';
row.appendChild(xCell);
const yCell = document.createElement('td');
yCell.textContent = obj.p ? obj.p[1]?.toFixed(2) : '-';
row.appendChild(yCell);
const zCell = document.createElement('td');
zCell.textContent = obj.p ? obj.p[2]?.toFixed(2) : '-';
row.appendChild(zCell);
const rotCell = document.createElement('td');
if (obj.r !== undefined) rotCell.textContent = Array.isArray(obj.r) ? obj.r.map(v => v.toFixed(1)).join(', ') : obj.r.toFixed(1);
else rotCell.textContent = '-';
row.appendChild(rotCell);
const sizeCell = document.createElement('td');
if (obj.s !== undefined) sizeCell.textContent = Array.isArray(obj.s) ? obj.s.map(v => v.toFixed(2)).join(', ') : obj.s.toFixed(2);
else sizeCell.textContent = '-';
row.appendChild(sizeCell);
const colorCell = document.createElement('td');
colorCell.textContent = obj.c ? (Array.isArray(obj.c) ? obj.c.map(v => v.toFixed(2)).join(', ') : obj.c) : '-';
row.appendChild(colorCell);
const matCell = document.createElement('td');
matCell.textContent = obj.m || '-';
row.appendChild(matCell);
const specCell = document.createElement('td');
const params = getSpecialParams(obj);
const urlParam = params.find(p => p.startsWith('url:'));
const display = params.filter(p => !p.startsWith('url:'));
if (display.length > 0) {
specCell.className = 'special-params';
specCell.title = display.join(' | ');
if (urlParam && obj.n === 'Photo') {
const url = urlParam.substring(4);
const link = document.createElement('a');
link.href = '#';
link.textContent = display.join(' | ');
link.className = 'photo-link';
link.onclick = (e) => { e.preventDefault(); openUrl(url); };
specCell.appendChild(link);
} else {
specCell.textContent = display.join(' | ');
}
} else {
specCell.textContent = '-';
}
row.appendChild(specCell);
const actionCell = document.createElement('td');
const delBtn = document.createElement('button');
delBtn.textContent = '��️';
delBtn.className = 'delete-btn';
delBtn.title = 'Удалить';
delBtn.onclick = () => deleteObject(currentObjects.findIndex(o => o === obj));
actionCell.appendChild(delBtn);
row.appendChild(actionCell);
fragment.appendChild(row);
});
tbody.appendChild(fragment);
updateSortIndicators();
updateStatsDisplay();
updateModifiedStatus();
setTimeout(updateContainerHeight, 50);
}
function updateStatsDisplay() {
const stats = document.getElementById('stats');
const regularObjects = getRegularObjects();
const dups = regularObjects.filter((o, i) => isDuplicate(o, i, regularObjects)).length;
const total = regularObjects.length;
const filtered = filterObjects(regularObjects, searchTerm).length;
const deleted = originalObjects.filter(o => o.n !== 'group').length - total;
stats.innerHTML = `
<div class="stats-info">
<div class="stat-badge">�� Всего: ${total}</div>
<div class="stat-badge ${dups > 0 ? 'warning-badge' : ''}">⚠️ Дублей: ${dups}</div>
${deleted > 0 ? `<div class="stat-badge" style="background-color:#f44336;">��️ Удалено: ${deleted}</div>` : ''}
${searchTerm ? `<div class="stat-badge success-badge">�� Найдено: ${filtered}</div>` : ''}
</div>
<div class="stats-info">
<div class="stat-badge">�� Сортировка: ${currentSort.field ? `${currentSort.field.toUpperCase()} ${currentSort.order === 'asc' ? '↑' : '↓'}` : 'нет'}</div>
</div>
`;
}
function deleteObject(idx) {
const obj = currentObjects[idx];
if (!obj) return;
const name = obj.n || 'объект';
if (confirm(`Удалить "${name}"?`)) {
for (const group of currentObjects) {
if (group.n === 'group' && group.objects) {
const pos = group.objects.findIndex(g => g === obj);
if (pos !== -1) {
group.objects.splice(pos, 1);
}
}
}
currentObjects.splice(idx, 1);
const emptyGroups = [];
for (let i = 0; i < currentObjects.length; i++) {
if (currentObjects[i].n === 'group' && (!currentObjects[i].objects || currentObjects[i].objects.length === 0)) {
emptyGroups.push(i);
}
}
emptyGroups.reverse().forEach(i => currentObjects.splice(i, 1));
if (currentLocation) currentLocation.objects = currentObjects;
originalObjects = [...currentObjects];
isModified = false;
renderTable();
updateStatus(`��️ Удален: ${name}`, 'warning');
}
}
function updateStatus(msg, type = 'info') {
statusBar.innerHTML = msg;
statusBar.style.display = 'block';
statusBar.style.backgroundColor = type === 'warning' ? '#d32f2f' : type === 'success' ? '#4caf50' : '#2d2d2d';
setTimeout(() => {
if (isModified) statusBar.innerHTML = `✏️ Есть изменения | ${new Date().toLocaleTimeString()}`;
else statusBar.innerHTML = `✅ Готов | ${new Date().toLocaleTimeString()}`;
statusBar.style.backgroundColor = '#2d2d2d';
}, 2500);
}
function updateLocationPanel() {
if (!currentLocation) return;
const div = document.getElementById('locationParams');
div.innerHTML = '';
const items = [
{ label: '�� Точка входа', value: currentLocation.respawn ? `[${currentLocation.respawn.p?.map(v => v.toFixed(2)).join(', ')}]${currentLocation.respawn.r !== undefined ? `, поворот: ${currentLocation.respawn.r.toFixed(1)}°` : ''}` : '-' },
{ label: '��️ Погода', value: currentLocation.weather || '-' },
{ label: '�� Уровень воды', value: currentLocation.oceanlevel?.toFixed(2) || '-' },
{ label: '�� Ambient', value: currentLocation.ambient ? `[${currentLocation.ambient.map(v => v.toFixed(2)).join(', ')}]` : '-' }
];
items.forEach(item => {
const p = document.createElement('div');
p.className = 'param-item';
p.innerHTML = `<span class="param-label">${item.label}:</span> <span class="param-value">${item.value}</span>`;
div.appendChild(p);
});
}
function saveFile() {
if (!currentLocation) return;
currentLocation.objects = currentObjects;
// Компактный формат (без отступов) по умолчанию
const json = JSON.stringify(currentLocation);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
let name = fileName;
if (!name.endsWith('.world')) name = 'location_modified.world';
else if (!name.includes('_modified')) name = name.replace(/\.world$/, '_modified.world');
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
originalObjects = [...currentObjects];
isModified = false;
updateStatus(`�� Сохранен: ${name}`, 'success');
}
function resetFilters() {
searchTerm = '';
currentSort = { field: null, order: 'asc' };
searchInput.value = '';
renderTable();
updateStatus('�� Фильтры сброшены', 'info');
}
function loadFile(file) {
if (!file.name.endsWith('.world')) {
alert('Пожалуйста, выберите файл с расширением .world');
return;
}
showLoader(true);
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
if (!data.respawn || !data.objects) throw new Error('Неверный формат файла .world');
currentLocation = data;
currentObjects = [...data.objects];
originalObjects = [...data.objects];
fileName = file.name;
isModified = false;
updateLocationPanel();
renderTable();
locationPanel.style.display = 'block';
objectsSection.style.display = 'block';
fileInfo.innerHTML = `�� ${file.name} (${(file.size / 1024).toFixed(2)} KB) | Объектов: ${currentObjects.filter(o => o.n !== 'group').length}`;
updateStatus(`✅ Загружен: ${file.name}`, 'success');
} catch (err) {
alert('Ошибка: ' + err.message);
} finally {
showLoader(false);
}
};
reader.onerror = function() {
alert('Ошибка чтения файла');
showLoader(false);
};
reader.readAsText(file);
}
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length && files[0].name.endsWith('.world')) loadFile(files[0]);
else alert('Пожалуйста, выберите файл с расширением .world');
});
dropZone.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => { if (e.target.files.length) loadFile(e.target.files[0]); });
document.getElementById('tableHeader').addEventListener('click', (e) => {
const th = e.target.closest('th');
if (!th) return;
const field = th.getAttribute('data-sort');
if (!field) return;
if (currentSort.field === field) currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
else { currentSort.field = field; currentSort.order = 'asc'; }
renderTable();
});
searchInput.addEventListener('input', (e) => { searchTerm = e.target.value; renderTable(); });
resetBtn.addEventListener('click', resetFilters);
deleteDuplicatesBtn.addEventListener('click', deleteAllDuplicates);
downloadBtn.addEventListener('click', saveFile);
scrollToTopBtn.addEventListener('click', scrollToTop);
scrollToBottomBtn.addEventListener('click', scrollToBottom);
applyOffsetBtn.addEventListener('click', applyOffset);
window.addEventListener('resize', () => {
setTimeout(updateContainerHeight, 100);
});
updateStatus('✨ Готов к работе', 'info');
setTimeout(() => {
updateContainerHeight();
}, 100);
</script>
</body</html>