Add BladeBall/JustHub
This commit is contained in:
parent
13f4a5818a
commit
83de91fe82
449
BladeBall/JustHub
Normal file
449
BladeBall/JustHub
Normal file
@ -0,0 +1,449 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
local Players = game:GetService("Players")
|
||||
local TweenService = game:GetService("TweenService")
|
||||
local VirtualInputManager = game:GetService("VirtualInputManager")
|
||||
|
||||
local Player = Players.LocalPlayer
|
||||
if not Player then error("LocalPlayer tidak ditemukan!") end
|
||||
|
||||
-- Cache fungsi matematika untuk performa
|
||||
local min = math.min
|
||||
local max = math.max
|
||||
local abs = math.abs
|
||||
local deg = math.deg
|
||||
local acos = math.acos
|
||||
local sqrt = math.sqrt
|
||||
local clamp = math.clamp
|
||||
|
||||
-- Flag debug (set false jika tidak ingin banyak log)
|
||||
local DEBUG = true
|
||||
local function AdvancedLog(level, message, data)
|
||||
if not DEBUG then return end
|
||||
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
|
||||
print(string.format("[%s] [%s]: %s", timestamp, level, message))
|
||||
if data then
|
||||
for k, v in pairs(data) do
|
||||
print(string.format(" %s: %s", k, tostring(v)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Konstanta State
|
||||
local STATE_IDLE = "Idle"
|
||||
local STATE_TRACKING = "Tracking"
|
||||
local STATE_ANTICIPATING = "Anticipating"
|
||||
local STATE_PARIED = "Parried"
|
||||
local STATE_COOLDOWN = "Cooldown"
|
||||
local STATE_SPAM = "Spam"
|
||||
|
||||
local MAX_IMPACT_TIME = 4.0 -- Maksimum waktu impact (detik)
|
||||
local INCOMING_ANGLE_THRESHOLD = 20 -- Threshold sudut (derajat)
|
||||
|
||||
---------------------------------
|
||||
-- Advanced Parry Controller (State Machine)
|
||||
---------------------------------
|
||||
local AdvancedParryController = {}
|
||||
AdvancedParryController.__index = AdvancedParryController
|
||||
|
||||
function AdvancedParryController.new()
|
||||
local self = setmetatable({}, AdvancedParryController)
|
||||
self.Player = Player
|
||||
self.State = STATE_IDLE
|
||||
self.LastParryTime = tick()
|
||||
self.CooldownTime = 0.75 -- Cooldown normal (detik)
|
||||
self.BaseThreshold = 0.45 -- Threshold dasar (detik)
|
||||
self.BallStopped = false
|
||||
self.PreviousVelocity = nil
|
||||
self.SmoothedAccel = Vector3.new(0, 0, 0)
|
||||
self.CurrentBall = nil
|
||||
self.Connection = nil
|
||||
self.LastSpamTime = 0
|
||||
self.BaseSpamInterval = 0.1
|
||||
|
||||
-- GUI Full Screen Popup
|
||||
self.FullScreenGui = nil
|
||||
self.AdditionalInfoLabel = nil
|
||||
|
||||
-- Overpower Mode: parry akan menjadi jauh lebih agresif dan responsif
|
||||
self.OverpowerMode = true
|
||||
self.OverpowerThresholdMultiplier = 0.5 -- Mengurangi threshold agar trigger lebih cepat
|
||||
self.OverpowerCooldown = 0.1 -- Cooldown sangat rendah pada mode ini
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- Dapatkan ping lokal (detik)
|
||||
function AdvancedParryController:GetLocalPing()
|
||||
local success, ping = pcall(function() return self.Player:GetNetworkPing() end)
|
||||
if not success then
|
||||
AdvancedLog("WARN", "Gagal mendapatkan ping, default 0.1 detik.", nil)
|
||||
return 0.1
|
||||
end
|
||||
return ping or 0.1
|
||||
end
|
||||
|
||||
-- Hitung threshold dinamis berdasarkan ping (dikurangi jika OverpowerMode aktif)
|
||||
function AdvancedParryController:ComputeDynamicThreshold()
|
||||
local adjustmentFactor = 0.5
|
||||
local threshold = self.BaseThreshold + (self:GetLocalPing() * adjustmentFactor)
|
||||
if self.OverpowerMode then
|
||||
threshold = threshold * self.OverpowerThresholdMultiplier
|
||||
end
|
||||
return threshold
|
||||
end
|
||||
|
||||
-- Seleksi bola target terbaik (paling mengancam) dari folder "Balls"
|
||||
function AdvancedParryController:SelectTargetBall()
|
||||
local ballsFolder = workspace:FindFirstChild("Balls")
|
||||
local bestBall = nil
|
||||
local bestTImpact = math.huge
|
||||
if ballsFolder then
|
||||
local character = self.Player.Character
|
||||
local HRP = character and character:FindFirstChild("HumanoidRootPart")
|
||||
if not HRP then return nil, math.huge end
|
||||
for _, ball in ipairs(ballsFolder:GetChildren()) do
|
||||
if ball:GetAttribute("realBall") and ball:GetAttribute("target") == self.Player.Name then
|
||||
local distance = (HRP.Position - ball.Position).Magnitude
|
||||
local ballVelocity
|
||||
local success = pcall(function() ballVelocity = ball.zoomies.VectorVelocity end)
|
||||
if success and typeof(ballVelocity) == "Vector3" then
|
||||
-- Untuk seleksi, asumsikan percepatan nol
|
||||
local tImpact = self:PredictImpactTime(distance, ballVelocity, Vector3.new(0,0,0))
|
||||
if tImpact < bestTImpact then
|
||||
bestTImpact = tImpact
|
||||
bestBall = ball
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return bestBall, bestTImpact
|
||||
end
|
||||
|
||||
-- Reset koneksi event bila diperlukan
|
||||
function AdvancedParryController:ResetConnection()
|
||||
if self.Connection then
|
||||
self.Connection:Disconnect()
|
||||
self.Connection = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Inisialisasi listener bola untuk mendeteksi perubahan atribut target
|
||||
function AdvancedParryController:InitializeBallListener()
|
||||
self:ResetConnection()
|
||||
local ball = self:SelectTargetBall() -- Menggunakan target terbaik
|
||||
if ball then
|
||||
self.CurrentBall = ball
|
||||
self.Connection = ball:GetAttributeChangedSignal("target"):Connect(function()
|
||||
self.State = STATE_TRACKING
|
||||
AdvancedLog("INFO", "Atribut target bola berubah, reset state ke TRACKING.", {Ball = ball.Name})
|
||||
end)
|
||||
AdvancedLog("INFO", "Listener bola diinisialisasi.", {Ball = ball.Name})
|
||||
else
|
||||
self.CurrentBall = nil
|
||||
AdvancedLog("WARN", "Bola tidak ditemukan. Listener tidak diinisialisasi.", nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- Inisialisasi GUI full-screen (popup hitam) dengan animasi smooth.
|
||||
-- Tampilan: layar penuh yang sedikit transparan, di tengah ada tulisan "Status : Auto Parry",
|
||||
-- label tambahan untuk informasi penting (misal: Ping & State) dan tombol Exit.
|
||||
function AdvancedParryController:InitializeGUI()
|
||||
local playerGui = self.Player:WaitForChild("PlayerGui")
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
screenGui.Name = "FullScreenPopupGui"
|
||||
screenGui.ResetOnSpawn = false
|
||||
screenGui.IgnoreGuiInset = true -- Menutupi seluruh layar (termasuk area inset)
|
||||
screenGui.Parent = playerGui
|
||||
|
||||
-- Frame full screen dengan background hitam (mulai dari transparansi penuh untuk animasi)
|
||||
local fullScreenFrame = Instance.new("Frame")
|
||||
fullScreenFrame.Name = "FullScreenFrame"
|
||||
fullScreenFrame.BackgroundColor3 = Color3.new(0, 0, 0)
|
||||
fullScreenFrame.BackgroundTransparency = 1 -- Mulai transparan, akan tween ke 0.2
|
||||
fullScreenFrame.Size = UDim2.new(1, 0, 1, 0)
|
||||
fullScreenFrame.Position = UDim2.new(0, 0, 0, 0)
|
||||
fullScreenFrame.Parent = screenGui
|
||||
|
||||
-- Label utama untuk status (di tengah layar)
|
||||
local statusLabel = Instance.new("TextLabel")
|
||||
statusLabel.Name = "StatusLabel"
|
||||
statusLabel.BackgroundTransparency = 1
|
||||
statusLabel.Text = "Just - Hub : Auto Parry"
|
||||
statusLabel.TextColor3 = Color3.new(1, 1, 1)
|
||||
statusLabel.Font = Enum.Font.SourceSansBold
|
||||
statusLabel.TextSize = 36
|
||||
statusLabel.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
statusLabel.Position = UDim2.new(0.5, 0, 0.35, 0)
|
||||
-- Mulai dengan scale 0 (ukuran 0) untuk animasi scale-in
|
||||
statusLabel.Size = UDim2.new(0, 0, 0, 0)
|
||||
statusLabel.Parent = fullScreenFrame
|
||||
|
||||
-- Label tambahan untuk informasi penting (misal: ping dan state)
|
||||
local additionalInfoLabel = Instance.new("TextLabel")
|
||||
additionalInfoLabel.Name = "AdditionalInfoLabel"
|
||||
additionalInfoLabel.BackgroundTransparency = 1
|
||||
additionalInfoLabel.Text = "Loading info..."
|
||||
additionalInfoLabel.TextColor3 = Color3.new(1, 1, 1)
|
||||
additionalInfoLabel.Font = Enum.Font.SourceSans
|
||||
additionalInfoLabel.TextSize = 24
|
||||
additionalInfoLabel.AnchorPoint = Vector2.new(0.5, 0.5)
|
||||
additionalInfoLabel.Position = UDim2.new(0.5, 0, 0.5, 0)
|
||||
additionalInfoLabel.Size = UDim2.new(0, 400, 0, 50)
|
||||
additionalInfoLabel.Parent = fullScreenFrame
|
||||
self.AdditionalInfoLabel = additionalInfoLabel
|
||||
|
||||
-- Tombol Exit di bawah label tambahan
|
||||
local exitButton = Instance.new("TextButton")
|
||||
exitButton.Name = "ExitButton"
|
||||
exitButton.Text = "Exit"
|
||||
exitButton.TextColor3 = Color3.new(1, 1, 1)
|
||||
exitButton.Font = Enum.Font.SourceSansBold
|
||||
exitButton.TextSize = 28
|
||||
exitButton.BackgroundColor3 = Color3.new(0.3, 0.3, 0.3)
|
||||
exitButton.Size = UDim2.new(0, 150, 0, 50)
|
||||
exitButton.AnchorPoint = Vector2.new(0.5, 0)
|
||||
exitButton.Position = UDim2.new(0.5, 0, 0.65, 0)
|
||||
exitButton.Parent = fullScreenFrame
|
||||
|
||||
exitButton.MouseButton1Click:Connect(function()
|
||||
Player:Kick("Anda telah keluar dari game.")
|
||||
end)
|
||||
|
||||
self.FullScreenGui = screenGui
|
||||
|
||||
-- Tween untuk animasi fade-in (background transparency dari 1 ke 0.2)
|
||||
local tweenInfo = TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.Out)
|
||||
local tweenFrame = TweenService:Create(fullScreenFrame, tweenInfo, {BackgroundTransparency = 0.2})
|
||||
tweenFrame:Play()
|
||||
|
||||
-- Tween untuk animasi scale-in label utama (dari 0 ke ukuran asli)
|
||||
local tweenLabel = TweenService:Create(statusLabel, tweenInfo, {Size = UDim2.new(0, 400, 0, 100)})
|
||||
tweenLabel:Play()
|
||||
|
||||
-- Tween untuk animasi scale-in label informasi tambahan (sedikit scale-up)
|
||||
local tweenInfoLabel = TweenService:Create(additionalInfoLabel, tweenInfo, {TextTransparency = 0})
|
||||
tweenInfoLabel:Play()
|
||||
end
|
||||
|
||||
-- Fungsi untuk mengirim event parry (klik mouse) dengan delay minimal
|
||||
function AdvancedParryController:AttemptParry()
|
||||
VirtualInputManager:SendMouseButtonEvent(0, 0, 0, true, game, 0)
|
||||
task.delay(0.01, function()
|
||||
VirtualInputManager:SendMouseButtonEvent(0, 0, 0, false, game, 0)
|
||||
end)
|
||||
self.LastParryTime = tick()
|
||||
end
|
||||
|
||||
-- Lakukan exponential smoothing pada percepatan (lebih agresif pada mode overpowered)
|
||||
function AdvancedParryController:SmoothAcceleration(currentVelocity, dt)
|
||||
local alpha = self.OverpowerMode and 0.8 or 0.4
|
||||
if not self.PreviousVelocity then
|
||||
self.PreviousVelocity = currentVelocity
|
||||
return Vector3.new(0, 0, 0)
|
||||
end
|
||||
local instantAccel = (currentVelocity - self.PreviousVelocity) / dt
|
||||
self.SmoothedAccel = self.SmoothedAccel:Lerp(instantAccel, alpha)
|
||||
self.PreviousVelocity = currentVelocity
|
||||
return self.SmoothedAccel
|
||||
end
|
||||
|
||||
-- Prediksi waktu impact menggunakan rumus: s = vt + ½at²
|
||||
function AdvancedParryController:PredictImpactTime(distance, currentVelocity, accel)
|
||||
local v = currentVelocity.Magnitude
|
||||
local a = accel.Magnitude
|
||||
if abs(a) < 0.001 then
|
||||
return (v > 0) and (distance / v) or math.huge
|
||||
end
|
||||
local discriminant = v * v + 2 * a * distance
|
||||
if discriminant < 0 then return math.huge end
|
||||
local t1 = (-v + sqrt(discriminant)) / a
|
||||
local t2 = (-v - sqrt(discriminant)) / a
|
||||
local t = math.huge
|
||||
if t1 > 0 then t = t1 end
|
||||
if t2 > 0 and t2 < t then t = t2 end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Prediksi posisi bola saat impact (asumsi percepatan konstan)
|
||||
function AdvancedParryController:PredictImpactPosition(ballPos, currentVelocity, accel, t)
|
||||
return ballPos + currentVelocity * t + 0.5 * accel * t * t
|
||||
end
|
||||
|
||||
-- Hitung sudut incoming: antara arah kecepatan bola dan vektor dari bola ke HRP
|
||||
function AdvancedParryController:ComputeIncomingAngle(ballVelocity, HRP, ballPos)
|
||||
local mag = ballVelocity.Magnitude
|
||||
if mag <= 0 then return 180 end
|
||||
local incomingVec = (HRP.Position - ballPos).Unit
|
||||
local velDir = ballVelocity.Unit
|
||||
local dot = clamp(velDir:Dot(incomingVec), -1, 1)
|
||||
return deg(acos(dot))
|
||||
end
|
||||
|
||||
-- Update state berdasarkan data bola, karakter, prediksi, dan validasi ekstra
|
||||
function AdvancedParryController:UpdateState(dt)
|
||||
local now = tick()
|
||||
local ball, tImpactCandidate = self:SelectTargetBall()
|
||||
if not ball then
|
||||
self.State = STATE_IDLE
|
||||
AdvancedLog("DEBUG", "Tidak ada bola target ditemukan.", nil)
|
||||
self:InitializeBallListener()
|
||||
return
|
||||
end
|
||||
self.CurrentBall = ball
|
||||
|
||||
local character = self.Player.Character
|
||||
local HRP = character and character:FindFirstChild("HumanoidRootPart")
|
||||
if not HRP then
|
||||
self.State = STATE_IDLE
|
||||
AdvancedLog("WARN", "HumanoidRootPart tidak ditemukan.", nil)
|
||||
return
|
||||
end
|
||||
|
||||
local success, ballVelocity = pcall(function() return ball.zoomies.VectorVelocity end)
|
||||
if not success or typeof(ballVelocity) ~= "Vector3" then
|
||||
AdvancedLog("ERROR", "VectorVelocity bola tidak valid.", {Error = tostring(ballVelocity)})
|
||||
return
|
||||
end
|
||||
|
||||
local speed = ballVelocity.Magnitude
|
||||
local dynamicThreshold = self:ComputeDynamicThreshold()
|
||||
local threshold = min(dynamicThreshold, MAX_IMPACT_TIME)
|
||||
local distance = (HRP.Position - ball.Position).Magnitude
|
||||
|
||||
-- Hitung percepatan, waktu impact, dan incoming angle
|
||||
local prevVelocity = self.PreviousVelocity or ballVelocity
|
||||
local accel = self:SmoothAcceleration(ballVelocity, dt)
|
||||
local tImpact = self:PredictImpactTime(distance, ballVelocity, accel)
|
||||
local incomingAngle = self:ComputeIncomingAngle(ballVelocity, HRP, ball.Position)
|
||||
|
||||
-- Validasi target bola
|
||||
local targetAttr = ball:GetAttribute("target")
|
||||
if type(targetAttr) ~= "string" or targetAttr ~= self.Player.Name then
|
||||
self.State = STATE_IDLE
|
||||
AdvancedLog("DEBUG", "Bola tidak mengincar local player.", {Target = tostring(targetAttr)})
|
||||
return
|
||||
end
|
||||
|
||||
-- MODE OVERPOWER: Jika tImpact sudah sangat rendah, trigger parry langsung (bypass kondisi normal)
|
||||
if self.OverpowerMode and tImpact <= threshold * 1.5 then
|
||||
self:AttemptParry()
|
||||
self.State = STATE_PARIED
|
||||
AdvancedLog("ACTION", "OVERPOWER: Forced parry triggered.", {tImpact = tImpact, IncomingAngle = incomingAngle})
|
||||
end
|
||||
|
||||
-- Jika bola berada dalam rentang 10-20 studs dengan incoming angle rendah
|
||||
if distance >= 10 and distance <= 20 and incomingAngle < INCOMING_ANGLE_THRESHOLD then
|
||||
self:AttemptParry()
|
||||
self.State = STATE_PARIED
|
||||
AdvancedLog("ACTION", "Direct parry triggered.", {Distance = distance, Angle = incomingAngle, tImpact = tImpact})
|
||||
end
|
||||
|
||||
-- Mode SPAM: Jika bola sangat dekat (<= 10 studs)
|
||||
if distance <= 10 then
|
||||
if self.State ~= STATE_SPAM then
|
||||
self.State = STATE_SPAM
|
||||
AdvancedLog("INFO", "Entering SPAM mode.", {Distance = distance})
|
||||
end
|
||||
local spamInterval = max(self.BaseSpamInterval * (1 - (speed / 100)), 0.03)
|
||||
if now - self.LastSpamTime >= spamInterval then
|
||||
self:AttemptParry()
|
||||
self.LastSpamTime = now
|
||||
AdvancedLog("ACTION", "Spam parry triggered.", {Distance = distance, SpamInterval = spamInterval})
|
||||
end
|
||||
return
|
||||
elseif self.State == STATE_SPAM then
|
||||
self.State = STATE_TRACKING
|
||||
AdvancedLog("INFO", "Exiting SPAM mode.", {Distance = distance})
|
||||
end
|
||||
|
||||
-- Jika kecepatan bola sangat rendah, anggap bola berhenti
|
||||
if speed < 0.1 then
|
||||
if not self.BallStopped then
|
||||
self.BallStopped = true
|
||||
self.State = STATE_IDLE
|
||||
AdvancedLog("INFO", "Ball stopped.", {Speed = speed})
|
||||
end
|
||||
return
|
||||
elseif self.BallStopped then
|
||||
self.BallStopped = false
|
||||
if self.State == STATE_IDLE then
|
||||
self.State = STATE_TRACKING
|
||||
AdvancedLog("INFO", "Ball resumed.", {Speed = speed})
|
||||
end
|
||||
end
|
||||
|
||||
-- Hitung curve angle (perubahan arah) antara frame sebelumnya dan saat ini
|
||||
local curveAngle = 0
|
||||
if prevVelocity.Magnitude > 0 and ballVelocity.Magnitude > 0 then
|
||||
local dot = clamp(prevVelocity.Unit:Dot(ballVelocity.Unit), -1, 1)
|
||||
curveAngle = deg(acos(dot))
|
||||
end
|
||||
|
||||
AdvancedLog("DEBUG", "Ball metrics updated.", {
|
||||
Speed = speed,
|
||||
CurveAngle = curveAngle,
|
||||
Distance = distance,
|
||||
tImpact = tImpact,
|
||||
DynamicThreshold = dynamicThreshold,
|
||||
IncomingAngle = incomingAngle,
|
||||
State = self.State
|
||||
})
|
||||
|
||||
if tImpact < dynamicThreshold * 0.5 and self.State ~= STATE_ANTICIPATING then
|
||||
self.State = STATE_ANTICIPATING
|
||||
AdvancedLog("INFO", "Entering ANTICIPATING.", {tImpact = tImpact})
|
||||
end
|
||||
|
||||
if (self.State == STATE_TRACKING or self.State == STATE_ANTICIPATING) then
|
||||
if tImpact <= threshold or (curveAngle >= 15 and tImpact <= MAX_IMPACT_TIME) then
|
||||
self:AttemptParry()
|
||||
self.State = STATE_PARIED
|
||||
AdvancedLog("ACTION", "Parry triggered.", {tImpact = tImpact, CurveAngle = curveAngle})
|
||||
end
|
||||
end
|
||||
|
||||
if self.State == STATE_PARIED then
|
||||
local effectiveCooldown = self.OverpowerMode and self.OverpowerCooldown or self.CooldownTime
|
||||
if now - self.LastParryTime >= (threshold + effectiveCooldown) then
|
||||
self.State = STATE_COOLDOWN
|
||||
AdvancedLog("INFO", "Entering COOLDOWN.", {Elapsed = now - self.LastParryTime})
|
||||
end
|
||||
end
|
||||
|
||||
if self.State == STATE_COOLDOWN then
|
||||
local effectiveCooldownFinish = self.OverpowerMode and (self.OverpowerCooldown + 0.05) or (self.CooldownTime + 0.5)
|
||||
if now - self.LastParryTime >= (threshold + effectiveCooldownFinish) then
|
||||
self.State = STATE_TRACKING
|
||||
AdvancedLog("INFO", "Cooldown finished, resuming TRACKING.", nil)
|
||||
end
|
||||
end
|
||||
|
||||
if curveAngle > 45 and tImpact < threshold then
|
||||
self.State = STATE_TRACKING
|
||||
AdvancedLog("INFO", "Reset parry due to sharp curve.", {CurveAngle = curveAngle})
|
||||
end
|
||||
|
||||
if distance <= 0 then
|
||||
AdvancedLog("WARN", "Invalid distance.", {Distance = distance})
|
||||
end
|
||||
|
||||
-- Update label tambahan dengan informasi penting (misal: ping & state)
|
||||
if self.AdditionalInfoLabel then
|
||||
local pingMS = math.floor(self:GetLocalPing() * 1000)
|
||||
self.AdditionalInfoLabel.Text = string.format("Ping: %d ms | State: %s", pingMS, self.State)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------
|
||||
-- Main Update Loop
|
||||
-------------------------------
|
||||
local controller = AdvancedParryController.new()
|
||||
controller:InitializeBallListener()
|
||||
controller:InitializeGUI()
|
||||
|
||||
RunService.Heartbeat:Connect(function(dt)
|
||||
pcall(function()
|
||||
controller:UpdateState(dt)
|
||||
end)
|
||||
end)
|
Loading…
Reference in New Issue
Block a user