local world = require("world") local player = require("player") local block_scanner = require("block_iterator") local creator = require("creator") local threads = require("threads") -- Кеширование функций API local getBlock = world.getBlock local floor = math.floor local sqrt = math.sqrt local clock = os.clock local floor, ceil = math.floor, math.ceil local iterator = nil local sculkSensors = {} local sculkShriekers = {} local currentWardens = {} -- Таблицы для рендера local renderCache = {} local shriekerRenderCache = {} local woolSuggestions = {} local wardenRenderCache = {} local sensorRenderCache = {} -- Новое: кеш для отображения безопасных сенсоров -- === НАСТРОЙКИ === local S_RADIUS = 8 local SEARCH_RADIUS = 32 local REBUILD_COOLDOWN = 1.5 local BATCH_SIZE = 9000 local MAX_RENDER_DIST = 22 local MAX_RENDER_DIST_SQ = MAX_RENDER_DIST * MAX_RENDER_DIST local function getPosKey(x, y, z) return x .. "," .. y .. "," .. z end local function getDistSq(x1, y1, z1, x2, y2, z2) return (x1 - x2) ^ 2 + (y1 - y2) ^ 2 + (z1 - z2) ^ 2 end local wool_ids = { [213] = true, [221] = true, [220] = true, [228] = true, [225] = true, [227] = true, [214] = true, [217] = true, [218] = true, [226] = true, [222] = true, [216] = true, [224] = true, [223] = true, [215] = true, [219] = true, } local function isReplaceable(name) if not name then return true end name = name:lower() if name:find("wool") then return false end if name:find("stairs") or name:find("slab") or name:find("fence") or name:find("wall") or name:find("gate") or name:find("door") then return false end local can_replace = { "air", "vein", "water", "flower", "carpet", "vine", "snow", "fern", "button" } for _, pattern in ipairs(can_replace) do if name:find(pattern) then return true end end return false end local function getPlacementWeight(x, y, z) local offsets = { { 0, -1, 0, 100 }, { 0, 1, 0, 100 }, { 1, 0, 0, 100 }, { -1, 0, 0, 100 }, { 0, 0, 1, 100 }, { 0, 0, -1, 100 } } for i = 1, #offsets do local o = offsets[i] local b = getBlock(x + o[1], y + o[2], z + o[3]) if b and not isReplaceable(b.identifier:lower()) then return o[4] end end return 1 end local woolBlockCache = {} local function isBlockedByWool(p1, p2) -- Генерируем уникальный ключ для пары точек (сортируем, чтобы путь A->B и B->A был одним ключом) local x1, y1, z1 = p1.x, p1.y, p1.z local x2, y2, z2 = p2.x, p2.y, p2.z local key if x1 < x2 or (x1 == x2 and y1 < y2) or (x1 == x2 and y1 == y2 and z1 < z2) then key = x1 .. "," .. y1 .. "," .. z1 .. ":" .. x2 .. "," .. y2 .. "," .. z2 else key = x2 .. "," .. y2 .. "," .. z2 .. ":" .. x1 .. "," .. y1 .. "," .. z1 end if woolBlockCache[key] ~= nil then return woolBlockCache[key] end local dx, dy, dz = (x2 + 0.5) - (x1 + 0.5), (y2 + 0.5) - (y1 + 0.5), (z2 + 0.5) - (z1 + 0.5) local dist = sqrt(dx * dx + dy * dy + dz * dz) local steps = math.ceil(dist * 5) local res = false for i = 1, steps - 1 do local t = i / steps local px, py, pz = (x1 + 0.5) + dx * t, (y1 + 0.5) + dy * t, (z1 + 0.5) + dz * t local bx, by, bz = floor(px), floor(py), floor(pz) local b = getBlock(bx, by, bz) if b and b.identifier:find("wool") then res = true; break end local fx, fy, fz = px - bx, py - by, pz - bz local bias = 0.2 if (fx < bias or fx > 1 - bias) and (fz < bias or fz > 1 - bias) then local offX, offZ = fx < 0.5 and -1 or 1, fz < 0.5 and -1 or 1 local s1, s2 = getBlock(bx + offX, by, bz), getBlock(bx, by, bz + offZ) if s1 and s1.identifier:find("wool") and s2 and s2.identifier:find("wool") then res = true; break end end if (fy < bias or fy > 1 - bias) and (fx < bias or fx > 1 - bias) then local offX, offY = fx < 0.5 and -1 or 1, fy < 0.5 and -1 or 1 local s1, s2 = getBlock(bx + offX, by, bz), getBlock(bx, by + offY, bz) if s1 and s1.identifier:find("wool") and s2 and s2.identifier:find("wool") then res = true; break end end if (fy < bias or fy > 1 - bias) and (fz < bias or fz > 1 - bias) then local offY, offZ = fy < 0.5 and -1 or 1, fz < 0.5 and -1 or 1 local s1, s2 = getBlock(bx, by + offY, bz), getBlock(bx, by, bz + offZ) if s1 and s1.identifier:find("wool") and s2 and s2.identifier:find("wool") then res = true; break end end end woolBlockCache[key] = res return res end local function rebuildNetwork() woolBlockCache = {} local nextRenderCache, nextShriekerCache, nextWardenCache, nextSensorCache = {}, {}, {}, {} local sensorToShriekers = {} local shriekerToSensors = {} local blockScores = {} local activePaths = {} local uniqueFloor = {} local p = player.entity local px, py, pz = p.x, p.y, p.z -- 1. Связываем сенсоры и крикуны for seKey, sensor in pairs(sculkSensors) do for sKey, shrieker in pairs(sculkShriekers) do local dSq = getDistSq(sensor.x, sensor.y, sensor.z, shrieker.x, shrieker.y, shrieker.z) if dSq <= 65 then -- 8 блоков радиус if not isBlockedByWool(sensor, shrieker) then local pathId = seKey .. "->" .. sKey activePaths[pathId] = true shriekerToSensors[sKey] = (shriekerToSensors[sKey] or 0) + 1 sensorToShriekers[seKey] = sensorToShriekers[seKey] or {} table.insert(sensorToShriekers[seKey], sKey) -- Поиск мест для шерсти (код из оригинала оставлен для краткости) local dx, dy, dz = (shrieker.x - sensor.x), (shrieker.y - sensor.y), (shrieker.z - sensor.z) local dist = sqrt(dSq) for i = 1, floor(dist) do local t = i / dist local bx, by, bz = floor(sensor.x + dx * t + 0.5), floor(sensor.y + dy * t + 0.5), floor(sensor.z + dz * t + 0.5) if getDistSq(bx, by, bz, px, py, pz) < MAX_RENDER_DIST_SQ then if not (bx == sensor.x and by == sensor.y and bz == sensor.z) and not (bx == shrieker.x and by == shrieker.y and bz == shrieker.z) then local b = getBlock(bx, by, bz) if isReplaceable(b and b.identifier:lower() or "air") then local bKey = getPosKey(bx, by, bz) if not blockScores[bKey] then blockScores[bKey] = { x = bx, y = by, z = bz, paths = {}, weight = getPlacementWeight(bx, by, bz) } end table.insert(blockScores[bKey].paths, pathId) end end end end end end end end -- 2. Новое: Поиск сенсоров, которые безопасно ломать for seKey, sensor in pairs(sculkSensors) do -- Сенсор полезен только если он реально может активировать крикун if sensorToShriekers[seKey] then local isSafeToBreak = true -- Проверяем, не услышит ли ЛЮБОЙ ДРУГОЙ сенсор ломание этого for otherKey, otherSensor in pairs(sculkSensors) do if seKey ~= otherKey then -- Если другой сенсор тоже подключен к какому-то крикуну if sensorToShriekers[otherKey] then local dSq = getDistSq(sensor.x, sensor.y, sensor.z, otherSensor.x, otherSensor.y, otherSensor.z) -- Вибрация от ломания блока летит на 8 блоков if dSq <= 65 and not isBlockedByWool(sensor, otherSensor) then isSafeToBreak = false break end end end end if isSafeToBreak then table.insert(nextSensorCache, { box = creator.createBox(sensor.x + 0.2, sensor.y + 0.2, sensor.z + 0.2, sensor.x + 0.8, sensor.y + 0.8, sensor.z + 0.8), x = sensor.x, y = sensor.y, z = sensor.z }) end end end -- 3. Жадный алгоритм выбора шерсти local nextWoolSuggestions = {} local remainingPaths = {} local pathsCount = 0 for pathId in pairs(activePaths) do remainingPaths[pathId] = true; pathsCount = pathsCount + 1 end local availableBlocks = {} for _, v in pairs(blockScores) do table.insert(availableBlocks, v) end local suggestedCount = 0 while pathsCount > 0 and suggestedCount < 4 do local bestBlockIdx, minDistanceSq = -1, math.huge for i = 1, #availableBlocks do local b = availableBlocks[i] local coversPath = false for j = 1, #b.paths do if remainingPaths[b.paths[j]] then coversPath = true; break end end if coversPath then local dSq = getDistSq(b.x, b.y, b.z, px, py, pz) if dSq < minDistanceSq then minDistanceSq = dSq; bestBlockIdx = i end end end if bestBlockIdx == -1 then break end local winner = availableBlocks[bestBlockIdx] for _, pathId in ipairs(winner.paths) do if remainingPaths[pathId] then remainingPaths[pathId] = nil; pathsCount = pathsCount - 1 end end table.insert(nextWoolSuggestions, { box = creator.createBox(winner.x + 0.1, winner.y + 0.1, winner.z + 0.1, winner.x + 0.9, winner.y + 0.9, winner.z + 0.9), isFloating = winner.weight < 30, alpha = winner.weight >= 30 and 180 or 80 }) table.remove(availableBlocks, bestBlockIdx) suggestedCount = suggestedCount + 1 end -- 4. Красный пол for seKey, sensor in pairs(sculkSensors) do if sensorToShriekers[seKey] and getDistSq(sensor.x, sensor.y, sensor.z, px, py, pz) < MAX_RENDER_DIST_SQ then local radius = S_RADIUS for dx = -radius, radius do for dy = -radius, radius do for dz = -radius, radius do if (dx * dx + dz * dz) <= (radius * radius) and (dy * dy) <= (radius * radius + 6) then local tx, ty, tz = sensor.x + dx, sensor.y + dy, sensor.z + dz local b = getBlock(tx, ty, tz) if b and not isReplaceable(b.identifier:lower()) then local bAbove = getBlock(tx, ty + 1, tz) if bAbove and isReplaceable(bAbove.identifier:lower()) then if not b.identifier:find("wool") and not isBlockedByWool({ x = tx, y = ty + 1, z = tz }, sensor) then local fKey = tx .. "," .. ty .. "," .. tz if not uniqueFloor[fKey] then uniqueFloor[fKey] = true table.insert(nextRenderCache, creator.createBox(tx, ty + 1.005, tz, tx + 1, ty + 1.02, tz + 1)) end end end end end end end end end end -- 5. Крикуны и Вардены (без изменений) for sKey, shrieker in pairs(sculkShriekers) do local state = "off" if shriekerToSensors[sKey] then state = "green" for seKey, targets in pairs(sensorToShriekers) do local hits = false for i = 1, #targets do if targets[i] == sKey then hits = true; break end end if hits and #targets > 1 then state = "yellow"; break end end end table.insert(nextShriekerCache, { box = creator.createBox(shrieker.x, shrieker.y, shrieker.z, shrieker.x + 1, shrieker.y + 1, shrieker.z + 1), state = state }) end for i = 1, #currentWardens do local w = currentWardens[i] table.insert(nextWardenCache, { box = w.box, blocked = isBlockedByWool(p, { x = w.x, y = w.y + 1.5, z = w.z }) }) end renderCache, shriekerRenderCache, woolSuggestions, wardenRenderCache, sensorRenderCache = nextRenderCache, nextShriekerCache, nextWoolSuggestions, nextWardenCache, nextSensorCache end -- В рендерер добавляем отображение сенсоров registerWorldRenderer(function(ctx) local p = player.entity if not p then return end -- 1. Красный пол for i = 1, #renderCache do ctx.renderFilled(renderCache[i], 255, 0, 0, 60, false) end -- 2. Розовая застройка local item = p.main_hand or p.off_hand if item and wool_ids[item.id] then for i = 1, #woolSuggestions do local s = woolSuggestions[i] ctx.renderFilled(s.box, 255, 20, 147, s.alpha, true) ctx.renderOutline(s.box, 255, 255, 255, 200, 1.0, true) ctx.renderText(s.box.getCenter().x, s.box.getCenter().y, s.box.getCenter().z, "Wool", 0.8, 255, 255, 255, true) end end -- 3. Безопасные сенсоры (НОВОЕ) for i = 1, #sensorRenderCache do local s = sensorRenderCache[i] ctx.renderFilled(s.box, 0, 255, 200, 100, true) ctx.renderOutline(s.box, 0, 255, 255, 255, 2.0, true) ctx.renderText(s.box.getCenter().x, s.box.getCenter().y + 0.4, s.box.getCenter().z, "BREAK ME", 0.7, 0, 255, 255, true) ctx.renderText(s.box.getCenter().x, s.box.getCenter().y + 0.2, s.box.getCenter().z, "Safe to disable", 0.5, 255, 255, 255, true) end -- 4. Вардены for i = 1, #wardenRenderCache do local w = wardenRenderCache[i] if not w.blocked then ctx.renderOutline(w.box, 255, 0, 0, 255, 2.5, true) ctx.renderFilled(w.box, 255, 0, 0, 40, true) else ctx.renderOutline(w.box, 0, 255, 255, 180, 1.0, true) end end -- 5. Крикуны for i = 1, #shriekerRenderCache do local d = shriekerRenderCache[i] local color = d.state == "yellow" and { 255, 200, 0 } or (d.state == "green" and { 0, 255, 0 } or { 255, 255, 255 }) local text = d.state == "yellow" and "Cannot be broken" or (d.state == "green" and "It can be broken" or "Neutralized") local icon = d.state == "yellow" and "!" or "✔" ctx.renderText(d.box.getCenter().x, d.box.getCenter().y, d.box.getCenter().z, icon, 0.7, 255, 255, 255, true) ctx.renderText(d.box.getCenter().x, d.box.getCenter().y - 0.2, d.box.getCenter().z, text, 0.5, 255, 255, 255, true) if d.state ~= "off" then ctx.renderFilled(d.box, color[1], color[2], color[3], 120, true) else ctx.renderOutline(d.box, 255, 255, 255, 40, 1.0, true) end end end) -- Остальная часть скрипта (registerClientTick, registerBlockUpdate) остается без изменений registerClientTick(function() local p = player.entity if not p then return end local px, py, pz = floor(p.x), floor(p.y), floor(p.z) local entities = world.getLivingEntities() local wardens = {} for i = 1, #entities do local e = entities[i] if e.type:find("warden") then table.insert(wardens, { x = e.x, y = e.y, z = e.z, box = e.box }) end end currentWardens = wardens if not lastX or getDistSq(px, py, pz, lastX, lastY, lastZ) > 64 then local limitSq = 2304 for k, s in pairs(sculkSensors) do if getDistSq(s.x, s.y, s.z, px, py, pz) > limitSq then sculkSensors[k] = nil end end for k, s in pairs(sculkShriekers) do if getDistSq(s.x, s.y, s.z, px, py, pz) > limitSq then sculkShriekers[k] = nil end end iterator = block_scanner.new_iterator(px, py, pz, SEARCH_RADIUS) lastX, lastY, lastZ = px, py, pz end local batch = iterator.next_batch(BATCH_SIZE) for i = 1, #batch do local e = batch[i] local name = e[4].identifier local k = getPosKey(e[1], e[2], e[3]) if name:find("sculk_sensor") then sculkSensors[k] = { x = e[1], y = e[2], z = e[3] } elseif name:find("sculk_shrieker") then sculkShriekers[k] = { x = e[1], y = e[2], z = e[3] } end end if clock() - (lastRebuild or 0) > REBUILD_COOLDOWN then lastRebuild = clock() threads.startThread(function() rebuildNetwork() end) end end) registerBlockUpdate(function(info) if info.position then local p, n = info.position, info.new.identifier local k = getPosKey(p.x, p.y, p.z) if n:find("sculk_sensor") then sculkSensors[k] = { x = p.x, y = p.y, z = p.z } elseif n:find("sculk_shrieker") then sculkShriekers[k] = { x = p.x, y = p.y, z = p.z } else sculkSensors[k] = nil; sculkShriekers[k] = nil end lastRebuild = 0 end end)