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, ceil, abs = math.floor, math.ceil, math.abs local sqrt = math.sqrt local clock = os.clock local find = string.find local huge = math.huge local iterator = nil local sculkSensors = {} local sculkShriekers = {} -- Таблицы для рендера 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 + 33554432) << 38) | ((y + 1024) << 26) | (z + 33554432) 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 find(name, "wool", 1, true) then return false end if find(name, "stairs", 1, true) or find(name, "slab", 1, true) or find(name, "fence", 1, true) or find(name, "wall", 1, true) or find(name, "gate", 1, true) or find(name, "door", 1, true) then return false end local can_replace = { "air", "vein", "water", "flower", "carpet", "vine", "snow", "fern", "button" } for i = 1, #can_replace do if find(name, can_replace[i], 1, true) 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) -- Получаем уникальные ID для обеих точек local k1 = getPosKey(p1.x, p1.y, p1.z) local k2 = getPosKey(p2.x, p2.y, p2.z) -- Сортируем ID, чтобы ключ был одинаковым для путей A->B и B->A local key if k1 < k2 then key = k1 .. ":" .. k2 else key = k2 .. ":" .. k1 end if woolBlockCache[key] ~= nil then return woolBlockCache[key] end -- Центры блоков для трассировки local cx1, cy1, cz1 = p1.x + 0.5, p1.y + 0.5, p1.z + 0.5 local cx2, cy2, cz2 = p2.x + 0.5, p2.y + 0.5, p2.z + 0.5 local dx, dy, dz = cx2 - cx1, cy2 - cy1, cz2 - cz1 local dist = sqrt(dx * dx + dy * dy + dz * dz) if dist < 0.1 then return false end local steps = math.ceil(dist * 15) local res = false for i = 1, steps - 1 do local t = i / steps local px, py, pz = cx1 + dx * t, cy1 + dy * t, cz1 + dz * t local bx, by, bz = floor(px), floor(py), floor(pz) -- 1. Проверка прямого попадания в блок шерсти if not (bx == p1.x and by == p1.y and bz == p1.z) and not (bx == p2.x and by == p2.y and bz == p2.z) then local b = getBlock(bx, by, bz) if b and b.identifier:find("wool") then res = true; break end end -- 2. Логика диагонального "зажима" (высокоточная) local fx, fy, fz = px - bx, py - by, pz - bz local b_limit = 0.15 -- Плоскость XZ (вертикальный зажим) if (fx < b_limit or fx > 1 - b_limit) and (fz < b_limit or fz > 1 - b_limit) then -- Проверяем, что луч проходит сквозь толщу блока по высоте, а не по грани if fy > b_limit and fy < 1 - b_limit 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 end -- Плоскость XY if (fx < b_limit or fx > 1 - b_limit) and (fy < b_limit or fy > 1 - b_limit) then if fz > b_limit and fz < 1 - b_limit 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 end -- Плоскость YZ if (fy < b_limit or fy > 1 - b_limit) and (fz < b_limit or fz > 1 - b_limit) then if fx > b_limit and fx < 1 - b_limit 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 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 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) if dSq <= 65 and not isBlockedByWool(sensor, otherSensor) then isSafeToBreak = false break end end end end if isSafeToBreak then local cx, cy, cz = sensor.x + 0.5, sensor.y + 0.5, sensor.z + 0.5 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), cx = cx, cy = cy, cz = cz }) 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, 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 local cx, cy, cz = winner.x + 0.5, winner.y + 0.5, winner.z + 0.5 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), cx = cx, cy = cy, cz = cz, 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 find(b.identifier:lower(), "wool", 1, true) 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 local cx, cy, cz = shrieker.x + 0.5, shrieker.y + 0.5, shrieker.z + 0.5 table.insert(nextShriekerCache, { box = creator.createBox(shrieker.x, shrieker.y, shrieker.z, shrieker.x + 1, shrieker.y + 1, shrieker.z + 1), cx = cx, cy = cy, cz = cz, state = state }) 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 for i = 1, #renderCache do ctx.renderFilled(renderCache[i], 255, 0, 0, 60, false) end 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.cx, s.cy, s.cz, "Wool", 0.8, 255, 255, 255, true) end end 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.cx, s.cy + 0.4, s.cz, "BREAK ME", 0.7, 0, 255, 255, true) ctx.renderText(s.cx, s.cy + 0.2, s.cz, "Safe to disable", 0.5, 255, 255, 255, true) end 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 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.cx, d.cy, d.cz, icon, 0.7, 255, 255, 255, true) ctx.renderText(d.cx, d.cy - 0.2, d.cz, 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(function() local p = player.entity if not p then return end local px, py, pz = floor(p.x), floor(p.y), floor(p.z) 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 find(name, "sculk_sensor", 1, true) then sculkSensors[k] = { x = e[1], y = e[2], z = e[3] } elseif find(name, "sculk_shrieker", 1, true) 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 find(n, "sculk_sensor", 1, true) then sculkSensors[k] = { x = p.x, y = p.y, z = p.z } elseif find(n, "sculk_shrieker", 1, true) then sculkShriekers[k] = { x = p.x, y = p.y, z = p.z } else sculkSensors[k] = nil; sculkShriekers[k] = nil end lastRebuild = 0 end end)