diff --git a/docker-compose.yml b/docker-compose.yml
index 15078b4..e75a31a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,6 +15,8 @@ services:
environment:
- TZ=Asia/Novosibirsk
- DOCKER_ENABLED=true
+ - STATIC_ENABLED=true
+ - STATIC_RULES=crabs-games.art,^/minecraft/api/readme$,https://git.3crabs.space/api/v1/repos/user-penguin/gmancraft/contents/README.md?ref=main
- ASSETS_LOCATION=/web
- DEBUG=true
- SSL_TYPE=auto
diff --git a/web/minecraft/app.js b/web/minecraft/app.js
new file mode 100644
index 0000000..2151373
--- /dev/null
+++ b/web/minecraft/app.js
@@ -0,0 +1,35 @@
+const repository = 'https://git.3crabs.space/user-penguin/gmancraft';
+
+function parseLatestBuild(readme) {
+ // shortcut: формат строки актуальной версии README, при его смене нужен явный manifest.
+ const line = readme.split('\n').find(line => line.startsWith('Актуальная версия'));
+ const match = line?.match(/^Актуальная версия\s*—\s*\*\*(\d+\.\d+\.\d+)\*\*:\s*\[Скачать \1\]\((dist\/minecraft-lab-neoforge-spike-\1\/minecraft-lab-neoforge-spike-\1\.mrpack)\)/);
+ if (!match) throw new Error('Не найдена актуальная сборка');
+ return { version: match[1], url: `${repository}/raw/branch/main/${match[2]}` };
+}
+
+async function loadLatestBuild() {
+ const status = document.getElementById('build-status');
+ const download = document.getElementById('build-download');
+ try {
+ const response = await fetch('/minecraft/api/readme', {
+ cache: 'no-store',
+ signal: AbortSignal.timeout(10000),
+ });
+ if (!response.ok) throw new Error('README недоступен');
+ const data = await response.json();
+ if (data.encoding !== 'base64' || typeof data.content !== 'string') {
+ throw new Error('Неизвестный формат README');
+ }
+ const readme = new TextDecoder().decode(Uint8Array.from(atob(data.content), char => char.charCodeAt(0)));
+ const build = parseLatestBuild(readme);
+ status.textContent = `Актуальная версия — ${build.version}`;
+ download.href = build.url;
+ download.textContent = `Скачать сборку ${build.version}`;
+ download.hidden = false;
+ } catch {
+ status.textContent = 'Не удалось определить актуальную версию. Открой репозиторий по ссылке внизу страницы.';
+ }
+}
+
+loadLatestBuild();
diff --git a/web/minecraft/index.html b/web/minecraft/index.html
new file mode 100644
index 0000000..da740f0
--- /dev/null
+++ b/web/minecraft/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ gman-world — Minecraft
+
+
+
+
+
+
+ CRABS GAMES
+ MINECRAFT · GMAN-WORLD
+ Твой следующий
мир начинается здесь.
+ Скачай сборку, импортируй её в Prism Launcher и заходи на сервер.
+ Адрес сервера 90.189.218.191:25565
+
+
+
+
+ 01 / ИГРАТЬ
+ Minecraft-сборка
+ Проверяем актуальную версию…
+ Скачать сборку
+
+ Файл .mrpack · импорт через Prism Launcher
+
+ - Скачай актуальную сборку.
+ - В Prism Launcher выбери Add Instance → Import и укажи скачанный .mrpack.
+ - Дождись загрузки модов и запусти Minecraft.
+
+
+
+
+ 02 / ПЕРЕЙТИ НА НОВУЮ СБОРКУ
+ Перенос данных
+ Карта и метки Xaero, закладки рецептов JEI — из старой сборки в новую.
+ Скачать трансфер
+ XaeroMapTransfer.exe · Windows 10/11 · SHA-256
+
+ - Закрой Minecraft и запусти утилиту.
+ - Укажи старую папку игры слева, новую — справа.
+ - Нажми «Перенести данные» и дождись завершения.
+
+ После пересоздания серверного мира старую карту переносить не следует. Перед переносом проверь изменения версии в репозитории.
+
+
+
+
+
+
+
diff --git a/web/minecraft/style.css b/web/minecraft/style.css
new file mode 100644
index 0000000..dc6730f
--- /dev/null
+++ b/web/minecraft/style.css
@@ -0,0 +1,44 @@
+:root {
+ color-scheme: dark;
+ font-family: system-ui, sans-serif;
+ color: #e9eee6;
+ background: #101510;
+ line-height: 1.6;
+}
+
+* { box-sizing: border-box; }
+body { margin: 0; }
+main { max-width: 1120px; margin: auto; padding: 40px 28px; }
+a { color: #b9e58b; text-underline-offset: 4px; }
+a:hover { color: #dcffc1; }
+a:focus-visible { outline: 3px solid #b9e58b; outline-offset: 5px; }
+header { padding-bottom: 48px; }
+.brand { font-size: 14px; font-weight: 800; letter-spacing: .16em; text-decoration: none; }
+.eyebrow { color: #b9e58b; font-size: 12px; letter-spacing: .12em; font-weight: 700; }
+header .eyebrow { margin-top: 64px; }
+h1 { font-size: clamp(36px, 6vw, 64px); line-height: 1.1; letter-spacing: -.04em; margin: 18px 0 24px; }
+h2 { font-size: 27px; line-height: 1.2; margin: 18px 0; }
+.intro { max-width: 580px; color: #b7c3b3; font-size: 18px; }
+.server { display: flex; flex-wrap: wrap; gap: 8px 16px; color: #b7c3b3; }
+code { color: #e9eee6; font-size: 16px; overflow-wrap: anywhere; }
+.downloads { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
+section { border: 1px solid #344331; background: #192118; padding: 32px; border-radius: 12px; }
+section > p { color: #b7c3b3; }
+section > .eyebrow { color: #b9e58b; }
+.button { display: inline-block; background: #b9e58b; color: #101510; font-weight: 750; text-decoration: none; padding: 14px 22px; margin-top: 10px; border: 1px solid #b9e58b; border-radius: 6px; }
+.button:hover { color: #101510; background: #dcffc1; }
+.button[hidden] { display: none; }
+.secondary { background: transparent; color: #b9e58b; }
+.hint { font-size: 13px; }
+ol { padding-left: 22px; margin-top: 28px; }
+li { padding-left: 4px; margin-bottom: 12px; }
+.notice { font-size: 14px; border-left: 3px solid #b9e58b; padding-left: 14px; margin-top: 28px; }
+footer { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 16px; font-size: 13px; padding-top: 32px; }
+footer span { color: #b7c3b3; }
+
+@media (max-width: 720px) {
+ main { padding: 24px 18px; }
+ header .eyebrow { margin-top: 40px; }
+ .downloads { grid-template-columns: 1fr; }
+ section { padding: 24px; }
+}