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

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

Материал из MURS вики
мНет описания правки
Доработки кода
 
(не показаны 2 промежуточные версии этого же участника)
Строка 1: Строка 1:
[[MURS:Строителям|← Вернуться к разделу «MURS:Строителям»]]
{{MURS:Строителям}}


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


Строка 35: Строка 36:


<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
// Умное освещение окон с 3D-анализом плотности интерьера (JS Macros 1.19.4)
// =========================================================================
const radius = 15;        // Радиус сканирования вокруг игрока
// Умное автоматическое освещение окон (JS Macros 1.19.4)
const lightRatio = 0.3;  // 30% свет, 70% тьма
// =========================================================================
const blocksPerTick = 4;  // Ускорение: 4 блока за 1 тик (80 блоков/сек)
 
const radius = 20;        // Радиус сканирования вокруг игрока
const lightRatio = 0.3;  // Шанс горящего окна (30% свет, 70% тьма)
const blocksPerTick = 4;  // Скорость застройки (4 блока за 1 тик = 80 блоков/сек)
const silentMode = true;  // 🤫 ТИХИЙ РЕЖИМ: глушит системный спам команд в чат


// Идентификаторы блоков света и тьмы
// Идентификаторы блоков света и тьмы
Строка 46: Строка 51:
const player = Player.getPlayer();
const player = Player.getPlayer();


