local player = require("player") local STATES = { NONE = "none", BITE = "bite_detected", MINIGAME = "minigame", END = "end" } local state = STATES.NONE local isSneaking = false -- ⚙️ НАСТРОЙКИ local SLIDER_BYTE_START = 10 local SLIDER_BYTE_LEN = 3 local SNEAK_ON_THRESHOLD = 42 -- Если сглаженная позиция < 42% → ЗАЖИМАЕМ local SNEAK_OFF_THRESHOLD = 58 -- Если сглаженная позиция > 58% → ОТПУСКАЕМ local EMA_ALPHA = 0.35 -- Коэффициент сглаживания. 0.3-0.4 гасит "дрожание" пакетов -- Внутренние переменные local minVal, maxVal = math.huge, -math.huge local smoothedPercent = 50 local sampleCount = 0 local isCalibrated = false local tickCounter = 0 local dataLogTimer = 0 local function stripCodes(str) return str:gsub("§.", "") end local function getSliderValue(cleanStr) if #cleanStr < SLIDER_BYTE_START + SLIDER_BYTE_LEN - 1 then return nil end local sub = cleanStr:sub(SLIDER_BYTE_START, SLIDER_BYTE_START + SLIDER_BYTE_LEN - 1) local b1, b2, b3 = string.byte(sub, 1), string.byte(sub, 2), string.byte(sub, 3) return (b1 * 65536) + (b2 * 256) + (b3 or 0) end registerTitleEvent(function(text, isSubtitle) if not isSubtitle then local clean = stripCodes(text) if clean == "BITE!" then state = STATES.BITE minVal, maxVal = math.huge, -math.huge sampleCount = 0; isCalibrated = false smoothedPercent = 50; tickCounter = 0 if isSneaking then player.input.setPressedSneak(false); isSneaking = false end print("[MINIGAME] ▶ BITE! Запуск калибровки...") elseif clean == "FAIL!" then state = STATES.END end end end) registerActionBarEvent(function(text) if state ~= STATES.MINIGAME then return end local clean = stripCodes(text) local val = getSliderValue(clean) if not val then return end -- 🔒 Фаза калибровки (первые 15 сетевых пакетов) if not isCalibrated then if val < minVal then minVal = val end if val > maxVal then maxVal = val end sampleCount = sampleCount + 1 if sampleCount >= 15 and maxVal > minVal then -- Добавляем 3% запаса по краям, чтобы ползунок не упирался в 0/100% local margin = (maxVal - minVal) * 0.03 minVal = minVal - margin maxVal = maxVal + margin isCalibrated = true print(string.format("[CALIB] ✅ Диапазон зафиксирован. Min: %d | Max: %d | Range: %d", minVal, maxVal, maxVal - minVal)) end return end -- 📊 Расчет и сглаживание (EMA) local raw = ((val - minVal) / (maxVal - minVal)) * 100 raw = math.max(0, math.min(100, raw)) smoothedPercent = smoothedPercent * (1 - EMA_ALPHA) + raw * EMA_ALPHA end) registerMessageEvent(function(text, overlay, json) if overlay then local clean = stripCodes(text) -- Ловим стандартные окончания рыбалки if clean:find("xp") then state = STATES.END print("[MINIGAME] 🏁 Миниигра завершена.") end end end) registerClientTick(function() tickCounter = tickCounter + 1 -- 1️⃣ Старт миниигры if state == STATES.BITE then player.input.rightClick() state = STATES.MINIGAME return end -- 2️⃣ Логика управления шифтом (каждый тик) if state == STATES.MINIGAME and isCalibrated then local pos = smoothedPercent local targetSneak = nil local reason = "" if pos < SNEAK_ON_THRESHOLD then targetSneak = true reason = string.format("Pos < %d%%", SNEAK_ON_THRESHOLD) elseif pos > SNEAK_OFF_THRESHOLD then targetSneak = false reason = string.format("Pos > %d%%", SNEAK_OFF_THRESHOLD) else targetSneak = isSneaking -- Внутри мёртвой зоны 42-58% reason = "Deadzone" end -- Применяем только если состояние изменилось if targetSneak ~= isSneaking then player.input.setPressedSneak(targetSneak) isSneaking = targetSneak local action = targetSneak and "🔵 PRESS" or "🔴 RELEASE" print(string.format("[CTRL] %.1f%% → %s Shift | (%s)", pos, action, reason)) end -- 📝 Периодический вывод данных для сверки dataLogTimer = dataLogTimer + 1 if dataLogTimer >= 15 then dataLogTimer = 0 print(string.format("[DATA] Smooth: %.1f%% | Raw: %.1f%% | Sneak: %s", smoothedPercent, ((smoothedPercent - minVal) / (maxVal - minVal)) * 100, isSneaking and "ON" or "OFF")) end end -- 3️⃣ Сброс после окончания if state == STATES.END then player.input.setPressedSneak(false) player.input.rightClick() isSneaking = false state = STATES.NONE end end)