// ========================================================= // УПРАВЛЕНИЕ ВЕРСИЯМИ И CHANGELOG // ========================================================= let versionsList = []; let allChangelogs = []; let versionTagsMap = {}; let versionDatesMap = {}; let baseModsData = []; const versionFiles = { '1.60': 'combo/loadOrder.json', '1.59': 'combo/prevVer/1.59/loadOrder.json', }; // ========================================================= // ЗАГРУЗКА БАЗОВЫХ ДАННЫХ // ========================================================= function loadBaseMods(version, filePath) { const basePath = versionFiles[version] || versionFiles['1.60']; const baseDir = basePath.substring(0, basePath.lastIndexOf('/') + 1); const fullPath = baseDir + filePath; return fetch(fullPath) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { baseModsData = data; modsData = JSON.parse(JSON.stringify(data)); }) .catch(error => { throw error; }); } // ========================================================= // ЗАГРУЗКА ВЕРСИЙ // ========================================================= async function loadVersionsList(version, callback) { const basePath = versionFiles[version] || versionFiles['1.60']; const baseDir = basePath.substring(0, basePath.lastIndexOf('/') + 1); const versionsPath = baseDir + 'versions.json'; try { await loadBaseMods(version, 'loadOrder.json'); const response = await fetch(versionsPath); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); versionsList = data; allChangelogs = []; versionTagsMap = {}; versionDatesMap = {}; removeAllPriorityTags(); removeAllDates(); for (const versionData of versionsList) { await applyVersion(version, versionData); } renderLoadOrder(); updateSubVersionSelect(); if (versionsList.length > 0) { const lastVersion = versionsList[versionsList.length - 1]; await switchToVersion(lastVersion); } if (callback) callback(); } catch (error) { if (version !== '1.60') { loadVersionsList('1.60', callback); } } } // ========================================================= // ПРИМЕНЕНИЕ ВЕРСИИ // ========================================================= async function applyVersion(version, versionData) { const basePath = versionFiles[version] || versionFiles['1.60']; const baseDir = basePath.substring(0, basePath.lastIndexOf('/') + 1); let cleanPath = versionData.file; if (cleanPath.startsWith('./')) { cleanPath = cleanPath.substring(2); } const fullPath = baseDir + cleanPath; try { const response = await fetch(fullPath); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const fileVersion = data.version || versionData.version; const fileDate = data.date || new Date().toLocaleDateString(); // ========== 1. ОБРАБОТКА УДАЛЕНИЙ ========== if (data.changelog && data.changelog.removed) { const currentLang = localStorage.getItem('lang') || 'ru'; let removedItems = []; if (typeof data.changelog.removed === 'object' && !Array.isArray(data.changelog.removed)) { removedItems = data.changelog.removed[currentLang] || data.changelog.removed['ru'] || []; } else if (Array.isArray(data.changelog.removed)) { removedItems = data.changelog.removed; } removedItems = removedItems.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); if (removedItems.length > 0) { removedItems.forEach(removedName => { for (let i = modsData.length - 1; i >= 0; i--) { if (modsData[i].group) { if (modsData[i].items) { for (let j = modsData[i].items.length - 1; j >= 0; j--) { if (modsData[i].items[j].name === removedName || modsData[i].items[j].file === removedName) { modsData[i].items.splice(j, 1); } } if (modsData[i].items.length === 0) { modsData.splice(i, 1); } } } else { if (modsData[i].name === removedName || modsData[i].file === removedName) { modsData.splice(i, 1); } } } }); } } // ========== 2. ОБРАБОТКА ДОБАВЛЕНИЙ ========== if (data.changelog && data.changelog.added) { const currentLang = localStorage.getItem('lang') || 'ru'; let addedItems = []; if (typeof data.changelog.added === 'object' && !Array.isArray(data.changelog.added)) { addedItems = data.changelog.added[currentLang] || data.changelog.added['ru'] || []; } else if (Array.isArray(data.changelog.added)) { addedItems = data.changelog.added; } addedItems = addedItems.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); // ✅ ЧИТАЕМ ЗАДОМ НАПЕРЁД (снизу вверх) addedItems = addedItems.reverse(); if (addedItems.length > 0) { if (data.mods && Array.isArray(data.mods)) { addedItems.forEach(addedName => { let afterMod = null; let cleanName = addedName; const afterMatch = addedName.match(/:after:([^:]+)$/); if (afterMatch) { afterMod = afterMatch[1].trim(); cleanName = addedName.split(':after:')[0].trim(); } const modToAdd = data.mods.find(mod => mod.name === cleanName || mod.file === cleanName || mod.group === cleanName ); if (modToAdd) { if (afterMatch) { if (modToAdd.name) modToAdd.name = cleanName; if (modToAdd.group) modToAdd.group = cleanName; } const isGroup = !!modToAdd.group; const searchName = isGroup ? modToAdd.group : modToAdd.name; const searchFile = modToAdd.file; // Проверяем существование let exists = false; for (let i = 0; i < modsData.length; i++) { const currentIsGroup = !!modsData[i].group; if (isGroup && currentIsGroup) { if (modsData[i].group === searchName || modsData[i].name === searchName) { exists = true; break; } } if (!isGroup && !currentIsGroup) { if (modsData[i].file === searchFile || modsData[i].name === searchName) { exists = true; break; } } } // Если не нашли среди групп и модов - ищем внутри групп if (!exists) { for (let i = 0; i < modsData.length; i++) { if (modsData[i].group && modsData[i].items) { for (let j = 0; j < modsData[i].items.length; j++) { if (modsData[i].items[j].file === searchFile || modsData[i].items[j].name === searchName) { exists = true; break; } } } if (exists) break; } } if (exists) { // Обновляем существующий for (let i = 0; i < modsData.length; i++) { const currentIsGroup = !!modsData[i].group; if (isGroup && currentIsGroup) { if (modsData[i].group === searchName || modsData[i].name === searchName) { modsData[i] = JSON.parse(JSON.stringify(modToAdd)); break; } } if (!isGroup && !currentIsGroup) { if (modsData[i].file === searchFile || modsData[i].name === searchName) { modsData[i] = JSON.parse(JSON.stringify(modToAdd)); break; } } } // Если не обновили среди групп и модов - обновляем внутри групп if (!isGroup) { for (let i = 0; i < modsData.length; i++) { if (modsData[i].group && modsData[i].items) { for (let j = 0; j < modsData[i].items.length; j++) { if (modsData[i].items[j].file === searchFile || modsData[i].items[j].name === searchName) { modsData[i].items[j] = JSON.parse(JSON.stringify(modToAdd)); break; } } } } } } else { // Добавляем новый элемент (группу или мод) const modCopy = JSON.parse(JSON.stringify(modToAdd)); // Ищем индекс afterMod let afterIndex = -1; if (afterMod) { for (let i = 0; i < modsData.length; i++) { const item = modsData[i]; if (item.group) { if (item.group === afterMod || item.name === afterMod) { afterIndex = i; break; } if (item.items) { for (let j = 0; j < item.items.length; j++) { if (item.items[j].name === afterMod || item.items[j].file === afterMod) { afterIndex = i; break; } } } } else { if (item.name === afterMod || item.file === afterMod) { afterIndex = i; break; } } if (afterIndex !== -1) break; } } // Вставляем перед afterMod (индекс - 1) let newIndex; if (afterIndex !== -1) { newIndex = afterIndex; if (newIndex < 0) newIndex = 0; modCopy.index = newIndex; modsData.splice(newIndex, 0, modCopy); } else { newIndex = modsData.length; modCopy.index = newIndex; modsData.push(modCopy); } } } }); } } } // ========== 3. ОБРАБОТКА ОБНОВЛЕНИЙ ========== if (data.mods && Array.isArray(data.mods)) { data.mods.forEach(versionMod => { let found = false; const isGroup = !!versionMod.group; const searchName = isGroup ? versionMod.group : versionMod.name; const searchFile = versionMod.file; for (let i = 0; i < modsData.length; i++) { const currentIsGroup = !!modsData[i].group; if (isGroup && currentIsGroup) { if (modsData[i].group === searchName || modsData[i].name === searchName || modsData[i].group === versionMod.name) { modsData[i] = JSON.parse(JSON.stringify(versionMod)); found = true; break; } } if (!isGroup && !currentIsGroup) { if (modsData[i].file === searchFile || modsData[i].name === searchName) { modsData[i] = JSON.parse(JSON.stringify(versionMod)); found = true; break; } } } if (!found) { for (let i = 0; i < modsData.length; i++) { if (modsData[i].group && modsData[i].items) { for (let j = 0; j < modsData[i].items.length; j++) { if (modsData[i].items[j].file === searchFile || modsData[i].items[j].name === searchName) { modsData[i].items[j] = JSON.parse(JSON.stringify(versionMod)); found = true; break; } } } if (found) break; } } if (!found) { let alreadyAdded = false; for (let i = 0; i < modsData.length; i++) { if (modsData[i].group) { if (modsData[i].group === searchName || modsData[i].name === searchName) { alreadyAdded = true; break; } if (modsData[i].items) { for (let j = 0; j < modsData[i].items.length; j++) { if (modsData[i].items[j].file === searchFile || modsData[i].items[j].name === searchName) { alreadyAdded = true; break; } } } } else { if (modsData[i].file === searchFile || modsData[i].name === searchName) { alreadyAdded = true; break; } } if (alreadyAdded) break; } if (!alreadyAdded) { modsData.push(JSON.parse(JSON.stringify(versionMod))); } } }); } // ========== 4. ОБРАБОТКА ИЗМЕНЕНИЯ ПОРЯДКА ========== if (data.changelog && data.changelog.order_changed) { const currentLang = localStorage.getItem('lang') || 'ru'; let orderChanges = []; if (typeof data.changelog.order_changed === 'object' && !Array.isArray(data.changelog.order_changed)) { orderChanges = data.changelog.order_changed[currentLang] || data.changelog.order_changed['ru'] || []; } else if (Array.isArray(data.changelog.order_changed)) { orderChanges = data.changelog.order_changed; } orderChanges = orderChanges.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); // ✅ ЧИТАЕМ ЗАДОМ НАПЕРЁД (снизу вверх) orderChanges = orderChanges.reverse(); if (orderChanges.length > 0) { orderChanges.forEach(change => { const afterMatch = change.match(/:after:([^:]+)(?=:before:|$)/); const beforeMatch = change.match(/:before:([^:]+)$/); const itemName = change.split(':after:')[0].trim(); let afterMod = null; let beforeMod = null; if (afterMatch) { afterMod = afterMatch[1].trim(); } if (beforeMatch) { beforeMod = beforeMatch[1].trim(); } let itemIndex = -1; let afterIndex = -1; let beforeIndex = -1; for (let i = 0; i < modsData.length; i++) { if (modsData[i].group) { if (modsData[i].group === itemName || modsData[i].name === itemName) { itemIndex = i; } if (modsData[i].items) { for (let j = 0; j < modsData[i].items.length; j++) { if (modsData[i].items[j].name === itemName || modsData[i].items[j].file === itemName) { itemIndex = i; break; } } } if (afterMod && (modsData[i].group === afterMod || modsData[i].name === afterMod)) { afterIndex = i; } if (beforeMod && (modsData[i].group === beforeMod || modsData[i].name === beforeMod)) { beforeIndex = i; } } else { if (modsData[i].name === itemName || modsData[i].file === itemName) { itemIndex = i; } if (afterMod && (modsData[i].name === afterMod || modsData[i].file === afterMod)) { afterIndex = i; } if (beforeMod && (modsData[i].name === beforeMod || modsData[i].file === beforeMod)) { beforeIndex = i; } } } if (itemIndex !== -1 && (afterIndex !== -1 || beforeIndex !== -1)) { const item = modsData.splice(itemIndex, 1)[0]; if (itemIndex < afterIndex) afterIndex--; if (itemIndex < beforeIndex) beforeIndex--; let newIndex; if (afterIndex !== -1 && beforeIndex !== -1) { newIndex = afterIndex + 1; if (newIndex > beforeIndex) { newIndex = afterIndex + 1; } } else if (afterIndex !== -1) { newIndex = afterIndex + 1; } else if (beforeIndex !== -1) { newIndex = beforeIndex; } newIndex = newIndex - 1; if (newIndex > modsData.length) { modsData.push(item); } else { modsData.splice(newIndex, 0, item); } } }); } } // ========== 5. ДОБАВЛЕНИЕ ТЕГОВ И ДАТ ========== const addedTagsForVersion = []; const addedDatesForVersion = []; const isLoad = versionData.isLoad !== undefined ? versionData.isLoad : true; // Даты для всех модов из data.mods if (data.mods && Array.isArray(data.mods)) { data.mods.forEach(mod => { const modName = mod.name || mod.group; if (modName && modName.trim() !== '') { if (addDateToMod(modName, fileDate)) { addedDatesForVersion.push({ modName: modName, date: fileDate, type: 'unknown' }); } } }); } // ADDED if (data.changelog && data.changelog.added) { const currentLang = localStorage.getItem('lang') || 'ru'; let addedItems = []; if (typeof data.changelog.added === 'object' && !Array.isArray(data.changelog.added)) { addedItems = data.changelog.added[currentLang] || data.changelog.added['ru'] || []; } else if (Array.isArray(data.changelog.added)) { addedItems = data.changelog.added; } addedItems = addedItems.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); addedItems.forEach(addedName => { let cleanName = addedName; const afterMatch = addedName.match(/:after:([^:]+)$/); if (afterMatch) { cleanName = addedName.split(':after:')[0].trim(); } if (isLoad && addTagToMod(cleanName, '🆕')) { addedTagsForVersion.push({ modName: cleanName, tag: '🆕' }); } if (addDateToMod(cleanName, fileDate)) { const index = addedDatesForVersion.findIndex(d => d.modName === cleanName); if (index !== -1) { addedDatesForVersion.splice(index, 1); } addedDatesForVersion.push({ modName: cleanName, date: fileDate, type: 'added' }); } }); } // UPDATED if (data.changelog && data.changelog.updated) { const currentLang = localStorage.getItem('lang') || 'ru'; let updatedItems = []; if (typeof data.changelog.updated === 'object' && !Array.isArray(data.changelog.updated)) { updatedItems = data.changelog.updated[currentLang] || data.changelog.updated['ru'] || []; } else if (Array.isArray(data.changelog.updated)) { updatedItems = data.changelog.updated; } updatedItems = updatedItems.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); updatedItems.forEach(updatedName => { let cleanName = updatedName; const afterMatch = updatedName.match(/:after:([^:]+)$/); if (afterMatch) { cleanName = updatedName.split(':after:')[0].trim(); } if (isLoad && addTagToMod(cleanName, '🔄')) { addedTagsForVersion.push({ modName: cleanName, tag: '🔄' }); } if (addDateToMod(cleanName, fileDate)) { const index = addedDatesForVersion.findIndex(d => d.modName === cleanName); if (index !== -1) { addedDatesForVersion.splice(index, 1); } addedDatesForVersion.push({ modName: cleanName, date: fileDate, type: 'updated' }); } }); } // ORDER CHANGED if (data.changelog && data.changelog.order_changed) { const currentLang = localStorage.getItem('lang') || 'ru'; let orderItems = []; if (typeof data.changelog.order_changed === 'object' && !Array.isArray(data.changelog.order_changed)) { orderItems = data.changelog.order_changed[currentLang] || data.changelog.order_changed['ru'] || []; } else if (Array.isArray(data.changelog.order_changed)) { orderItems = data.changelog.order_changed; } orderItems = orderItems.filter(item => item && item.trim() !== '' && item.toLowerCase() !== 'nothing' && item.toLowerCase() !== 'ничего' ); orderItems.forEach(orderName => { let cleanName = orderName; cleanName = cleanName.split(':after:')[0].trim(); cleanName = cleanName.split(':before:')[0].trim(); if (isLoad && addTagToMod(cleanName, '📍')) { addedTagsForVersion.push({ modName: cleanName, tag: '📍' }); } if (addDateToMod(cleanName, fileDate)) { const index = addedDatesForVersion.findIndex(d => d.modName === cleanName); if (index !== -1) { addedDatesForVersion.splice(index, 1); } addedDatesForVersion.push({ modName: cleanName, date: fileDate, type: 'order_changed' }); } }); } versionTagsMap[fileVersion] = addedTagsForVersion; versionDatesMap[fileVersion] = addedDatesForVersion; // Сохраняем changelog if (data.changelog) { const cleanChangelog = cleanChangelogData(data.changelog); allChangelogs.push({ version: fileVersion, date: fileDate, changelog: cleanChangelog }); } else { allChangelogs.push({ version: fileVersion, date: fileDate, changelog: { added: { ru: [], en: [] }, updated: { ru: [], en: [] }, removed: { ru: [], en: [] }, order_changed: { ru: [], en: [] } } }); } } catch (error) { console.error('Error applying version:', error); } } // ========================================================= // ОЧИСТКА CHANGELOG // ========================================================= function cleanChangelogData(changelog) { const cleaned = {}; for (const [key, value] of Object.entries(changelog)) { if (typeof value === 'object' && !Array.isArray(value)) { cleaned[key] = {}; for (const [lang, items] of Object.entries(value)) { if (Array.isArray(items)) { cleaned[key][lang] = items.map(item => { if (typeof item === 'string') { let cleanItem = item.split(':after:')[0].trim(); cleanItem = cleanItem.split(':before:')[0].trim(); return cleanItem; } return item; }).filter(item => item && item.trim() !== ''); } else { cleaned[key][lang] = items; } } } else if (Array.isArray(value)) { cleaned[key] = value.map(item => { if (typeof item === 'string') { let cleanItem = item.split(':after:')[0].trim(); cleanItem = cleanItem.split(':before:')[0].trim(); return cleanItem; } return item; }).filter(item => item && item.trim() !== ''); } else { cleaned[key] = value; } } return cleaned; } // ========================================================= // РЕНДЕРИНГ CHANGELOG // ========================================================= function renderAllChangelogs() { const container = document.getElementById('changelogVersions'); if (!container) { return; } container.innerHTML = ''; if (!allChangelogs || allChangelogs.length === 0) { container.innerHTML = '