// Главный вектор взгляда игрока вглубь фасада
// Главный вектор взгляда игрока по сетке
function getPlayerInwardVector() {
function getPlayerInwardVector() {
     let yaw = player.getYaw() % 360;
     let yaw = player.getYaw() % 360;
     if (yaw < 0) yaw += 360;
     if (yaw < 0) yaw += 360;
   
 
     if (yaw >= 315 || yaw < 45) return { dx: 0, dz: 1 };  // Взгляд на Юг -> комната на Юге (+Z)
     if (yaw >= 315 || yaw < 45) return { dx: 0, dz: 1 };  // Взгляд на Юг -> комната на Юге (+Z)
     if (yaw >= 45 && yaw < 135) return { dx: -1, dz: 0 };  // Взгляд на Запад -> комната на Западе (-X)
     if (yaw >= 45 && yaw < 135) return { dx: -1, dz: 0 };  // Взгляд на Запад -> комната на Западе (-X)
Строка 64: Строка 69:
let lookZ = Math.cos(yawRad);
let lookZ = Math.cos(yawRad);


// Проверка: находится ли окно перед игроком (чтобы не застраивать спину)
// 🛡 Проверка сектора перед глазами игрока
function isInFront(x, z, px, pz) {
function isInFront(x, z, px, pz) {
     let dirX = x - px;
     let dirX = x - px;
Строка 70: Строка 75:
     if (dirX === 0 && dirZ === 0) return true;
     if (dirX === 0 && dirZ === 0) return true;
     let dot = dirX * lookX + dirZ * lookZ;
     let dot = dirX * lookX + dirZ * lookZ;
     return dot > -1.5;  
     return dot > -2.0;
}
}


// Проверка: является ли блок окном
// 🔍 ПРОВЕРКА ОКНА (Vanilla, Blockus, Macaw's, Chisel, Yuushya, Chipped)
function isWindow(id) {
function isWindow(id) {
    if (!id) return false;
     let s = String(id).toLowerCase();
     let s = String(id).toLowerCase();
   
 
     // 🚫 Исключаем парапеты, перила и внешние ограждения
     // 🚫 1. Исключаем дорожные моды (alt_fabroads)
     if (s.includes("parapet") || s.includes("railing") || s.includes("balcony_railing")) {
     if (s.startsWith("alt_fabroads:") ||
        s.startsWith("fabroads:") ||
        s.includes("asphalt") ||
        s.includes("marking") ||
        s.includes("curb") ||
        s.includes("traffic") ||
        s.includes("road_sign") ||
        s.includes("road")) {
         return false;
         return false;
    }
        }
 
        // 🚫 2. Исключаем конструктивные элементы фасадов, шторы и ограждения
        if (s.includes("sideway_frame") ||
            s.includes("parapet") ||
            s.includes("railing") ||
            s.includes("balcony_railing") ||
            s.includes("curtain") ||
            s.includes("roof") ||
            s.includes("fence") ||
            s.includes("shutter")) {
            return false;
            }


    return s.match(/yuushya:.*(window|glass|diagonal).*/) ||  
            // 🌟 3. Поддержка всех типов стекла
          s.match(/chipped:.*(glass|pane|window).*/) ||  
            return s.includes("window") ||
          s.match(/mcwwindows:.*(window|glass|pane).*/) ||  
            s.includes("glass") ||
          s.match(/chisel:.*glass.*/) ||  
            s.includes("pane") ||
          s.match(/minecraft:.*glass.*/) ||
            s.includes("beveled") ||
          s.match(/.*:.*(window|stained_glass|glass_pane).*/);
            s.includes("diagonal") ||
            s.includes("frosted") ||
            s.includes("stained_glass") ||
            (s.startsWith("yuushya:") && (s.includes("white_") || s.includes("black_") || s.includes("brown_") || s.includes("classical_")) && s.includes("diagonal"));
}
}


// Проверка: является ли элемент подоконником/нижней частью
// Проверка на воздух или заменяемый блок
function isBottomPart(id) {
function isAirOrReplaceable(id) {
    if (!id) return true;
     let s = String(id).toLowerCase();
     let s = String(id).toLowerCase();
     return s.includes("_bottom") ||  
     return s.includes("air") || s === "minecraft:light" || s.includes("structure_void");
          s.includes("window_bottom") ||  
}
          s.includes("sill") ||
 
          s.includes("_lower") ||
// Проверка на твердый блок стены здания
          s.includes("frame_bottom") ||
function isSolidWallBlock(x, y, z) {
          s.includes("parapet");
    let id = World.getBlock(x, y, z).getId();
    if (isAirOrReplaceable(id)) return false;
    if (id === blockLight || id === blockDark) return false;
    if (isWindow(id)) return false;
    return true;
}
}


// 🧠 УЛЬТИМАТИВНЫЙ ПОИСК ТОЧКИ ИНТЕРЬЕРА ЧЕРЕЗ АНАЛИЗ 3D-ОКРУЖЕНИЯ
// 📐 ЛОКАЛЬНЫЙ РАСЧЁТ ПРИЛЕГАЮЩЕЙ ТОЧКИ ДЛЯ СЛОЖНЫХ УГЛОВ (22.5°, 45°, 60°, дуги)
function getBestInteriorTarget(x, y, z, px, pz) {
function getBestCurvedInteriorTarget(x, y, z, px, pz) {
     let candidates = [
     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: 0, dz: -1 },
         { dx: 0, dz: -1 },
         { dx: 1, dz: 1 },
         { dx: 1, dz: 0 },
         { dx: -1, dz: 1 },
         { dx: -1, dz: 0 }
        { dx: 1, dz: -1 },
        { dx: -1, dz: -1 }
     ];
     ];


     let winDist = Math.hypot(x - px, z - pz);
     let winDistSq = (x - px) * (x - px) + (z - pz) * (z - pz);
    let vecX = x - px;
    let vecZ = z - pz;
 
    // Соседи-стекла для определения локальной касательной к стене
    let glassEast  = isWindow(World.getBlock(x + 1, y, z).getId());
    let glassWest  = isWindow(World.getBlock(x - 1, y, z).getId());
    let glassSouth = isWindow(World.getBlock(x, y, z + 1).getId());
    let glassNorth = isWindow(World.getBlock(x, y, z - 1).getId());
 
    let wallX = glassEast || glassWest || isSolidWallBlock(x + 1, y, z) || isSolidWallBlock(x - 1, y, z);
    let wallZ = glassSouth || glassNorth || isSolidWallBlock(x, y, z + 1) || isSolidWallBlock(x, y, z - 1);
 
     let bestTarget = null;
     let bestTarget = null;
     let maxScore = -9999;
     let bestScore = -999999;


     for (let i = 0; i < candidates.length; i++) {
     for (let i = 0; i < candidates.length; i++) {
Строка 126: Строка 166:


         let bId = World.getBlock(tx, ty, tz).getId();
         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;
         if (isWindow(bId) || isSolidWallBlock(tx, ty, tz)) continue;
        for (let ox = -1; ox <= 1; ox++) {
 
            for (let oy = -1; oy <= 1; oy++) {
        // Точка должна быть воздухом или существующим блоком света
                for (let oz = -1; oz <= 1; oz++) {
        if (!isAirOrReplaceable(bId) && bId !== blockLight && bId !== blockDark) continue;
                    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") {
        let targetDistSq = (tx - px) * (tx - px) + (tz - pz) * (tz - pz);
                        density++;
        if (targetDistSq < winDistSq - 0.05) continue;
                    }
 
                }
        let score = 0;
            }
 
         }
        // 1. Проекция на вектор вглубь здания от игрока
        let dotRay = c.dx * vecX + c.dz * vecZ;
         score += dotRay * 20;


         // 2. Направление относительно взгляда игрока
         // 2. Перпендикулярность локальной линии стены
         let dotPlayer = c.dx * mainInward.dx + c.dz * mainInward.dz;
         if (wallX && !wallZ && c.dz !== 0) score += 60; // Локальная стена по X -> нормаль Z
         let targetDist = Math.hypot(tx - px, tz - pz);
         if (wallZ && !wallX && c.dx !== 0) score += 60; // Локальная стена по Z -> нормаль X


         // Расчет итогового рейтинга интерьера
         // 3. Анализ замкнутости помещения (наличие пола/потолка/внутренних перегородок)
         let score = density;
         let surrounding = [
         if (dotPlayer > 0) score += 5;
            {ox: 0, oy: 1, oz: 0}, {ox: 0, oy: -1, oz: 0},
         if (dotPlayer < 0) score -= 15; // Жесткий штраф за вылет на улицу перед игроком
            {ox: 1, oy: 0, oz: 0}, {ox: -1, oy: 0, oz: 0},
        if (targetDist > winDist) score += 3;
            {ox: 0, oy: 0, oz: 1}, {ox: 0, oy: 0, oz: -1}
        ];
         let enclosed = 0;
         for (let s = 0; s < surrounding.length; s++) {
            let sid = World.getBlock(tx + surrounding[s].ox, ty + surrounding[s].oy, tz + surrounding[s].oz).getId();
            if (!isAirOrReplaceable(sid)) enclosed++;
        }
        score += enclosed * 5;


         if (score > maxScore) {
         if (score > bestScore) {
             maxScore = score;
             bestScore = score;
             bestTarget = { x: tx, y: ty, z: tz };
             bestTarget = { x: tx, y: ty, z: tz };
         }
         }
     }
     }


     // Если точная точка найдена
    if (bestTarget) return bestTarget;
     if (bestTarget) {
 
         return bestTarget;
     // Резервный расчет по направлению взгляда
     if (Math.abs(vecZ) >= Math.abs(vecX)) {
        let dz = (vecZ >= 0) ? 1 : -1;
        return { x: x, y: y, z: z + dz };
    } else {
        let dx = (vecX >= 0) ? 1 : -1;
         return { x: x + dx, y: y, z: z };
     }
     }
}


    // Запасной вариант строго по вектору помещения
