Перейти к содержанию

MURS:Специальные скрипты для особых задач

Материал из MURS вики
Версия от 11:33, 31 июля 2026; Tringoli (обсуждение | вклад) (Доработки кода)
(разн.) ← Предыдущая версия | Текущая версия (разн.) | Следующая версия → (разн.)

← Вернуться к разделу «MURS:Строителям»

Специальные скрипты для особых задач — подборка клиентских и серверных автоматизированных сценариев на базе JavaScript для ускорения рутинных процессов строительства в Муроморске и Муроморской области.

Иногда возникают ситуации, когда возможностей стандартизированных инструментов вроде WorldEdit или Axiom не хватает для решения специфических задач. Например:

  • Автоматическая высадка сложного случайного распределения растительности и деревьев;
  • Масштабное и затяжное терраформирование со сложными масками;
  • Проработка интерьеров и освещения в фасадах тысяч окон спальных районов.

В таких случаях на помощь приходят индивидуальные клиентские скрипты, выполняемые на стороне игрока и не требующие установки дополнительных плагинов на сервер.

Быстрый алгоритм настройки

Для запуска клиентских скриптов используется модификация JsMacros (или аналогичные моды на CraftScripts для WorldEdit).

  1. Скачайте и установите мод JsMacros в сборку (совместимую с версией 1.19.4).
  2. Подготовьте или напишите готовый файл скрипта с расширением `.js`.
  3. Поместите файл в папку сборки:
    .minecraft/config/jsMacros/Macros/
  4. Откройте меню мода в игре (по умолчанию клавиша G или через настройки управления).
  5. Назначьте вызов нужного скрипта на удобную горячую клавишу (Keybinding) или событие.
  6. Переместитесь на нужный участок застройки и нажмите горячую клавишу.

Пример: автоматизация расстановки освещения внутри зданий

В Муроморске насчитываются сотни многоэтажных жилых домов и промышленных зданий с тысячами оконных проёмов. Ручная расстановка блоков света внутри каждого окна занимает десятки часов, а массовое копирование командой //copy + //paste создаёт неестественный повтор рисунка светящихся окон.

Ниже представлен пример скрипта от Tringoli для автоматической случайной расстановки блоков света и темноты за блоками стёкол. Он применяется снаружи здания.

📖 Код скрипта

