MURS:Специальные скрипты для особых задач
← Вернуться к разделу «MURS:Строителям»
Специальные скрипты для особых задач — подборка клиентских и серверных автоматизированных сценариев на базе JavaScript для ускорения рутинных процессов строительства в Муроморске и Муроморской области.
Иногда возникают ситуации, когда возможностей стандартизированных инструментов вроде WorldEdit или Axiom не хватает для решения специфических задач. Например:
- Автоматическая высадка сложного случайного распределения растительности и деревьев;
- Масштабное и затяжное терраформирование со сложными масками;
- Проработка интерьеров и освещения в фасадах тысяч окон спальных районов.
В таких случаях на помощь приходят индивидуальные клиентские скрипты, выполняемые на стороне игрока и не требующие установки дополнительных плагинов на сервер.
Быстрый алгоритм настройки
Для запуска клиентских скриптов используется модификация JsMacros (или аналогичные моды на CraftScripts для WorldEdit).
- Скачайте и установите мод JsMacros в сборку (совместимую с версией 1.19.4).
- Подготовьте или напишите готовый файл скрипта с расширением `.js`.
- Поместите файл в папку сборки:
.minecraft/config/jsMacros/Macros/
- Откройте меню мода в игре (по умолчанию клавиша G или через настройки управления).
- Назначьте вызов нужного скрипта на удобную горячую клавишу (Keybinding) или событие.
- Переместитесь на нужный участок застройки и нажмите горячую клавишу.
Пример: автоматизация расстановки освещения внутри зданий
В Муроморске насчитываются сотни многоэтажных жилых домов и промышленных зданий с тысячами оконных проёмов. Ручная расстановка блоков света внутри каждого окна занимает десятки часов, а массовое копирование командой //copy + //paste создаёт неестественный повтор рисунка светящихся окон.
Ниже представлен пример скрипта от Tringoli для автоматической случайной расстановки блоков света и темноты за блоками стёкол. Он применяется снаружи здания.
📖 Код скрипта
// Умное освещение окон с 3D-анализом плотности интерьера (JS Macros 1.19.4)
const radius = 15; // Радиус сканирования вокруг игрока
const lightRatio = 0.3; // 30% свет, 70% тьма
const blocksPerTick = 4; // Ускорение: 4 блока за 1 тик (80 блоков/сек)
// Идентификаторы блоков света и тьмы
const blockLight = "chisel:large_tile/glowstone";
const blockDark = "minecraft:black_concrete";
const player = Player.getPlayer();
// Главный вектор взгляда игрока вглубь фасада
function getPlayerInwardVector() {
let yaw = player.getYaw() % 360;
if (yaw < 0) yaw += 360;
if (yaw >= 315 || yaw < 45) return { dx: 0, dz: 1 }; // Взгляд на Юг -> комната на Юге (+Z)
if (yaw >= 45 && yaw < 135) return { dx: -1, dz: 0 }; // Взгляд на Запад -> комната на Западе (-X)
if (yaw >= 135 && yaw < 225) return { dx: 0, dz: -1 }; // Взгляд на Север -> комната на Севере (-Z)
if (yaw >= 225 && yaw < 315) return { dx: 1, dz: 0 }; // Взгляд на Восток -> комната на Востоке (+X)
return { dx: 0, dz: -1 };
}
const mainInward = getPlayerInwardVector();
let yawRad = (player.getYaw() % 360) * Math.PI / 180;
let lookX = -Math.sin(yawRad);
let lookZ = Math.cos(yawRad);
// Проверка: находится ли окно перед игроком (чтобы не застраивать спину)
function isInFront(x, z, px, pz) {
let dirX = x - px;
let dirZ = z - pz;
if (dirX === 0 && dirZ === 0) return true;
let dot = dirX * lookX + dirZ * lookZ;
return dot > -1.5;
}
// Проверка: является ли блок окном
function isWindow(id) {
let s = String(id).toLowerCase();
// 🚫 Исключаем парапеты, перила и внешние ограждения
if (s.includes("parapet") || s.includes("railing") || s.includes("balcony_railing")) {
return false;
}
return s.match(/yuushya:.*(window|glass|diagonal).*/) ||
s.match(/chipped:.*(glass|pane|window).*/) ||
s.match(/mcwwindows:.*(window|glass|pane).*/) ||
s.match(/chisel:.*glass.*/) ||
s.match(/minecraft:.*glass.*/) ||
s.match(/.*:.*(window|stained_glass|glass_pane).*/);
}
// Проверка: является ли элемент подоконником/нижней частью
function isBottomPart(id) {
let s = String(id).toLowerCase();
return s.includes("_bottom") ||
s.includes("window_bottom") ||
s.includes("sill") ||
s.includes("_lower") ||
s.includes("frame_bottom") ||
s.includes("parapet");
}
// 🧠 УЛЬТИМАТИВНЫЙ ПОИСК ТОЧКИ ИНТЕРЬЕРА ЧЕРЕЗ АНАЛИЗ 3D-ОКРУЖЕНИЯ
function getBestInteriorTarget(x, y, z, px, pz) {
let candidates = [
{ dx: mainInward.dx, dz: mainInward.dz }, // Прямо вглубь по взгляду
{ dx: 1, dz: 0 },
{ dx: -1, dz: 0 },
{ dx: 0, dz: 1 },
{ dx: 0, dz: -1 },
{ dx: 1, dz: 1 },
{ dx: -1, dz: 1 },
{ dx: 1, dz: -1 },
{ dx: -1, dz: -1 }
];
let winDist = Math.hypot(x - px, z - pz);
let bestTarget = null;
let maxScore = -9999;
for (let i = 0; i < candidates.length; i++) {
let c = candidates[i];
let tx = x + c.dx;
let ty = y;
let tz = z + c.dz;
let bId = World.getBlock(tx, ty, tz).getId();
// Точка должна быть воздухом или уже стоящим блоком света/тьмы
if (bId !== "minecraft:air" && bId !== "minecraft:cave_air" && bId !== blockLight && bId !== blockDark) {
continue;
}
// 1. Оценка плотности окружающих конструкций (26 соседних блоков)
let density = 0;
for (let ox = -1; ox <= 1; ox++) {
for (let oy = -1; oy <= 1; oy++) {
for (let oz = -1; oz <= 1; oz++) {
if (ox === 0 && oy === 0 && oz === 0) continue;
let nid = World.getBlock(tx + ox, ty + oy, tz + oz).getId();
if (nid !== "minecraft:air" && nid !== "minecraft:cave_air") {
density++;
}
}
}
}
// 2. Направление относительно взгляда игрока
let dotPlayer = c.dx * mainInward.dx + c.dz * mainInward.dz;
let targetDist = Math.hypot(tx - px, tz - pz);
// Расчет итогового рейтинга интерьера
let score = density;
if (dotPlayer > 0) score += 5;
if (dotPlayer < 0) score -= 15; // Жесткий штраф за вылет на улицу перед игроком
if (targetDist > winDist) score += 3;
if (score > maxScore) {
maxScore = score;
bestTarget = { x: tx, y: ty, z: tz };
}
}
// Если точная точка найдена
if (bestTarget) {
return bestTarget;
}
// Запасной вариант строго по вектору помещения
return { x: x + mainInward.dx, y: y, z: z + mainInward.dz };
}
let px = Math.floor(player.getX());
let py = Math.floor(player.getY());
let pz = Math.floor(player.getZ());
let visited = new Set();
let groupsToBuild = [];
function getPosKey(x, y, z) {
return `${x},${y},${z}`;
}
Chat.log(`§e[Урбанист] §fСканирую фасад (3D-анализ интерьера эркеров и балконов)...`);
// 1. Поиск и группировка окон (Flood Fill)
for (let x = px - radius; x <= px + radius; x++) {
for (let y = py - radius; y <= py + radius; y++) {
for (let z = pz - radius; z <= pz + radius; z++) {
let key = getPosKey(x, y, z);
if (visited.has(key)) continue;
let blockId = World.getBlock(x, y, z).getId();
if (isWindow(blockId)) {
if (!isInFront(x, z, px, pz)) {
visited.add(key);
continue;
}
let stack = [{x: x, y: y, z: z}];
visited.add(key);
let emptyTargets = [];
let existingBlock = null;
while (stack.length > 0) {
let curr = stack.pop();
let currId = World.getBlock(curr.x, curr.y, curr.z).getId();
if (!isBottomPart(currId)) {
let target = getBestInteriorTarget(curr.x, curr.y, curr.z, px, pz);
let behindId = World.getBlock(target.x, target.y, target.z).getId();
if (behindId === blockLight || behindId === blockDark) {
existingBlock = behindId;
} else if (behindId === "minecraft:air" || behindId === "minecraft:cave_air") {
emptyTargets.push({ targetX: target.x, targetY: target.y, targetZ: target.z });
}
}
let neighbors = [
{x: curr.x + 1, y: curr.y, z: curr.z},
{x: curr.x - 1, y: curr.y, z: curr.z},
{x: curr.x, y: curr.y + 1, z: curr.z},
{x: curr.x, y: curr.y - 1, z: curr.z},
{x: curr.x, y: curr.y, z: curr.z + 1},
{x: curr.x, y: curr.y, z: curr.z - 1}
];
for (let i = 0; i < neighbors.length; i++) {
let n = neighbors[i];
let nKey = getPosKey(n.x, n.y, n.z);
if (!visited.has(nKey)) {
if (Math.abs(n.x - px) <= radius && Math.abs(n.y - py) <= radius && Math.abs(n.z - pz) <= radius) {
let nId = World.getBlock(n.x, n.y, n.z).getId();
if (isWindow(nId)) {
visited.add(nKey);
stack.push(n);
}
}
}
}
}
if (emptyTargets.length > 0) {
let selectedBlock = existingBlock ? existingBlock : ((Math.random() <= lightRatio) ? blockLight : blockDark);
groupsToBuild.push({
targets: emptyTargets,
selectedBlock: selectedBlock
});
}
}
}
}
}
Chat.log(`§e[Урбанист] §fСформировано групп окон: §a${groupsToBuild.length}§f. Начинаю застройку!`);
let changed = 0;
// 2. Застройка через /setblock
for (let i = 0; i < groupsToBuild.length; i++) {
let group = groupsToBuild[i];
for (let j = 0; j < group.targets.length; j++) {
let target = group.targets[j];
Chat.say(`/setblock ${target.targetX} ${target.targetY} ${target.targetZ} ${group.selectedBlock}`);
changed++;
if (changed % blocksPerTick === 0) {
Client.waitTick(1);
}
}
}
Chat.log(`§e[Урбанист] §fУспешно застроено блоков за стёклами: §a${changed}`);|
Полезный совет
Вы можете варьировать параметры в коде скрипта: например, устанавливать значение |
Преимущества метода
- Высокая скорость. Обработка фасада крупного 16-этажного дома занимает считанные секунды.
- Уникальность. Генератор случайных чисел исключает повторяющийся паттерн «включённых» окон.
- Автономность. Метод работает через клиентский мод и не требует прав администратора сервера на установку плагинов.
- Гибкость. По аналогичной схеме можно заменять блоки, расставлять мелкий декор или автоматизировать маски для WorldEdit.