// 🛡 Проверка наличия уже установленного цвета в соседней точке
     return { x: x + mainInward.dx, y: y, z: z + mainInward.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;
}
}


Строка 176: Строка 240:


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


function getPosKey(x, y, z) {
function getPosKey(x, y, z) {
Строка 182: Строка 247:
}
}


Chat.log(`§e[Урбанист] §fСканирую фасад (3D-анализ интерьера эркеров и балконов)...`);
Chat.log(`§e[Урбанист] §fСканирую фасад (Фикс кривых углов 22.5°/45°/60°: §aАКТИВЕН§f)...`);


// 1. Поиск и группировка окон (Flood Fill)
// 1. Поиск и группировка окон (8-направленный Flood Fill для сбора ломаных стен)
for (let x = px - radius; x <= px + radius; x++) {
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++) {
         for (let z = pz - radius; z <= pz + radius; z++) {
 
           
        if (!isInFront(x, z, px, pz)) continue;
 
         for (let y = py - radius; y <= py + radius; y++) {
 
             let key = getPosKey(x, y, z);
             let key = getPosKey(x, y, z);
             if (visited.has(key)) continue;
             if (visited.has(key)) continue;


             let blockId = World.getBlock(x, y, z).getId();
             let blockId = World.getBlock(x, y, z).getId();
           
 
             if (isWindow(blockId)) {
             if (isWindow(blockId)) {
                if (!isInFront(x, z, px, pz)) {
                    visited.add(key);
                    continue;
                }
                 let stack = [{x: x, y: y, z: z}];
                 let stack = [{x: x, y: y, z: z}];
                 visited.add(key);
                 visited.add(key);


                 let emptyTargets = [];
                 let rawWindowBlocks = [];
                let existingBlock = null;


                 while (stack.length > 0) {
                 while (stack.length > 0) {
                     let curr = stack.pop();
                     let curr = stack.pop();
                     let currId = World.getBlock(curr.x, curr.y, curr.z).getId();
                     rawWindowBlocks.push(curr);
 
                    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 = [
                     let neighbors = [
Строка 227: Строка 278:
                         {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},
                         {x: curr.x, y: curr.y, z: curr.z - 1}
                         {x: curr.x, y: curr.y, z: curr.z - 1},
                        // Диагональные связки для ступенчатых углов 22.5°/45°/60°
                        {x: curr.x + 1, y: curr.y, z: curr.z + 1},
                        {x: curr.x + 1, y: curr.y, z: curr.z - 1},
                        {x: curr.x - 1, y: curr.y, z: curr.z + 1},
                        {x: curr.x - 1, y: curr.y, z: curr.z - 1}
                     ];
                     ];


Строка 233: Строка 289:
                         let n = neighbors[i];
                         let n = neighbors[i];
                         let nKey = getPosKey(n.x, n.y, n.z);
                         let nKey = getPosKey(n.x, n.y, n.z);
                       
 
                         if (!visited.has(nKey)) {
                         if (!visited.has(nKey)) {
                             if (Math.abs(n.x - px) <= radius && Math.abs(n.y - py) <= radius && Math.abs(n.z - pz) <= radius) {
                             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();
                                 let nId = World.getBlock(n.x, n.y, n.z).getId();
                                 if (isWindow(nId)) {
                                 if (isWindow(nId)) {
                                     visited.add(nKey);
                                     visited.add(nKey);
Строка 246: Строка 303:
                 }
                 }


                 if (emptyTargets.length > 0) {
                 if (rawWindowBlocks.length > 0) {
                     let selectedBlock = existingBlock ? existingBlock : ((Math.random() <= lightRatio) ? blockLight : blockDark);
                     let groupTargets = [];
                   
                    let existingBlockInGroup = null;
                    groupsToBuild.push({
 
                        targets: emptyTargets,
                    for (let b = 0; b < rawWindowBlocks.length; b++) {
                        selectedBlock: selectedBlock
                        let wBlock = rawWindowBlocks[b];
                    });
 
                        // Локальный расчёт для каждой ступени криволинейного окна
                        let target = getBestCurvedInteriorTarget(wBlock.x, wBlock.y, wBlock.z, px, pz);
                        let behindId = World.getBlock(target.x, target.y, target.z).getId();
 
                        if (behindId === blockLight || behindId === blockDark) {
                            existingBlockInGroup = behindId;
                        } else if (isAirOrReplaceable(behindId)) {
                            let tKey = getPosKey(target.x, target.y, target.z);
                            if (!targetedCoords.has(tKey)) {
                                targetedCoords.add(tKey);
                                groupTargets.push({ targetX: target.x, targetY: target.y, targetZ: target.z });
                            }
                        }
                    }
 
                    if (groupTargets.length > 0) {
                        let chosenBlock = existingBlockInGroup;
 
                        if (!chosenBlock) {
                            for (let k = 0; k < groupTargets.length; k++) {
                                let local = getLocalExistingBlock(groupTargets[k].targetX, groupTargets[k].targetY, groupTargets[k].targetZ);
                                if (local) {
                                    chosenBlock = local;
                                    break;
                                }
                            }
                        }
 
                        if (!chosenBlock) {
                            chosenBlock = (Math.random() <= lightRatio) ? blockLight : blockDark;
                        }
 
                        for (let k = 0; k < groupTargets.length; k++) {
                            let t = groupTargets[k];
                            tasksToBuild.push({
                                targetX: t.targetX,
                                targetY: t.targetY,
                                targetZ: t.targetZ,
                                blockToPlace: chosenBlock
                            });
                        }
                    }
                 }
                 }
             }
             }
Строка 259: Строка 358:
}
}


Chat.log(`§e[Урбанист] §fСформировано групп окон: §a${groupsToBuild.length}§f. Начинаю застройку!`);
let lightCount = 0;
let darkCount = 0;
 
for (let i = 0; i < tasksToBuild.length; i++) {
    if (tasksToBuild[i].blockToPlace === blockLight) lightCount++;
    else darkCount++;
}
 
Chat.log(`§e[Урбанист] §fНайдено ячеек: §a${tasksToBuild.length}§f (💡 Свет: §e${lightCount}§f, ⬛ Тьма: §7${darkCount}§f). Застройка...`);
 
// 🤫 2. Включаем тихий режим
if (silentMode && tasksToBuild.length > 0) {
    Chat.say(`/gamerule sendCommandFeedback false`);
    Client.waitTick(1);
}


let changed = 0;
let changed = 0;


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


     for (let j = 0; j < group.targets.length; j++) {
     Chat.say(`/setblock ${task.targetX} ${task.targetY} ${task.targetZ} ${task.blockToPlace}`);
        let target = group.targets[j];
    changed++;


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


Chat.log(`§e[Урбанист] §fУспешно застроено блоков за стёклами: §a${changed}`);
// 🔊 4. Возвращаем вывод команд обратно
if (silentMode && tasksToBuild.length > 0) {
    Client.waitTick(1);
    Chat.say(`/gamerule sendCommandFeedback true`);
}
 
Chat.log(`§a[Урбанист] §fЗастройка успешно завершена! Обработано блоков: §e${changed}`);
</syntaxhighlight>
</syntaxhighlight>



Текущая версия от 14:08, 19 августа 2026

Строителям — раздел вики 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 = 20;        // Радиус сканирования вокруг игрока
const lightRatio = 0.3;   // Шанс горящего окна (30% свет, 70% тьма)
const blocksPerTick = 4;  // Скорость застройки (4 блока за 1 тик = 80 блоков/сек)
const silentMode = true;  // 🤫 ТИХИЙ РЕЖИМ: глушит системный спам команд в чат

// Идентификаторы блоков света и тьмы
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 > -2.0;
}

// 🔍 ПРОВЕРКА ОКНА (Vanilla, Blockus, Macaw's, Chisel, Yuushya, Chipped)
function isWindow(id) {
    if (!id) return false;
    let s = String(id).toLowerCase();

    // 🚫 1. Исключаем дорожные моды (alt_fabroads)
    if (s.startsWith("alt_fabroads:") ||
        s.startsWith("fabroads:") ||
        s.includes("asphalt") ||
        s.includes("marking") ||
        s.includes("curb") ||
        s.includes("traffic") ||
        s.includes("road_sign") ||
        s.includes("road")) {
        return false;
        }

        // 🚫 2. Исключаем конструктивные элементы фасадов, шторы и ограждения
        if (s.includes("sideway_frame") ||
            s.includes("parapet") ||
            s.includes("railing") ||
            s.includes("balcony_railing") ||
            s.includes("curtain") ||
            s.includes("roof") ||
            s.includes("fence") ||
            s.includes("shutter")) {
            return false;
            }

            // 🌟 3. Поддержка всех типов стекла
            return s.includes("window") ||
            s.includes("glass") ||
            s.includes("pane") ||
            s.includes("beveled") ||
            s.includes("diagonal") ||
            s.includes("frosted") ||
            s.includes("stained_glass") ||
            (s.startsWith("yuushya:") && (s.includes("white_") || s.includes("black_") || s.includes("brown_") || s.includes("classical_")) && s.includes("diagonal"));
}

// Проверка на воздух или заменяемый блок
function isAirOrReplaceable(id) {
    if (!id) return true;
    let s = String(id).toLowerCase();
    return s.includes("air") || s === "minecraft:light" || s.includes("structure_void");
}

// Проверка на твердый блок стены здания
function isSolidWallBlock(x, y, z) {
    let id = World.getBlock(x, y, z).getId();
    if (isAirOrReplaceable(id)) return false;
    if (id === blockLight || id === blockDark) return false;
    if (isWindow(id)) return false;
    return true;
}

// 📐 ЛОКАЛЬНЫЙ РАСЧЁТ ПРИЛЕГАЮЩЕЙ ТОЧКИ ДЛЯ СЛОЖНЫХ УГЛОВ (22.5°, 45°, 60°, дуги)
function getBestCurvedInteriorTarget(x, y, z, px, pz) {
    let candidates = [
        { dx: 0, dz: 1 },
        { dx: 0, dz: -1 },
        { dx: 1, dz: 0 },
        { dx: -1, dz: 0 }
    ];

    let winDistSq = (x - px) * (x - px) + (z - pz) * (z - pz);
    let vecX = x - px;
    let vecZ = z - pz;

    // Соседи-стекла для определения локальной касательной к стене
    let glassEast  = isWindow(World.getBlock(x + 1, y, z).getId());
    let glassWest  = isWindow(World.getBlock(x - 1, y, z).getId());
    let glassSouth = isWindow(World.getBlock(x, y, z + 1).getId());
    let glassNorth = isWindow(World.getBlock(x, y, z - 1).getId());

    let wallX = glassEast || glassWest || isSolidWallBlock(x + 1, y, z) || isSolidWallBlock(x - 1, y, z);
    let wallZ = glassSouth || glassNorth || isSolidWallBlock(x, y, z + 1) || isSolidWallBlock(x, y, z - 1);

    let bestTarget = null;
    let bestScore = -999999;

    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 (isWindow(bId) || isSolidWallBlock(tx, ty, tz)) continue;

        // Точка должна быть воздухом или существующим блоком света
        if (!isAirOrReplaceable(bId) && bId !== blockLight && bId !== blockDark) continue;

        // 🛡 Точка обязана быть глубже в здании, чем стекло (не на улице!)
        let targetDistSq = (tx - px) * (tx - px) + (tz - pz) * (tz - pz);
        if (targetDistSq < winDistSq - 0.05) continue;

        let score = 0;

        // 1. Проекция на вектор вглубь здания от игрока
        let dotRay = c.dx * vecX + c.dz * vecZ;
        score += dotRay * 20;

        // 2. Перпендикулярность локальной линии стены
        if (wallX && !wallZ && c.dz !== 0) score += 60; // Локальная стена по X -> нормаль Z
        if (wallZ && !wallX && c.dx !== 0) score += 60; // Локальная стена по Z -> нормаль X

        // 3. Анализ замкнутости помещения (наличие пола/потолка/внутренних перегородок)
        let surrounding = [
            {ox: 0, oy: 1, oz: 0}, {ox: 0, oy: -1, oz: 0},
            {ox: 1, oy: 0, oz: 0}, {ox: -1, oy: 0, oz: 0},
            {ox: 0, oy: 0, oz: 1}, {ox: 0, oy: 0, oz: -1}
        ];
        let enclosed = 0;
        for (let s = 0; s < surrounding.length; s++) {
            let sid = World.getBlock(tx + surrounding[s].ox, ty + surrounding[s].oy, tz + surrounding[s].oz).getId();
            if (!isAirOrReplaceable(sid)) enclosed++;
        }
        score += enclosed * 5;

        if (score > bestScore) {
            bestScore = score;
            bestTarget = { x: tx, y: ty, z: tz };
        }
    }

    if (bestTarget) return bestTarget;

    // Резервный расчет по направлению взгляда
    if (Math.abs(vecZ) >= Math.abs(vecX)) {
        let dz = (vecZ >= 0) ? 1 : -1;
        return { x: x, y: y, z: z + dz };
    } else {
        let dx = (vecX >= 0) ? 1 : -1;
        return { x: x + dx, y: y, z: z };
    }
}

// 🛡 Проверка наличия уже установленного цвета в соседней точке
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 targetedCoords = new Set();
let tasksToBuild = [];

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

Chat.log(`§e[Урбанист] §fСканирую фасад (Фикс кривых углов 22.5°/45°/60°: §aАКТИВЕН§f)...`);

// 1. Поиск и группировка окон (8-направленный Flood Fill для сбора ломаных стен)
for (let x = px - radius; x <= px + radius; x++) {
    for (let z = pz - radius; z <= pz + radius; z++) {

        if (!isInFront(x, z, px, pz)) continue;

        for (let y = py - radius; y <= py + radius; y++) {

            let key = getPosKey(x, y, z);
            if (visited.has(key)) continue;

            let blockId = World.getBlock(x, y, z).getId();

            if (isWindow(blockId)) {
                let stack = [{x: x, y: y, z: z}];
                visited.add(key);

                let rawWindowBlocks = [];

                while (stack.length > 0) {
                    let curr = stack.pop();
                    rawWindowBlocks.push(curr);

                    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},
                        // Диагональные связки для ступенчатых углов 22.5°/45°/60°
                        {x: curr.x + 1, y: curr.y, z: curr.z + 1},
                        {x: curr.x + 1, y: curr.y, z: curr.z - 1},
                        {x: curr.x - 1, y: curr.y, z: curr.z + 1},
                        {x: curr.x - 1, 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 (rawWindowBlocks.length > 0) {
                    let groupTargets = [];
                    let existingBlockInGroup = null;

                    for (let b = 0; b < rawWindowBlocks.length; b++) {
                        let wBlock = rawWindowBlocks[b];

                        // Локальный расчёт для каждой ступени криволинейного окна
                        let target = getBestCurvedInteriorTarget(wBlock.x, wBlock.y, wBlock.z, px, pz);
                        let behindId = World.getBlock(target.x, target.y, target.z).getId();

                        if (behindId === blockLight || behindId === blockDark) {
                            existingBlockInGroup = behindId;
                        } else if (isAirOrReplaceable(behindId)) {
                            let tKey = getPosKey(target.x, target.y, target.z);
                            if (!targetedCoords.has(tKey)) {
                                targetedCoords.add(tKey);
                                groupTargets.push({ targetX: target.x, targetY: target.y, targetZ: target.z });
                            }
                        }
                    }

                    if (groupTargets.length > 0) {
                        let chosenBlock = existingBlockInGroup;

                        if (!chosenBlock) {
                            for (let k = 0; k < groupTargets.length; k++) {
                                let local = getLocalExistingBlock(groupTargets[k].targetX, groupTargets[k].targetY, groupTargets[k].targetZ);
                                if (local) {
                                    chosenBlock = local;
                                    break;
                                }
                            }
                        }

                        if (!chosenBlock) {
                            chosenBlock = (Math.random() <= lightRatio) ? blockLight : blockDark;
                        }

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

let lightCount = 0;
let darkCount = 0;

for (let i = 0; i < tasksToBuild.length; i++) {
    if (tasksToBuild[i].blockToPlace === blockLight) lightCount++;
    else darkCount++;
}

Chat.log(`§e[Урбанист] §fНайдено ячеек: §a${tasksToBuild.length}§f (💡 Свет: §e${lightCount}§f, ⬛ Тьма: §7${darkCount}§f). Застройка...`);

// 🤫 2. Включаем тихий режим
if (silentMode && tasksToBuild.length > 0) {
    Chat.say(`/gamerule sendCommandFeedback false`);
    Client.waitTick(1);
}

let changed = 0;

// 3. Застройка
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);
    }
}

// 🔊 4. Возвращаем вывод команд обратно
if (silentMode && tasksToBuild.length > 0) {
    Client.waitTick(1);
    Chat.say(`/gamerule sendCommandFeedback true`);
}

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

Совет

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

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

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

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

См. также

[править код]