// =========================================================================
// Умное автоматическое освещение окон (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("sideway_frame") || 
        s.includes("parapet") || 
        s.includes("railing") || 
        s.includes("balcony") || 
        s.includes("mullion") || 
        s.includes("window_sill")) {
        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 isEdgePart(id) {
    let s = String(id).toLowerCase();
    return s.includes("_bottom") || 
           s.includes("_top") || 
           s.includes("window_bottom") || 
           s.includes("window_top") || 
           s.includes("sill") || 
           s.includes("_lower") ||
           s.includes("_upper") ||
           s.includes("parapet");
}

// 🧬 УМНОЕ ОПРЕДЕЛЕНИЕ СЕМЕЙСТВА ОКНА (Нормализация для разных модов)
function getWindowFamily(id) {
    let s = String(id).toLowerCase();

    // 1. Мод Chisel (chisel:light/glass, chisel:shale/glass, chisel:noborder/glass)
    if (s.startsWith("chisel:")) {
        let colorMatch = s.match(/(white|orange|magenta|light_blue|yellow|lime|pink|gray|light_gray|cyan|purple|blue|brown|green|red|black)_glass/);
        if (colorMatch) {
            return `chisel:${colorMatch[1]}_glass`;
        }
        return "chisel:glass";
    }

    // 2. Мод Macaw's Windows (mcwwindows:spruce_plank_window, mcwwindows:stripped_spruce_log_window)
    if (s.startsWith("mcwwindows:")) {
        let materials = [
            "spruce", "oak", "birch", "dark_oak", "jungle", "acacia", 
            "mangrove", "cherry", "bamboo", "crimson", "warped", "iron", 
            "quartz", "stone", "granite", "diorite", "andesite", "brick"
        ];
        for (let i = 0; i < materials.length; i++) {
            let mat = materials[i];
            if (s.includes(mat)) {
                return `mcwwindows:${mat}`;
            }
        }
        s = s.replace(/_(plank|stripped|log|pane|window|tall|gothic|parapet|four|two|single|double)$/g, "");
        return s;
    }

    // 3. Мод Yuushya (yuushya:white_windows_0/1/2, classical_white_window_top)
    if (s.startsWith("yuushya:")) {
        s = s.replace(/_(\d+|bottom|middle|top|upper|lower|left|right|center|corner|diagonal|part\d*|sideway_frame)$/g, "");
        s = s.replace(/_(\d+|bottom|middle|top|upper|lower|left|right|center|corner|diagonal|part\d*|sideway_frame)$/g, "");
        return s;
    }

    // 4. Ванилла и Chipped
    s = s.replace(/_(\d+|bottom|middle|top|upper|lower|left|right|center|corner|diagonal|part\d*)$/g, "");
    s = s.replace(/_(\d+|bottom|middle|top|upper|lower|left|right|center|corner|diagonal|part\d*)$/g, "");
    
    return s;
}

// 📐 СТРОГОЕ ВЕКТОРНОЕ ОПРЕДЕЛЕНИЕ ТОЧКИ ИНТЕРЬЕРА
function getExactInteriorTarget(x, y, z, px, pz) {
    let block = World.getBlock(x, y, z);
    let blockFacing = null;

    try {
        let state = block.getBlockState();
        if (state) {
            try {
                let f = state.get("facing");
                if (f) blockFacing = String(f).toLowerCase();
            } catch(e) {}

            if (!blockFacing) {
                let str = String(state);
                let match = str.match(/facing[=:]\s*([a-z_]+)/i);
                if (match) blockFacing = match[1].toLowerCase();
            }
        }
    } catch(e) {}

    let dx = 0, dz = 0;

    if (blockFacing) {
        if (blockFacing.includes("south")) dz = -1;
        else if (blockFacing.includes("north")) dz = 1;
        else if (blockFacing.includes("east")) dx = -1;
        else if (blockFacing.includes("west")) dx = 1;
    }

    if (dx === 0 && dz === 0) {
        dx = mainInward.dx;
        dz = mainInward.dz;
    }

    let winDistSq = (x - px) * (x - px) + (z - pz) * (z - pz);
    let targetDistSq = ((x + dx) - px) * ((x + dx) - px) + ((z + dz) - pz) * ((z + dz) - pz);

    if (targetDistSq < winDistSq) {
        dx = -dx;
        dz = -dz;
    }

    return { x: x + dx, y: y, z: z + dz };
}

// 🛡 ЛОКАЛЬНАЯ ПРОВЕРКА СУЩЕСТВУЮЩИХ БЛОКОВ
function getLocalExistingBlock(targetX, targetY, targetZ) {
    let offsets = [
        {x: 0, y: 1, z: 0}, {x: 0, y: -1, z: 0},
        {x: 1, y: 0, z: 0}, {x: -1, y: 0, z: 0},
        {x: 0, y: 0, z: 1}, {x: 0, y: 0, z: -1}
    ];
    for (let i = 0; i < offsets.length; i++) {
        let o = offsets[i];
        let bId = World.getBlock(targetX + o.x, targetY + o.y, targetZ + o.z).getId();
        if (bId === blockLight || bId === blockDark) {
            return bId;
        }
    }
    return null;
}

let px = Math.floor(player.getX());
let py = Math.floor(player.getY());
let pz = Math.floor(player.getZ());

let visited = new Set();
let tasksToBuild = [];

function getPosKey(x, y, z) {
    return `${x},${y},${z}`;
}

Chat.log(`§e[Урбанист] §fСканирую фасад (Нормализация модов Macaw's / Chisel / Yuushya: §aАКТИВНА§f)...`);

// 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 startFamily = getWindowFamily(blockId);
                let stack = [{x: x, y: y, z: z}];
                visited.add(key);

                let groupTargets = [];

                while (stack.length > 0) {
                    let curr = stack.pop();
                    let currId = World.getBlock(curr.x, curr.y, curr.z).getId();

                    if (!isEdgePart(currId)) {
                        let target = getExactInteriorTarget(curr.x, curr.y, curr.z, px, pz);
                        let behindId = World.getBlock(target.x, target.y, target.z).getId();

                        if (behindId === "minecraft:air" || behindId === "minecraft:cave_air") {
                            groupTargets.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) && getWindowFamily(nId) === startFamily) {
                                    visited.add(nKey);
                                    stack.push(n);
                                }
                            }
                        }
                    }
                }

                if (groupTargets.length > 0) {
                    let defaultGroupBlock = (Math.random() <= lightRatio) ? blockLight : blockDark;

                    for (let k = 0; k < groupTargets.length; k++) {
                        let t = groupTargets[k];
                        let localExist = getLocalExistingBlock(t.targetX, t.targetY, t.targetZ);
                        
                        tasksToBuild.push({
                            targetX: t.targetX,
                            targetY: t.targetY,
                            targetZ: t.targetZ,
                            blockToPlace: localExist ? localExist : defaultGroupBlock
                        });
                    }
                }
            }
        }
    }
}

Chat.log(`§e[Урбанист] §fСформировано корректных окон: §a${tasksToBuild.length}§f. Начинаю стройку!`);

let changed = 0;

// 2. Ускоренная застройка через /setblock
for (let i = 0; i < tasksToBuild.length; i++) {
    let task = tasksToBuild[i];

    Chat.say(`/setblock ${task.targetX} ${task.targetY} ${task.targetZ} ${task.blockToPlace}`);
    changed++;
    
    if (changed % blocksPerTick === 0) {
        Client.waitTick(1); 
    }
}

Chat.log(`§e[Урбанист] §fУспешно застроено блоков за стёклами: §a${changed}`);

Совет

Полезный совет

Вы можете варьировать параметры в коде скрипта: например, устанавливать значение const blocksPerTick в 10 и больше для быстрейшего расставления блоков света и темноты.

Преимущества метода

  • Высокая скорость. Обработка фасада крупного 16-этажного дома занимает считанные секунды.
  • Уникальность. Генератор случайных чисел исключает повторяющийся паттерн «включённых» окон.
  • Автономность. Метод работает через клиентский мод и не требует прав администратора сервера на установку плагинов.
  • Гибкость. По аналогичной схеме можно заменять блоки, расставлять мелкий декор или автоматизировать маски для WorldEdit.

См. также