(() => {
'use strict';
const API = ''; // same origin (root -> launcher server)
const I18N = {
ru: {
'nav.server': 'Сервер',
'nav.highlights': 'Хайлайты',
'nav.season': 'Сезон',
'nav.features': 'Возможности',
'nav.download': 'Скачать',
'nav.news': 'Новости',
'nav.play': 'Играть сейчас',
'hero.kicker': 'КАСТОМНЫЙ MINECRAFT ЛАУНЧЕР',
'hero.title': 'Очнись от
холода.',
'hero.sub': 'Zern оживляет твои любимые модпаки. Один лаунчер, любой загрузчик, без лишних хлопот — создан для сервера ZernMC и не только.',
'hero.download': 'Скачать лаунчер',
'hero.explore': 'Обзор',
'hero.scroll': 'Листай вниз',
'server.title': 'Сервер жив.',
'server.sub': 'ZernMC — это кастомный Minecraft-сервер со своим лором, самописными плагинами и живым комьюнити. Вот что происходит прямо сейчас.',
'server.status': 'Статус',
'server.checking': 'Проверяем…',
'server.players': 'Игроков онлайн',
'server.mode': 'Режим',
'server.season': 'Сезон Prologue',
'server.online': 'Онлайн',
'server.offline': 'Оффлайн',
'server.unavailable': 'Недоступно',
'highlights.title': 'Хайлайты сервера.',
'highlights.sub': 'Моменты, запечатлённые на сервере ZernMC — войны, постройки и хаос.',
'carousel.prev': 'Назад',
'carousel.next': 'Вперёд',
'carousel.cap1': 'Zern',
'carousel.cap3': 'Это честно не я',
'carousel.cap5': 'Спавн ТЦ (старое)',
'carousel.cap6': 'Спавн склад (старое)',
'carousel.alt': 'Хайлайт Zern',
'features.title': 'Всё, что тебе нужно.',
'features.sub': 'Лаунчер, который делает всю тяжёлую работу, чтобы ты просто играл.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Все основные модлоадеры подхватываются автоматически. Библиотеки предзагружаются и проверяются, так что установка работает — даже на медленном интернете.',
'features.ui.title': 'Тёмный, быстрый нативный UI',
'features.ui.sub': 'Лёгкий интерфейс на JavaFX с чистым тёмным оформлением. Быстро запускается, мгновенно реагирует и не мешает.',
'features.proxy.title': 'Умные зеркала и прокси',
'features.proxy.sub': 'Загрузки идут через быстрые зеркала с автоматическим фолбэком. Заблокировали или зарезали скорость? Прокси подхватит — ты больше не зависнешь на 80%.',
'features.news.title': 'Новости и статус сервера',
'features.news.sub': 'Чейнджлоги, ивенты и обновления приходят прямо в лаунчер. Ты всегда знаешь, что нового, ещё до нажатия «Играть».',
'features.pack.title': 'Установка сборки в один клик',
'features.pack.sub': 'Выбери сборку и жми «Установить». Манифесты версий, ванильные ассеты и библиотеки подтягиваются и проверяются автоматически. Просто.',
'theme.title': 'Что внутри сезона.',
'theme.sub': 'Prologue — модовый сезон на базе Create: Aeronautics, война и выживание. ОБТ — полигон, где всё это обкатывается.',
'theme.zarya.tag': 'ОБТ · 1.20.1 Forge',
'theme.zarya.title': 'Война в Заре',
'theme.zarya.sub': 'Двухдневный ивент в городе Заря: группировка «Феникс» атакует, «Ласточка» обороняется, а жители выбирают сторону — или поднимают бунт. Военные правила.',
'theme.obt.tag': 'ОБТ · тестовая стадия',
'theme.obt.title': 'Открытый бета-сервер',
'theme.obt.sub': 'Бета — это ивентовый сервер: городские бои, сценарий атомного реактора и правила, которые лягут в основу настоящего сезона.',
'theme.air.tag': 'Сезон · 1.21.1 NeoForge',
'theme.air.title': 'Корабли в небе',
'theme.air.sub': 'Create: Aeronautics даёт дирижабли, субмарины и летающие механизмы. Поезда, дизели, большие пушки и атомный реактор — осваивай.',
'theme.year.tag': 'Сезон · Prologue',
'theme.year.title': 'Марафон на год',
'theme.year.sub': 'Сезон — это марафон, а не спринт. Начни эту главу с первого дня и строй её вместе с комьюнити.',
'theme.lore.tag': 'Сервер',
'theme.lore.title': 'Свой лор и плагины',
'theme.lore.sub': 'Самописные плагины, ивенты под историю сервера и системы, которые из коробки работают с лаунчером.',
'theme.community.tag': 'Комьюнити',
'theme.community.title': 'Голос и друзья',
'theme.community.sub': 'Simple Voice Chat в игре, система друзей в лаунчере и Telegram-комьюнити, которое реально рулит — решения принимаются голосованием игроков.',
'download.title': 'Скачать Zern.',
'download.sub': 'Скачай лаунчер для Windows и заходи на сервер ZernMC.',
'download.latest': 'Скачать актуальную версию',
'download.mirrors': 'Зеркала',
'download.note': 'Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.',
'news.title': 'Что нового.',
'news.sub': 'Последние чейнджлоги и обновления команды Zern.',
'news.empty': 'Новостей пока нет.',
'news.error': 'Новости временно недоступны.',
'news.update': 'Обновление',
'footer.note': 'Не аффилирован с Mojang или Microsoft. Minecraft — торговая марка Mojang AB.'
},
en: {
'nav.server': 'Server',
'nav.highlights': 'Highlights',
'nav.season': 'Season',
'nav.features': 'Features',
'nav.download': 'Download',
'nav.news': 'News',
'nav.play': 'Play now',
'hero.kicker': 'CUSTOM MINECRAFT LAUNCHER',
'hero.title': 'Awaken from
the cold.',
'hero.sub': 'Zern brings your favourite modpacks to life. One launcher, every loader, no hassle — built for the ZernMC server and beyond.',
'hero.download': 'Download launcher',
'hero.explore': 'Explore',
'hero.scroll': 'Scroll to explore',
'server.title': 'The server is alive.',
'server.sub': 'ZernMC is a custom Minecraft server with its own lore, hand-written plugins and a living community. This is what\'s happening right now.',
'server.status': 'Status',
'server.checking': 'Checking…',
'server.players': 'Players online',
'server.mode': 'Mode',
'server.season': 'Season Prologue',
'server.online': 'Online',
'server.offline': 'Offline',
'server.unavailable': 'Unavailable',
'highlights.title': 'Server highlights.',
'highlights.sub': 'Moments captured on the ZernMC server — wars, builds and chaos.',
'carousel.prev': 'Previous',
'carousel.next': 'Next',
'carousel.cap1': 'Zern',
'carousel.cap3': 'This is honestly not me',
'carousel.cap5': 'Spawn mall (old)',
'carousel.cap6': 'Spawn storage (old)',
'carousel.alt': 'Zern highlight',
'features.title': 'Everything you need.',
'features.sub': 'A launcher that does the heavy lifting, so you can just play.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Every major mod loader handled automatically. Libraries are prefetched and verified, so installs just work — even on slow networks.',
'features.ui.title': 'Dark, fast, native UI',
'features.ui.sub': 'A lightweight JavaFX interface with a clean dark theme. Starts quick, responds instantly, and stays out of your way.',
'features.proxy.title': 'Smart mirrors & proxy',
'features.proxy.sub': 'Downloads route through fast mirrors with automatic failover. Blocked or throttled? The proxy takes over so you never stall at 80%.',
'features.news.title': 'News & server status',
'features.news.sub': 'Changelogs, events and updates stream straight into the launcher. You always know what\'s new before you hit play.',
'features.pack.title': 'One-click pack install',
'features.pack.sub': 'Pick a pack, hit install. Version manifests, vanilla assets and libraries are pulled and verified automatically. Simple.',
'theme.title': 'What\'s inside the season.',
'theme.sub': 'Prologue — a modded season built around Create: Aeronautics, war and survival. OBT is the testing ground for the whole idea.',
'theme.zarya.tag': 'OBT · 1.20.1 Forge',
'theme.zarya.title': 'War in Zarya',
'theme.zarya.sub': 'Two-day event set in the city of Zarya: the Fenix group attacks, Lastochka defends, and civilians pick a side — or rise up. Military rules apply.',
'theme.obt.tag': 'OBT · test stage',
'theme.obt.title': 'Open beta server',
'theme.obt.sub': 'The beta is an event server: city battles, an atomic-reactor city-life scenario and rule sets that will shape the real season.',
'theme.air.tag': 'Season · 1.21.1 NeoForge',
'theme.air.title': 'Ships in the sky',
'theme.air.sub': 'Create: Aeronautics gives you airships, submarines and flying contraptions. Trains, diesel engines, big cannons and a nuclear reactor to master.',
'theme.year.tag': 'Season · Prologue',
'theme.year.title': 'A year-long run',
'theme.year.sub': 'Each season is a marathon, not a sprint. Start this chapter from day one and shape it with the community.',
'theme.lore.tag': 'Server',
'theme.lore.title': 'Own lore & plugins',
'theme.lore.sub': 'Hand-written plugins, events written for the server story, and systems the launcher integrates with out of the box.',
'theme.community.tag': 'Community',
'theme.community.title': 'Voice & friends',
'theme.community.sub': 'Simple Voice Chat in-game, a friends system in the launcher and a Telegram community that runs the show — decisions are voted on by players.',
'download.title': 'Get Zern.',
'download.sub': 'Download the launcher for Windows and join the ZernMC server.',
'download.latest': 'Download latest',
'download.mirrors': 'Mirrors',
'download.note': 'No paywalls, no ads. Just a launcher. System requirements: Windows 10+, 4 GB RAM.',
'news.title': 'What\'s new.',
'news.sub': 'Latest changelogs and updates from the Zern team.',
'news.empty': 'No news yet.',
'news.error': 'News is temporarily unavailable.',
'news.update': 'Update',
'footer.note': 'Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.'
}
};
const STORAGE_KEY = 'zern_lang';
function getLang() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'ru' || saved === 'en') return saved;
} catch (e) { /* ignore */ }
return 'ru'; // default: Russian
}
let currentLang = getLang();
function tr(key) {
const dict = I18N[currentLang] || I18N.ru;
return dict[key] != null ? dict[key] : key;
}
function applyI18n() {
document.documentElement.lang = currentLang;
const toggle = document.getElementById('langToggleLabel');
if (toggle) toggle.textContent = currentLang === 'ru' ? 'EN' : 'RU';
document.querySelectorAll('[data-i18n]').forEach((el) => {
el.textContent = tr(el.dataset.i18n);
});
document.querySelectorAll('[data-i18n-html]').forEach((el) => {
el.innerHTML = tr(el.dataset.i18nHtml);
});
document.querySelectorAll('[data-i18n-aria]').forEach((el) => {
el.setAttribute('aria-label', tr(el.dataset.i18nAria));
});
// Carousel captions & alts are created dynamically with tr() at init
document.querySelectorAll('.carousel-caption[data-caption-key]').forEach((el) => {
el.textContent = tr(el.dataset.captionKey);
});
document.querySelectorAll('.carousel-slide img').forEach((img) => {
const slide = img.closest('.carousel-slide');
const cap = slide && slide.querySelector('.carousel-caption[data-caption-key]');
if (cap) img.alt = tr(cap.dataset.captionKey);
else img.alt = tr('carousel.alt');
});
// Live-update dynamic strings
const statusValue = document.getElementById('srv-status');
if (statusValue && statusValue.dataset.state) {
updateStatusUI(statusValue.dataset.state);
}
}
function toggleLang() {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
try { localStorage.setItem(STORAGE_KEY, currentLang); } catch (e) { /* ignore */ }
applyI18n();
}
function fmtSize(bytes) {
if (!bytes) return '—';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
let n = bytes;
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
return n.toFixed(1) + ' ' + units[i];
}
async function loadLauncherInfo() {
try {
const res = await fetch(API + '/launcher/info');
if (!res.ok) return;
const data = await res.json();
const version = data.current_version || '—';
const heroVersion = document.getElementById('hero-version');
const heroSize = document.getElementById('hero-size');
const dlVersion = document.getElementById('download-version');
const dlSize = document.getElementById('download-size');
const dlBtn = document.getElementById('download-btn');
if (heroVersion) heroVersion.textContent = version;
if (dlVersion) dlVersion.textContent = version;
if (data.files && data.files.zips && data.files.zips.length) {
const latest = data.files.zips[0];
if (heroSize) heroSize.textContent = fmtSize(latest.size);
if (dlSize) dlSize.textContent = fmtSize(latest.size);
}
if (dlBtn && data.new_format && data.new_format.download_url) {
dlBtn.href = API + data.new_format.download_url;
}
} catch (e) {
console.warn('[site] failed to load launcher info', e);
}
}
async function loadMirrors() {
try {
const res = await fetch(API + '/launcher/mirrors');
if (!res.ok) return;
const data = await res.json();
const container = document.getElementById('mirrors');
if (!container || !data.mirrors || !data.mirrors.length) return;
const tags = data.mirrors.slice(0, 5).map((m) => {
const a = document.createElement('a');
a.className = 'mirror-tag';
a.href = (m.url || '') + '/launcher/download/latest';
a.textContent = m.name || m;
return a;
});
tags.forEach((t) => container.appendChild(t));
} catch (e) {
console.warn('[site] failed to load mirrors', e);
}
}
async function loadServerStatus() {
const statusEl = document.getElementById('srv-status');
const playersEl = document.getElementById('srv-players');
const versionEl = document.getElementById('srv-version');
if (!statusEl) return;
try {
const res = await fetch(API + '/api/status');
const data = await res.json();
if (data.online) {
statusEl.dataset.state = 'online';
if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`;
if (versionEl && data.version) versionEl.textContent = data.version;
} else {
statusEl.dataset.state = 'offline';
if (playersEl) playersEl.textContent = '— / —';
}
} catch (e) {
statusEl.dataset.state = 'unavailable';
if (playersEl) playersEl.textContent = '— / —';
}
updateStatusUI(statusEl.dataset.state);
}
function updateStatusUI(state) {
const statusEl = document.getElementById('srv-status');
const dot = document.querySelector('#srv-status .status-dot');
if (!statusEl) return;
if (state === 'online') {
if (dot) dot.className = 'status-dot status-dot--online';
statusEl.innerHTML = '' + tr('server.online') + '';
} else if (state === 'offline') {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '' + tr('server.offline') + '';
} else {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '' + tr('server.unavailable') + '';
}
}
const HIGHLIGHTS = [
{ file: '1.jpg', type: 'image', captionKey: 'carousel.cap1' },
{ file: '2.mp4', type: 'video', captionKey: null },
{ file: '3.jpg', type: 'image', captionKey: 'carousel.cap3' },
{ file: '4.mp4', type: 'video', captionKey: null },
{ file: '5.jpg', type: 'image', captionKey: 'carousel.cap5' },
{ file: '6.jpg', type: 'image', captionKey: 'carousel.cap6' },
{ file: '7.jpg', type: 'image', captionKey: null },
{ file: '8.jpg', type: 'image', captionKey: null },
{ file: '9.jpg', type: 'image', captionKey: null },
{ file: '10.jpg', type: 'image', captionKey: null },
{ file: '11.jpg', type: 'image', captionKey: null },
{ file: '12.jpg', type: 'image', captionKey: null },
{ file: '13.jpg', type: 'image', captionKey: null },
{ file: '14.jpg', type: 'image', captionKey: null },
{ file: '15.jpg', type: 'image', captionKey: null },
];
function initCarousel() {
const track = document.getElementById('carousel');
const dotsWrap = document.getElementById('carouselDots');
const prev = document.getElementById('carouselPrev');
const next = document.getElementById('carouselNext');
if (!track) return;
HIGHLIGHTS.forEach((h, i) => {
const slide = document.createElement('div');
slide.className = 'carousel-slide';
if (h.type === 'video') {
const vid = document.createElement('video');
vid.src = '/highlights/' + h.file;
vid.controls = true;
vid.autoplay = true;
vid.loop = true;
vid.muted = true;
vid.playsInline = true;
slide.appendChild(vid);
} else {
const img = document.createElement('img');
img.src = '/highlights/' + h.file;
img.alt = h.captionKey ? tr(h.captionKey) : tr('carousel.alt');
img.loading = 'lazy';
slide.appendChild(img);
}
if (h.captionKey) {
const cap = document.createElement('span');
cap.className = 'carousel-caption';
cap.dataset.captionKey = h.captionKey;
cap.textContent = tr(h.captionKey);
slide.appendChild(cap);
}
track.appendChild(slide);
const dot = document.createElement('span');
dot.className = 'carousel-dot' + (i === 0 ? ' active' : '');
dot.addEventListener('click', () => goto(i));
dotsWrap.appendChild(dot);
});
let index = 0;
let timer = null;
function goto(i) {
index = (i + HIGHLIGHTS.length) % HIGHLIGHTS.length;
track.style.transform = 'translateX(-' + index * 100 + '%)';
dotsWrap.querySelectorAll('.carousel-dot').forEach((d, k) => {
d.classList.toggle('active', k === index);
});
}
function nextSlide() { goto(index + 1); }
function prevSlide() { goto(index - 1); }
function start() {
stop();
timer = setInterval(nextSlide, 6500);
}
function stop() {
if (timer) { clearInterval(timer); timer = null; }
}
if (prev) prev.addEventListener('click', () => { prevSlide(); start(); });
if (next) next.addEventListener('click', () => { nextSlide(); start(); });
track.addEventListener('mouseenter', stop);
track.addEventListener('mouseleave', start);
start();
}
function stripMarkup(text) {
if (!text) return '';
return text
.replace(/\[[^\]]*photo=[^\]]*\]/g, '')
.replace(/\[[^\]]*\]/g, '')
.replace(/\*\*\*/g, '')
.replace(/\*\*/g, '')
.replace(/[*_~`#]/g, '')
.replace(/\n+/g, ' ')
.trim();
}
async function loadNews() {
const grid = document.getElementById('news-grid');
if (!grid) return;
try {
const res = await fetch(API + '/news');
if (!res.ok) throw new Error('news fetch failed');
const data = await res.json();
const news = (data.news || []).slice(0, 6);
if (!news.length) { grid.innerHTML = '
' + tr('news.empty') + '
'; return; } grid.innerHTML = ''; news.forEach((n) => { const card = document.createElement('article'); card.className = 'news-card'; const tags = document.createElement('div'); tags.className = 'news-tags'; if (n.type) { const t = document.createElement('span'); t.className = 'news-tag'; t.textContent = n.type; tags.appendChild(t); } if (n.version) { const v = document.createElement('span'); v.className = 'news-tag news-tag--version'; v.textContent = n.version; tags.appendChild(v); } const title = document.createElement('h3'); title.textContent = n.title || tr('news.update'); const body = document.createElement('p'); const text = stripMarkup(n.body); body.textContent = text.length > 220 ? text.slice(0, 220) + '…' : text; card.appendChild(tags); card.appendChild(title); card.appendChild(body); grid.appendChild(card); }); } catch (e) { console.warn('[site] failed to load news', e); grid.innerHTML = '' + tr('news.error') + '
'; } } function initNav() { const nav = document.getElementById('nav'); const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 24); window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); } function initParallax() { const els = document.querySelectorAll('[data-parallax]'); if (!els.length) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const onScroll = () => { const y = window.scrollY; if (y > window.innerHeight) return; els.forEach((el) => { const speed = parseFloat(el.dataset.parallax); el.style.transform = 'translateY(' + (y * speed) + 'px)'; }); }; window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); } function initReveal() { const els = document.querySelectorAll('.section-head, .feature-card, .download-card, .news-card, .server-card, .theme-card, .carousel'); if (!('IntersectionObserver' in window)) { els.forEach((el) => el.classList.add('visible')); return; } const io = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('visible'); io.unobserve(entry.target); } }); }, { threshold: 0.12 }); els.forEach((el) => { el.classList.add('reveal'); io.observe(el); }); } function initYear() { const el = document.getElementById('year'); if (el) el.textContent = new Date().getFullYear(); } document.addEventListener('DOMContentLoaded', () => { applyI18n(); const toggle = document.getElementById('langToggle'); if (toggle) toggle.addEventListener('click', toggleLang); loadLauncherInfo(); loadMirrors(); loadNews(); loadServerStatus(); setInterval(loadServerStatus, 15000); initCarousel(); initNav(); initParallax(); initReveal(); initYear(); }); })();