image alt
Р Е Д А К Т О Р

МЕДИА

ПРАВИЛА

БАЗА ДАННЫХ

РЕГИСТРАЦИЯ

КРАФТ

СКАЧАТЬ

} } } 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>

VirtualWorld // Генератор Лабиринта

Ширина  :
Высота :
КнопкаКнопка