
KeylessUniversal

do
local genv = getgenv and getgenv() or nil
if not (genv and genv["LPH_NO_VIRTUALIZE"]) and not _G["LPH_NO_VIRTUALIZE"] then
loadstring([[ function LPH_NO_VIRTUALIZE(f) return f end ]])()
end
end
local function PP_SCRAMBLE_STR(v) return v end
local function PP_SCRAMBLE_NUM(v) return v end
local function PP_SCRAMBLE_RE_NUM(v) return v end
_G.PP_SCRAMBLE_STR = _G.PP_SCRAMBLE_STR or PP_SCRAMBLE_STR
_G.PP_SCRAMBLE_NUM = _G.PP_SCRAMBLE_NUM or PP_SCRAMBLE_NUM
_G.PP_SCRAMBLE_RE_NUM = _G.PP_SCRAMBLE_RE_NUM or PP_SCRAMBLE_RE_NUM
shared = shared or {}
shared.Lycoris = shared.Lycoris or { silent = false }
local NO_HITBOX_ONLY = true
local NO_HITBOX_REACTION_BIAS_SECONDS = -0.045
local NO_HITBOX_SKILL_POLL_SECONDS = 0.015
local NO_HITBOX_PART_POLL_SECONDS = 0.03
local NO_HITBOX_MIN_BLOCK_SECONDS = 0.35
local NO_HITBOX_ACTIVE_GUARD_PULSE_SECONDS = 0.18
local activeSkillDef = nil
local executeDefensiveAction = function(_, _, actionFn)
if type(actionFn) == "function" then
return actionFn()
end
return nil
end
local handleAutoCounterTiming = function() end
local Modules = {}
local function defineModule(name, factory)
Modules[name] = factory
end
local function require(name)
local module = Modules[name]
if module == nil then
error('Missing module: ' .. tostring(name))
end
if type(module) == 'function' then
local result = module()
Modules[name] = result
return result
end
return module
end
-- MODULE: Features/Combat/AttributeListener
defineModule("Features/Combat/AttributeListener", function()
-- AttributeListener module.
local AttributeListener = {}
---@modules Utility.Maid
local Maid = require("Utility/Maid")
---@module Utility.Signal
local Signal = require("Utility/Signal")
-- Services.
local players = game:GetService("Players")
local function soundPredictionSeconds()
return 0.15 + Defender.rdelay()
end
-- Attribute maid.
local attributeMaid = Maid.new()
---On character added.
---@param character Model
local function onCharacterAdded(character)
end
---On character removing.
---@param character Model
local function onCharacterRemoving(character)
end
---Is block currently held? (FHeld)
---@return boolean
function AttributeListener.fheld()
local localPlayer = players.LocalPlayer
local character = localPlayer and localPlayer.Character
if not character then
return false
end
local fheld = character:FindFirstChild("FHeld")
if not fheld or not fheld:IsA("BoolValue") then
local live = workspace:FindFirstChild("Live")
local liveChar = live and localPlayer and live:FindFirstChild(localPlayer.Name)
fheld = liveChar and liveChar:FindFirstChild("FHeld")
if not fheld or not fheld:IsA("BoolValue") then
return false
end
end
return fheld.Value == true
end
---Initialize AttributeListener module.
function AttributeListener.init()
local localPlayer = players.LocalPlayer
local characterAddedSignal = Signal.new(localPlayer.CharacterAdded)
local characterRemovingSignal = Signal.new(localPlayer.CharacterRemoving)
attributeMaid:add(characterAddedSignal:connect("AttributeListener_OnCharacterAdded", function(character)
onCharacterAdded(character)
end))
attributeMaid:add(characterRemovingSignal:connect("AttributeListener_OnCharacterRemoving", function(character)
onCharacterRemoving(character)
end))
if localPlayer.Character then
onCharacterAdded(localPlayer.Character)
end
end
---Detach AttributeListener module.
function AttributeListener.detach()
attributeMaid:clean()
end
-- Return AttributeListener module.
return AttributeListener
end)
-- MODULE: Features/Combat/Defense
defineModule("Features/Combat/Defense", function()
---@module Utility.Signal
local Signal = require('Utility/Signal')
---@module Utility.Maid
local Maid = require('Utility/Maid')
---@module Features.Combat.Objects.AnimatorDefender
local AnimatorDefender = require('Features/Combat/Objects/AnimatorDefender')
---@module Features.Combat.Objects.SoundDefender
local SoundDefender = require('Features/Combat/Objects/SoundDefender')
---@module Features.Combat.Objects.PartDefender
local PartDefender = require('Features/Combat/Objects/PartDefender')
---@module Features.Combat.PositionHistory
local PositionHistory = require('Features/Combat/PositionHistory')
---@module Utility.Logger
local Logger = require('Utility/Logger')
---@module Utility.TaskSpawner
local TaskSpawner = require('Utility/TaskSpawner')
local Defense = {}
local players = game:GetService('Players')
local runService = game:GetService('RunService')
local workspaceService = game:GetService('Workspace')
local defenseMaid = Maid.new()
local liveMaid = Maid.new()
local defenderObjects = {}
local defenderAnimationObjects = {}
local defenderSoundObjects = {}
local defenderPartObjects = {}
local lastHistoryUpdate = os.clock()
local addAnimatorDefender = LPH_NO_VIRTUALIZE(function(animator)
local animationDefender = AnimatorDefender.new(animator)
defenderObjects[animator] = animationDefender
defenderAnimationObjects[animator] = animationDefender
end)
local addSoundDefender = LPH_NO_VIRTUALIZE(function(sound)
local part = sound:FindFirstAncestorWhichIsA('BasePart')
if not part then
return
end
local soundDefender = SoundDefender.new(sound, part)
defenderObjects[sound] = soundDefender
defenderSoundObjects[sound] = soundDefender
end)
local addPartDefender = LPH_NO_VIRTUALIZE(function(part)
local partDefender = PartDefender.new(part)
if not partDefender then
return
end
defenderObjects[part] = partDefender
defenderPartObjects[part] = partDefender
end)
local onLiveDescendantAdded = LPH_NO_VIRTUALIZE(function(descendant)
if descendant:IsA('Animator') then
return addAnimatorDefender(descendant)
end
if descendant:IsA('Sound') then
return addSoundDefender(descendant)
end
end)
local function isStandModel(model)
if not model or not model:IsA("Model") then
return false
end
local stands = workspaceService:FindFirstChild("Stands")
if stands and model:IsDescendantOf(stands) then
return true
end
local live = workspaceService:FindFirstChild("Live")
if not live then
return false
end
for _, entity in next, live:GetChildren() do
local standValue = entity:FindFirstChild("STANDOBJ")
if standValue and standValue.Value and standValue.Value:IsA("Model") then
if model == standValue.Value or model:IsDescendantOf(standValue.Value) then
return true
end
end
end
return false
end
local onWorldDescendantAdded = LPH_NO_VIRTUALIZE(function(descendant)
if descendant:IsA('Animator') then
local model = descendant:FindFirstAncestorWhichIsA("Model")
if isStandModel(model) then
return addAnimatorDefender(descendant)
end
return
end
if descendant:IsA("AnimationController") then
local model = descendant:FindFirstAncestorWhichIsA("Model")
if isStandModel(model) then
local animator = descendant:FindFirstChildOfClass("Animator")
if not animator then
animator = Instance.new("Animator")
animator.Parent = descendant
end
return addAnimatorDefender(animator)
end
return
end
if descendant:IsA("Model") and isStandModel(descendant) then
for _, child in next, descendant:GetDescendants() do
if child:IsA("Animator") then
addAnimatorDefender(child)
elseif child:IsA("AnimationController") then
local animator = child:FindFirstChildOfClass("Animator")
if not animator then
animator = Instance.new("Animator")
animator.Parent = child
end
addAnimatorDefender(animator)
end
end
return
end
if descendant:IsA('BasePart') then
return addPartDefender(descendant)
end
end)
local onLiveDescendantRemoved = LPH_NO_VIRTUALIZE(function(descendant)
local object = defenderObjects[descendant]
if not object then
return
end
object:detach()
defenderObjects[descendant] = nil
defenderAnimationObjects[descendant] = nil
defenderSoundObjects[descendant] = nil
defenderPartObjects[descendant] = nil
end)
local liveInstance = nil
local function clearLiveState()
for _, object in next, defenderObjects do
object:detach()
end
defenderObjects = {}
defenderAnimationObjects = {}
defenderSoundObjects = {}
defenderPartObjects = {}
end
local function attachLive(live)
if liveInstance == live then
return
end
liveInstance = live
liveMaid:clean()
clearLiveState()
if not live then
return
end
liveMaid:mark(live.DescendantAdded:Connect(onLiveDescendantAdded))
liveMaid:mark(live.DescendantRemoving:Connect(onLiveDescendantRemoved))
for _, descendant in next, live:GetDescendants() do
onLiveDescendantAdded(descendant)
end
end
local updateHistory = LPH_NO_VIRTUALIZE(function()
lastHistoryUpdate = os.clock()
local character = players.LocalPlayer and players.LocalPlayer.Character
if not character then
return
end
local humanoidRootPart = character:FindFirstChild('HumanoidRootPart')
if not humanoidRootPart then
return
end
PositionHistory.add(players.LocalPlayer, humanoidRootPart.CFrame, tick())
for _, player in next, players:GetPlayers() do
if player == players.LocalPlayer then
continue
end
local pcharacter = player.Character
if not pcharacter then
continue
end
local proot = pcharacter:FindFirstChild('HumanoidRootPart')
if not proot then
continue
end
PositionHistory.add(pcharacter, proot.CFrame, tick())
end
end)
local updateDefenders = LPH_NO_VIRTUALIZE(function()
for _, object in next, defenderAnimationObjects do
if object and object.update then
object:update()
end
end
for _, object in next, defenderPartObjects do
if object and object.update then
object:update()
end
end
end)
function Defense.init()
local renderStepped = Signal.new(runService.RenderStepped)
local postSimulation = Signal.new(runService.PostSimulation)
defenseMaid:mark(renderStepped:connect('Defense_UpdateHistory', updateHistory))
defenseMaid:mark(postSimulation:connect('Defense_UpdateDefenders', updateDefenders))
defenseMaid:mark(workspaceService.DescendantAdded:Connect(onWorldDescendantAdded))
defenseMaid:mark(workspaceService.DescendantRemoving:Connect(onLiveDescendantRemoved))
defenseMaid:mark(workspaceService.ChildAdded:Connect(function(child)
if child.Name == "Live" then
attachLive(child)
end
end))
defenseMaid:mark(workspaceService.ChildRemoved:Connect(function(child)
if child == liveInstance then
attachLive(nil)
end
end))
attachLive(workspaceService:FindFirstChild("Live"))
for _, descendant in next, workspaceService:GetDescendants() do
onWorldDescendantAdded(descendant)
end
Logger.warn('Defense initialized (animation + sound).')
end
function Defense.detach()
liveMaid:clean()
clearLiveState()
defenseMaid:clean()
liveInstance = nil
Logger.warn('Defense detached.')
end
function Defense.blocking()
for _, object in next, defenderAnimationObjects do
if object and object.blocking and object:blocking() then
return true
end
end
return false
end
return Defense
end)
-- MODULE: Features/Combat/Objects/AnimatorDefender
defineModule("Features/Combat/Objects/AnimatorDefender", function()
---@module Features.Combat.Objects.Defender
local Defender = require("Features/Combat/Objects/Defender")
---@module Utility.Signal
local Signal = require("Utility/Signal")
---@module Game.Timings.SaveManager
local SaveManager = require("Game/Timings/SaveManager")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.InputClient
local InputClient = require("Game/InputClient")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Game.Timings.PlaybackData
local PlaybackData = require("Game/Timings/PlaybackData")
local M1AutoTrade = require("Features/Combat/M1AutoTrade")
---@module GUI.Library
local Library = require("GUI/Library")
local workspaceService = game:GetService("Workspace")
local function isBlacklistedM1(aid)
if not Library or not Library.InfoLoggerData or not Library.InfoLoggerData.KeyBlacklistList then
return false
end
return Library.InfoLoggerData.KeyBlacklistList[aid] == true
end
_G.isBlacklistedM1 = isBlacklistedM1
local function isStandModel(model)
if not model or not model:IsA("Model") then
return false
end
local stands = workspaceService:FindFirstChild("Stands")
if stands and model:IsDescendantOf(stands) then
return true
end
local live = workspaceService:FindFirstChild("Live")
if not live then
return false
end
for _, entity in next, live:GetChildren() do
local standValue = entity:FindFirstChild("STANDOBJ")
if standValue and standValue.Value and standValue.Value:IsA("Model") then
if model == standValue.Value or model:IsDescendantOf(standValue.Value) then
return true
end
end
end
return false
end
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module Features.Combat.Objects.RepeatInfo
local RepeatInfo = require("Features/Combat/Objects/RepeatInfo")
---@module Features.Combat.Objects.HitboxOptions
local HitboxOptions = require("Features/Combat/Objects/HitboxOptions")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Game.Timings.PartTiming
local PartTiming = require("Game/Timings/PartTiming")
---@module Game.Timings.Action
local Action = require("Game/Timings/Action")
---@module Features.Combat.Objects.Task
local Task = require("Features/Combat/Objects/Task")
---@module Utility.TaskSpawner
local TaskSpawner = require("Utility/TaskSpawner")
---@module Features.Combat.PositionHistory
local PositionHistory = require("Features/Combat/PositionHistory")
---@module Utility.OriginalStore
local OriginalStore = require("Utility/OriginalStore")
local runService = game:GetService("RunService")
local hcCache = {}
local hcCacheFrame = 0
runService.RenderStepped:Connect(LPH_NO_VIRTUALIZE(function()
hcCacheFrame = hcCacheFrame + 1
hcCache = {}
end))
local function hcKey(prefix, options, position, hitbox, shape, timing, action)
local ent = options.entity or options.part or options.cframe or ""
local filter = options.filter or ""
local timingId = timing and (timing._id or timing.name) or ""
local actionId = action and (action._type or action.name) or ""
local actionWhen = action and action._when or ""
local pos = position or Vector3.zero
local ptime = options.ptime or 0
local spredict = options.spredict and 1 or 0
local fhb = timing and timing.fhb or false
local duih = timing and timing.duih or false
return string.format(
"%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%.4f|%.2f,%.2f,%.2f|%.2f,%.2f,%.2f",
prefix or "base",
tostring(ent),
tostring(filter),
tostring(shape),
tostring(timingId),
tostring(actionId),
tostring(actionWhen),
tostring(spredict),
tostring(fhb),
tostring(duih),
ptime,
hitbox.X, hitbox.Y, hitbox.Z,
pos.X, pos.Y, pos.Z
)
end
---@module Features.Combat.AttributeListener
local AttributeListener = require("Features/Combat/AttributeListener")
---@class AnimatorDefender: Defender
---@field animator Animator
---@field entity Model
---@field kfmaid Maid
---@field heffects Instance[]
---@field keyframes Action[]
---@field offset number?
---@field timing AnimationTiming?
---@field pbdata table<AnimationTrack, PlaybackData> Playback data to be recorded.
---@field rpbdata table<string, PlaybackData> Recorded playback data. Optimization so we don't have to constantly reiterate over recorded data.
---@field manimations table<number, Animation>
---@field track AnimationTrack? Don't be confused. This is the **valid && last** animation track played.
---@field maid Maid This maid is cleaned up after every new animation track. Safe to use for on-animation-track setup.
local AnimatorDefender = setmetatable({}, { __index = Defender })
AnimatorDefender.__index = AnimatorDefender
AnimatorDefender.__type = "Animation"
-- Services.
local players = game:GetService("Players")
-- Constants.
local MAX_REPEAT_TIME = 5.0
local HISTORY_STEPS = 5.0
local PREDICT_FACING_DELTA = 0.3
---Is animation stopped? Made into a function for de-duplication.
---@param self AnimatorDefender
---@param track AnimationTrack
---@param timing AnimationTiming
---@return boolean
AnimatorDefender.stopped = LPH_NO_VIRTUALIZE(function(self, track, timing)
if
not NO_HITBOX_ONLY
and Configuration.expectToggleValue("AllowFailure")
and not timing.rpue
and Random.new():NextNumber(1.0, 100.0) <= (Configuration.expectOptionValue("IgnoreAnimationEndRate") or 0.0)
then
return false, self:notify(timing, "Intentionally ignoring animation end to simulate human error.")
end
if not timing.iae and not track.IsPlaying then
return true, self:notify(timing, "Animation stopped playing.")
end
if timing.iae and not timing.ieae and not track.IsPlaying and track.TimePosition < track.Length then
return true, self:notify(timing, "Animation stopped playing early.")
end
end)
---Repeat conditional. Extra parameter 'track' added on.
---@param self AnimatorDefender
---@param info RepeatInfo
---@return boolean
AnimatorDefender.rc = LPH_NO_VIRTUALIZE(function(self, info)
---@note: There are cases where we might not have a track. If it's not handled properly, it will throw an error.
-- Perhaps, the animation can end and we're handling a different repeat conditional.
if not info.track then
return Logger.warn(
"(%s) Did you forget to pass the track? Or perhaps you forgot to place a hook before using this function.",
PP_SCRAMBLE_STR(info.timing.name)
)
end
if self:stopped(info.track, info.timing) then
return false
end
if info.timing.iae and os.clock() - info.start >= ((info.timing.mat / 1000) or MAX_REPEAT_TIME) then
return self:notify(info.timing, "Max animation timeout exceeded.")
end
return true
end)
---Run predict facing hitbox check.
---@param self AnimatorDefender
---@param options HitboxOptions
---@return boolean
AnimatorDefender.pfh = LPH_NO_VIRTUALIZE(function(self, options)
local yrate = PositionHistory.yrate(self.entity)
if not yrate then
return false
end
local root = self.entity:FindFirstChild("HumanoidRootPart")
if not root then
return false
end
local localRoot = players.LocalPlayer.Character and players.LocalPlayer.Character:FindFirstChild("HumanoidRootPart")
if not localRoot then
return false
end
if math.abs(yrate) < PREDICT_FACING_DELTA then
return
end
local clone = options:clone()
clone.spredict = false
clone.hcolor = Color3.new(0, 1, 1)
clone.mcolor = Color3.new(1, 1, 0)
local result = false
local store = OriginalStore.new()
store:run(root, "CFrame", CFrame.lookAt(root.Position, localRoot.Position), function()
result = self:hc(clone, nil)
end)
return result
end)
---Run past hitbox check.
---@param timing Timing
---@param options HitboxOptions
---@return boolean
AnimatorDefender.phd = LPH_NO_VIRTUALIZE(function(self, timing, options)
for _, cframe in next, PositionHistory.stepped(self.entity, HISTORY_STEPS, timing.phds) or {} do
local clone = options:clone()
clone.spredict = false
clone.cframe = cframe
clone.hcolor = Color3.new(0.839215, 0.976470, 0.537254)
clone.mcolor = Color3.new(0.564705, 0, 1)
if not self:hc(clone, nil) then
continue
end
return true
end
end)
---Get extrapolated seconds.
---@param self Defender
---@param timing AnimationTiming
---@return number
AnimatorDefender.fsecs = LPH_NO_VIRTUALIZE(function(self, timing)
local player = players:GetPlayerFromCharacter(self.entity)
local sd = (player and player:GetAttribute("AveragePing") or 50.0) / 2000
return (timing.pfht or 0.15) + (sd + Defender.rdelay())
end)
---Run our facing extrapolation / interpolation.
AnimatorDefender.fpc = LPH_NO_VIRTUALIZE(function(self, timing, options)
if timing.duih then
return false
end
if timing.pfh and self:pfh(options) then
return true
end
if timing.phd and self:phd(timing, options) then
return true
end
end)
---Check if we're in a valid state to proceed with the action.
---@param self AnimatorDefender
---@param timing AnimationTiming
---@param action Action
---@return boolean
AnimatorDefender.valid = LPH_NO_VIRTUALIZE(function(self, timing, action)
if not Defender.valid(self, timing, action) then
return false
end
if not self.track then
return self:notify(timing, "No current track.")
end
if not self.entity then
return self:notify(timing, "No entity found.")
end
local target = self:target(self.entity)
if not target then
return self:notify(timing, "Not a viable target.")
end
local root = self.entity:FindFirstChild("HumanoidRootPart")
if not root then
return self:notify(timing, "No humanoid root part found.")
end
if self:stopped(self.track, timing) then
return false
end
local options = HitboxOptions.new(root, timing)
options.spredict = not timing.duih and not timing.dp
options.ptime = self:fsecs(timing)
options.action = action
options.entity = self.entity
local info = RepeatInfo.new(timing)
info.track = self.track
local hc = self:hc(options, timing.duih and info or nil)
if hc then
return true
end
local pc = self:fpc(timing, options)
if pc then
return true
end
return self:notify(timing, "Not in hitbox.")
end)
---Add a new Keyframe action.
---@param self AnimatorDefender
---@param action Action
---@param tp number
function AnimatorDefender:akeyframe(action, tp)
-- Set time position.
action.tp = tp
---@note: These have to be sent in by a module, so the hitbox and the name also have to get fixed.
action["_type"] = PP_SCRAMBLE_STR(action["_type"])
action["name"] = PP_SCRAMBLE_STR(action["name"])
action["hitbox"] = Vector3.new(
PP_SCRAMBLE_RE_NUM(action["hitbox"].X),
PP_SCRAMBLE_RE_NUM(action["hitbox"].Y),
PP_SCRAMBLE_RE_NUM(action["hitbox"].Z)
)
if action["shape"] ~= nil then
action["shape"] = PP_SCRAMBLE_STR(action["shape"])
end
if action["offset"] then
action["offset"] = Vector3.new(
PP_SCRAMBLE_RE_NUM(action["offset"].X),
PP_SCRAMBLE_RE_NUM(action["offset"].Y),
PP_SCRAMBLE_RE_NUM(action["offset"].Z)
)
end
-- Insert in list.
table.insert(self.keyframes, action)
end
---Get time position of current track.
---@return number?
function AnimatorDefender:tp()
if not self.track or self.offset == nil then
return nil
end
---@note: Compensate for ping. Convert seconds to time position by adjusting for speed.
--- Higher speed means it will delay earlier.
--- Smaller speed means it will delay later.
return self.track.TimePosition + ((self.offset + self.sdelay()) / self.track.Speed)
end
---Get latest keyframe action that we've exceeded.
---@return Action?
AnimatorDefender.latest = LPH_NO_VIRTUALIZE(function(self)
local latestKeyframe = nil
local latestTimePosition = nil
for _, keyframe in next, self.keyframes do
if (self:tp() or 0.0) <= keyframe.tp then
continue
end
if latestTimePosition and keyframe.tp <= latestTimePosition then
continue
end
latestTimePosition = keyframe.tp
latestKeyframe = keyframe
end
return latestKeyframe
end)
---Update handling.
---@param self AnimatorDefender
AnimatorDefender.update = LPH_NO_VIRTUALIZE(function(self)
for track, data in next, self.pbdata do
-- Don't process tracks.
if not Configuration.expectToggleValue("ShowAnimationVisualizer") then
self.pbdata[track] = nil
continue
end
-- Check if the track is playing.
if not track.IsPlaying then
-- Remove out of 'pbdata' and put it in to the recorded table.
self.pbdata[track] = nil
self.rpbdata[tostring(track.Animation.AnimationId)] = data
-- Continue to next playback data.
continue
end
-- Start tracking the animation's speed.
data:astrack(track.Speed)
end
-- Run on validated track & timing.
if not self.track or not self.timing then
return
end
if not self.track.IsPlaying then
return
end
-- Find the latest keyframe that we have exceeded, if there is even any.
local latest = self:latest()
if not latest then
return
end
-- Clear the keyframes that we have exceeded.
local tp = self:tp() or 0.0
for idx, keyframe in next, self.keyframes do
if tp <= keyframe.tp then
continue
end
self.keyframes[idx] = nil
end
-- Log.
self:notify(
self.timing,
"(%.2f) (really %.2f) Keyframe action '%s' with type '%s' is being executed.",
tp,
self.track.TimePosition,
PP_SCRAMBLE_STR(latest.name),
PP_SCRAMBLE_STR(latest._type)
)
-- Ok, run action of this keyframe.
self.maid:mark(
TaskSpawner.spawn(
string.format("KeyframeAction_%s", PP_SCRAMBLE_STR(latest._type)),
self.handle,
self,
self.timing,
latest,
false
)
)
end)
---Virtualized processing checks.
---@param track AnimationTrack
---@return boolean
function AnimatorDefender:pvalidate(track)
if track.Priority == Enum.AnimationPriority.Core then
return false
end
return true
end
---Process animation track.
---@todo: AP telemetry - aswell as tracking effects that are added with timestamps and current ping to that list.
---@param self AnimatorDefender
---@param track AnimationTrack
AnimatorDefender.process = LPH_NO_VIRTUALIZE(function(self, track)
local localCharacter = players.LocalPlayer.Character
local isLocal = localCharacter and self.entity == localCharacter
if not self:pvalidate(track) then
return
end
-- Clean up Keyframe maid.
self.kfmaid:clean()
-- Add to playback data list.
if Configuration.expectToggleValue("ShowAnimationVisualizer") then
self.pbdata[track] = PlaybackData.new(self.entity)
end
-- Animation ID.
local aid = tostring(track.Animation.AnimationId)
-- In logging range?
local distance = self:distance(self.entity)
local logAll = Configuration.expectToggleValue("LogAllAnimations")
if logAll and not distance then
distance = 0
end
local ilr = logAll
or (
distance
and distance >= (Configuration.expectOptionValue("MinimumLoggerDistance") or 0)
and distance <= (Configuration.expectOptionValue("MaximumLoggerDistance") or 0)
)
-- Keyframe logging.
local keyframeReached = Signal.new(track.KeyframeReached)
self.kfmaid:add(keyframeReached:connect("AnimationDefender_OnKeyFrameReached", function(kfname)
if not ilr then
return
end
Library:AddKeyFrameEntry(distance, aid, kfname, track.TimePosition, false)
end))
---@type AnimationTiming?
local timing = self:initial(self.entity, SaveManager.as, self.entity.Name, aid)
if not timing then
if isLocal then
M1AutoTrade.handleLocalM1(nil, track)
end
return
end
if ilr then
Library:AddExistAnimEntry(self.entity.Name, distance, timing)
end
if isStandModel(self.entity) then
if Configuration.expectToggleValue("EnableAutoDefense") and isBlacklistedM1(aid) then
InputClient.blockHoldSeconds(0.3)
end
return
end
if isLocal then
M1AutoTrade.handleLocalM1(timing, track)
return
end
if type(handleAutoCounterTiming) == "function" then
handleAutoCounterTiming(timing, "Animation", distance)
end
M1AutoTrade.handle(self, timing, distance)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
local humanoidRootPart = self.entity:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then
return
end
---@note: Clean up previous tasks that are still waiting or suspended because they're in a different track.
self:clean()
-- Set current data.
self.timing = timing
self.track = track
self.offset = self.rdelay()
if timing._id == "rbxassetid://116007627948983" then
local dummyAction = { ihbc = true }
if not self:valid(timing, dummyAction) then
return
end
local dist = distance or self:distance(self.entity) or math.huge
if dist <= 40 then
InputClient.dash("forward")
InputClient.m1Hold(0.8)
else
InputClient.dash("back")
task.delay(1.2, function()
InputClient.dash("back")
end)
end
return
end
-- Fake mistime rate.
---@type Action?
local _, faction = next(timing.actions._data)
-- Obviously, we don't want any modules where we don't know how many actions there are.
-- We don't want any actions that have a count that is not equal to 1.
-- We need to check if we can atleast dash, because we will be going to are fallback.
-- We must also check if our action isn't too short or is not a parry type, defeating the purpose.
if
not NO_HITBOX_ONLY
and Configuration.expectToggleValue("AllowFailure")
and not timing.rpue
and timing.actions:count() == 1
and Random.new():NextNumber(1.0, 100.0) <= (Configuration.expectOptionValue("FakeMistimeRate") or 0.0)
and faction
and PP_SCRAMBLE_STR(faction._type) == "Parry"
and faction:when() > (self.rtt() + 0.6)
then
InputClient.deflect()
self:notify(timing, "Intentionally mistimed to simulate human error.")
end
---@note: Start processing the timing. Add the actions if we're not RPUE.
if not timing.rpue then
return self:actions(timing)
end
if not self:cooldownAllow(timing) then
return
end
-- Start RPUE.
local info = RepeatInfo.new(timing, self.rdelay())
info.track = track
self:mark(Task.new(string.format("RPUE_%s_%i", timing.name, 0), function()
return timing:rsd() - info.irdelay - self.sdelay()
end, timing.punishable, timing.after, self.rpue, self, self.entity, timing, info))
-- Notify.
if not LRM_UserNote or LRM_UserNote == "tester" then
self:notify(
timing,
"Added RPUE '%s' (%.2fs, then every %.2fs) with ping '%.2f' (changing) subtracted.",
PP_SCRAMBLE_STR(timing.name),
timing:rsd(),
timing:rpd(),
self.rtt()
)
else
self:notify(
timing,
"Added RPUE '%s' ([redacted], then every [redacted]) with ping '%.2f' (changing) subtracted.",
PP_SCRAMBLE_STR(timing.name),
self.rtt()
)
end
end)
---Clean up the defender.
function AnimatorDefender:clean()
-- Empty data.
self.keyframes = {}
self.heffects = {}
-- Empty Keyframe maid.
self.kfmaid:clean()
-- Clean through base method.
Defender.clean(self)
end
---Create new AnimatorDefender object.
---@param animator Animator
---@return AnimatorDefender
function AnimatorDefender.new(animator)
local entity = animator:FindFirstAncestorWhichIsA("Model")
if not entity then
return error(string.format("AnimatorDefender.new(%s) - no entity.", animator:GetFullName()))
end
local self = setmetatable(Defender.new(), AnimatorDefender)
local animationPlayed = Signal.new(animator.AnimationPlayed)
self.animator = animator
self.entity = entity
self.kfmaid = Maid.new()
self.track = nil
self.timing = nil
self.rdelay = nil
self.heffects = {}
self.keyframes = {}
self.pbdata = {}
self.rpbdata = {}
self.maid:mark(animationPlayed:connect(
"AnimatorDefender_OnAnimationPlayed",
LPH_NO_VIRTUALIZE(function(track)
self:process(track)
end)
))
return self
end
-- Return AnimatorDefender module.
return AnimatorDefender
end)
-- MODULE: Features/Combat/Objects/Defender
defineModule("Features/Combat/Objects/Defender", function()
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Features.Combat.Objects.Task
local Task = require("Features/Combat/Objects/Task")
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module GUI.Library
local Library = require("GUI/Library")
---@module Utility.TaskSpawner
local TaskSpawner = require("Utility/TaskSpawner")
---@module Features.Combat.Targeting
local Targeting = require("Features/Combat/Targeting")
---@module Features.Combat.PositionHistory
local PositionHistory = require("Features/Combat/PositionHistory")
---@module Features.Combat.Objects.HitboxOptions
local HitboxOptions = require("Features/Combat/Objects/HitboxOptions")
---@module Game.InputClient
local InputClient = require("Game/InputClient")
---@module Features.Combat.AttributeListener
local AttributeListener = require("Features/Combat/AttributeListener")
---@module Game.Keybinding
local Keybinding = require("Game/Keybinding")
---@module Utility.OriginalStore
local OriginalStore = require("Utility/OriginalStore")
---@class Defender
---@field tasks Task[]
---@field tmaid Maid Cleaned up every clean cycle.
---@field rhook table<string, function> Hooked functions that we can restore on clean-up.
---@field markers table<string, boolean> Blocking markers for unknown length timings. If the entry exists and is true, then we're blocking.
---@field maid Maid
---@field hmaid Maid
local Defender = {}
Defender.__index = Defender
Defender.__type = "Defender"
-- Services.
local stats = game:GetService("Stats")
local userInputService = game:GetService("UserInputService")
local players = game:GetService("Players")
local textChatService = game:GetService("TextChatService")
local debrisService = game:GetService("Debris")
-- Constants.
local MAX_VISUALIZATION_TIME = 5.0
local MAX_REPEAT_WAIT = 10.0
local PREDICTION_LENIENCY_MULTI = 5.0
local PREDICTION_EXTRA_SECONDS = 0
local PREDICTION_MAX_SECONDS = 0.5
local PREDICTION_SPEED_REF = 16
local PREDICTION_MIN_SCALE = 0.6
local PREDICTION_MAX_SCALE = 1.4
local blockTagCounter = 0
local defenseCooldowns = setmetatable({}, { __mode = "k" })
local function timingKey(timing)
if timing and type(timing.id) == "function" then
local ok, key = pcall(timing.id, timing)
if ok and key then
return tostring(key)
end
end
return tostring(timing and timing.name or "Unknown")
end
local function nextBlockTagId()
blockTagCounter += 1
return string.format("Defender_StartBlock_%d", blockTagCounter)
end
---Log a miss to the UI library with distance check.
---@param type string
---@param key string
---@param name string?
---@param distance number
---@param parent string? If provided, will be shown in the log.
---@return boolean
function Defender:miss(type, key, name, distance, parent)
if not Configuration.expectToggleValue("ShowLoggerWindow") then
return false
end
if
distance < (Configuration.expectOptionValue("MinimumLoggerDistance") or 0)
or distance > (Configuration.expectOptionValue("MaximumLoggerDistance") or 0)
then
return false
end
Library:AddMissEntry(type, key, name, distance, parent)
return true
end
---Fetch distance.
---@param from Model? | BasePart?
---@return number?
function Defender:distance(from)
if not from then
return
end
local entRootPart = from
if from:IsA("Model") then
entRootPart = from:FindFirstChild("HumanoidRootPart")
end
if not entRootPart then
return
end
local localCharacter = players.LocalPlayer.Character
if not localCharacter then
return
end
local localRootPart = localCharacter:FindFirstChild("HumanoidRootPart")
if not localRootPart then
return
end
return (entRootPart.Position - localRootPart.Position).Magnitude
end
---Find target - hookable function.
---@param self Defender
---@param entity Model
---@return Target?
Defender.target = LPH_NO_VIRTUALIZE(function(self, entity)
return Targeting.find(entity)
end)
---Repeat until parry end.
---@param self Defender
---@param entity Model
---@param timing AnimationTiming
---@param info RepeatInfo
Defender.rpue = LPH_NO_VIRTUALIZE(function(self, entity, timing, info)
local distance = self:distance(entity)
if not distance then
return Logger.warn("Stopping RPUE '%s' because the distance is not valid.", PP_SCRAMBLE_STR(timing.name))
end
if timing and (distance < PP_SCRAMBLE_NUM(timing.imdd) or distance > PP_SCRAMBLE_NUM(timing.imxd)) then
return self:notify(timing, "Distance is out of range.")
end
if not self:rc(info) then
return Logger.warn(
"Stopping RPUE '%s' because the repeat condition is not valid.",
PP_SCRAMBLE_STR(timing.name)
)
end
local target = self:target(entity)
local options = HitboxOptions.new(CFrame.new(), timing)
options.spredict = true
options.part = target and target.root
options.entity = entity
local success = target and self:hc(options, timing.duih and info or nil)
info.index = info.index + 1
self:mark(Task.new(string.format("RPUE_%s_%i", PP_SCRAMBLE_STR(timing.name), info.index), function()
return timing:rpd() - info.irdelay - self.sdelay()
end, timing.punishable, timing.after, self.rpue, self, self.entity, timing, info))
if not target then
return Logger.warn("Skipping RPUE '%s' because the target is not valid.", PP_SCRAMBLE_STR(timing.name))
end
if not success then
return Logger.warn("Skipping RPUE '%s' because we are not in the hitbox.", PP_SCRAMBLE_STR(timing.name))
end
if not timing.srpn then
self:notify(timing, "(%i) Action 'RPUE Parry' is being executed.", info.index)
end
InputClient.block(true)
InputClient.block(false)
end)
---Check if we're in a valid state to proceed with action handling. Extend me.
---@param self Defender
---@param timing Timing
---@param action Action
---@return boolean
Defender.valid = LPH_NO_VIRTUALIZE(function(self, timing, action)
if not Configuration.expectToggleValue("EnableAutoDefense") then
if Configuration.expectToggleValue("EnableM1AutoTrade") then
return self:notify(timing, "Auto Defense disabled (M1 Trade only).")
end
return self:notify(timing, "Auto Defense disabled.")
end
local character = players.LocalPlayer.Character
if not character then
return self:notify(timing, "No character found.")
end
local integer = Random.new():NextNumber(1.0, 100.0)
local rate = Configuration.expectOptionValue("FailureRate") or 0.0
if not NO_HITBOX_ONLY and Configuration.expectToggleValue("AllowFailure") and integer <= rate then
return self:notify(timing, "(%i <= %i) Intentionally did not run.", integer, rate)
end
local selectedFilters = Configuration.expectOptionValue("AutoDefenseFilters") or {}
local function filterEnabled(name, legacy)
return selectedFilters[name] or (legacy and selectedFilters[legacy])
end
local currentState = character:GetAttribute("CurrentState")
if filterEnabled("Disable When Knocked Recently") and AttributeListener.krecently and AttributeListener.krecently() then
return self:notify(timing, "User was knocked recently.")
end
if filterEnabled("Disable When In Dash") and currentState == "Dashing" then
return self:notify(timing, "User is dashing.")
end
if filterEnabled("Disable When In Flashstep") and currentState == "Flashstep" then
return self:notify(timing, "User is flashstepping.")
end
if currentState == "Attacking" or currentState == "Skill" then
return self:notify(timing, "Currently attacking.")
end
local chatInputBarConfiguration = textChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
if
filterEnabled("Disable When Textbox Focused")
and (userInputService:GetFocusedTextBox() or (chatInputBarConfiguration and chatInputBarConfiguration.IsFocused))
then
return self:notify(timing, "User is typing in a text box.")
end
if filterEnabled("Disable When Window Not Active") and iswindowactive and not iswindowactive() then
return self:notify(timing, "Window is not active.")
end
if filterEnabled("Disable When Holding Block")
and userInputService:IsKeyDown((Keybinding.info and Keybinding.info["Block / Parry"]) or Enum.KeyCode.F)
then
return self:notify(timing, "User is holding block.")
end
if timing.tag == "M1" and filterEnabled("Filter Out M1s") then
return self:notify(timing, "Attacker is using a 'M1' attack.")
end
if (timing.tag == "Skill" or timing.tag == "Mantra") and filterEnabled("Filter Out Skills", "Filter Out Mantras") then
return self:notify(timing, "Attacker is using a 'Skill' attack.")
end
if (timing.tag == "Counter" or timing.tag == "Critical")
and filterEnabled("Filter Out Counters", "Filter Out Criticals")
then
return self:notify(timing, "Attacker is using a 'Counter' attack.")
end
if (timing.tag == "Other" or timing.tag == "Undefined")
and filterEnabled("Filter Out Others", "Filter Out Undefined")
then
return self:notify(timing, "Attacker is using an 'Other' attack.")
end
return true
end)
function Defender:cooldownAllow(timing)
local ms = Configuration.expectOptionValue("DefenseCooldownMs") or 0
if NO_HITBOX_ONLY and ms <= 0 then
ms = 45
end
if ms <= 0 then
return true
end
local entity = self.entity or self.owner or self.part
if not entity then
return true
end
local key = timingKey(timing)
local now = os.clock()
local bucket = defenseCooldowns[entity]
if not bucket then
bucket = {}
defenseCooldowns[entity] = bucket
end
local last = bucket[key]
if last and (now - last) < (ms / 1000) then
return false
end
bucket[key] = now
return true
end
---Check if any parts that are in our filter were hit.
---@note: Solara fallback.
local function checkParts(parts, filter)
for _, part in next, parts do
for _, fpart in next, filter do
if part ~= fpart and not part:IsDescendantOf(fpart) then
continue
end
return true
end
end
return false
end
---Visualize a position and size.
---@param self Defender
---@param identifier number? If the identifier is nil, then we will auto-generate one for each visualization.
---@param cframe CFrame
---@param size Vector3
---@param color Color3
---@param shape string?
Defender.visualize = LPH_NO_VIRTUALIZE(function(self, identifier, cframe, size, color, shape)
local id = identifier or self.hmaid:uid()
local vpart = self.hmaid[id]
local okParent, parent = true, nil
if vpart then
okParent, parent = pcall(function()
return vpart.Parent
end)
end
if vpart and (not okParent or parent == nil) then
pcall(function()
vpart:Destroy()
end)
self.hmaid[id] = nil
vpart = nil
end
vpart = vpart or Instance.new("Part")
local hitboxShape = shape == "Ball" and "Ball" or "Box"
local renderSize = size
if hitboxShape == "Ball" then
local radius = math.max(size.X, size.Y, size.Z) / 2
renderSize = Vector3.new(radius * 2, radius * 2, radius * 2)
vpart.Shape = Enum.PartType.Ball
else
vpart.Shape = Enum.PartType.Block
end
local ok = pcall(function()
vpart.Parent = workspace
end)
if not ok then
self.hmaid[id] = nil
return
end
vpart.Anchored = true
vpart.CanCollide = false
vpart.CanQuery = false
vpart.CanTouch = false
vpart.Material = Enum.Material.ForceField
vpart.CastShadow = false
vpart.Size = renderSize
vpart.CFrame = cframe
vpart.Color = color
vpart.Name = string.format("RW_Visualization_%i", id)
vpart.Transparency = Configuration.expectToggleValue("EnableVisualizations") and 0.2 or 1.0
if self.hmaid[id] then
return
end
self.hmaid[id] = vpart
debrisService:AddItem(vpart, MAX_VISUALIZATION_TIME)
end)
---Run hitbox check. Returns wheter if the hitbox is being touched.
---@todo: An issue is that the player's current look vector will not be the same as when they attack due to a parry timing being seperate from the attack causing this check to fail.
---@param self Defender
---@param cframe CFrame
---@param fd boolean
---@param size Vector3
---@param filter Instance[]
---@param shape string?
---@return boolean?, CFrame?
Defender.hitbox = LPH_NO_VIRTUALIZE(function(self, cframe, fd, size, filter, shape)
local shouldManualFilter = getexecutorname
and (getexecutorname():match("Solara") or getexecutorname():match("Xeno"))
local overlapParams = OverlapParams.new()
overlapParams.FilterDescendantsInstances = shouldManualFilter and {} or filter
overlapParams.FilterType = shouldManualFilter and Enum.RaycastFilterType.Exclude or Enum.RaycastFilterType.Include
local character = players.LocalPlayer.Character
if not character then
return nil, nil
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return nil, nil
end
-- Used CFrame.
local usedCFrame = cframe
if fd then
usedCFrame = usedCFrame * CFrame.new(0, 0, -(size.Z / 2))
end
local parts = nil
local hitboxShape = shape == "Ball" and "Ball" or "Box"
if hitboxShape == "Ball" then
local radius = math.max(size.X, size.Y, size.Z) / 2
parts = workspace:GetPartBoundsInRadius(usedCFrame.Position, radius, overlapParams)
else
-- Parts in bounds.
parts = workspace:GetPartBoundsInBox(usedCFrame, size, overlapParams)
end
-- Return result.
return shouldManualFilter and checkParts(parts, filter) or #parts > 0, usedCFrame
end)
---Check initial state.
---@param self Defender
---@param from Model? | BasePart?
---@param pair TimingContainerPair
---@param name string
---@param key string
---@return Timing?
Defender.initial = LPH_NO_VIRTUALIZE(function(self, from, pair, name, key)
-- Find timing.
local timing = pair:index(key)
-- Fetch distance.
local distance = self:distance(from)
if not distance then
return nil
end
-- Check for distance; if we have a timing.
if timing and (distance < PP_SCRAMBLE_NUM(timing.imdd) or distance > PP_SCRAMBLE_NUM(timing.imxd)) then
return nil
end
-- Check for no timing. If so, let's log a miss.
---@note: Ignore return value.
if not timing then
self:miss(self.__type, key, name, distance, from and tostring(from.Parent) or nil)
return nil
end
-- Return timing.
return timing
end)
---Logger notify.
---@param self Defender
---@param timing Timing
---@param str string
Defender.notify = LPH_NO_VIRTUALIZE(function(self, timing, str, ...)
if not Configuration.expectToggleValue("EnableNotifications") then
return
end
Logger.notify("[%s] (%s) %s", PP_SCRAMBLE_STR(timing.name), self.__type, string.format(str, ...))
end)
---@note: Perhaps one day, we can get better approximations for these.
--- These used to rely on GetNetworkPing which we assumed would be sending or atleast receiving delay.
--- That is incorrect, it is RakNet ping thereby being RTT.
local function safePingSeconds()
local network = stats:FindFirstChild("Network")
if not network then
return 0
end
local serverStatsItem = network:FindFirstChild("ServerStatsItem")
if not serverStatsItem then
return 0
end
local dataPingItem = serverStatsItem:FindFirstChild("Data Ping")
if not dataPingItem then
return 0
end
local ok, value = pcall(function()
return dataPingItem:GetValue()
end)
if not ok or type(value) ~= "number" then
return 0
end
return value / 1000
end
---Get receiving delay.
---@return number
function Defender.rdelay()
return math.max(safePingSeconds() / 2, 0.0)
end
---Get sending delay.
---@return number
function Defender.sdelay()
return math.max(safePingSeconds() / 2, 0.0)
end
---Get data ping.
---@note: https://devforum.roblox.com/t/in-depth-information-about-robloxs-remoteevents-instance-replication-and-physics-replication-w-sources/1847340
---@note: The forum post above is misleading, not only is it the RTT time, please note that this also takes into account all delays like frame time.
---@note: This is our round-trip time (e.g double the ping) since we have a receiving delay (replication) and a sending delay when we send the input to the server.
---@todo: For every usage, the sending delay needs to be continously updated. The receiving one must be calculated once at initial send for AP ping compensation.
---@return number
function Defender.rtt()
return safePingSeconds()
end
---Repeat conditional.
---@param self Defender
---@param info RepeatInfo
---@return boolean
Defender.rc = LPH_NO_VIRTUALIZE(function(self, info)
if os.clock() - info.start >= MAX_REPEAT_WAIT then
return false
end
return true
end)
---Handle delay until in hitbox.
---@param self Defender
---@param options HitboxOptions
---@param info RepeatInfo
---@return boolean
Defender.duih = LPH_NO_VIRTUALIZE(function(self, options, info)
local clone = options:clone()
clone.hmid = self.hmaid:uid()
while task.wait() do
if not self:rc(info) then
return false
end
if not self:hc(clone, nil) then
continue
end
return true
end
end)
---Handle hitbox check options.
---@param self Defender
---@param options HitboxOptions
---@param info RepeatInfo? Pass this in if you want to use the delay until in hitbox.
---@return boolean
Defender.hc = LPH_NO_VIRTUALIZE(function(self, options, info)
local action = options.action
local timing = options.timing
-- Run basic validation.
local character = players.LocalPlayer.Character
if not character then
return false
end
if NO_HITBOX_ONLY then
return true
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return false
end
if action and action.ihbc then
return true
end
-- If we have info, then we want to delay until in hitbox.
if info then
return self:duih(options, info)
end
-- Fetch the data that we need.
local hitbox = options:hitbox()
local shape = options:shape()
local position = options:pos()
-- Run hitbox check (cached per frame).
local cacheKey = hcKey and hcKey("base", options, position, hitbox, shape, timing, action) or nil
local cached = cacheKey and hcCache[cacheKey] or nil
local result, usedCFrame
if cached and cached.frame == hcCacheFrame then
result = cached.result
usedCFrame = cached.usedCFrame
else
result, usedCFrame = self:hitbox(position, timing.fhb, hitbox, options.filter, shape)
if cacheKey then
hcCache[cacheKey] = { frame = hcCacheFrame, result = result, usedCFrame = usedCFrame }
end
end
if usedCFrame then
self:visualize(options.hmid, usedCFrame, hitbox, options:ghcolor(result), shape)
self:visualize(options.hmid and options.hmid + 1 or nil, root.CFrame, root.Size, options:ghcolor(result), "Box")
end
if not options.spredict or result then
return result
end
-- Run prediction check.
local closest = PositionHistory.closest(players.LocalPlayer, tick() - (self.sdelay() * PREDICTION_LENIENCY_MULTI))
if not closest then
return false
end
local basePtime = options.ptime or 0
if basePtime <= 0 then
return false
end
local store = OriginalStore.new()
local extraSeconds = Configuration.expectOptionValue("PredictionExtraSeconds")
if type(extraSeconds) ~= "number" then
extraSeconds = PREDICTION_EXTRA_SECONDS
end
local speed = options.part and options.part.AssemblyLinearVelocity and options.part.AssemblyLinearVelocity.Magnitude or 0
local scale = math.clamp(speed / PREDICTION_SPEED_REF, PREDICTION_MIN_SCALE, PREDICTION_MAX_SCALE)
options.ptime = basePtime + (extraSeconds * scale)
if options.ptime > PREDICTION_MAX_SECONDS then
options.ptime = PREDICTION_MAX_SECONDS
end
local eposition = options:extrapolate()
-- Run check.
local predKey = hcKey and hcKey("pred", options, eposition, hitbox, shape, timing, action) or nil
local predCached = predKey and hcCache[predKey] or nil
if predCached and predCached.frame == hcCacheFrame then
result = predCached.result
usedCFrame = predCached.usedCFrame
else
store:run(root, "CFrame", closest, function()
result, usedCFrame = self:hitbox(eposition, timing.fhb, hitbox, options.filter, shape)
end)
if predKey then
hcCache[predKey] = { frame = hcCacheFrame, result = result, usedCFrame = usedCFrame }
end
end
options.ptime = basePtime
-- Visualize predicted hitbox.
if usedCFrame then
self:visualize(options.hmid and options.hmid + 1 or nil, usedCFrame, hitbox, options:gphcolor(result), shape)
self:visualize(options.hmid and options.hmid + 1 or nil, root.CFrame, root.Size, options:gphcolor(result), "Box")
end
-- Return result.
return result
end)
---Handle end block.
---@param self Defender
Defender.bend = LPH_NO_VIRTUALIZE(function(self)
local tagId = self.blockTagId or nextBlockTagId()
self.blockTagId = tagId
InputClient.blockTag(tagId, false)
end)
---Handle action.
---@param self Defender
---@param timing Timing
---@param action Action
---@param notify boolean
Defender.handle = LPH_NO_VIRTUALIZE(function(self, timing, action, notify)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
if not self:valid(timing, action) then
return
end
if not notify then
self:notify(timing, "Action type '%s' is being executed.", PP_SCRAMBLE_STR(action._type))
end
local actionType = PP_SCRAMBLE_STR(action._type)
local reactionDelay = NO_HITBOX_ONLY and 0 or (Configuration.expectOptionValue("ReactionDelaySeconds") or 0)
if reactionDelay > 0 and actionType ~= "Start Block" and actionType ~= "End Block" then
task.wait(reactionDelay)
end
-- Dash instead of parry.
local dashReplacement = (not NO_HITBOX_ONLY)
and (Random.new():NextNumber(1.0, 100.0) <= (Configuration.expectOptionValue("DashInsteadOfParryRate") or 0.0))
if actionType ~= "Parry" then
dashReplacement = false
end
if not Configuration.expectToggleValue("AllowFailure") then
dashReplacement = false
end
if timing.actions:count() ~= 1 then
dashReplacement = false
end
if actionType == "Start Block" then
local tagId = self.blockTagId or nextBlockTagId()
self.blockTagId = tagId
return InputClient.blockTag(tagId, true, 20.0)
end
if actionType == "End Block" then
local tagId = self.blockTagId or nextBlockTagId()
self.blockTagId = tagId
return InputClient.blockTag(tagId, false)
end
if actionType == "Dash" then
local dashDir = nil
if action.name then
local lname = string.lower(action.name)
if lname:find("right", 1, true) then
dashDir = "right"
elseif lname:find("left", 1, true) then
dashDir = "left"
elseif lname:find("forward", 1, true) then
dashDir = "forward"
elseif lname:find("back", 1, true) then
dashDir = "back"
end
end
return InputClient.dash(dashDir)
end
if actionType == "Jump" then
return InputClient.jump()
end
if dashReplacement and not timing.nfdb then
self:notify(timing, "Action type 'Parry' replaced to 'Dash' type.")
return InputClient.dash()
end
return InputClient.deflect()
end)
---Check if we have input blocking tasks.
---@param self Defender
---@return boolean
Defender.blocking = LPH_NO_VIRTUALIZE(function(self)
for _, marker in next, self.markers do
if not marker then
continue
end
return true
end
for _, task in next, self.tasks do
if not task:blocking() then
continue
end
return true
end
end)
---Mark task.
---@param task Task
function Defender:mark(task)
self.tasks[#self.tasks + 1] = task
end
---Clean up hooks.
function Defender:clhook()
for key, old in next, self.rhook do
if not self[key] then
continue
end
self[key] = old
end
self.rhook = {}
end
---Clean up all tasks.
---@param self Defender
Defender.clean = LPH_NO_VIRTUALIZE(function(self)
-- Clean-up hooks.
self:clhook()
-- Clear temporary maid.
self.tmaid:clean()
-- Clear markers.
self.markers = {}
-- Clean up hitboxes.
self.hmaid:clean()
local stopBlock = false
for idx, task in next, self.tasks do
-- Cancel task.
task:cancel()
-- Clear in table.
self.tasks[idx] = nil
if task.identifier == "Start Block" or task.identifier == "End Block" then
stopBlock = true
end
end
if stopBlock then
local tagId = self.blockTagId or nextBlockTagId()
self.blockTagId = tagId
InputClient.blockTag(tagId, false)
end
end)
---Process module.
---@param self Defender
---@param timing Timing
---@varargs any
Defender.module = LPH_NO_VIRTUALIZE(function(self, timing, ...)
return self:notify(timing, "Module manager is disabled.")
end)
---Add a action to the defender object.
---@param self Defender
---@param timing Timing
---@param action Action
Defender.action = LPH_NO_VIRTUALIZE(function(self, timing, action)
if timing.umoa then
action["_type"] = PP_SCRAMBLE_STR(action["_type"])
action["name"] = PP_SCRAMBLE_STR(action["name"])
action["_when"] = PP_SCRAMBLE_RE_NUM(action["_when"])
action["hitbox"] = Vector3.new(
PP_SCRAMBLE_RE_NUM(action["hitbox"].X),
PP_SCRAMBLE_RE_NUM(action["hitbox"].Y),
PP_SCRAMBLE_RE_NUM(action["hitbox"].Z)
)
if action["shape"] ~= nil then
action["shape"] = PP_SCRAMBLE_STR(action["shape"])
end
if action["offset"] then
action["offset"] = Vector3.new(
PP_SCRAMBLE_RE_NUM(action["offset"].X),
PP_SCRAMBLE_RE_NUM(action["offset"].Y),
PP_SCRAMBLE_RE_NUM(action["offset"].Z)
)
end
end
-- Get initial receive delay.
local rdelay = self.rdelay()
-- Add action.
self:mark(Task.new(PP_SCRAMBLE_STR(action._type), function()
return action:when() - rdelay - self.sdelay()
end, timing.punishable, timing.after, self.handle, self, timing, action))
-- Log.
if not LRM_UserNote or LRM_UserNote == "tester" then
self:notify(
timing,
"Added action '%s' (%.2fs) with ping '%.2f' (changing) subtracted.",
PP_SCRAMBLE_STR(action.name),
action:when(),
self.rtt()
)
else
self:notify(
timing,
"Added action '%s' ([redacted]) with ping '%.2f' (changing) subtracted.",
PP_SCRAMBLE_STR(action.name),
self.rtt()
)
end
end)
---Add actions from timing to defender object.
---@param self Defender
---@param timing Timing
Defender.actions = LPH_NO_VIRTUALIZE(function(self, timing)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
if not self:cooldownAllow(timing) then
return
end
for _, action in next, timing.actions:get() do
self:action(timing, action)
end
end)
---Safely replace a function in the defender object.
---@param key string
---@param new function
---@return boolean, function
function Defender:hook(key, new)
-- Check if we're already hooked.
if self.rhook[key] then
Logger.warn("Cannot hook '%s' because it is already hooked.", key)
return false, nil
end
-- Get our assumed old / target function.
local old = self[key]
-- Check if function.
if typeof(old) ~= "function" then
Logger.warn("Cannot hook '%s' because it is not a function.", key)
return false, nil
end
-- Create hook.
self[key] = new
-- Add to hook table with the old function so we can restore it on clean-up.
self.rhook[key] = old
-- Log.
Logger.warn("Hooked '%s' with new function.", key)
return true, old
end
---Detach defender object.
function Defender:detach()
-- Clean self.
self:clean()
self.maid:clean()
-- Set object nil.
self = nil
end
---Create new Defender object.
---@return Defender
function Defender.new()
local self = setmetatable({}, Defender)
self.tasks = {}
self.rhook = {}
self.tmaid = Maid.new()
self.maid = Maid.new()
self.hmaid = Maid.new()
self.markers = {}
self.lvisualization = os.clock()
self.blockTagId = nextBlockTagId()
return self
end
-- Return Defender module.
return Defender
end)
-- MODULE: Features/Combat/Objects/SoundDefender
defineModule("Features/Combat/Objects/SoundDefender", function()
---@module Features.Combat.Objects.Defender
local Defender = require("Features/Combat/Objects/Defender")
---@module Game.Timings.SaveManager
local SaveManager = require("Game/Timings/SaveManager")
---@module Utility.Signal
local Signal = require("Utility/Signal")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Features.Combat.Objects.RepeatInfo
local RepeatInfo = require("Features/Combat/Objects/RepeatInfo")
---@module Features.Combat.Objects.HitboxOptions
local HitboxOptions = require("Features/Combat/Objects/HitboxOptions")
---@class SoundDefender: Defender
---@field owner Model? The owner of the part.
---@field sound Sound The sound that we're defending.
---@field part BasePart A part that we can base the position off of.
local SoundDefender = setmetatable({}, { __index = Defender })
SoundDefender.__index = SoundDefender
SoundDefender.__type = "Sound"
-- Services.
local players = game:GetService("Players")
---Check if we're in a valid state to proceed with the action.
---@param self SoundDefender
---@param timing PartTiming
---@param action Action
---@return boolean
SoundDefender.valid = LPH_NO_VIRTUALIZE(function(self, timing, action)
if not Defender.valid(self, timing, action) then
return false
end
if self.owner and not self:target(self.owner) then
return self:notify(timing, "Not a viable target.")
end
local character = players.LocalPlayer.Character
if not character then
return self:notify(timing, "No character found.")
end
local options = HitboxOptions.new(self.part, timing, { character })
options.spredict = true
options.ptime = soundPredictionSeconds()
options.entity = self.owner or self.part
options.action = action
if not self:hc(options, timing.duih and RepeatInfo.new(timing) or nil) then
return self:notify(timing, "Not in hitbox.")
end
return true
end)
---Repeat conditional.
---@param self SoundDefender
---@param _ RepeatInfo
---@return boolean
SoundDefender.rc = LPH_NO_VIRTUALIZE(function(self, _)
if not self.sound.IsPlaying then
return false
end
return true
end)
---Process sound playing.
---@param self SoundDefender
SoundDefender.process = LPH_NO_VIRTUALIZE(function(self)
---@type SoundTiming?
local timing = self:initial(
self.owner or self.part,
SaveManager.ss,
self.owner and self.owner.Name or self.part.Name,
tostring(self.sound.SoundId)
)
if not timing then
return
end
local distance = self:distance(self.owner or self.part)
if type(handleAutoCounterTiming) == "function" then
handleAutoCounterTiming(timing, "Sound", distance)
end
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
if players.LocalPlayer.Character and self.owner == players.LocalPlayer.Character then
return
end
---@note: Clean up previous tasks that are still waiting or suspended because they're in a different track.
self:clean()
-- Add actions.
return self:actions(timing)
end)
---Create new SoundDefender object.
---@param sound Sound
---@param part BasePart
---@return SoundDefender
function SoundDefender.new(sound, part)
local self = setmetatable(Defender.new(), SoundDefender)
local soundPlayed = Signal.new(sound.Played)
self.sound = sound
self.part = part
self.owner = sound:FindFirstAncestorWhichIsA("Model")
self.maid:mark(soundPlayed:connect(
"SoundDefender_OnSoundPlayed",
LPH_NO_VIRTUALIZE(function()
self:process()
end)
))
if sound.Playing then
self:process()
end
return self
end
-- Return SoundDefender module.
return SoundDefender
end)
-- MODULE: Features/Combat/Objects/PartDefender
defineModule("Features/Combat/Objects/PartDefender", function()
---@module Features.Combat.Objects.Defender
local Defender = require("Features/Combat/Objects/Defender")
---@module Game.Timings.SaveManager
local SaveManager = require("Game/Timings/SaveManager")
---@module Features.Combat.Objects.RepeatInfo
local RepeatInfo = require("Features/Combat/Objects/RepeatInfo")
---@module Features.Combat.Objects.HitboxOptions
local HitboxOptions = require("Features/Combat/Objects/HitboxOptions")
---@class PartDefender: Defender
---@field part BasePart
---@field timing PartTiming
---@field touched boolean Determines whether if we touched the timing in the past.
local PartDefender = setmetatable({}, { __index = Defender })
PartDefender.__index = PartDefender
PartDefender.__type = "Part"
-- Services.
local players = game:GetService("Players")
local HIT_COLOR = Color3.new(0, 1, 0)
local MISS_COLOR = Color3.new(1, 0, 0)
local exclusiveUntil = {}
local function partPredictionSeconds()
return 0.15 + Defender.rdelay()
end
local function applyWorldOffset(cframe, offset)
if not offset or offset == Vector3.zero then
return cframe
end
local rotation = cframe - cframe.Position
local applied = (cframe.RightVector * offset.X)
+ (cframe.UpVector * offset.Y)
+ (cframe.LookVector * offset.Z)
return CFrame.new(cframe.Position + applied) * rotation
end
local function normalizeCharName(value)
return string.lower(tostring(value or "")):gsub("[^%w]", "")
end
local function getLastLoadedChar(player)
if not player then
return nil
end
local raw = player:GetAttribute("LastLoadedChar")
if raw ~= nil then
return raw
end
local character = player.Character
if character then
return character:GetAttribute("LastLoadedChar")
end
return nil
end
local function matchesCharRequirement(part, timing)
if not timing.pchar or timing.pchar == "" then
return true
end
local directChar = part:GetAttribute("LastLoadedChar")
if not directChar then
local parent = part.Parent
if parent then
directChar = parent:GetAttribute("LastLoadedChar")
end
end
if not directChar then
local modelAttr = part:FindFirstAncestorWhichIsA("Model")
if modelAttr then
directChar = modelAttr:GetAttribute("LastLoadedChar")
end
end
if directChar then
return normalizeCharName(directChar) == normalizeCharName(timing.pchar)
end
local owner = nil
local model = part:FindFirstAncestorWhichIsA("Model")
if model then
owner = players:GetPlayerFromCharacter(model)
end
local creator = part:FindFirstChild("creator") or part:FindFirstChild("Creator")
if not owner and creator and creator.Value and creator.Value:IsA("Player") then
owner = creator.Value
end
local ownerAttr = part:GetAttribute("Owner")
if not owner and typeof(ownerAttr) == "Instance" and ownerAttr:IsA("Player") then
owner = ownerAttr
end
local ownerId = part:GetAttribute("OwnerUserId")
if not owner and type(ownerId) == "number" then
owner = players:GetPlayerByUserId(ownerId)
end
if not owner then
local closest = nil
local closestDist = math.huge
local targetNorm = normalizeCharName(timing.pchar)
for _, plr in ipairs(players:GetPlayers()) do
if plr == players.LocalPlayer then
continue
end
local raw = getLastLoadedChar(plr)
if raw and normalizeCharName(raw) == targetNorm then
local char = plr.Character
local root = char and (char:FindFirstChild("HumanoidRootPart") or char:FindFirstChild("Torso"))
if root then
local dist = (root.Position - part.Position).Magnitude
if dist < closestDist then
closestDist = dist
closest = plr
end
end
end
end
owner = closest
end
if owner == players.LocalPlayer then
return false
end
local raw = getLastLoadedChar(owner)
if not raw then
return false
end
if raw == timing.pchar then
return true
end
return normalizeCharName(raw) == normalizeCharName(timing.pchar)
end
local function matchesColorRequirement(part, timing)
if not timing.pbrick and not timing.prgb then
return true
end
if timing.pbrick then
local partBrick = part.BrickColor and part.BrickColor.Name or nil
if not partBrick or string.lower(partBrick) ~= string.lower(timing.pbrick) then
return false
end
end
if timing.prgb then
local r = timing.prgb.r or timing.prgb.R or timing.prgb[1]
local g = timing.prgb.g or timing.prgb.G or timing.prgb[2]
local b = timing.prgb.b or timing.prgb.B or timing.prgb[3]
if type(r) ~= "number" or type(g) ~= "number" or type(b) ~= "number" then
return false
end
local color = part.Color
if not color then
return false
end
local pr = math.floor(color.R * 255 + 0.5)
local pg = math.floor(color.G * 255 + 0.5)
local pb = math.floor(color.B * 255 + 0.5)
if math.abs(pr - r) > 1 or math.abs(pg - g) > 1 or math.abs(pb - b) > 1 then
return false
end
end
return true
end
local function timingSpecificity(timing)
local score = 0
if timing.pbrick then
score += 1
end
if timing.prgb then
score += 2
end
if timing.pchar then
score += 3
end
return score
end
local function matchesTiming(part, timing)
local pname = timing.pname
local partName = string.lower(part.Name)
if pname and pname ~= "" then
if string.lower(pname) ~= partName then
return false
end
elseif string.lower(timing.name) ~= partName then
return false
end
if not matchesColorRequirement(part, timing) then
return false
end
if not matchesCharRequirement(part, timing) then
return false
end
return true
end
local function resolveTiming(part)
local best = nil
local bestScore = -1
for _, timing in ipairs(SaveManager.ps:list()) do
if not matchesTiming(part, timing) then
continue
end
local score = timingSpecificity(timing)
if score > bestScore then
bestScore = score
best = timing
end
end
return best
end
local function exclusiveKey(timing)
if timing.pname and timing.pname ~= "" then
return timing.pname
end
return timing.name or ""
end
local function exclusiveActive(timing)
local seconds = timing.exclusiveSeconds or 0
if seconds <= 0 then
return false
end
local key = exclusiveKey(timing)
local untilTime = exclusiveUntil[key]
if not untilTime then
return false
end
if os.clock() >= untilTime then
exclusiveUntil[key] = nil
return false
end
return true
end
local function markExclusive(timing)
local seconds = timing.exclusiveSeconds or 0
if seconds <= 0 then
return
end
local key = exclusiveKey(timing)
exclusiveUntil[key] = os.clock() + seconds
end
---Get CFrame.
---@param self PartDefender
---@return CFrame
PartDefender.cframe = LPH_NO_VIRTUALIZE(function(self)
return self.timing.uhc and self.part.CFrame or CFrame.new(self.part.Position)
end)
---Check if we're in a valid state to proceed with the action.
---@param self PartDefender
---@param timing PartTiming
---@param action Action
---@return boolean
PartDefender.valid = LPH_NO_VIRTUALIZE(function(self, timing, action)
if not Defender.valid(self, timing, action) then
return false
end
if exclusiveActive(timing) then
return false
end
local character = players.LocalPlayer.Character
if not character then
return self:notify(timing, "No character found.")
end
local options = HitboxOptions.new(self.part, timing, { character })
options.spredict = true
options.ptime = partPredictionSeconds()
options.entity = self.entity or self.part
options.action = action
if self.hmid then
options.hmid = self.hmid
end
if not self.timing.duih then
if not self:hc(options, timing.duih and RepeatInfo.new(timing) or nil) then
return self:notify(timing, "Not in hitbox.")
end
end
return true
end)
---Update PartDefender object.
---@param self PartDefender
PartDefender.update = LPH_NO_VIRTUALIZE(function(self)
if not self.timing.duih then
return
end
if exclusiveActive(self.timing) then
return
end
if #self.tasks > 0 then
return
end
local localPlayer = players.LocalPlayer
if not localPlayer then
return
end
local character = localPlayer.Character
if not character then
return
end
local hb = self.timing.hitbox
hb = Vector3.new(PP_SCRAMBLE_NUM(hb.X), PP_SCRAMBLE_NUM(hb.Y), PP_SCRAMBLE_NUM(hb.Z))
local shape = self.timing.shape and PP_SCRAMBLE_STR(self.timing.shape) or "Box"
local options = HitboxOptions.new(self.part, self.timing, { character })
options.spredict = true
options.ptime = partPredictionSeconds()
options.entity = self.entity or self.part
if not self.hmid then
self.hmid = self.hmaid:uid()
end
options.hmid = self.hmid
local touching = self:hc(options, nil)
if not touching then
return
end
if self.touched and touching then
return
end
self.touched = touching
self:clean()
markExclusive(self.timing)
if type(handleAutoCounterTiming) == "function" then
handleAutoCounterTiming(self.timing, "Part", self:distance(self.part))
end
return self:actions(self.timing)
end)
function PartDefender:detach()
Defender.detach(self)
end
---Create new PartDefender object.
---@param part BasePart
---@param timing PartTiming?
---@return PartDefender?
function PartDefender.new(part, timing)
local self = setmetatable(Defender.new(), PartDefender)
self.part = part
self.entity = part:FindFirstAncestorWhichIsA("Model")
local selected = timing or resolveTiming(part)
if not selected then
self:initial(part, SaveManager.ps, nil, part.Name)
return nil
end
local distance = self:distance(part)
if not distance then
return nil
end
if distance < PP_SCRAMBLE_NUM(selected.imdd) or distance > PP_SCRAMBLE_NUM(selected.imxd) then
return nil
end
self.timing = selected
self.touched = false
if not self.timing then
return nil
end
if self.timing.umoa then
self:module(self.timing)
end
if type(handleAutoCounterTiming) == "function" then
handleAutoCounterTiming(self.timing, "Part", distance)
end
if not self.timing.umoa and not self.timing.duih then
self:actions(self.timing)
end
return self
end
-- Return PartDefender module.
return PartDefender
end)
-- MODULE: Features/Combat/Objects/HitboxOptions
defineModule("Features/Combat/Objects/HitboxOptions", function()
---@class HitboxOptions
---@note: Options for the hitbox check.
---@field part BasePart? If this is specified and it exists, it will be used for the position.
---@field cframe CFrame? Else, the part's CFrame will be used.
---@field timing Timing|AnimationTiming|SoundTiming
---@field action Action?
---@field filter Instance[]
---@field spredict boolean If true, a check will run for predicted positions.
---@field ptime number? The predicted time in seconds for extrapolation.
---@field entity Model? The entity for extrapolation.
---@field phcolor Color3 The color for predicted hitboxes.
---@field pmcolor Color3 The color for predicted missed hitboxes.
---@field hcolor Color3 The color for hitboxes.
---@field mcolor Color3 The color for missed hitboxes.
---@field hmid number? Hitbox visualization ID for normal hitbox check.
local HitboxOptions = {}
HitboxOptions.__index = HitboxOptions
-- Services.
local players = game:GetService("Players")
local function applyHitboxWorldOffset(cframe, offset)
if not offset or offset == Vector3.zero then
return cframe
end
local rotation = cframe - cframe.Position
local applied = (cframe.RightVector * offset.X)
+ (cframe.UpVector * offset.Y)
+ (cframe.LookVector * offset.Z)
return CFrame.new(cframe.Position + applied) * rotation
end
---Hit color.
---@return Color3
function HitboxOptions:ghcolor(result)
return result and self.hcolor or self.mcolor
end
---Predicted hit color.
---@return Color3
function HitboxOptions:gphcolor(result)
return result and self.phcolor or self.pmcolor
end
---Cloned hitbox options.
---@return HitboxOptions
function HitboxOptions:clone()
local options = setmetatable({}, HitboxOptions)
options.action = self.action
options.spredict = self.spredict
options.entity = self.entity
options.ptime = self.ptime
options.entity = self.entity
options.phcolor = self.phcolor
options.pmcolor = self.pmcolor
options.hcolor = self.hcolor
options.mcolor = self.mcolor
options.hmid = self.hmid
options.filter = self.filter
options.part = self.part
options.cframe = self.cframe
options.timing = self.timing
return options
end
---Get the hitbox shape.
---@return string
function HitboxOptions:shape()
local shape = self.action and self.action.shape or self.timing.shape
if self.timing.duih then
shape = self.timing.shape
end
if typeof(shape) == "string" then
shape = PP_SCRAMBLE_STR(shape)
end
return shape == "Ball" and "Ball" or "Box"
end
---Get the hitbox size.
---@return Vector3
function HitboxOptions:hitbox()
local hitbox = self.action and self.action.hitbox or self.timing.hitbox
if self.timing.duih then
hitbox = self.timing.hitbox
end
hitbox = Vector3.new(PP_SCRAMBLE_NUM(hitbox.X), PP_SCRAMBLE_NUM(hitbox.Y), PP_SCRAMBLE_NUM(hitbox.Z))
return hitbox
end
---Get the hitbox offset.
---@return Vector3
function HitboxOptions:offset()
local offset = self.action and self.action.offset or self.timing.offset
if self.timing.duih then
offset = self.timing.offset
end
if typeof(offset) ~= "Vector3" then
return Vector3.zero
end
return Vector3.new(PP_SCRAMBLE_NUM(offset.X), PP_SCRAMBLE_NUM(offset.Y), PP_SCRAMBLE_NUM(offset.Z))
end
---Get extrapolated position.
---@return CFrame
HitboxOptions.extrapolate = LPH_NO_VIRTUALIZE(function(self)
if not self.part then
return error("HitboxOptions.extrapolate - unimplemented for CFrame")
end
if not self.entity then
return error("HitboxOptions.extrapolate - no entity specified")
end
if not self.ptime then
return error("HitboxOptions.extrapolate - no predicted time specified")
end
-- Return the extrapolated position.
local cframe = self.part.CFrame + (self.part.AssemblyLinearVelocity * self.ptime)
return applyHitboxWorldOffset(cframe, self:offset())
end)
---Get position.
---@return CFrame
HitboxOptions.pos = LPH_NO_VIRTUALIZE(function(self)
local cframe = nil
if self.cframe then
cframe = self.cframe
end
if self.part then
cframe = self.part.CFrame
end
if not cframe then
return error("HitboxOptions.pos - impossible condition")
end
return applyHitboxWorldOffset(cframe, self:offset())
end)
---Create new HitboxOptions object.
---@param target Instance|CFrame
---@param timing Timing|AnimationTiming|SoundTiming
---@param filter Instance[]?
---@return HitboxOptions
HitboxOptions.new = LPH_NO_VIRTUALIZE(function(target, timing, filter)
local self = setmetatable({}, HitboxOptions)
self.part = typeof(target) == "Instance" and target:IsA("BasePart") and target
self.cframe = typeof(target) == "CFrame" and target
self.timing = timing
self.action = nil
self.filter = filter or {}
self.spredict = false
self.hmid = nil
self.entity = nil
self.phcolor = Color3.new(1, 0, 1)
self.pmcolor = Color3.new(0.349019, 0.345098, 0.345098)
self.hcolor = Color3.new(0, 1, 0)
self.mcolor = Color3.new(1, 0, 0)
self.ptime = nil
if not self.part and not self.cframe then
return error("HitboxOptions: No part or CFrame specified.")
end
if filter then
return self
end
local character = players.LocalPlayer.Character
if not character then
return self
end
self.filter = { character }
return self
end)
-- Return HitboxOptions module.
return HitboxOptions
end)
-- MODULE: Features/Combat/Objects/RepeatInfo
defineModule("Features/Combat/Objects/RepeatInfo", function()
---@note: Typed object that represents information. It's not really a true class but just needs to store the correct data.
---@class RepeatInfo
---@field track AnimationTrack?
---@field timing Timing
---@field start number
---@field index number
---@field irdelay number Initial receive delay.
local RepeatInfo = {}
RepeatInfo.__index = RepeatInfo
---Create new RepeatInfo object.
---@param timing Timing
---@param irdelay number
---@return RepeatInfo
function RepeatInfo.new(timing, irdelay)
local self = setmetatable({}, RepeatInfo)
self.track = nil
self.timing = timing
self.start = os.clock()
self.index = 0
self.irdelay = irdelay
return self
end
-- Return RepeatInfo module.
return RepeatInfo
end)
-- MODULE: Features/Combat/Objects/Target
defineModule("Features/Combat/Objects/Target", function()
---@note: Typed object that represents a target. It's not really a true class but just needs to store the correct data.
---@class Target
---@field character Model
---@field humanoid Humanoid
---@field root BasePart
---@field dc number Distance to crosshair.
---@field fov number Field of view to target.
---@field du number Distance to us.
local Target = {}
---Create new Target object.
---@param character Model
---@param humanoid Humanoid
---@param root BasePart
---@param dc number
---@param fov number
---@param du number
---@return Target
function Target.new(character, humanoid, root, dc, fov, du)
local self = setmetatable({}, Target)
self.character = character
self.humanoid = humanoid
self.root = root
self.dc = dc
self.fov = fov
self.du = du
return self
end
-- Return Target module.
return Target
end)
-- MODULE: Features/Combat/Objects/Task
defineModule("Features/Combat/Objects/Task", function()
---@module Utility.TaskSpawner
local TaskSpawner = require("Utility/TaskSpawner")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@class Task
---@field thread thread
---@field identifier string
---@field _when number A timestamp when the task will be executed.
---@field punishable number A window in seconds where the task can be punished.
---@field after number A window in seconds where the task can be executed.
---@field delay function
local Task = {}
Task.__index = Task
---Check if task should block the input.
---@return boolean
function Task:blocking()
if not (coroutine.status(self.thread) ~= "dead") then
return false
end
-- We've exceeded the execution time. Block if we're within the after window.
local whenAt = Task.when(self)
if os.clock() >= whenAt then
return os.clock() <= whenAt + self.after
end
---@note: Allow us to do inputs up until a certain amount of time before the task happens.
return os.clock() >= whenAt - self.punishable
end
---Cancel task.
function Task:cancel()
if coroutine.status(self.thread) ~= "suspended" then
return
end
task.cancel(self.thread)
end
---Get when approximately the task will be executed.
---@return number
function Task:when()
local base = self._when
if type(base) ~= "number" then
local legacy = self.when
if type(legacy) == "number" then
base = legacy
else
base = 0
end
end
local delay = self.delay
if type(delay) == "function" then
return base + delay()
end
return base + (delay or 0)
end
---Create new Task object.
---@param identifier string
---@param delay function
---@param punishable number
---@param after number
---@param callback function
---@vararg any
---@return Task
function Task.new(identifier, delay, punishable, after, callback, ...)
local self = setmetatable({}, Task)
self.identifier = identifier
self._when = os.clock()
self.delay = delay
self.punishable = punishable
self.after = after
self.thread = TaskSpawner.delay("Action_" .. identifier, delay, callback, ...)
if not self.punishable or self.punishable <= 0 then
self.punishable = Configuration.expectOptionValue("DefaultPunishableWindow") or 0.7
end
if not self.after or self.after <= 0 then
self.after = Configuration.expectOptionValue("DefaultAfterWindow") or 0.1
end
return self
end
-- Return Task module.
return Task
end)
-- MODULE: Features/Combat/PositionHistory
defineModule("Features/Combat/PositionHistory", function()
-- PositionHistory module.
local PositionHistory = {}
-- Histories table.
local histories = {}
-- Max history seconds.
local MAX_HISTORY_SECS = 3.0
---Add an entry to the history list.
---@param idx any
---@param position CFrame
---@param timestamp number
function PositionHistory.add(idx, position, timestamp)
local history = histories[idx] or {}
if not histories[idx] then
histories[idx] = history
end
history[#history + 1] = {
position = position,
timestamp = timestamp,
}
while true do
local tail = history[1]
if not tail then
break
end
if tick() - tail.timestamp <= MAX_HISTORY_SECS then
break
end
table.remove(history, 1)
end
end
---Get the horizontal angular velocity (yaw rate) for a current index.
---@param index any
---@return number?
function PositionHistory.yrate(index)
local history = histories[index]
if not history or #history < 2 then
return nil
end
local latest = history[#history]
local previous = history[#history - 1]
local dt = latest.timestamp - previous.timestamp
if dt <= 1e-4 then
return nil
end
local prevLook = Vector3.new(previous.position.LookVector.X, 0, previous.position.LookVector.Z).Unit
local latestLook = Vector3.new(latest.position.LookVector.X, 0, latest.position.LookVector.Z).Unit
local dot = prevLook:Dot(latestLook)
local crossY = prevLook:Cross(latestLook).Y
local angle = math.atan2(crossY, dot)
return angle / dt
end
---Divides the history into a number of equal steps and returns the position at each step.
---@param idx any
---@param steps number
---@param phds number History second limit for past hitbox detection.
---@return CFrame[]?
function PositionHistory.stepped(idx, steps, phds)
local history = histories[idx]
if not history or #history == 0 then
return nil
end
if not steps or steps <= 0 then
return nil
end
local vhistory = {}
local vhtime = history[#history].timestamp
for _, data in next, history do
if vhtime - data.timestamp > phds then
continue
end
vhistory[#vhistory + 1] = data.position
end
if #vhistory == 0 then
return {}
end
local count = math.min(steps, #vhistory)
local out = table.create(count)
for cidx = 1, count do
out[cidx] = vhistory[math.max(math.floor((cidx * #vhistory) / count), 1)]
end
return out
end
---Get closest position (in time) to a timestamp.
---@param idx any
---@param timestamp number
---@return CFrame?
function PositionHistory.closest(idx, timestamp)
if not histories[idx] then
return nil
end
local closestDelta = nil
local closestPosition = nil
for _, data in next, histories[idx] do
local delta = math.abs(timestamp - data.timestamp)
if closestDelta and delta >= closestDelta then
continue
end
closestPosition = data.position
closestDelta = delta
end
return closestPosition
end
-- Return PositionHistory module.
return PositionHistory
end)
-- MODULE: Features/Combat/Targeting
defineModule("Features/Combat/Targeting", function()
-- Targeting module.
---@note: Glorified extended non-utility Entities file.
local Targeting = {}
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Features.Combat.Objects.Target
local Target = require("Features/Combat/Objects/Target")
---@module Utility.Table
local Table = require("Utility/Table")
-- Services.
local players = game:GetService("Players")
local userInputService = game:GetService("UserInputService")
local targetCache = {
lastAt = 0,
lastMouse = nil,
lastLocalPos = nil,
lastCamCFrame = nil,
targets = nil,
sortType = nil,
maxTargets = nil,
}
local function shouldRebuildTargets(localRootPart, currentCamera, sortType, maxTargets)
if not targetCache.targets then
return true
end
local now = os.clock()
if now - (targetCache.lastAt or 0) > 0.05 then
return true
end
if targetCache.sortType ~= sortType or targetCache.maxTargets ~= maxTargets then
return true
end
local mousePos = userInputService:GetMouseLocation()
local lastMouse = targetCache.lastMouse
if not lastMouse then
return true
end
local dx = mousePos.X - lastMouse.X
local dy = mousePos.Y - lastMouse.Y
if (dx * dx + dy * dy) > 4 then
return true
end
if not localRootPart then
return true
end
local lastPos = targetCache.lastLocalPos
if not lastPos or (localRootPart.Position - lastPos).Magnitude > 2 then
return true
end
if not currentCamera then
return true
end
local lastCam = targetCache.lastCamCFrame
if not lastCam then
return true
end
local camCFrame = currentCamera.CFrame
if (camCFrame.Position - lastCam.Position).Magnitude > 0.5 then
return true
end
if camCFrame.LookVector:Dot(lastCam.LookVector) < 0.999 then
return true
end
return false
end
local function isAlly(player)
local localPlayer = players.LocalPlayer
if not localPlayer or not player then
return false
end
local ok, status = pcall(localPlayer.GetFriendStatus, localPlayer, player)
if not ok then
return false
end
return status == Enum.FriendStatus.Friend
end
---Get a list of all viable targets.
---@return Target[]
Targeting.viable = LPH_NO_VIRTUALIZE(function()
local ents = workspace:FindFirstChild("Live")
if not ents then
return {}
end
local localCharacter = players.LocalPlayer.Character
if not localCharacter then
return {}
end
local localRootPart = localCharacter and localCharacter:FindFirstChild("HumanoidRootPart")
if not localRootPart then
return {}
end
local currentCamera = workspace.CurrentCamera
if not currentCamera then
return {}
end
local targets = {}
for _, entity in next, ents:GetChildren() do
if entity == localCharacter then
continue
end
local playerFromCharacter = players:GetPlayerFromCharacter(entity)
if not playerFromCharacter then
continue
end
if Configuration.expectToggleValue("IgnorePlayers") then
continue
end
local humanoid = entity:FindFirstChildWhichIsA("Humanoid")
if not humanoid then
continue
end
local rootPart = entity:FindFirstChild("HumanoidRootPart")
if not rootPart then
continue
end
if humanoid.Health <= 0 then
continue
end
if playerFromCharacter then
local usernameList = Options["UsernameList"]
local displayNameFound = table.find(usernameList.Values, playerFromCharacter.DisplayName)
local usernameFound = table.find(usernameList.Values, playerFromCharacter.Name)
if displayNameFound or usernameFound then
continue
end
end
local fieldOfViewToEntity =
currentCamera.CFrame.LookVector:Dot((localRootPart.Position - rootPart.Position).Unit)
local currentDistance = (rootPart.Position - localRootPart.Position).Magnitude
if currentDistance > Configuration.expectOptionValue("DistanceLimit") then
continue
end
if
playerFromCharacter
and isAlly(playerFromCharacter)
and Configuration.expectToggleValue("IgnoreAllies")
then
continue
end
local mousePosition = userInputService:GetMouseLocation()
local unitRay = workspace.CurrentCamera:ScreenPointToRay(mousePosition.X, mousePosition.Y)
local distanceToCrosshair = unitRay:Distance(rootPart.Position)
targets[#targets + 1] =
Target.new(entity, humanoid, rootPart, distanceToCrosshair, fieldOfViewToEntity, currentDistance)
end
return targets
end)
---Get the best targets through sorting.
---@return Target[]
Targeting.best = LPH_NO_VIRTUALIZE(function()
local sortType = Configuration.expectOptionValue("PlayerSelectionType")
local maxTargets = Configuration.expectOptionValue("MaxTargets")
local localCharacter = players.LocalPlayer and players.LocalPlayer.Character
local localRootPart = localCharacter and localCharacter:FindFirstChild("HumanoidRootPart")
local currentCamera = workspace.CurrentCamera
if localRootPart and currentCamera and not shouldRebuildTargets(localRootPart, currentCamera, sortType, maxTargets) then
return targetCache.targets
end
local targets = Targeting.viable()
local sortFunction = nil
if sortType == "Closest To Crosshair" then
sortFunction = function(first, second)
return first.dc < second.dc
end
end
if sortType == "Closest In Distance" then
sortFunction = function(first, second)
return first.du < second.du
end
end
if sortType == "Least Health" then
sortFunction = function(first, second)
return first.humanoid.Health < second.humanoid.Health
end
end
table.sort(targets, sortFunction)
local sliced = Table.slice(targets, 1, maxTargets)
targetCache.targets = sliced
targetCache.lastAt = os.clock()
targetCache.lastMouse = userInputService:GetMouseLocation()
targetCache.lastLocalPos = localRootPart and localRootPart.Position or nil
targetCache.lastCamCFrame = currentCamera and currentCamera.CFrame or nil
targetCache.sortType = sortType
targetCache.maxTargets = maxTargets
return sliced
end)
---Find our model from a list of best targets.
---@param model Model
---@return Target?
Targeting.find = LPH_NO_VIRTUALIZE(function(model)
for _, target in next, Targeting.best() do
if target.character ~= model then
continue
end
return target
end
end)
-- Return Targeting module.
return Targeting
end)
-- MODULE: Game/InputClient
defineModule("Game/InputClient", function()
local InputClient = {}
local players = game:GetService('Players')
local workspaceService = game:GetService('Workspace')
local runService = game:GetService('RunService')
local virtualInput = game:GetService('VirtualInputManager')
local userInputService = game:GetService('UserInputService')
local Configuration = require('Utility/Configuration')
local Keybinding = require('Game/Keybinding')
local setThreadIdentity = setthreadidentity or set_thread_identity or (syn and syn.set_thread_identity)
local getThreadIdentity = getthreadidentity or get_thread_identity or (syn and syn.get_thread_identity)
local ROBLOX_IDENTITY = 8
local getgc = getgc
local getinfo = debug.getinfo
local hookmetamethod = hookmetamethod
local newcclosure = newcclosure
local clock = os.clock
local DODGE_CAMERA_SPOOF_SECONDS = 0.30
local JUMP_HOLD_SECONDS = 0.05
local M1_MIN_HOLD_SECONDS = 0.02
local BLOCK_TYPE_DEFLECT = 1
local BLOCK_TYPE_NORMAL = 2
local BLOCK_RESEND_COOLDOWN = 0.05
local blockQueue = {}
local blockStack = {}
local blockId = 0
local blockEvent = nil
local fHeldValue = nil
local lastBlockSendAt = 0
local lastUnblockSendAt = 0
local dodgeFunc = nil
local dodgeResolveCooldownUntil = 0
local cameraSpoofActive = false
local cameraSpoofCFrame = nil
local cameraSpoofEpoch = 0
local cameraSpoofUntil = 0
local cameraInstance = nil
local cameraHooked = false
local cameraIndexOld = nil
local cachedLogger = nil
local lastDodgeErrorAt = 0
local blockOff
local function isManualBlockHeld()
local key = Keybinding and Keybinding.info and Keybinding.info.Block
if typeof(key) ~= "EnumItem" or key == Enum.KeyCode.Unknown then
return false
end
return userInputService:IsKeyDown(key)
end
local function resetDashState()
blockQueue = {}
blockStack = {}
blockId = 0
fHeldValue = nil
blockEvent = nil
lastBlockSendAt = 0
lastUnblockSendAt = 0
dodgeFunc = nil
dodgeResolveCooldownUntil = 0
cameraSpoofActive = false
cameraSpoofCFrame = nil
cameraSpoofEpoch = 0
cameraSpoofUntil = 0
cameraInstance = nil
if blockOff then
task.defer(blockOff)
end
end
do
local lp = players.LocalPlayer
if not lp then
players:GetPropertyChangedSignal("LocalPlayer"):Wait()
lp = players.LocalPlayer
end
if lp then
lp.CharacterAdded:Connect(function()
resetDashState()
end)
lp.CharacterRemoving:Connect(function()
resetDashState()
end)
end
end
local function getLogger()
if cachedLogger then
return cachedLogger
end
local ok, logger = pcall(require, 'Utility/Logger')
if ok then
cachedLogger = logger
return cachedLogger
end
return nil
end
local function withRobloxIdentity(action)
local ok, err
if setThreadIdentity and getThreadIdentity then
local prev = getThreadIdentity()
if prev ~= ROBLOX_IDENTITY then
setThreadIdentity(ROBLOX_IDENTITY)
end
ok, err = pcall(action)
if prev ~= ROBLOX_IDENTITY then
setThreadIdentity(prev)
end
else
ok, err = pcall(action)
end
if not ok then
local logger = getLogger()
if logger and logger.warn then
logger.warn("VirtualInputManager failed: %s", tostring(err))
else
warn(string.format("[Cottom hub AB] VirtualInputManager failed: %s", tostring(err)))
end
end
return ok
end
local VIM_SendKeyEvent = virtualInput and virtualInput.SendKeyEvent or nil
local function sendKeyEvent(isDown, keyCode)
if not virtualInput or not VIM_SendKeyEvent then
return false
end
return withRobloxIdentity(function()
VIM_SendKeyEvent(virtualInput, isDown == true, keyCode, false, game)
end) == true
end
local function sendMouseButtonEvent(isDown)
if not virtualInput then
return false
end
local pos = userInputService:GetMouseLocation()
return withRobloxIdentity(function()
virtualInput:SendMouseButtonEvent(pos.X, pos.Y, 0, isDown == true, game, 0)
end) == true
end
local function useControllerM1()
return Configuration.expectToggleValue("UseControllerM1") == true
end
local function sendControllerM1(isDown)
if not useControllerM1() then
return false
end
return sendKeyEvent(isDown == true, Enum.KeyCode.ButtonR2)
end
local function safeGetInfo(...)
if not getinfo then
return nil
end
local ok, info = pcall(getinfo, ...)
if not ok then
return nil
end
return info
end
local function resolveBlockEvent()
if blockEvent and blockEvent.Parent ~= nil and players.LocalPlayer and players.LocalPlayer.Backpack and blockEvent:IsDescendantOf(players.LocalPlayer.Backpack) then
return blockEvent
end
local lp = players.LocalPlayer
if not lp then
return nil
end
local backpack = lp:FindFirstChild("Backpack")
local traits = backpack and backpack:FindFirstChild("ServerTraits")
local input = traits and traits:FindFirstChild("Input")
if input and input:IsA("RemoteEvent") then
blockEvent = input
return input
end
return nil
end
local function resolveToolRange(tool)
if not tool then
return nil
end
local config = tool:FindFirstChild("Config")
local rangeValue = config and config:FindFirstChild("range")
if not rangeValue then
rangeValue = tool:FindFirstChild("range") or tool:FindFirstChild("Range")
end
if rangeValue and rangeValue.Value ~= nil then
return tostring(rangeValue.Value)
end
local attr = tool:GetAttribute("range") or tool:GetAttribute("Range")
if attr ~= nil then
return tostring(attr)
end
return nil
end
local function buildFirePayload(toolName, rangeValue)
if not toolName or toolName == "" then
return nil
end
local cam = workspaceService.CurrentCamera
if not cam then
return nil
end
local mousePos = userInputService:GetMouseLocation()
local ray = cam:ViewportPointToRay(mousePos.X, mousePos.Y)
local dir = ray.Direction
if dir.Magnitude <= 0 then
return nil
end
local origin = ray.Origin
local hitPos = origin + dir * 500
local mouseHit = CFrame.new(hitPos, hitPos + dir)
local humanoid = players.LocalPlayer and players.LocalPlayer.Character and players.LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
local air = humanoid and humanoid.FloorMaterial == Enum.Material.Air or false
return {
air = air,
mousehit = mouseHit,
camdir = dir.Unit,
neutral = true,
campos = origin,
range = rangeValue or "2",
ToolName = toolName,
}
end
local function resolveFHeld()
local lp = players.LocalPlayer
if not lp then
return nil
end
local char = lp.Character
local flag = char and char:FindFirstChild("FHeld")
if flag and flag:IsA("BoolValue") then
fHeldValue = flag
return flag
end
local live = workspaceService:FindFirstChild("Live")
local liveChar = live and live:FindFirstChild(lp.Name)
flag = liveChar and liveChar:FindFirstChild("FHeld")
if flag and flag:IsA("BoolValue") then
fHeldValue = flag
return flag
end
return nil
end
local function getBlockingState()
local flag = fHeldValue
local char = players.LocalPlayer and players.LocalPlayer.Character or nil
if flag then
if flag.Parent == nil then
flag = nil
elseif char and not flag:IsDescendantOf(char) then
flag = nil
end
end
if not flag then
flag = resolveFHeld()
end
if flag then
return flag.Value == true, true
end
return false, false
end
local function blockOn()
local ev = resolveBlockEvent()
if ev then
ev:FireServer("f")
return true
end
return false
end
blockOff = function()
local ev = resolveBlockEvent()
if ev then
ev:FireServer("foff")
return true
end
return false
end
local function nextBlockId(prefix)
blockId += 1
return string.format("%s_%d", prefix or "Block", blockId)
end
local function queueBlock(kind, duration)
local wasEmpty = next(blockQueue) == nil
local id = nextBlockId(kind == BLOCK_TYPE_DEFLECT and "Deflect" or "Block")
blockQueue[id] = { start = clock(), type = kind, dt = duration }
if wasEmpty then
local blocking = getBlockingState()
if not blocking then
blockOn()
lastBlockSendAt = clock()
end
end
return id
end
local function queueBlockWithId(id, kind, duration)
if not id or id == "" then
return queueBlock(kind, duration)
end
local wasEmpty = next(blockQueue) == nil
blockQueue[id] = { start = clock(), type = kind, dt = duration }
if wasEmpty then
local blocking = getBlockingState()
if not blocking then
blockOn()
lastBlockSendAt = clock()
end
end
return id
end
local function unqueueBlock(id)
if not id or not blockQueue[id] then
return
end
if timing._id == "rbxassetid://13631792671" then
local dummyAction = { ihbc = true }
if not self:valid(timing, dummyAction) then
return
end
local dist = distance or self:distance(self.entity) or math.huge
local holdSeconds = dist <= 40 and 1.2 or 1.6
InputClient.blockHoldSeconds(holdSeconds)
return
end
blockQueue[id] = nil
end
local function stopBlockId(id)
if not id or not blockQueue[id] then
return
end
blockQueue[id] = nil
end
local function blockAcquire()
local id = queueBlock(BLOCK_TYPE_NORMAL, nil)
blockStack[#blockStack + 1] = id
end
local function blockRelease()
local idx = #blockStack
if idx <= 0 then
return
end
local id = blockStack[idx]
blockStack[idx] = nil
unqueueBlock(id)
end
local function cleanupBlockQueue(blocking)
local now = clock()
for id, data in next, blockQueue do
if data.dt and now - data.start >= data.dt then
blockQueue[id] = nil
elseif data.type == BLOCK_TYPE_DEFLECT and blocking then
blockQueue[id] = nil
end
end
end
local blockSyncConn = nil
local lastWantsBlock = false
local function ensureBlockSync()
if blockSyncConn then
return
end
blockSyncConn = runService.RenderStepped:Connect(LPH_NO_VIRTUALIZE(function()
local blocking, hasFlag = getBlockingState()
cleanupBlockQueue(blocking)
local wantsBlock = next(blockQueue) ~= nil
if wantsBlock then
if not blocking then
local now = clock()
if now - lastBlockSendAt >= BLOCK_RESEND_COOLDOWN then
blockOn()
lastBlockSendAt = now
end
end
lastWantsBlock = true
return
end
local autoDefenseEnabled = Configuration.expectToggleValue("EnableAutoDefense")
local m1TradeEnabled = Configuration.expectToggleValue("EnableM1AutoTrade")
if not autoDefenseEnabled and not m1TradeEnabled then
lastWantsBlock = false
return
end
if m1TradeEnabled and not autoDefenseEnabled then
if isManualBlockHeld() then
lastWantsBlock = false
return
end
if not lastWantsBlock then
return
end
end
if blocking or not hasFlag then
local now = clock()
if now - lastUnblockSendAt >= BLOCK_RESEND_COOLDOWN then
blockOff()
lastUnblockSendAt = now
end
end
lastWantsBlock = false
end))
end
ensureBlockSync()
function InputClient.block(state)
if state == true then
blockAcquire()
else
blockRelease()
end
end
function InputClient.deflect()
local holdSeconds = Configuration.expectOptionValue("BlockHoldSeconds")
if holdSeconds == nil then
holdSeconds = (Configuration.expectOptionValue("DeflectHoldTime") or 0) / 1000
end
return InputClient.blockHoldSeconds(math.max(holdSeconds or 0, 0))
end
function InputClient.blockHoldSeconds(seconds)
local duration = seconds or 0
if duration <= 0 then
return false
end
queueBlock(BLOCK_TYPE_NORMAL, duration)
return true
end
function InputClient.fireTool(toolName, rangeValue)
local ev = resolveBlockEvent()
if not ev then
return false
end
local payload = buildFirePayload(toolName, rangeValue)
if not payload then
return false
end
local ok = pcall(ev.FireServer, ev, "fire", payload)
return ok == true
end
function InputClient.fireToolObject(tool)
if not tool then
return false
end
return InputClient.fireTool(tool.Name, resolveToolRange(tool))
end
function InputClient.blockTap(seconds)
local duration = seconds or 0
if duration <= 0 then
return false
end
blockOn()
task.delay(0.02, blockOn)
task.delay(duration, function()
blockOff()
end)
return true
end
function InputClient.blockTag(id, state, duration, deflect)
if not id or id == "" then
return InputClient.block(state)
end
if state then
local kind = deflect and BLOCK_TYPE_DEFLECT or BLOCK_TYPE_NORMAL
queueBlockWithId(id, kind, duration)
return true
end
stopBlockId(id)
return true
end
local function normalizeKeyCode(key)
if typeof(key) == "EnumItem" and key.EnumType == Enum.KeyCode then
return key
end
return nil
end
function InputClient.keyHold(keyCode, seconds)
local duration = seconds or 0
if duration <= 0 then
return false
end
local key = normalizeKeyCode(keyCode)
if not key then
return false
end
if not sendKeyEvent(true, key) then
return false
end
task.delay(duration, function()
sendKeyEvent(false, key)
end)
return true
end
local function resolveJumpKey()
local key = Keybinding and Keybinding.info and Keybinding.info.Jump or Enum.KeyCode.Space
if typeof(key) ~= "EnumItem" or key == Enum.KeyCode.Unknown then
key = Enum.KeyCode.Space
end
return key
end
function InputClient.jumpHold(seconds)
local duration = seconds or JUMP_HOLD_SECONDS
return InputClient.keyHold(resolveJumpKey(), duration)
end
local function resolveSprintKey()
local key = Keybinding and Keybinding.info and (Keybinding.info.Sprint or Keybinding.info.Run)
if typeof(key) ~= "EnumItem" or key == Enum.KeyCode.Unknown then
key = Enum.KeyCode.LeftShift
end
return key
end
function InputClient.sprintHold(seconds)
local duration = seconds or 0
if duration <= 0 then
return false
end
return InputClient.keyHold(resolveSprintKey(), duration)
end
function InputClient.jump()
local key = Keybinding and Keybinding.info and Keybinding.info.Jump or Enum.KeyCode.Space
if typeof(key) ~= "EnumItem" or key == Enum.KeyCode.Unknown then
key = Enum.KeyCode.Space
end
if not sendKeyEvent(true, key) then
return false
end
task.delay(JUMP_HOLD_SECONDS, function()
sendKeyEvent(false, key)
end)
return true
end
function InputClient.tapKey(keyCode, holdSeconds)
if typeof(keyCode) ~= "EnumItem" then
return false
end
local duration = holdSeconds or 0.02
if not sendKeyEvent(true, keyCode) then
return false
end
task.delay(duration, function()
sendKeyEvent(false, keyCode)
end)
return true
end
function InputClient.m1Hold(seconds)
local duration = seconds or 0
if duration <= 0 then
return false
end
InputClient._m1AutoUntil = (InputClient._m1AutoUntil or 0)
InputClient._m1AutoUntil = math.max(InputClient._m1AutoUntil, clock() + duration + 0.06)
if useControllerM1() then
if InputClient._m1KeyDown then
sendControllerM1(false)
InputClient._m1KeyDown = false
end
else
if InputClient._m1Down then
sendMouseButtonEvent(false)
InputClient._m1Down = false
end
end
InputClient._m1Token = (InputClient._m1Token or 0) + 1
local token = InputClient._m1Token
if useControllerM1() then
if not sendControllerM1(true) then
return false
end
InputClient._m1KeyDown = true
else
if not sendMouseButtonEvent(true) then
return false
end
InputClient._m1Down = true
end
local function releaseIfStill(token)
if InputClient._m1Token ~= token then
return
end
if useControllerM1() then
sendControllerM1(false)
InputClient._m1KeyDown = false
else
sendMouseButtonEvent(false)
InputClient._m1Down = false
end
end
task.delay(duration, function()
releaseIfStill(token)
end)
task.delay(duration + 0.03, function()
releaseIfStill(token)
end)
task.delay(duration + 0.06, function()
releaseIfStill(token)
end)
return true
end
function InputClient.m1Release()
InputClient._m1Token = (InputClient._m1Token or 0) + 1
if useControllerM1() then
if InputClient._m1KeyDown then
sendControllerM1(false)
InputClient._m1KeyDown = false
end
else
if InputClient._m1Down then
sendMouseButtonEvent(false)
InputClient._m1Down = false
end
end
InputClient._m1AutoUntil = 0
return true
end
function InputClient.m1Tap(seconds)
local duration = seconds or M1_MIN_HOLD_SECONDS
return InputClient.m1Hold(duration)
end
local m1ReleaseConn = nil
local function ensureM1Release()
if m1ReleaseConn then
return
end
m1ReleaseConn = runService.RenderStepped:Connect(LPH_NO_VIRTUALIZE(function()
if useControllerM1() then
if InputClient._m1KeyDown and InputClient._m1AutoUntil and clock() > InputClient._m1AutoUntil then
InputClient._m1ReleaseTries = (InputClient._m1ReleaseTries or 0) + 1
sendControllerM1(false)
if InputClient._m1ReleaseTries >= 6 then
InputClient._m1KeyDown = false
InputClient._m1AutoUntil = 0
InputClient._m1ReleaseTries = 0
end
else
InputClient._m1ReleaseTries = 0
end
return
end
if InputClient._m1Down and InputClient._m1AutoUntil and clock() > InputClient._m1AutoUntil then
-- Retry release for a few frames if VIM misses the up event.
InputClient._m1ReleaseTries = (InputClient._m1ReleaseTries or 0) + 1
sendMouseButtonEvent(false)
if InputClient._m1ReleaseTries >= 6 then
InputClient._m1Down = false
InputClient._m1AutoUntil = 0
InputClient._m1ReleaseTries = 0
end
else
InputClient._m1ReleaseTries = 0
end
end))
end
ensureM1Release()
local function normalizeDirection(direction)
if not direction or direction == "" then
direction = Configuration.expectOptionValue("DefaultDashDirection") or "S"
end
if direction == "Random" then
local options = { "W", "A", "S", "D" }
direction = options[math.random(1, #options)]
end
if typeof(direction) == "EnumItem" then
direction = tostring(direction.Name)
end
direction = tostring(direction)
if direction == "W" or direction == "w" then
direction = "forward"
elseif direction == "A" or direction == "a" then
direction = "left"
elseif direction == "D" or direction == "d" then
direction = "right"
elseif direction == "S" or direction == "s" then
direction = "back"
else
direction = direction:lower()
end
if direction == "forward" or direction == "back" or direction == "left" or direction == "right" then
return direction
end
return "back"
end
local function resolveDodgeFunction()
if dodgeFunc then
return dodgeFunc
end
if dodgeResolveCooldownUntil > 0 and clock() < dodgeResolveCooldownUntil then
return nil
end
local gcList = getgc(true)
for i = 1, #gcList do
local fn = gcList[i]
if type(fn) == "function" then
local info = getinfo(fn, "nS")
if info and info.name == "dodge" and info.source and info.source:find("LocalCharacterScript", 1, true) then
dodgeFunc = fn
break
end
end
end
if not dodgeFunc then
dodgeResolveCooldownUntil = clock() + 1
end
return dodgeFunc
end
local function getDashLookVector(direction, hrpCFrame)
if not hrpCFrame then
return nil
end
local d = direction or "forward"
if d == "back" then
return -hrpCFrame.LookVector
elseif d == "left" then
return -hrpCFrame.RightVector
elseif d == "right" then
return hrpCFrame.RightVector
end
return hrpCFrame.LookVector
end
local function computeSpoofCameraCFrame(direction)
local char = players.LocalPlayer and players.LocalPlayer.Character or nil
if not char then
return nil
end
local hrp = char:FindFirstChild("HumanoidRootPart") or char:FindFirstChild("Torso")
if not hrp then
return nil
end
local lookVec = getDashLookVector(direction, hrp.CFrame)
if not lookVec then
return nil
end
local pos = hrp.Position
return CFrame.new(pos, pos + lookVec)
end
local function ensureCameraHook()
if cameraHooked then
return
end
local function isDodgeOnStack()
if not dodgeFunc then
return false
end
for level = 2, 7 do
local info = safeGetInfo(level, 'f')
if info and info.func == dodgeFunc then
return true
end
end
return false
end
local handler = LPH_NO_VIRTUALIZE(function(self, key)
if cameraSpoofActive and cameraInstance and self == cameraInstance then
if cameraSpoofUntil > 0 and clock() > cameraSpoofUntil then
cameraSpoofActive = false
cameraSpoofCFrame = nil
cameraSpoofUntil = 0
return cameraIndexOld(self, key)
end
if not isDodgeOnStack() then
return cameraIndexOld(self, key)
end
if key == 'CFrame' or key == 'CoordinateFrame' or key == 'Focus' then
return cameraSpoofCFrame
end
end
return cameraIndexOld(self, key)
end)
local wrapper = handler
if type(newcclosure) == 'function' then
wrapper = newcclosure(handler)
end
local old = hookmetamethod(game, '__index', wrapper)
if type(old) ~= 'function' then
return
end
cameraIndexOld = old
cameraHooked = true
end
local function normalizeDashDirection(direction)
if direction == "left" then
return "right"
end
return direction
end
local function callDodge()
local fn = resolveDodgeFunction()
if not fn then
return false
end
local ok, err = pcall(fn, "dodge")
if not ok then
dodgeFunc = nil
dodgeResolveCooldownUntil = clock() + 0.5
local now = clock()
if now - lastDodgeErrorAt > 2 then
lastDodgeErrorAt = now
local logger = getLogger()
if logger and logger.warn then
logger.warn("Dash failed: %s", tostring(err))
else
warn(string.format("[Cottom hub AB] Dash failed: %s", tostring(err)))
end
end
return false
end
return true
end
local function dodgeDash(direction)
if not resolveDodgeFunction() then
return false
end
direction = normalizeDashDirection(direction)
local spoof = computeSpoofCameraCFrame(direction)
local epoch = cameraSpoofEpoch + 1
if spoof ~= nil then
cameraInstance = workspaceService.CurrentCamera
cameraSpoofEpoch = epoch
cameraSpoofCFrame = spoof
cameraSpoofActive = true
cameraSpoofUntil = clock() + DODGE_CAMERA_SPOOF_SECONDS
ensureCameraHook()
else
cameraSpoofActive = false
cameraSpoofCFrame = nil
cameraSpoofUntil = 0
cameraInstance = nil
end
local ok = callDodge()
if spoof ~= nil then
local restore = function()
if cameraSpoofEpoch ~= epoch then
return
end
cameraSpoofActive = false
cameraSpoofCFrame = nil
cameraSpoofUntil = 0
end
if DODGE_CAMERA_SPOOF_SECONDS > 0 then
task.delay(DODGE_CAMERA_SPOOF_SECONDS, restore)
else
restore()
end
end
return ok
end
function InputClient.dash(direction)
return dodgeDash(normalizeDirection(direction))
end
return InputClient
end)
-- MODULE: Features/Combat/UsingSkillWatcher
defineModule("Features/Combat/UsingSkillWatcher", function()
local UsingSkillWatcher = { rules = {} }
local Players = game:GetService("Players")
local workspaceService = game:GetService("Workspace")
local userInputService = game:GetService("UserInputService")
local textChatService = game:GetService("TextChatService")
local InputClient = require("Game/InputClient")
local Configuration = require("Utility/Configuration")
local Library = require("GUI/Library")
local AttributeListener = require("Features/Combat/AttributeListener")
local Defender = require("Features/Combat/Objects/Defender")
local Logger = require("Utility/Logger")
local LocalPlayer = Players.LocalPlayer
if not LocalPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
LocalPlayer = Players.LocalPlayer
end
local lower = string.lower
local hitboxDefender = Defender.new()
local clock = os.clock
local pollActive = false
local pollTask = nil
local pollInterval = NO_HITBOX_SKILL_POLL_SECONDS
local NO_HITBOX_COOLDOWN = 0.03
local MAX_NO_HITBOX_DELAY = 0.06
local skillContext = {}
local SKILL_LOCK_BASE = 0.08
local runSkillAction
local skillDelaySeconds
local fireNoHitbox
local function noHitboxReactionBiasSeconds()
return NO_HITBOX_REACTION_BIAS_SECONDS
end
local function noHitboxSkillPollSeconds()
return NO_HITBOX_SKILL_POLL_SECONDS
end
local function normalizeShape(shape)
return shape == "Ball" and "Ball" or "Box"
end
local function passesDistance(def, rule)
if rule.imdd == nil and rule.imxd == nil then
return true
end
local distance = hitboxDefender:distance(def.character)
if not distance then
return false
end
local minDistance = rule.imdd or 0
local maxDistance = rule.imxd or math.huge
return distance >= minDistance and distance <= maxDistance
end
local function passesHitbox(def, rule)
local _ = def
local _rule = rule
return true
end
local function passesHitboxRaw(def, rule)
local _ = def
local _rule = rule
return true
end
local function blockAcquire()
return InputClient.block(true)
end
local function blockRelease()
return InputClient.block(false)
end
local function blockHoldSeconds(seconds)
return InputClient.blockHoldSeconds(seconds)
end
local function jumpNow()
return InputClient.jump()
end
local function allowActions(tag)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return false
end
local character = Players.LocalPlayer and Players.LocalPlayer.Character
if not character then
return false
end
local selectedFilters = Configuration.expectOptionValue("AutoDefenseFilters") or {}
local function filterEnabled(name, legacy)
return selectedFilters[name] or (legacy and selectedFilters[legacy])
end
local currentState = character:GetAttribute("CurrentState")
if filterEnabled("Disable When Knocked Recently") and AttributeListener.krecently and AttributeListener.krecently() then
return false
end
if filterEnabled("Disable When In Dash") and currentState == "Dashing" then
return false
end
if filterEnabled("Disable When In Flashstep") and currentState == "Flashstep" then
return false
end
if currentState == "Attacking" or currentState == "Skill" then
return false
end
local chatInputBarConfiguration = textChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
if filterEnabled("Disable When Textbox Focused")
and (userInputService:GetFocusedTextBox() or (chatInputBarConfiguration and chatInputBarConfiguration.IsFocused))
then
return false
end
if filterEnabled("Disable When Window Not Active") and iswindowactive and not iswindowactive() then
return false
end
if filterEnabled("Disable When Holding Block")
and userInputService:IsKeyDown(Enum.KeyCode.F)
then
return false
end
if tag == "Skill" and filterEnabled("Filter Out Skills", "Filter Out Mantras") then
return false
end
return true
end
local function normalizeSkillValue(raw)
local lowerValue = lower(raw)
if lowerValue:find("%s") then
return lowerValue, lowerValue:gsub("%s+", "")
end
return lowerValue, lowerValue
end
local function canonicalSkillValue(raw)
return tostring(raw or ""):gsub("[^%w]", "")
end
local function isWordBoundaryChar(ch)
return ch == nil or ch == "" or not string.match(ch, "[%w]")
end
local function boundedContains(haystack, needle)
if type(haystack) ~= "string" or type(needle) ~= "string" or needle == "" then
return false
end
local from = 1
while true do
local s, e = haystack:find(needle, from, true)
if not s then
return false
end
local prev = (s > 1) and haystack:sub(s - 1, s - 1) or nil
local nextc = (e < #haystack) and haystack:sub(e + 1, e + 1) or nil
if isWordBoundaryChar(prev) and isWordBoundaryChar(nextc) then
return true
end
from = s + 1
end
end
local function getSkillLogIdentity(def)
local plr = def.player
if not plr and def.character then
plr = Players:GetPlayerFromCharacter(def.character)
end
local name = (plr and plr.Name) or (def.character and def.character.Name) or "Unknown"
return name, plr == LocalPlayer
end
local function updateSkillEntryLabel(entry, tag, who, state, value, seconds, secondsLabel)
if not entry or not entry.Label then
return
end
local displayValue = value
if displayValue == "" then
displayValue = "(cleared)"
end
local timeText = os.date("%H:%M:%S")
local suffix = string.format(" | at=%s", timeText)
if type(seconds) == "number" then
local label = secondsLabel or "elapsed"
suffix = suffix .. string.format(" | %s=%.3fs", label, seconds)
end
entry.Label.Text = string.format("(%s) %s UsingSkill %s: %s%s", tag, who, state, displayValue, suffix)
end
local function matchSkill(lowerValue, compactValue, token, spacedToken)
local tokenLower = lower(tostring(token or ""))
if tokenLower == "" then
return false
end
local spacedLower = spacedToken and lower(tostring(spacedToken)) or nil
if spacedLower and (lowerValue == spacedLower or boundedContains(lowerValue, spacedLower)) then
return true
end
if lowerValue == tokenLower or boundedContains(lowerValue, tokenLower) then
return true
end
local canonicalValue = canonicalSkillValue(lowerValue)
local canonicalCompact = canonicalSkillValue(compactValue)
local canonicalToken = canonicalSkillValue(tokenLower)
if canonicalToken ~= "" and (canonicalValue == canonicalToken or canonicalCompact == canonicalToken) then
return true
end
if spacedLower then
local canonicalSpaced = canonicalSkillValue(spacedLower)
if canonicalSpaced ~= "" and (canonicalValue == canonicalSpaced or canonicalCompact == canonicalSpaced) then
return true
end
end
return false
end
local function jumpTwice()
InputClient.jump()
task.delay(0.08, function()
InputClient.jump()
end)
end
local function blockFor(seconds)
local hold = seconds or 0
if hold <= 0 and NO_HITBOX_ONLY then
hold = NO_HITBOX_MIN_BLOCK_SECONDS
end
if hold > 0 then
InputClient.blockHoldSeconds(hold)
end
end
local function dash(dir)
InputClient.dash(dir)
end
local function dashAfter(seconds, dir, withJump)
local delaySeconds = seconds or 0
if delaySeconds <= 0 then
if withJump then
jumpTwice()
end
return dash(dir)
end
task.delay(delaySeconds, function()
if withJump then
jumpTwice()
end
dash(dir)
end)
end
local function blockAfter(seconds, duration)
local delaySeconds = seconds or 0
if delaySeconds <= 0 then
return blockFor(duration)
end
task.delay(delaySeconds, function()
blockFor(duration)
end)
end
local localSkillEndRules = {}
local function addSkillRule(token, spacedToken, onEnd)
localSkillEndRules[#localSkillEndRules + 1] = {
token = token,
spacedToken = spacedToken,
onEnd = onEnd,
}
end
local function addAssetRule(id, onEnd)
local token = "rbxassetid://" .. tostring(id)
addSkillRule(token, nil, onEnd)
addSkillRule(tostring(id), nil, onEnd)
end
addSkillRule("uzumakibarrage", "uzumaki barrage", function()
blockFor(0.3)
end)
addSkillRule("shadowclone", "shadow clone", function()
blockFor(0.8)
end)
addSkillRule("clonethrow", "clone throw", function()
blockFor(1.2)
end)
addSkillRule("fireballjutsu", "fireball jutsu", function(_, _, _, duration)
if duration and duration >= 2.0 then
blockFor(3)
else
dashAfter(0.65, "right")
end
end)
addSkillRule("chidori", nil, function()
dash("back")
end)
addSkillRule("fumashuriken", "fuma shuriken", function()
blockFor(1.2)
end)
addSkillRule("lionsbarrage", "lion's barrage", function()
blockFor(0.8)
end)
addSkillRule("headhunterjutsu", "headhunter jutsu", function()
blockFor(0.8)
end)
addSkillRule("kunai", nil, function()
blockFor(1.1)
end)
addSkillRule("primarylotus", "primary lotus", function()
blockFor(0.6)
end)
addSkillRule("shuriken", nil, function()
blockFor(0.5)
end)
addSkillRule("leafhurricane", "leaf hurricane", function()
blockFor(0.8)
end)
addSkillRule("risingwind", "rising wind", function()
dash("back")
end)
addSkillRule("rotation", nil, function()
dash("back")
end)
addSkillRule("explosivekunai", "explosive kunai", function()
blockFor(0.8)
end)
addSkillRule("64palms", "64 palms", function()
blockFor(6)
end)
addSkillRule("airpalm", "air palm", function()
blockFor(0.7)
end)
addSkillRule("waterclone", "water clone", function()
blockFor(1.2)
end)
addSkillRule("decapitate", nil, function()
task.delay(0.6, function()
jumpTwice()
dash("back")
end)
end)
addSkillRule("swordthrow", "sword throw", function()
blockFor(0.5)
end)
addSkillRule("sandcoffin", "sand coffin", function()
blockFor(0.3)
end)
addSkillRule("sandtsunami", "sand tsunami", function()
jumpTwice()
dash("right")
end)
addSkillRule("sandspikes", "sand spikes", function()
blockFor(1.2)
end)
addSkillRule("chidoristream", "chidori stream", function()
blockFor(1.1)
end)
addSkillRule("chidorisenbon", "chidori senbon", function()
blockFor(0.75)
end)
addSkillRule("chidoritruespear", "chidori true spear", function()
jumpTwice()
dash("right")
end)
addSkillRule("rasengancombo", "rasengan combo", function()
blockFor(0.3)
end)
addSkillRule("shadowclones", "shadow clones", function()
blockFor(0.9)
end)
addSkillRule("rasenganthrow", "rasengan throw", function()
task.delay(0.7, function()
jumpTwice()
dash("right")
end)
end)
addSkillRule("shurikenassualt", "shuriken assualt", function()
blockFor(0.8)
end)
addSkillRule("demonicillusion", "demonic illusion", function()
blockFor(0.65)
end)
addSkillRule("asurapath", "asura path", function()
blockAfter(0.3, 0.75)
end)
addSkillRule("sixpathscombo", "six paths combo", function()
blockFor(1.1)
end)
addSkillRule("almightypush", "almighty push", function(_, _, _, duration)
if duration and duration > 1.5 then
dash("back")
else
blockFor(1.2)
end
end)
addSkillRule("universalpull", "universal pull", function()
blockFor(0.8)
end)
addSkillRule("nunchaku", nil, function()
blockFor(6)
end)
addSkillRule("severehurricane", "severe hurricane", function()
task.delay(0.19, function()
jumpTwice()
dash("right")
end)
end)
addSkillRule("dynamicentry", "dynamic entry", function()
blockFor(1.5)
end)
addSkillRule("flipkick", "flip kick", function()
blockAfter(0.4, 0.9)
end)
addSkillRule("vicegrip", "vice grip", function()
blockFor(1.1)
end)
addSkillRule("ligerbomb", "liger bomb", function()
blockFor(0.4)
end)
addSkillRule("sharkbombjutsu", "shark bomb jutsu", function()
task.delay(0.5, function()
dash("right")
end)
end)
addSkillRule("waterprison", "water prison", function()
blockAfter(0.7, 0.4)
end)
addSkillRule("samehadafeast", "samehada feast", function()
jumpTwice()
dash("back")
end)
addAssetRule(3240243801, function()
blockFor(0.3)
end)
addAssetRule(3240242879, function()
blockFor(0.3)
end)
addAssetRule(3240241715, function()
blockFor(0.3)
end)
addAssetRule(3240240417, function()
blockFor(0.3)
end)
addAssetRule(3240245051, function()
dash("back")
end)
addAssetRule(3263871265, function()
blockFor(0.3)
end)
addAssetRule(3263870104, function()
blockFor(0.3)
end)
addAssetRule(3263925841, function()
dash("back")
end)
addAssetRule(12831969898, function()
dashAfter(0.2, "right")
end)
addAssetRule(14437150122, function()
blockFor(0.3)
end)
addAssetRule(14437148019, function()
blockFor(0.3)
end)
addAssetRule(14437145085, function()
blockFor(0.3)
end)
addAssetRule(14436312737, function()
blockFor(0.3)
end)
addAssetRule(14437152064, function()
dash("back")
end)
local function handleLocalSkillEnd(rawValue, durationSeconds)
if type(rawValue) ~= "string" or rawValue == "" then
return
end
local lowerValue, compactValue = normalizeSkillValue(rawValue)
local bestRule = nil
local bestLen = -1
for _, rule in ipairs(localSkillEndRules) do
if rule and rule.token and matchSkill(lowerValue, compactValue, rule.token, rule.spacedToken) then
local len = #tostring(rule.token)
if rule.spacedToken and #rule.spacedToken > len then
len = #rule.spacedToken
end
if len > bestLen then
bestLen = len
bestRule = rule
end
end
end
if bestRule and type(bestRule.onEnd) == "function" then
bestRule.onEnd(nil, lowerValue, compactValue, durationSeconds)
end
end
local function deferHitboxAction(def, key, rule, delaySeconds, callback)
local delay = delaySeconds or 0
local info = def.stateInfo and def.stateInfo[key]
if not info then
return false
end
info.delayToken = (info.delayToken or 0) + 1
local token = info.delayToken
task.delay(delay, function()
if not def.states or def.states[key] ~= true then
return
end
if info.delayToken ~= token then
return
end
if not allowActions("Skill") then
return
end
if not passesDistance(def, rule) then
return
end
if type(callback) == "function" then
local ok, err = pcall(function()
executeDefensiveAction(def, rule, callback)
end)
if not ok then
Logger.warn("UsingSkill deferred callback failed: %s", tostring(err))
end
end
end)
return true
end
local function logUsingSkill(def, rawValue, isLocal)
local toggleKey = isLocal and "LogUsingSkillLocal" or "LogUsingSkillOthers"
if not Configuration.expectToggleValue(toggleKey) then
def.lastLoggedValue = nil
def.lastSkillValue = nil
def.lastSkillStart = nil
def.lastSkillEntry = nil
def.lastSkillUpdateAt = nil
def.lastSkillToken = (def.lastSkillToken or 0) + 1
return
end
local value = type(rawValue) == "string" and rawValue or ""
local now = clock()
local playerName = (def.player and def.player.Name) or (def.character and def.character.Name) or "Unknown"
local tag = isLocal and "Local" or "Other"
if def.lastLoggedValue == value then
if value ~= "" and def.lastSkillEntry and def.lastSkillStart then
if not def.lastSkillUpdateAt or (now - def.lastSkillUpdateAt) >= 0.1 then
def.lastSkillUpdateAt = now
local elapsed = now - def.lastSkillStart
updateSkillEntryLabel(def.lastSkillEntry, tag, playerName, "Running", value, elapsed, "elapsed")
end
end
return
end
local previousValue = def.lastSkillValue or ""
if value ~= previousValue then
if previousValue ~= "" then
local duration = def.lastSkillStart and (now - def.lastSkillStart) or nil
if def.lastSkillEntry then
updateSkillEntryLabel(def.lastSkillEntry, tag, playerName, "End", previousValue, duration, "lasted")
end
if Library and Library.AddUsingSkillEntry then
Library:AddUsingSkillEntry(playerName, previousValue, isLocal, "End", duration)
end
if isLocal then
handleLocalSkillEnd(previousValue, duration)
end
def.lastSkillEntry = nil
def.lastSkillUpdateAt = nil
def.lastSkillToken = (def.lastSkillToken or 0) + 1
end
if value ~= "" then
def.lastSkillValue = value
def.lastSkillStart = now
if Library and Library.AddUsingSkillEntry then
def.lastSkillToken = (def.lastSkillToken or 0) + 1
local token = def.lastSkillToken
Library:AddUsingSkillEntry(playerName, value, isLocal, "Start", nil, function(entry)
if def.lastSkillToken ~= token or def.lastSkillValue ~= value then
return
end
def.lastSkillEntry = entry
def.lastSkillUpdateAt = now
end)
end
else
def.lastSkillValue = ""
def.lastSkillStart = nil
def.lastSkillEntry = nil
def.lastSkillUpdateAt = nil
def.lastSkillToken = (def.lastSkillToken or 0) + 1
end
end
def.lastLoggedValue = value
end
local usingSkillDefs = {}
function UsingSkillWatcher.setRules(rules)
UsingSkillWatcher.rules = rules or {}
for _, rule in pairs(UsingSkillWatcher.rules) do
if typeof(rule.hitbox) == "table" and typeof(rule.hitbox.X) == "number" then
rule.hitbox = Vector3.new(rule.hitbox.X, rule.hitbox.Y, rule.hitbox.Z)
end
if typeof(rule.offset) == "table" and typeof(rule.offset.X) == "number" then
rule.offset = Vector3.new(rule.offset.X, rule.offset.Y, rule.offset.Z)
end
if rule.shape then
rule.shape = normalizeShape(rule.shape)
end
if rule.fhb == nil then
rule.fhb = true
end
end
end
function UsingSkillWatcher.blockAcquire()
blockAcquire()
end
function UsingSkillWatcher.blockRelease()
blockRelease()
end
function UsingSkillWatcher.blockHoldSeconds(seconds)
return blockHoldSeconds(seconds)
end
function UsingSkillWatcher.jump()
return jumpNow()
end
function UsingSkillWatcher.deferHitbox(def, key, delaySeconds, callback)
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if not rule or not def then
return false
end
return deferHitboxAction(def, key, rule, delaySeconds, callback)
end
local function updateOwner(def)
local obj = def.obj
local character = obj and obj:FindFirstAncestorWhichIsA("Model")
def.character = character
def.player = character and Players:GetPlayerFromCharacter(character) or nil
end
local function safeRuleStart(def, rule, lowerValue, compactValue)
local ok, err = pcall(function()
executeDefensiveAction(def, rule, function()
return rule.onStart(def, lowerValue, compactValue)
end)
end)
if not ok then
Logger.warn("UsingSkill onStart failed: %s", tostring(err))
end
end
local function executeRuleOnStart(def, key, rule, lowerValue, compactValue)
if not def or not rule or type(rule.onStart) ~= "function" then
return
end
if rule.ihbc then
if fireNoHitbox then
fireNoHitbox(def, key, rule, lowerValue, compactValue)
else
safeRuleStart(def, rule, lowerValue, compactValue)
end
return
end
if runSkillAction then
runSkillAction(def, key, rule, lowerValue, compactValue, function()
safeRuleStart(def, rule, lowerValue, compactValue)
end)
else
safeRuleStart(def, rule, lowerValue, compactValue)
end
end
function UsingSkillWatcher.update(def)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
updateOwner(def)
local plr = def.player
local skill = def.obj
local rawValue = skill and skill.Value
local now = clock()
if plr == LocalPlayer then
logUsingSkill(def, rawValue, true)
if def and def.states then
for key, active in pairs(def.states) do
if active then
def.states[key] = false
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if rule and type(rule.onStop) == "function" then
rule.onStop(def)
end
end
end
end
return
end
if plr then
logUsingSkill(def, rawValue, false)
end
if not plr then
if def and def.states then
for key, active in pairs(def.states) do
if active then
def.states[key] = false
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if rule and type(rule.onStop) == "function" then
rule.onStop(def)
end
end
end
end
return
end
if not skill or skill.Parent == nil then
if def and def.states then
for key, active in pairs(def.states) do
if active then
def.states[key] = false
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if rule and type(rule.onStop) == "function" then
rule.onStop(def)
end
end
end
end
return
end
local value = skill.Value
if type(value) ~= "string" then
if def and def.states then
for key, active in pairs(def.states) do
if active then
def.states[key] = false
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if rule and type(rule.onStop) == "function" then
rule.onStop(def)
end
end
end
end
return
end
local lowerValue, compactValue = normalizeSkillValue(value)
def.lastValue = lowerValue
local canAct = allowActions("Skill")
def.states = def.states or {}
def.stateInfo = def.stateInfo or {}
for key, rule in pairs(UsingSkillWatcher.rules) do
if not rule or not rule.token then
continue
end
local info = def.stateInfo[key]
if not info then
info = { holdUntil = 0 }
def.stateInfo[key] = info
end
local tokenMatched = canAct and matchSkill(lowerValue, compactValue, rule.token, rule.spacedToken)
if tokenMatched and type(rule.match) == "function" and not rule.match(def, lowerValue, compactValue) then
tokenMatched = false
end
local isDeferred = rule.deferHitboxSeconds and rule.deferHitboxSeconds > 0
local distanceMatched = tokenMatched and passesDistance(def, rule)
local wasActive = def.states[key] == true
if NO_HITBOX_ONLY then
if distanceMatched then
if not wasActive then
def.states[key] = true
info.hitboxActive = true
info.guardPulseAt = now
if handleAutoCounterTiming then
handleAutoCounterTiming({ tag = "Skill" }, "SkillWatcher", hitboxDefender:distance(def.character))
end
blockFor((rule and rule.minBlockSeconds) or NO_HITBOX_MIN_BLOCK_SECONDS)
if type(rule.onStart) == "function" then
executeRuleOnStart(def, key, rule, lowerValue, compactValue)
end
else
if type(rule.onTick) == "function" then
rule.onTick(def, lowerValue, compactValue)
else
local pulseAt = info.guardPulseAt or 0
if now - pulseAt >= NO_HITBOX_ACTIVE_GUARD_PULSE_SECONDS then
info.guardPulseAt = now
blockFor((rule and rule.minBlockSeconds) or NO_HITBOX_MIN_BLOCK_SECONDS)
end
end
end
elseif wasActive then
def.states[key] = false
info.hitboxActive = false
if info.trackToken then
info.trackToken = info.trackToken + 1
end
if type(rule.onStop) == "function" then
rule.onStop(def, lowerValue, compactValue)
end
end
else
local ruleMatched = distanceMatched and passesHitbox(def, rule)
local hitboxWasActive = info.hitboxActive == true
if tokenMatched and not wasActive then
def.states[key] = true
info.hitboxActive = false
if handleAutoCounterTiming then
handleAutoCounterTiming({ tag = "Skill" }, "SkillWatcher", hitboxDefender:distance(def.character))
end
if isDeferred and type(rule.onStart) == "function" then
executeRuleOnStart(def, key, rule, lowerValue, compactValue)
end
elseif not tokenMatched and wasActive then
def.states[key] = false
info.hitboxActive = false
if info.trackToken then
info.trackToken = info.trackToken + 1
end
if type(rule.onStop) == "function" then
rule.onStop(def, lowerValue, compactValue)
end
end
if tokenMatched and not isDeferred then
if ruleMatched and not hitboxWasActive then
info.hitboxActive = true
executeRuleOnStart(def, key, rule, lowerValue, compactValue)
elseif ruleMatched and hitboxWasActive then
if type(rule.onTick) == "function" then
rule.onTick(def, lowerValue, compactValue)
end
elseif not ruleMatched and hitboxWasActive then
info.hitboxActive = false
end
end
end
end
end
function UsingSkillWatcher.queueUpdate(def)
if not def then
return
end
UsingSkillWatcher.update(def)
end
local function detachUsingSkill(obj)
local def = usingSkillDefs[obj]
if not def then
return
end
local lastSkillValue = def.lastSkillValue or ""
if lastSkillValue ~= "" then
local playerName, isLocal = getSkillLogIdentity(def)
local tag = isLocal and "Local" or "Other"
local duration = def.lastSkillStart and (clock() - def.lastSkillStart) or nil
if def.lastSkillEntry then
updateSkillEntryLabel(def.lastSkillEntry, tag, playerName, "End", lastSkillValue, duration, "lasted")
end
if Library and Library.AddUsingSkillEntry then
Library:AddUsingSkillEntry(playerName, lastSkillValue, isLocal, "End", duration)
end
def.lastSkillValue = ""
def.lastSkillStart = nil
def.lastSkillEntry = nil
def.lastSkillUpdateAt = nil
def.lastSkillToken = (def.lastSkillToken or 0) + 1
end
if def.states then
for key, active in pairs(def.states) do
if active then
def.states[key] = false
local rule = UsingSkillWatcher.rules and UsingSkillWatcher.rules[key]
if rule and type(rule.onStop) == "function" then
rule.onStop(def)
end
end
end
end
usingSkillDefs[obj] = nil
if def.valueConn then
def.valueConn:Disconnect()
end
if def.ancestryConn then
def.ancestryConn:Disconnect()
end
end
local function attachUsingSkill(obj)
if usingSkillDefs[obj] ~= nil then
return false
end
local def = { obj = obj, player = nil, character = nil, lastValue = nil, states = {} }
usingSkillDefs[obj] = def
if obj.GetPropertyChangedSignal then
def.valueConn = obj:GetPropertyChangedSignal("Value"):Connect(function()
UsingSkillWatcher.queueUpdate(def)
end)
else
def.valueConn = obj.Changed:Connect(function()
UsingSkillWatcher.queueUpdate(def)
end)
end
def.ancestryConn = obj.AncestryChanged:Connect(function()
if not obj:IsDescendantOf(game) then
detachUsingSkill(obj)
return
end
UsingSkillWatcher.queueUpdate(def)
end)
UsingSkillWatcher.queueUpdate(def)
return true
end
local function onDescendantAdded(descendant)
if descendant.Name == "UsingSkill" and descendant:IsA("ValueBase") then
attachUsingSkill(descendant)
end
end
local function onDescendantRemoving(descendant)
if usingSkillDefs[descendant] ~= nil then
detachUsingSkill(descendant)
end
end
local liveInstance = nil
local liveAddedConn = nil
local liveRemovingConn = nil
local liveChildAddedConn = nil
local liveChildRemovedConn = nil
local function clearLiveState()
for obj in pairs(usingSkillDefs) do
detachUsingSkill(obj)
end
end
local function attachLive(live)
if liveInstance == live then
return
end
if liveAddedConn then
liveAddedConn:Disconnect()
liveAddedConn = nil
end
if liveRemovingConn then
liveRemovingConn:Disconnect()
liveRemovingConn = nil
end
liveInstance = live
if not live then
clearLiveState()
return
end
liveAddedConn = live.DescendantAdded:Connect(onDescendantAdded)
liveRemovingConn = live.DescendantRemoving:Connect(onDescendantRemoving)
for _, descendant in ipairs(live:GetDescendants()) do
onDescendantAdded(descendant)
end
end
function UsingSkillWatcher.init()
attachLive(workspaceService:FindFirstChild("Live"))
if not pollActive then
pollActive = true
pollTask = task.spawn(LPH_NO_VIRTUALIZE(function()
while pollActive do
local interval = NO_HITBOX_ONLY and noHitboxSkillPollSeconds() or pollInterval
task.wait(interval)
for _, def in pairs(usingSkillDefs) do
UsingSkillWatcher.update(def)
end
end
end))
end
liveChildAddedConn = workspaceService.ChildAdded:Connect(function(child)
if child.Name == "Live" then
attachLive(child)
end
end)
liveChildRemovedConn = workspaceService.ChildRemoved:Connect(function(child)
if child == liveInstance then
attachLive(nil)
end
end)
end
skillDelaySeconds = function()
if NO_HITBOX_ONLY then
return 0
end
local biased = Defender.rdelay() + noHitboxReactionBiasSeconds()
return math.clamp(math.max(0, biased), 0, MAX_NO_HITBOX_DELAY)
end
local function getSkillContext(def)
if not def or not def.character then
return nil
end
local ctx = skillContext[def.character]
if not ctx then
ctx = { lockUntil = 0, lockPriority = 0, lastSkillAt = {} }
skillContext[def.character] = ctx
end
return ctx
end
runSkillAction = function(def, key, rule, lowerValue, compactValue, actionFn)
local _ = compactValue
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
local ctx = getSkillContext(def)
if not ctx then
return
end
local now = clock()
local distance = hitboxDefender:distance(def.character) or math.huge
local value = tostring(lowerValue or "")
local threat = (rule and tonumber(rule.priority)) or 1
if distance <= 16 then
threat = threat + 2.6
elseif distance <= 30 then
threat = threat + 1.8
elseif distance <= 50 then
threat = threat + 1.1
elseif distance <= 75 then
threat = threat + 0.6
else
threat = threat + 0.2
end
if value:find("grab", 1, true) or value:find("rush", 1, true) or value:find("combo", 1, true) then
threat = threat + 1.4
elseif value:find("barrage", 1, true) or value:find("gatling", 1, true) or value:find("onslaught", 1, true) then
threat = threat + 1.1
elseif value:find("meteor", 1, true) or value:find("final", 1, true) or value:find("ult", 1, true) then
threat = threat + 1.2
end
local prio = 1
local cdMs = 75
local lockSeconds = (rule and rule.lockSeconds) or SKILL_LOCK_BASE
if threat >= 4.8 then
prio = 3
cdMs = 35
lockSeconds = math.max(lockSeconds, 0.10)
elseif threat >= 3.2 then
prio = 2
cdMs = 52
lockSeconds = math.max(lockSeconds, 0.08)
else
prio = 1
cdMs = 75
lockSeconds = math.max(lockSeconds, 0.06)
end
if rule and type(rule.priority) == "number" then
prio = math.max(prio, rule.priority)
end
if rule and type(rule.cooldownMs) == "number" then
cdMs = math.max(0, rule.cooldownMs)
end
if not NO_HITBOX_ONLY and ctx.lockUntil and now < ctx.lockUntil and prio < (ctx.lockPriority or 0) then
return
end
local last = ctx.lastSkillAt[key]
if last and now - last < (cdMs / 1000) then
return
end
ctx.lastSkillAt[key] = now
ctx.lockPriority = prio
ctx.lockUntil = now + (NO_HITBOX_ONLY and 0.03 or lockSeconds)
local delay = 0
if not NO_HITBOX_ONLY then
delay = rule.adaptiveDelay == false and 0 or skillDelaySeconds()
end
if delay > 0 then
task.delay(delay, function()
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
local prev = activeSkillDef
activeSkillDef = def
actionFn()
activeSkillDef = prev
end)
else
local prev = activeSkillDef
activeSkillDef = def
actionFn()
activeSkillDef = prev
end
end
fireNoHitbox = function(def, key, rule, lowerValue, compactValue)
if not def or not rule or type(rule.onStart) ~= "function" then
return
end
def.stateInfo = def.stateInfo or {}
local info = def.stateInfo[key]
if not info then
info = { holdUntil = 0 }
def.stateInfo[key] = info
end
local now = clock()
local cooldown = rule.nhcd or NO_HITBOX_COOLDOWN
if info.nhLast and now - info.nhLast < cooldown then
return
end
info.nhLast = now
local function safeStart()
local ok, err = pcall(function()
executeDefensiveAction(def, rule, function()
return rule.onStart(def, lowerValue, compactValue)
end)
end)
if not ok then
Logger.warn("UsingSkill onStart failed: %s", tostring(err))
end
end
if runSkillAction then
runSkillAction(def, key, rule, lowerValue, compactValue, safeStart)
else
safeStart()
end
end
function UsingSkillWatcher.detach()
if liveAddedConn then
liveAddedConn:Disconnect()
liveAddedConn = nil
end
if liveRemovingConn then
liveRemovingConn:Disconnect()
liveRemovingConn = nil
end
if liveChildAddedConn then
liveChildAddedConn:Disconnect()
liveChildAddedConn = nil
end
if liveChildRemovedConn then
liveChildRemovedConn:Disconnect()
liveChildRemovedConn = nil
end
clearLiveState()
liveInstance = nil
InputClient.block(false)
pollActive = false
pollTask = nil
end
UsingSkillWatcher._allowActions = allowActions
UsingSkillWatcher._passesDistance = passesDistance
UsingSkillWatcher._passesHitboxRaw = passesHitboxRaw
return UsingSkillWatcher
end)
-- MODULE: Features/Combat/M1AutoTrade
defineModule("Features/Combat/M1AutoTrade", function()
local M1AutoTrade = {}
local Players = game:GetService("Players")
local userInputService = game:GetService("UserInputService")
local workspaceService = game:GetService("Workspace")
local Configuration = require("Utility/Configuration")
local InputClient = require("Game/InputClient")
local Table = require("Utility/Table")
local AutoCombo = require("Features/Combat/AutoCombo")
local Logger = require("Utility/Logger")
local lastTradeAt = {}
local TRADE_COOLDOWN = 0.15
local METERS_TO_STUDS = 3.57
local lastHitObj = nil
local lastHitConn = nil
local lastHitDescConn = nil
local lastHitAt = 0
local lastHitTarget = nil
local localM1WindowStart = 0
local localM1WindowUntil = 0
local localM1Token = 0
local LOCAL_M1_RANGE_STUDS = 15
local localInitDone = false
local clearingLastHit = false
local function safeLower(value)
local lowerFn = string and string.lower
if lowerFn then
return lowerFn(tostring(value or ""))
end
return tostring(value or "")
end
local function isLikelyM1Name(name)
local lowered = safeLower(name)
if lowered == "" then
return false
end
if lowered == "m1" then
return true
end
if lowered:find("m1_")
or lowered:find("_m1")
or lowered:find(" m1")
or lowered:find("m1 ")
or lowered:find("-m1")
or lowered:find("m1-")
then
return true
end
if lowered:find("light") or lowered:find("basic") or lowered:find("jab") then
return true
end
return false
end
local function isLocalUsingSkill()
local localCharacter = Players.LocalPlayer and Players.LocalPlayer.Character
if not localCharacter then
return false
end
local state = localCharacter:GetAttribute("CurrentState")
if state == "Skill" then
return true
end
local skillValue = localCharacter:FindFirstChild("UsingSkill", true)
if skillValue and skillValue:IsA("ValueBase") then
local raw = skillValue.Value
if type(raw) == "string" then
local value = safeLower(raw)
if value ~= "" and value ~= "none" and value ~= "idle" then
return true
end
end
end
return false
end
local function resolveLastHit(character)
if not character then
return nil
end
local direct = character:FindFirstChild("LastHit")
if direct and direct:IsA("ValueBase") then
return direct
end
local descendant = character:FindFirstChild("LastHit", true)
if descendant and descendant:IsA("ValueBase") then
return descendant
end
return nil
end
local clearValueBase
local listCloseTargets
clearValueBase = function(valueObj)
if not valueObj or not valueObj:IsA("ValueBase") then
return
end
pcall(function()
if valueObj:IsA("ObjectValue") then
valueObj.Value = nil
elseif valueObj:IsA("StringValue") then
valueObj.Value = ""
elseif valueObj:IsA("BoolValue") then
valueObj.Value = false
elseif valueObj:IsA("NumberValue") or valueObj:IsA("IntValue") then
valueObj.Value = 0
else
valueObj.Value = nil
end
end)
end
local function startComboIfReady()
if not Configuration.expectToggleValue("EnableAutoCombo") then
return false
end
if not Configuration.expectToggleValue("AutoComboOnM1Hit") then
return false
end
if AutoCombo and AutoCombo.isRunning and AutoCombo.isRunning() then
return false
end
if isLocalUsingSkill() then
return false
end
local profileName = Configuration.expectOptionValue("AutoComboProfile") or ""
if profileName == "" then
if Logger and Logger.longNotify then
Logger.longNotify("Please select an Auto-Combo profile.")
end
return false
end
AutoCombo.run(profileName)
return true
end
local function openLocalM1Window()
if not Configuration.expectToggleValue("EnableAutoCombo") then
return
end
if not Configuration.expectToggleValue("AutoComboOnM1Hit") then
return
end
if AutoCombo and AutoCombo.isRunning and AutoCombo.isRunning() then
return
end
if isLocalUsingSkill() then
return
end
local holdMs = Configuration.expectOptionValue("AutoComboOnM1HoldMs") or 250
local holdSeconds = math.max(holdMs, 10) / 1000
local now = os.clock()
localM1WindowStart = now
localM1WindowUntil = now + holdSeconds
local targets = listCloseTargets(LOCAL_M1_RANGE_STUDS)
if #targets == 0 then
return
end
local snapshot = {}
for _, target in ipairs(targets) do
snapshot[target.character] = target.humanoid.Health
end
localM1Token = localM1Token + 1
local token = localM1Token
local deadline = localM1WindowUntil
task.spawn(function()
while os.clock() <= deadline do
if token ~= localM1Token then
return
end
if AutoCombo and AutoCombo.isRunning and AutoCombo.isRunning() then
return
end
if isLocalUsingSkill() then
return
end
for _, target in ipairs(listCloseTargets(LOCAL_M1_RANGE_STUDS)) do
local prev = snapshot[target.character]
if prev and target.humanoid.Health < prev then
startComboIfReady()
return
end
end
task.wait(0.03)
end
end)
end
local function handleLastHitChanged()
if clearingLastHit then
return
end
local obj = lastHitObj
if not obj then
return
end
local raw = nil
local ok = pcall(function()
raw = obj.Value
end)
if not ok then
return
end
if raw == nil or raw == "" then
return
end
lastHitTarget = raw
lastHitAt = os.clock()
clearingLastHit = true
clearValueBase(obj)
clearingLastHit = false
end
local function isM1Block03(timing)
if not timing or timing.tag ~= "M1" then
return false
end
local startBlock = false
local endBlock03 = false
for _, action in next, timing.actions._data do
local atype = tostring(action._type or "")
if atype == "Start Block" then
startBlock = true
elseif atype == "End Block" then
local whenMs = tonumber(action._when) or 0
if whenMs == 300 then
endBlock03 = true
end
end
end
return startBlock and endBlock03
end
local function isAlly(player)
local localPlayer = Players.LocalPlayer
if not localPlayer or not player then
return false
end
local ok, status = pcall(localPlayer.GetFriendStatus, localPlayer, player)
if not ok then
return false
end
return status == Enum.FriendStatus.Friend
end
listCloseTargets = function(maxDistanceStuds)
local ents = workspaceService:FindFirstChild("Live")
if not ents then
return {}
end
local localCharacter = Players.LocalPlayer and Players.LocalPlayer.Character
if not localCharacter then
return {}
end
local localRootPart = localCharacter:FindFirstChild("HumanoidRootPart") or localCharacter:FindFirstChild("Torso")
if not localRootPart then
return {}
end
local targets = {}
for _, entity in next, ents:GetChildren() do
if entity == localCharacter then
continue
end
local playerFromCharacter = Players:GetPlayerFromCharacter(entity)
if playerFromCharacter and Configuration.expectToggleValue("IgnorePlayers") then
continue
end
if playerFromCharacter and isAlly(playerFromCharacter) and Configuration.expectToggleValue("IgnoreAllies") then
continue
end
local humanoid = entity:FindFirstChildWhichIsA("Humanoid")
if not humanoid or humanoid.Health <= 0 then
continue
end
local rootPart = entity:FindFirstChild("HumanoidRootPart") or entity:FindFirstChild("Torso")
if not rootPart then
continue
end
local distance = (rootPart.Position - localRootPart.Position).Magnitude
if distance <= maxDistanceStuds then
targets[#targets + 1] = {
character = entity,
humanoid = humanoid,
root = rootPart,
distance = distance,
}
end
end
return targets
end
local function bestTargets()
local ents = workspaceService:FindFirstChild("Live")
if not ents then
return {}
end
local localCharacter = Players.LocalPlayer and Players.LocalPlayer.Character
if not localCharacter then
return {}
end
local localRootPart = localCharacter:FindFirstChild("HumanoidRootPart")
if not localRootPart then
return {}
end
local currentCamera = workspaceService.CurrentCamera
if not currentCamera then
return {}
end
local targets = {}
local maxDistance = Configuration.expectOptionValue("M1TradeDistanceHit") or 11.1
maxDistance = maxDistance * METERS_TO_STUDS
for _, entity in next, ents:GetChildren() do
if entity == localCharacter then
continue
end
local playerFromCharacter = Players:GetPlayerFromCharacter(entity)
if not playerFromCharacter then
continue
end
if playerFromCharacter and Configuration.expectToggleValue("IgnorePlayers") then
continue
end
local humanoid = entity:FindFirstChildWhichIsA("Humanoid")
if not humanoid or humanoid.Health <= 0 then
continue
end
local rootPart = entity:FindFirstChild("HumanoidRootPart")
if not rootPart then
continue
end
local distance = (rootPart.Position - localRootPart.Position).Magnitude
if distance > maxDistance then
continue
end
if playerFromCharacter and isAlly(playerFromCharacter) and Configuration.expectToggleValue("IgnoreAllies") then
continue
end
local mousePosition = userInputService:GetMouseLocation()
local unitRay = workspaceService.CurrentCamera:ScreenPointToRay(mousePosition.X, mousePosition.Y)
local distanceToCrosshair = unitRay:Distance(rootPart.Position)
local fieldOfViewToEntity = currentCamera.CFrame.LookVector:Dot((localRootPart.Position - rootPart.Position).Unit)
targets[#targets + 1] = {
character = entity,
root = rootPart,
humanoid = humanoid,
dc = distanceToCrosshair,
fov = fieldOfViewToEntity,
du = distance,
}
end
local sortType = Configuration.expectOptionValue("M1TradeSelectionType") or "Closest In Distance"
if sortType == "Closest To Crosshair" then
table.sort(targets, function(a, b)
return a.dc < b.dc
end)
elseif sortType == "Least Health" then
table.sort(targets, function(a, b)
return a.humanoid.Health < b.humanoid.Health
end)
else
table.sort(targets, function(a, b)
return a.du < b.du
end)
end
local maxTargets = Configuration.expectOptionValue("M1TradeMaxTargets") or 1
return Table.slice(targets, 1, maxTargets)
end
local function isAllowedTarget(entity)
for _, target in next, bestTargets() do
if target.character == entity then
return true
end
end
return false
end
local function doM1Trade()
local holdMs = Configuration.expectOptionValue("M1TradeBlockHoldMs") or 160
local waitMs = Configuration.expectOptionValue("M1TradeBlockWaitMs") or 0
local holdSeconds = math.max(holdMs, 10) / 1000
local waitSeconds = math.max(waitMs, 0) / 1000
if waitSeconds > 0 then
task.wait(waitSeconds)
end
InputClient.blockTap(holdSeconds)
if Configuration.expectToggleValue("M1TradeManualOnly") then
return
end
task.delay(holdSeconds + 0.005, function()
local oneTap = Configuration.expectToggleValue("M1TradeOneAutoM1")
local totalClicks = oneTap and 1 or 5
local gapMs = Configuration.expectOptionValue("M1TradeClickGapMs") or 40
local holdMs = Configuration.expectOptionValue("M1TradeClickHoldMs") or 30
local gapSeconds = math.max(gapMs, 10) / 1000
local holdSecondsClick = math.max(holdMs, 10) / 1000
if totalClicks == 1 then
if InputClient._m1Down or InputClient._m1KeyDown then
InputClient.m1Release()
task.wait(0.02)
end
InputClient.m1Tap(holdSecondsClick)
InputClient.m1Release()
return
end
for i = 1, totalClicks do
InputClient.m1Tap(holdSecondsClick)
if i < totalClicks then
task.wait(gapSeconds)
end
end
InputClient.m1Release()
end)
end
function M1AutoTrade.handle(defender, timing, distance)
if not Configuration.expectToggleValue("EnableM1AutoTrade") then
return
end
if not isM1Block03(timing) then
return
end
if not defender or not defender.entity then
return
end
local dist = distance or defender:distance(defender.entity) or math.huge
local maxDistance = (Configuration.expectOptionValue("M1TradeDistanceHit") or 11.1) * METERS_TO_STUDS
if dist > maxDistance then
return
end
if not isAllowedTarget(defender.entity) then
return
end
local last = lastTradeAt[defender.entity]
local now = os.clock()
if last and now - last < TRADE_COOLDOWN then
return
end
lastTradeAt[defender.entity] = now
doM1Trade()
end
function M1AutoTrade.handleEntity(timing, entity, distance)
if not Configuration.expectToggleValue("EnableM1AutoTrade") then
return
end
if not isM1Block03(timing) then
return
end
if not entity then
return
end
local dist = distance or math.huge
local maxDistance = (Configuration.expectOptionValue("M1TradeDistanceHit") or 11.1) * METERS_TO_STUDS
if dist > maxDistance then
return
end
if not isAllowedTarget(entity) then
return
end
local last = lastTradeAt[entity]
local now = os.clock()
if last and now - last < TRADE_COOLDOWN then
return
end
lastTradeAt[entity] = now
doM1Trade()
end
function M1AutoTrade.handleLocalM1(timing, track)
local isM1 = timing and timing.tag == "M1"
if not isM1 and not timing and track then
if isLikelyM1Name(track.Name) then
isM1 = true
else
local anim = track.Animation
if anim and isLikelyM1Name(anim.Name) then
isM1 = true
else
local aid = (anim and anim.AnimationId) or track.AnimationId
if isLikelyM1Name(aid) then
isM1 = true
end
end
end
end
if not isM1 then
return
end
openLocalM1Window()
end
function M1AutoTrade.init()
if localInitDone then
return
end
localInitDone = true
local localPlayer = Players.LocalPlayer
if not localPlayer then
Players:GetPropertyChangedSignal("LocalPlayer"):Wait()
localPlayer = Players.LocalPlayer
end
local liveChildConn = nil
local function detachLastHit()
if lastHitConn then
lastHitConn:Disconnect()
lastHitConn = nil
end
if lastHitDescConn then
lastHitDescConn:Disconnect()
lastHitDescConn = nil
end
lastHitObj = nil
lastHitAt = 0
lastHitTarget = nil
localM1WindowStart = 0
localM1WindowUntil = 0
end
local function attachLastHit(character)
detachLastHit()
if not character then
return
end
local hit = resolveLastHit(character)
if not hit then
local live = workspaceService:FindFirstChild("Live")
local liveChar = live and localPlayer and live:FindFirstChild(localPlayer.Name)
if liveChar and liveChar ~= character then
hit = resolveLastHit(liveChar)
if hit then
character = liveChar
end
end
end
if hit then
lastHitObj = hit
if hit.GetPropertyChangedSignal then
lastHitConn = hit:GetPropertyChangedSignal("Value"):Connect(handleLastHitChanged)
else
lastHitConn = hit.Changed:Connect(handleLastHitChanged)
end
end
lastHitDescConn = character.DescendantAdded:Connect(function(desc)
if lastHitObj then
return
end
if desc.Name == "LastHit" and desc:IsA("ValueBase") then
lastHitObj = desc
if desc.GetPropertyChangedSignal then
lastHitConn = desc:GetPropertyChangedSignal("Value"):Connect(handleLastHitChanged)
else
lastHitConn = desc.Changed:Connect(handleLastHitChanged)
end
end
end)
end
local function watchLive(live)
if liveChildConn then
liveChildConn:Disconnect()
liveChildConn = nil
end
if not live then
return
end
liveChildConn = live.ChildAdded:Connect(function(child)
if not localPlayer or not child then
return
end
if child.Name == localPlayer.Name then
attachLastHit(child)
end
end)
local existing = live:FindFirstChild(localPlayer.Name)
if existing and not lastHitObj then
attachLastHit(existing)
end
end
localPlayer.CharacterAdded:Connect(attachLastHit)
localPlayer.CharacterRemoving:Connect(function()
detachLastHit()
end)
if localPlayer.Character then
attachLastHit(localPlayer.Character)
end
watchLive(workspaceService:FindFirstChild("Live"))
workspaceService.ChildAdded:Connect(function(child)
if child and child.Name == "Live" then
watchLive(child)
end
end)
workspaceService.ChildRemoved:Connect(function(child)
if child and child.Name == "Live" then
if liveChildConn then
liveChildConn:Disconnect()
liveChildConn = nil
end
end
end)
userInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then
return
end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
openLocalM1Window()
return
end
if input.UserInputType == Enum.UserInputType.Gamepad1 and input.KeyCode == Enum.KeyCode.ButtonR2 then
openLocalM1Window()
end
end)
end
return M1AutoTrade
end)
-- MODULE: Features/Combat/AutoCombo
defineModule("Features/Combat/AutoCombo", function()
local AutoCombo = {}
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ContextActionService = game:GetService("ContextActionService")
local Configuration = require("Utility/Configuration")
local Logger = require("Utility/Logger")
local Filesystem = require("Utility/Filesystem")
local Serializer = require("Utility/Serializer")
local Deserializer = require("Utility/Deserializer")
local InputClient = require("Game/InputClient")
local Targeting = require("Features/Combat/Targeting")
local TaskSpawner = require("Utility/TaskSpawner")
local MOVE_BLOCK_ACTION = "AutoCombo_BlockMove"
local moveBlockActive = false
local function setMoveBlock(enabled)
if not ContextActionService then
return
end
if enabled then
if moveBlockActive then
return
end
moveBlockActive = true
pcall(function()
ContextActionService:BindActionAtPriority(
MOVE_BLOCK_ACTION,
function()
return Enum.ContextActionResult.Sink
end,
false,
Enum.ContextActionPriority.High.Value,
Enum.KeyCode.W,
Enum.KeyCode.A,
Enum.KeyCode.S,
Enum.KeyCode.D,
Enum.KeyCode.Up,
Enum.KeyCode.Down,
Enum.KeyCode.Left,
Enum.KeyCode.Right
)
end)
else
if not moveBlockActive then
return
end
moveBlockActive = false
pcall(function()
ContextActionService:UnbindAction(MOVE_BLOCK_ACTION)
end)
end
end
local fs = Filesystem.new("CottomHub-AutoCombos")
local comboState = {
token = 0,
running = false,
currentStep = nil,
lockedTarget = nil,
lastTarget = nil,
}
local function sanitizeName(name)
name = tostring(name or "")
name = name:gsub("[\\/%:%*%?\"<>|]", "")
name = name:gsub("^%s+", ""):gsub("%s+$", "")
return name
end
local function safeNumber(value, fallback)
local num = tonumber(value)
if num == nil then
return fallback
end
return num
end
local function optionValue(key, fallback)
local val = Configuration.expectOptionValue(key)
if type(val) == "number" then
return val
end
return fallback
end
local function normalizeOffset(value)
if typeof(value) == "Vector3" then
return { x = value.X, y = value.Y, z = value.Z }
end
if type(value) == "table" then
local x = tonumber(value.x or value.X or value[1]) or 0
local y = tonumber(value.y or value.Y or value[2]) or 0
local z = tonumber(value.z or value.Z or value[3]) or 0
return { x = x, y = y, z = z }
end
return { x = 0, y = 0, z = 0 }
end
local function vectorFromOffset(value)
local offset = normalizeOffset(value)
return Vector3.new(offset.x or 0, offset.y or 0, offset.z or 0)
end
local function coerceStep(step)
if type(step) ~= "table" then
return nil
end
local stype = tostring(step.type or "")
if stype == "" then
return nil
end
local out = { type = stype }
if stype == "Skill" then
out.slot = math.clamp(safeNumber(step.slot, 1), 1, 4)
if type(step.toolName) == "string" and step.toolName ~= "" then
out.toolName = step.toolName
end
local delayAfter = tonumber(step.delayAfterMs)
if delayAfter ~= nil then
out.delayAfterMs = delayAfter
end
elseif stype == "Wait" then
out.durationMs = safeNumber(step.durationMs, 0)
elseif stype == "M1" then
out.holdMs = safeNumber(step.holdMs, 60)
out["repeat"] = math.max(1, safeNumber(step["repeat"], 1))
out.gapMs = safeNumber(step.gapMs, 60)
elseif stype == "Uptilt" then
out.jumpHoldMs = safeNumber(step.jumpHoldMs, optionValue("AutoComboUptiltJumpHoldMs", 120))
out.m1HoldMs = safeNumber(step.m1HoldMs, optionValue("AutoComboUptiltM1HoldMs", 60))
out.delayBeforeM1Ms = safeNumber(step.delayBeforeM1Ms, 30)
elseif stype == "Upfling" then
out.jumpHoldMs = math.max(0, safeNumber(step.jumpHoldMs, 400))
out.m1HoldMs = math.max(10, safeNumber(step.m1HoldMs, 230))
elseif stype == "JumpHold" then
out.holdMs = math.max(0, safeNumber(step.holdMs, 400))
elseif stype == "Jump" then
out.count = math.max(1, safeNumber(step.count, 1))
out.holdMs = math.max(10, safeNumber(step.holdMs, 50))
out.gapMs = math.max(0, safeNumber(step.gapMs, 120))
elseif stype == "TweenToHead" then
out.maxDistance = safeNumber(step.maxDistance, optionValue("AutoComboTweenMaxDistance", 15))
local durationSeconds = tonumber(step.durationSeconds or step.duration or step.tweenDuration)
if durationSeconds and durationSeconds > 0 then
out.durationSeconds = durationSeconds
end
out.alignFeet = step.alignFeet == true
local speed = tonumber(step.speed)
if not speed then
local legacyTime = tonumber(step.tweenTime)
if legacyTime and legacyTime > 0 and out.maxDistance and out.maxDistance > 0 then
speed = out.maxDistance / legacyTime
end
end
out.speed = safeNumber(speed, optionValue("AutoComboTweenSpeed", 60))
out.offset = normalizeOffset(step.offset)
elseif stype == "MoveToTarget" then
local duration = tonumber(step.durationSeconds)
if not duration then
local legacyTimeout = tonumber(step.timeoutMs)
if legacyTimeout then
duration = legacyTimeout / 1000
end
end
out.durationSeconds = safeNumber(duration, optionValue("AutoComboMoveToDurationSeconds", 1.2))
out.stopDistance = safeNumber(step.stopDistance, optionValue("AutoComboMoveToStopDistance", 4))
out.useSprint = step.useSprint == true
elseif stype == "Dash" then
local dir = tostring(step.dir or "right")
if dir ~= "left" and dir ~= "right" and dir ~= "forward" and dir ~= "back" then
dir = "right"
end
out.dir = dir
elseif stype == "CameraPitch" then
out.pitchDegrees = safeNumber(step.pitchDegrees, -45)
out.durationMs = math.max(0, safeNumber(step.durationMs, 0))
out.holdMs = math.max(0, safeNumber(step.holdMs, 0))
out.resetMs = math.max(0, safeNumber(step.resetMs, 0))
end
out.parallel = step.parallel == true
if step.delayAfterMs ~= nil then
out.delayAfterMs = safeNumber(step.delayAfterMs, 0)
end
return out
end
local function coerceProfile(data)
if type(data) ~= "table" then
return nil
end
local profile = {
name = tostring(data.name or "Unnamed"),
steps = {},
meta = type(data.meta) == "table" and data.meta or {},
}
if type(data.steps) == "table" then
for _, step in ipairs(data.steps) do
local normalized = coerceStep(step)
if normalized then
table.insert(profile.steps, normalized)
end
end
end
return profile
end
function AutoCombo.listProfiles()
local ok, files = pcall(fs.list, fs)
if not ok or type(files) ~= "table" then
return {}
end
local list = {}
for _, file in ipairs(files) do
local name = tostring(file)
if name:sub(-5) == ".json" then
table.insert(list, name:sub(1, -6))
end
end
table.sort(list)
return list
end
function AutoCombo.loadProfile(name)
local clean = sanitizeName(name)
if clean == "" then
return nil, "empty"
end
local ok, content = pcall(fs.read, fs, clean .. ".json")
if not ok then
return nil, "missing"
end
local ok2, parsed = pcall(Deserializer.unmarshal_one, content)
if not ok2 then
return nil, "parse"
end
local profile = coerceProfile(parsed)
if not profile then
return nil, "invalid"
end
profile.name = clean
return profile
end
function AutoCombo.saveProfile(name, data)
local clean = sanitizeName(name)
if clean == "" then
return false, "empty"
end
local profile = coerceProfile(data or {})
if not profile then
return false, "invalid"
end
profile.name = clean
profile.meta = profile.meta or {}
profile.meta.updatedAt = os.time()
if not profile.meta.createdAt then
profile.meta.createdAt = os.time()
end
local ok, payload = pcall(Serializer.marshal, profile)
if not ok then
return false, "serialize"
end
local ok2 = pcall(fs.write, fs, clean .. ".json", payload)
if not ok2 then
return false, "write"
end
return true
end
function AutoCombo.deleteProfile(name)
local clean = sanitizeName(name)
if clean == "" then
return false, "empty"
end
local ok = pcall(fs.delete, fs, clean .. ".json")
if not ok then
return false, "delete"
end
return true
end
function AutoCombo.stop()
comboState.token = (comboState.token or 0) + 1
comboState.running = false
comboState.currentStep = nil
comboState.lockedTarget = nil
comboState.lastTarget = nil
end
function AutoCombo.isRunning()
return comboState.running == true
end
local function isRunning(token)
return comboState.running and token == comboState.token
end
local function isTargetValid(target)
if not target or not target.character or not target.root or not target.humanoid then
return false
end
if target.character.Parent == nil or target.root.Parent == nil then
return false
end
if target.humanoid.Health <= 0 then
return false
end
return true
end
local function selectTarget()
local targets = Targeting.best()
return targets and targets[1] or nil
end
local function resolveTarget(state)
if Configuration.expectToggleValue("AutoComboLockTarget") then
if isTargetValid(state.lockedTarget) then
return state.lockedTarget
end
if Configuration.expectToggleValue("AutoComboRetargetOnInvalid") then
state.lockedTarget = selectTarget()
return state.lockedTarget
end
return nil
end
if Configuration.expectToggleValue("AutoComboRetargetOnInvalid") then
state.lastTarget = selectTarget()
return state.lastTarget
end
return state.lastTarget
end
local function ensureTargetAlive(state)
if not Configuration.expectToggleValue("AutoComboLockTarget") then
return true
end
if not state.lockedTarget then
if Configuration.expectToggleValue("AutoComboRetargetOnInvalid") then
state.lockedTarget = selectTarget()
end
return true
end
if not isTargetValid(state.lockedTarget) then
if Configuration.expectToggleValue("AutoComboRetargetOnInvalid") then
state.lockedTarget = selectTarget()
end
if state.lockedTarget and not isTargetValid(state.lockedTarget) then
AutoCombo.stop()
return false
end
end
return true
end
local function getLocalRoot()
local character = Players.LocalPlayer and Players.LocalPlayer.Character
if not character then
return nil, nil, nil
end
local root = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChild("Torso")
local humanoid = character:FindFirstChildOfClass("Humanoid")
return character, root, humanoid
end
local function moveHumanoid(humanoid, direction)
if not humanoid or not humanoid.Parent then
return false
end
local moveDirection = direction
if typeof(moveDirection) ~= "Vector3" then
moveDirection = Vector3.zero
end
if moveDirection.Magnitude > 1 then
moveDirection = moveDirection.Unit
end
local ok = pcall(function()
humanoid:Move(moveDirection, false)
end)
return ok
end
local function moveHumanoidTo(humanoid, position)
if not humanoid or not humanoid.Parent then
return false
end
if typeof(position) ~= "Vector3" then
return false
end
local ok = pcall(function()
humanoid:MoveTo(position)
end)
return ok
end
local waitWithCancel = LPH_NO_VIRTUALIZE(function(seconds, token)
if seconds <= 0 then
return true
end
local endTime = os.clock() + seconds
while true do
if not isRunning(token) then
return false
end
local remaining = endTime - os.clock()
if remaining <= 0 then
return true
end
task.wait(math.min(remaining, 0.03))
end
end)
local autoComboHotbarRef = nil
local function resolveAutoComboHotbar()
if autoComboHotbarRef and autoComboHotbarRef.Parent then
return autoComboHotbarRef
end
local playerGui = Players.LocalPlayer and Players.LocalPlayer:FindFirstChild("PlayerGui")
if not playerGui then
return nil
end
local hud = playerGui:FindFirstChild("HUD")
local backpackGui = hud and hud:FindFirstChild("Backpack")
local hotbar = backpackGui and backpackGui:FindFirstChild("Hotbar")
if not hotbar then
for _, inst in ipairs(playerGui:GetDescendants()) do
if inst.Name == "Hotbar" then
hotbar = inst
break
end
end
end
autoComboHotbarRef = hotbar
return hotbar
end
local function resolveAutoComboToolName(slot)
local hotbar = resolveAutoComboHotbar()
if not hotbar then
return nil
end
local slotFrame = hotbar:FindFirstChild(tostring(slot))
if not slotFrame then
return nil
end
local toolNameObj = slotFrame:FindFirstChild("ToolName") or slotFrame:FindFirstChild("Name")
if not toolNameObj then
return nil
end
if toolNameObj:IsA("TextLabel") or toolNameObj:IsA("TextButton") then
return toolNameObj.Text
end
if toolNameObj:IsA("StringValue") then
return toolNameObj.Value
end
return nil
end
local function resolveAutoComboTool(slot)
local backpack = Players.LocalPlayer and Players.LocalPlayer:FindFirstChild("Backpack")
if backpack then
for _, tool in ipairs(backpack:GetChildren()) do
if tool:IsA("Tool") then
local attrSlot = tool:GetAttribute("Slot") or tool:GetAttribute("HotbarSlot") or tool:GetAttribute("Index")
if attrSlot and tonumber(attrSlot) == slot then
return tool
end
local slotValue = tool:FindFirstChild("Slot") or tool:FindFirstChild("Index")
if slotValue and tonumber(slotValue.Value) == slot then
return tool
end
end
end
end
local character = Players.LocalPlayer and Players.LocalPlayer.Character
return character and character:FindFirstChildOfClass("Tool") or nil
end
local function fireSkill(step)
if not InputClient then
return false
end
local slot = math.clamp(safeNumber(step.slot, 1), 1, 4)
local keyMap = {
[1] = Enum.KeyCode.One,
[2] = Enum.KeyCode.Two,
[3] = Enum.KeyCode.Three,
[4] = Enum.KeyCode.Four,
}
local key = keyMap[slot]
if InputClient.tapKey and key then
return InputClient.tapKey(key)
end
return false
end
local doM1 = LPH_NO_VIRTUALIZE(function(step, token)
local repeatCount = math.max(1, safeNumber(step["repeat"], 1))
local holdSeconds = math.max(10, safeNumber(step.holdMs, 60)) / 1000
local gapSeconds = math.max(0, safeNumber(step.gapMs, 60)) / 1000
for i = 1, repeatCount do
if not isRunning(token) then
return
end
InputClient.m1Hold(holdSeconds)
if not waitWithCancel(holdSeconds + (i < repeatCount and gapSeconds or 0), token) then
return
end
end
end)
local doUptilt = LPH_NO_VIRTUALIZE(function(step, token)
local jumpHoldSeconds = math.max(10, safeNumber(step.jumpHoldMs, optionValue("AutoComboUptiltJumpHoldMs", 120))) / 1000
local m1HoldSeconds = math.max(10, safeNumber(step.m1HoldMs, optionValue("AutoComboUptiltM1HoldMs", 60))) / 1000
local delayBeforeM1 = math.max(0, safeNumber(step.delayBeforeM1Ms, 30)) / 1000
InputClient.jumpHold(jumpHoldSeconds)
if delayBeforeM1 > 0 then
if not waitWithCancel(delayBeforeM1, token) then
return
end
end
InputClient.m1Hold(m1HoldSeconds)
waitWithCancel(m1HoldSeconds, token)
end)
local doUpfling = LPH_NO_VIRTUALIZE(function(step, token)
local jumpHoldSeconds = math.max(10, safeNumber(step.jumpHoldMs, 400)) / 1000
local m1HoldSeconds = math.max(10, safeNumber(step.m1HoldMs, 230)) / 1000
InputClient.jumpHold(jumpHoldSeconds)
InputClient.m1Hold(m1HoldSeconds)
waitWithCancel(math.max(jumpHoldSeconds, m1HoldSeconds), token)
end)
local doJumpHold = LPH_NO_VIRTUALIZE(function(step, token)
local holdSeconds = math.max(0, safeNumber(step.holdMs, 400)) / 1000
if holdSeconds <= 0 then
return
end
InputClient.jumpHold(holdSeconds)
waitWithCancel(holdSeconds, token)
end)
local doJump = LPH_NO_VIRTUALIZE(function(step, token)
local count = math.max(1, safeNumber(step.count, 1))
local holdSeconds = math.max(0, safeNumber(step.holdMs, 50)) / 1000
local gapSeconds = math.max(0, safeNumber(step.gapMs, 120)) / 1000
for i = 1, count do
if not isRunning(token) then
return
end
if holdSeconds > 0 then
InputClient.jumpHold(holdSeconds)
if not waitWithCancel(holdSeconds, token) then
return
end
else
InputClient.jump()
end
if i < count and gapSeconds > 0 then
if not waitWithCancel(gapSeconds, token) then
return
end
end
end
end)
local doTween = LPH_NO_VIRTUALIZE(function(step, target, token, state)
if not target or not target.root then
return
end
local character, localRoot, humanoid = getLocalRoot()
if not localRoot then
return
end
local maxDistance = safeNumber(step.maxDistance, optionValue("AutoComboTweenMaxDistance", 15))
local offset = vectorFromOffset(step.offset)
local durationSeconds = tonumber(step.durationSeconds)
local speed = math.max(0, safeNumber(step.speed, optionValue("AutoComboTweenSpeed", 60)))
local function resolveHead()
return target.character and target.character:FindFirstChild("Head") or target.root
end
local head = resolveHead()
if not head then
return
end
local dist = (head.Position - localRoot.Position).Magnitude
if not durationSeconds then
if maxDistance > 0 and dist > maxDistance then
return
end
end
local totalTime = 0
if durationSeconds and durationSeconds > 0 then
totalTime = durationSeconds
elseif speed > 0 then
totalTime = dist / speed
end
if totalTime <= 0 then
return
end
local function computeRelativeOffset(vec)
if not step.relativeOffset then
return vec
end
local fallbackBasis = (localRoot and localRoot.CFrame)
or (target and target.root and target.root.CFrame)
or CFrame.new()
local basis = nil
if step.relativeToLocal then
basis = (localRoot and localRoot.CFrame) or fallbackBasis
else
basis = (target and target.root and target.root.CFrame) or fallbackBasis
end
local forward = Vector3.new(basis.LookVector.X, 0, basis.LookVector.Z)
if forward.Magnitude > 1e-3 then
forward = forward.Unit
else
forward = Vector3.new(0, 0, -1)
end
local right = Vector3.new(basis.RightVector.X, 0, basis.RightVector.Z)
if right.Magnitude > 1e-3 then
right = right.Unit
else
right = Vector3.new(1, 0, 0)
end
return right * vec.X + Vector3.new(0, 1, 0) * vec.Y + forward * vec.Z
end
local function computeOffset(headInstance)
if not step.alignFeet then
return computeRelativeOffset(offset)
end
local character = Players.LocalPlayer and Players.LocalPlayer.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local hip = humanoid and humanoid.HipHeight or (localRoot.Size.Y / 2)
local headHalf = headInstance and headInstance.Size and headInstance.Size.Y / 2 or 1.5
local baseOffset = offset + Vector3.new(0, headHalf + hip, 0)
return computeRelativeOffset(baseOffset)
end
local start = os.clock()
while os.clock() - start < totalTime do
if not isRunning(token) then
return
end
local liveCharacter, liveRoot, liveHumanoid = getLocalRoot()
if not liveCharacter or not liveRoot then
return
end
character = liveCharacter
localRoot = liveRoot
humanoid = liveHumanoid or humanoid
if not isTargetValid(target) then
if Configuration.expectToggleValue("AutoComboRetargetOnInvalid") and state then
target = resolveTarget(state)
end
if not isTargetValid(target) then
if Configuration.expectToggleValue("AutoComboLockTarget") and state and state.lockedTarget then
AutoCombo.stop()
end
return
end
end
head = resolveHead()
if not head then
return
end
local dest = head.Position + computeOffset(head)
local current = localRoot.Position
local toDest = dest - current
local distance = toDest.Magnitude
if humanoid then
moveHumanoid(humanoid, localRoot.CFrame.LookVector)
end
if distance > 0.001 then
local dt = RunService.RenderStepped:Wait()
local stepDist = speed > 0 and speed * dt or distance
if stepDist > distance then
stepDist = distance
end
local newPos = current + toDest.Unit * stepDist
local rotation = localRoot.CFrame - localRoot.CFrame.Position
localRoot.CFrame = CFrame.new(newPos) * rotation
else
RunService.RenderStepped:Wait()
end
end
if humanoid then
moveHumanoid(humanoid, Vector3.zero)
end
end)
local doMoveTo = LPH_NO_VIRTUALIZE(function(step, target, token, state)
local character, localRoot, humanoid = getLocalRoot()
if not character or not localRoot then
return
end
if not humanoid then
return
end
local duration = math.max(0, safeNumber(step.durationSeconds, optionValue("AutoComboMoveToDurationSeconds", 1.2)))
local stopDistance = math.max(0, safeNumber(step.stopDistance, optionValue("AutoComboMoveToStopDistance", 4)))
local useSprint = step.useSprint == true
local sprintHold = math.max(0, safeNumber(optionValue("AutoComboSprintHoldMs", 350), 350)) / 1000
local repathSeconds = math.max(0.03, safeNumber(step.repathSeconds, 0.06))
if duration <= 0 then
return
end
local movementLocked = false
local function stopMoveTo()
local _, liveRoot, liveHumanoid = getLocalRoot()
if liveHumanoid and liveRoot then
moveHumanoidTo(liveHumanoid, liveRoot.Position)
elseif humanoid and localRoot then
moveHumanoidTo(humanoid, localRoot.Position)
end
end
local function lockMovement()
if movementLocked then
return
end
movementLocked = true
setMoveBlock(true)
end
local function unlockMovement()
if not movementLocked then
return
end
movementLocked = false
setMoveBlock(false)
end
lockMovement()
local start = os.clock()
local lastSprint = 0
local lastPos = target and target.root and target.root.Position or nil
local nextMoveToAt = 0
local lastMoveToPos = nil
while os.clock() - start <= duration do
if not isRunning(token) then
stopMoveTo()
unlockMovement()
return
end
local liveCharacter, liveRoot, liveHumanoid = getLocalRoot()
if not liveCharacter or not liveRoot or not liveHumanoid then
stopMoveTo()
unlockMovement()
return
end
character = liveCharacter
localRoot = liveRoot
humanoid = liveHumanoid
if target and isTargetValid(target) then
state.lastTarget = target
lastPos = target.root.Position
else
target = resolveTarget(state)
if target and isTargetValid(target) then
state.lastTarget = target
lastPos = target.root.Position
else
if Configuration.expectToggleValue("AutoComboLockTarget") and state.lockedTarget then
AutoCombo.stop()
end
stopMoveTo()
unlockMovement()
return
end
end
if lastPos then
local toDest = lastPos - localRoot.Position
local flat = Vector3.new(toDest.X, 0, toDest.Z)
local dist = flat.Magnitude
if stopDistance > 0 and dist <= stopDistance then
break
end
if dist > 1e-3 then
local now = os.clock()
local shouldUpdate = (not lastMoveToPos)
or ((lastMoveToPos - lastPos).Magnitude > 1)
or (now >= nextMoveToAt)
if shouldUpdate then
moveHumanoidTo(humanoid, lastPos)
lastMoveToPos = lastPos
nextMoveToAt = now + repathSeconds
end
end
end
if useSprint and sprintHold > 0 then
local now = os.clock()
if now - lastSprint >= sprintHold * 0.8 then
InputClient.sprintHold(sprintHold)
lastSprint = now
end
end
RunService.Heartbeat:Wait()
end
stopMoveTo()
unlockMovement()
end)
local function executeStep(step, token, state)
if step.type == "Wait" then
waitWithCancel((step.durationMs or 0) / 1000, token)
elseif step.type == "Skill" then
fireSkill(step)
elseif step.type == "M1" then
doM1(step, token)
elseif step.type == "Uptilt" then
doUptilt(step, token)
elseif step.type == "Upfling" then
doUpfling(step, token)
elseif step.type == "JumpHold" then
doJumpHold(step, token)
elseif step.type == "Jump" then
doJump(step, token)
elseif step.type == "TweenToHead" then
local target = resolveTarget(state)
if not target or not isTargetValid(target) then
if Configuration.expectToggleValue("AutoComboLockTarget") and state.lockedTarget then
AutoCombo.stop()
end
return
end
doTween(step, target, token, state)
elseif step.type == "MoveToTarget" then
local target = resolveTarget(state)
if not target or not isTargetValid(target) then
if Configuration.expectToggleValue("AutoComboLockTarget") and state.lockedTarget then
AutoCombo.stop()
end
return
end
doMoveTo(step, target, token, state)
elseif step.type == "Dash" then
local dir = step.dir or "right"
InputClient.dash(dir)
elseif step.type == "CameraPitch" then
local cam = workspace.CurrentCamera
if not cam then
return
end
local pitch0, yaw0, roll0 = cam.CFrame:ToOrientation()
local targetPitch = math.rad(step.pitchDegrees or -45)
local duration = math.max(0, (step.durationMs or 0) / 1000)
local hold = math.max(0, (step.holdMs or 0) / 1000)
local reset = math.max(0, (step.resetMs or 0) / 1000)
local function lerpPitch(fromPitch, toPitch, seconds)
if seconds <= 0 then
cam.CFrame = CFrame.new(cam.CFrame.Position) * CFrame.fromOrientation(toPitch, yaw0, roll0)
return
end
local start = os.clock()
while os.clock() - start < seconds do
if not isRunning(token) then
return
end
local alpha = math.clamp((os.clock() - start) / seconds, 0, 1)
local p = fromPitch + (toPitch - fromPitch) * alpha
cam.CFrame = CFrame.new(cam.CFrame.Position) * CFrame.fromOrientation(p, yaw0, roll0)
RunService.RenderStepped:Wait()
end
end
lerpPitch(pitch0, targetPitch, duration)
if hold > 0 then
if not waitWithCancel(hold, token) then
return
end
end
if reset > 0 then
lerpPitch(targetPitch, pitch0, reset)
end
end
end
local runProfile = LPH_NO_VIRTUALIZE(function(profile, token)
if Configuration.expectToggleValue("AutoComboLockTarget") then
comboState.lockedTarget = selectTarget()
end
for index, step in ipairs(profile.steps or {}) do
if not isRunning(token) then
break
end
if not ensureTargetAlive(comboState) then
break
end
comboState.currentStep = index
local normalized = coerceStep(step)
if normalized then
if normalized.parallel == true then
TaskSpawner.spawn("AutoCombo_Step_" .. tostring(normalized.type), LPH_NO_VIRTUALIZE(function()
if not isRunning(token) then
return
end
executeStep(normalized, token, comboState)
end))
else
executeStep(normalized, token, comboState)
end
end
if not isRunning(token) then
break
end
local delayMs = normalized and normalized.delayAfterMs
if delayMs == nil then
delayMs = optionValue("AutoComboDefaultStepDelayMs", 100)
end
if delayMs and delayMs > 0 then
if not waitWithCancel(delayMs / 1000, token) then
break
end
end
end
if token == comboState.token then
comboState.running = false
comboState.currentStep = nil
comboState.lockedTarget = nil
comboState.lastTarget = nil
if Logger and Logger.notify then
Logger.notify("Auto-Combo finished: %s", profile.name or "Profile")
end
end
end)
function AutoCombo.run(profileName)
if not Configuration.expectToggleValue("EnableAutoCombo") then
return
end
local profile = nil
if type(profileName) == "table" then
profile = coerceProfile(profileName)
else
profile = AutoCombo.loadProfile(profileName)
end
if not profile then
Logger.longNotify("Auto-Combo profile '%s' could not be loaded.", tostring(profileName))
return
end
AutoCombo.stop()
comboState.running = true
comboState.token = (comboState.token or 0) + 1
local token = comboState.token
TaskSpawner.spawn("AutoCombo_Run", LPH_NO_VIRTUALIZE(function()
runProfile(profile, token)
end))
end
return AutoCombo
end)
-- MODULE: Game/Keybinding
defineModule("Game/Keybinding", function()
-- Keybinding module.
local Keybinding = { info = {} }
---@module Utility.Signal
local Signal = require("Utility/Signal")
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module Utility.TaskSpawner
local TaskSpawner = require("Utility/TaskSpawner")
-- Services.
local players = game:GetService("Players")
local replicatedStorage = game:GetService("ReplicatedStorage")
-- Maids.
local keybindingMaid = Maid.new()
---Refresh keybind data.
local function refreshKeybindData()
local character = players.LocalPlayer.Character or players.LocalPlayer.CharacterAdded:Wait()
repeat
task.wait()
until character:GetAttribute("KeybindsLoaded")
local requests = replicatedStorage:WaitForChild("Requests")
local getKeybindInfo = requests:WaitForChild("GetKeybindsInfo")
for _, keybindGroup in next, getKeybindInfo:InvokeServer() or {} do
for keybindType, keybindCode in next, keybindGroup do
local success, result = pcall(function()
return Enum.KeyCode[keybindCode]
end)
Keybinding.info[keybindType] = success and result or Enum.KeyCode.Unknown
end
end
end
---Initialize Keybinding module.
function Keybinding.init()
local remotes = replicatedStorage:WaitForChild("Remotes")
local sendKeybindInfo = remotes:WaitForChild("SendKeybindInfo")
local sendKeybindInfoEvent = Signal.new(sendKeybindInfo.OnClientEvent)
keybindingMaid:add(sendKeybindInfoEvent:connect("Keybinding_SkiOnClientEvent", refreshKeybindData))
keybindingMaid:add(TaskSpawner.spawn("Keybinding_UpdateKeybinds", refreshKeybindData))
end
---Detach Keybinding module.
function Keybinding.detach()
keybindingMaid:clean()
end
-- Return Keybinding module.
return Keybinding
end)
-- MODULE: Game/PlayerScanning
defineModule("Game/PlayerScanning", function()
-- Player scanning is handled here.
local PlayerScanning = {
scanQueue = {},
scanDataCache = {},
friendCache = {},
waitingForLoad = {},
readyList = {},
scanning = false,
}
---@module Utility.CoreGuiManager
local CoreGuiManager = require("Utility/CoreGuiManager")
---@module Utility.Signal
local Signal = require("Utility/Signal")
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
-- Services.
local players = game:GetService("Players")
local httpService = game:GetService("HttpService")
local collectionService = game:GetService("CollectionService")
local runService = game:GetService("RunService")
-- Instances.
local moderatorSound = CoreGuiManager.imark(Instance.new("Sound"))
-- Maid.
local playerScanningMaid = Maid.new()
-- Timestamp.
local lastRateLimit = nil
---Fetch name.
local function fetchName(player)
local spoofName = Configuration.expectToggleValue("InfoSpoofing")
and Configuration.expectToggleValue("SpoofOtherPlayers")
return spoofName and "[REDACTED]"
or string.format("(%s) %s", player:GetAttribute("CharacterName") or "Unknown Character Name", player.Name)
end
---Run player scans.
local runPlayerScans = LPH_NO_VIRTUALIZE(function()
local localPlayer = players.LocalPlayer
if not localPlayer then
return
end
for player, _ in next, PlayerScanning.scanQueue do
if shared.Lycoris.dpscanning then
continue
end
if not PlayerScanning.scanDataCache[player] then
local handledSuccess, handledResult = nil, nil
local unhandledSuccess, unhandledResult = pcall(function()
handledSuccess, handledResult = PlayerScanning.getStaffRank(player)
end)
if not unhandledSuccess then
Logger.warn(
"Scan player %s ran into error '%s' while getting staff rank.",
player.Name,
unhandledResult
)
Logger.longNotify("Failed to scan player %s for moderator status.", fetchName(player), unhandledResult)
PlayerScanning.scanQueue[player] = nil
continue
end
if not handledSuccess then
continue
end
if Configuration.expectToggleValue("NotifyMod") and handledResult then
Logger.longNotify("%s is a staff member with the rank '%s' in group.", fetchName(player), handledResult)
if Configuration.expectToggleValue("NotifyModSound") then
moderatorSound.SoundId = "rbxassetid://6045346303"
moderatorSound.PlaybackSpeed = 1
moderatorSound.Volume = Configuration.expectOptionValue("NotifyModSoundVolume") or 10
moderatorSound:Play()
end
end
PlayerScanning.scanDataCache[player] = { staffRank = handledResult }
end
PlayerScanning.scanQueue[player] = nil
PlayerScanning.friendCache[player] = localPlayer:GetFriendStatus(player) == Enum.FriendStatus.Friend
Logger.warn("Player scanning finished scanning %s in queue.", fetchName(player))
end
end)
---Are there moderators in the server?
---@return table
function PlayerScanning.hasModerators()
for _, scanData in next, PlayerScanning.scanDataCache do
if not scanData.staffRank then
continue
end
return true
end
return false
end
---Is a player an ally?
---@param player Player
---@return boolean
function PlayerScanning.isAlly(player)
return PlayerScanning.friendCache[player]
end
---Fetch roblox data.
---@param url string
---@return boolean, string?
local function fetchRobloxData(url)
if lastRateLimit and os.clock() - lastRateLimit <= 30 then
return false, "On rate-limit cooldown."
end
local response = request({
Url = url,
Method = "GET",
Headers = {
["Content-Type"] = "application/json",
},
})
if response.StatusCode == 429 then
Logger.longNotify("Player scanning is being rate-limited and results will be delayed.")
Logger.longNotify("Please stay in the server with caution.")
lastRateLimit = os.clock()
return false, "Rate-limited."
end
if not response then
return error("Failed to fetch Roblox data.")
end
if not response.Success then
return error(
string.format("Failed to successfully fetch Roblox data with status code %i.", response.StatusCode)
)
end
if not response.Body then
return error("Failed to find Roblox data.")
end
return true, httpService:JSONDecode(response.Body)
end
---Get staff rank - nil if they're not a staff.
---@param player Player
---@return boolean, string?
function PlayerScanning.getStaffRank(player)
local responseSuccess, responseData =
fetchRobloxData(("https://groups.roblox.com/v2/users/%i/groups/roles?includeLocked=true"):format(player.UserId))
if not responseSuccess then
return false, responseData
end
local character = player.Character
if character and character:GetAttribute("ContentCreator") then
return true, "Content Creator"
end
for _, groupData in next, responseData.data do
if groupData.group.id ~= 32740991 and groupData.group.id ~= 13077028 then
continue
end
if groupData.role.rank <= 0 then
continue
end
return true, groupData.role.name
end
return true, nil
end
---Update player scanning.
---@note: Request will yield - so we need a debounce to prevent multiple scan loops.
---@note: We must defer the error back to the caller and reset the scanning debounce so errors will not break the scanning loop.
function PlayerScanning.update()
if PlayerScanning.scanning then
return
end
PlayerScanning.scanning = true
local success, result = pcall(runPlayerScans)
PlayerScanning.scanning = false
if success then
return
end
return error(result)
end
---On friend status changed.
---@param player Player
---@param status Enum.FriendStatus
function PlayerScanning.friend(player, status)
PlayerScanning.friendCache[player] = status == Enum.FriendStatus.Friend
end
---On player added.
---@param player Player
function PlayerScanning.onPlayerAdded(player)
if player == players.LocalPlayer then
return
end
PlayerScanning.scanQueue[player] = true
end
---On player removing.
---@param player Player
function PlayerScanning.onPlayerRemoving(player)
PlayerScanning.scanQueue[player] = nil
PlayerScanning.scanDataCache[player] = nil
PlayerScanning.friendCache[player] = nil
PlayerScanning.waitingForLoad[player] = nil
end
---Initialize PlayerScanning.
function PlayerScanning.init()
-- Signals.
local playerAddedSignal = Signal.new(players.PlayerAdded)
local playerRemovingSignal = Signal.new(players.PlayerRemoving)
local renderSteppedSignal = Signal.new(runService.RenderStepped)
local friendStatusChanged = Signal.new(players.LocalPlayer.FriendStatusChanged)
-- Connect events.
playerScanningMaid:add(friendStatusChanged:connect("PlayerScanning_OnFriendStatusChanged", PlayerScanning.friend))
playerScanningMaid:add(renderSteppedSignal:connect("PlayerScanning_Update", PlayerScanning.update))
playerScanningMaid:add(playerAddedSignal:connect("PlayerScanning_OnPlayerAdded", PlayerScanning.onPlayerAdded))
playerScanningMaid:add(
playerRemovingSignal:connect("PlayerScanning_OnPlayerRemoving", PlayerScanning.onPlayerRemoving)
)
-- Run event(s) for existing players.
for _, player in next, players:GetPlayers() do
PlayerScanning.onPlayerAdded(player)
end
end
---Detach PlayerScanning.
function PlayerScanning.detach()
playerScanningMaid:clean()
end
-- Return PlayerScanning module.
return PlayerScanning
end)
-- MODULE: Game/Timings/Action
defineModule("Game/Timings/Action", function()
---@class Action
---@field _type string
---@field _when number When the action will occur in miliseconds. Never access directly.
---@field hitbox Vector3 The hitbox of the action.
---@field shape string The hitbox shape ("Box" or "Ball").
---@field offset Vector3 The hitbox offset.
---@field ihbc boolean Ignore hitbox check.
---@field name string The name of the action.
---@field tp number Time position. Never accessible unless inside of a module or inside of real code. This is never serialized.
local Action = {}
Action.__index = Action
---Getter for when in seconds.
---@return number
function Action:when()
return PP_SCRAMBLE_NUM(self._when) / 1000
end
---Load from partial values.
---@param values table
function Action:load(values)
if typeof(values._type) == "string" then
self._type = values._type
end
if typeof(values.when) == "number" then
self._when = values.when
end
if typeof(values.name) == "string" then
self.name = values.name
end
if typeof(values.hitbox) == "table" then
self.hitbox = Vector3.new(values.hitbox.X or 0, values.hitbox.Y or 0, values.hitbox.Z or 0)
elseif typeof(values.hitbox) == "Vector3" then
self.hitbox = values.hitbox
end
if typeof(values.ihbc) == "boolean" then
self.ihbc = values.ihbc
end
if typeof(values.shape) == "string" then
self.shape = values.shape
end
if typeof(values.offset) == "table" then
self.offset = Vector3.new(values.offset.X or 0, values.offset.Y or 0, values.offset.Z or 0)
elseif typeof(values.offset) == "Vector3" then
self.offset = values.offset
end
end
---Equals check.
---@param other Action
---@return boolean
function Action:equals(other)
if self._type ~= other._type then
return false
end
if self._when ~= other._when then
return false
end
if self.name ~= other.name then
return false
end
if self.hitbox ~= other.hitbox then
return false
end
if self.ihbc ~= other.ihbc then
return false
end
if self.shape ~= other.shape then
return false
end
if self.offset ~= other.offset then
return false
end
return true
end
---Clone action.
---@return Action
function Action:clone()
local clone = Action.new()
clone._type = self._type
clone._when = self._when
clone.name = self.name
clone.hitbox = self.hitbox
clone.ihbc = self.ihbc
clone.shape = self.shape
clone.offset = self.offset
return clone
end
---Return a serializable table.
---@return table
function Action:serialize()
return {
_type = self._type,
when = self._when,
name = self.name,
hitbox = {
X = self.hitbox.X,
Y = self.hitbox.Y,
Z = self.hitbox.Z,
},
shape = self.shape,
offset = {
X = self.offset.X,
Y = self.offset.Y,
Z = self.offset.Z,
},
ihbc = self.ihbc,
}
end
---Create new Action object.
---@param values table?
---@return Action
function Action.new(values)
local self = setmetatable({}, Action)
self._type = "N/A"
self._when = 0
self.name = ""
self.hitbox = Vector3.zero
self.ihbc = false
self.shape = "Box"
self.offset = Vector3.zero
self.tp = 0
if values then
self:load(values)
end
return self
end
-- Return Action module.
return Action
end)
-- MODULE: Game/Timings/ActionContainer
defineModule("Game/Timings/ActionContainer", function()
---@module Game.Timings.Action
local Action = require("Game/Timings/Action")
---@class ActionContainer
---@field _data table<string, Action>
local ActionContainer = {}
ActionContainer.__index = ActionContainer
---Clone action container.
---@return ActionContainer
function ActionContainer:clone()
local clone = ActionContainer.new()
for _, action in next, self._data do
clone:push(action:clone())
end
return clone
end
---Equal check.
---@param other ActionContainer
---@return boolean
function ActionContainer:equals(other)
if self:count() ~= other:count() then
return false
end
for name, action in next, self._data do
local otherAction = other:find(name)
if not otherAction then
return false
end
if not action:equals(otherAction) then
return false
end
end
return true
end
---Find a action from name.
---@param name string
---@return Action?
function ActionContainer:find(name)
return self._data[name]
end
---Remove a action from the list.
---@param action Action
function ActionContainer:remove(action)
self._data[action.name] = nil
self._count = self._count - 1
end
---Push a action to the list.
---@param action Action
function ActionContainer:push(action)
local name = action.name
---@note: Action array keys must all be unique.
if self._data[name] then
return error(string.format("Action name '%s' already exists in container.", name))
end
self._data[name] = action
self._count = self._count + 1
end
---Load from partial values.
---@param values table
function ActionContainer:load(values)
for _, data in next, values do
self:push(Action.new(data))
end
end
---List all action names.
---@return string[]
function ActionContainer:names()
local names = {}
for name, _ in next, self._data do
table.insert(names, name)
end
return names
end
---Get action count.
---@return number
function ActionContainer:count()
return self._count
end
---Clear actions.
function ActionContainer:clear()
self._data = {}
end
---Get action data.
---@return table<string, Action>
function ActionContainer:get()
return self._data
end
---Return a serializable table.
---@return table
function ActionContainer:serialize()
local data = {}
for _, action in next, self._data do
table.insert(data, action:serialize())
end
return data
end
---Create new ActionContainer object.
---@param values table?
---@return ActionContainer
function ActionContainer.new(values)
local self = setmetatable({}, ActionContainer)
self._data = {}
self._count = 0
if values then
self:load(values)
end
return self
end
-- Return ActionContainer module.
return ActionContainer
end)
-- MODULE: Game/Timings/AnimationTiming
defineModule("Game/Timings/AnimationTiming", function()
---@module Game.Timings.Timing
local Timing = require("Game/Timings/Timing")
---@class AnimationTiming: Timing
---@field id string Animation ID.
---@field rpue boolean Repeat parry until end.
---@field _rsd number Repeat start delay in miliseconds. Never access directly.
---@field _rpd number Delay between each repeat parry in miliseconds. Never access directly.
---@field iae boolean Flag to see whether or not this timing should ignore animation end.
---@field phd boolean Past hitbox detection.
---@field pfh boolean Predict hitboxes facing.
---@field phds number History seconds for past hitbox detection.
---@field pfht number Extrapolation time for hitbox prediction.
---@field ieae boolean Flag to see whether or not this timing should ignore early animation end.
---@field mat number Max animation timeout in milliseconds.
---@field dp boolean Disable prediction.
local AnimationTiming = setmetatable({}, { __index = Timing })
AnimationTiming.__index = AnimationTiming
---Timing ID.
---@return string
function AnimationTiming:id()
return self._id
end
---Getter for repeat start delay in seconds
---@return number
function AnimationTiming:rsd()
return PP_SCRAMBLE_NUM(self._rsd) / 1000
end
---Getter for repeat parry delay in seconds.
---@return number
function AnimationTiming:rpd()
return PP_SCRAMBLE_NUM(self._rpd) / 1000
end
---Load from partial values.
---@param values table
function AnimationTiming:load(values)
Timing.load(self, values)
if typeof(values._id) == "string" then
self._id = values._id
end
if typeof(values.rsd) == "string" then
self._rsd = tonumber(values.rsd) or 0.0
end
if typeof(values.rpd) == "string" then
self._rpd = tonumber(values.rpd) or 0.0
end
if typeof(values.rsd) == "number" then
self._rsd = values.rsd
end
if typeof(values.rpd) == "number" then
self._rpd = values.rpd
end
if typeof(values.rpue) == "boolean" then
self.rpue = values.rpue
end
if typeof(values.iae) == "boolean" then
self.iae = values.iae
end
if typeof(values.ieae) == "boolean" then
self.ieae = values.ieae
end
if typeof(values.mat) == "number" then
self.mat = values.mat
end
if typeof(values.phd) == "boolean" then
self.phd = values.phd
end
if typeof(values.pfh) == "boolean" then
self.pfh = values.pfh
end
if typeof(values.phds) == "number" then
self.phds = values.phds
end
if typeof(values.pfht) == "number" then
self.pfht = values.pfht
end
if typeof(values.dp) == "boolean" then
self.dp = values.dp
end
end
---Clone timing.
---@return AnimationTiming
function AnimationTiming:clone()
local clone = setmetatable(Timing.clone(self), AnimationTiming)
clone._rsd = self._rsd
clone._rpd = self._rpd
clone._id = self._id
clone.rpue = self.rpue
clone.iae = self.iae
clone.ieae = self.ieae
clone.mat = self.mat
clone.phd = self.phd
clone.pfh = self.pfh
clone.phds = self.phds
clone.pfht = self.pfht
clone.dp = self.dp
return clone
end
---Return a serializable table.
---@return AnimationTiming
function AnimationTiming:serialize()
local serializable = Timing.serialize(self)
serializable._id = self._id
serializable.rsd = self._rsd
serializable.rpd = self._rpd
serializable.rpue = self.rpue
serializable.iae = self.iae
serializable.ieae = self.ieae
serializable.mat = self.mat
serializable.phd = self.phd
serializable.pfh = self.pfh
serializable.phds = self.phds
serializable.pfht = self.pfht
serializable.dp = self.dp
return serializable
end
---Create a new animation timing.
---@param values table?
---@return AnimationTiming
function AnimationTiming.new(values)
local self = setmetatable(Timing.new(), AnimationTiming)
self.dp = false
self._id = ""
self._rsd = 0
self._rpd = 0
self.rpue = false
self.iae = false
self.ieae = false
self.mat = 2000
self.phd = false
self.pfh = false
self.phds = 0
self.pfht = 0.15
if values then
self:load(values)
end
return self
end
-- Return AnimationTiming module.
return AnimationTiming
end)
-- MODULE: Game/Timings/ModuleManager
defineModule("Game/Timings/ModuleManager", function()
-- Internal modules if they exist, provided by to by preprocessor.
local INTERNAL_MODULES = {}
local INTERNAL_GLOBALS = {}
-- Module manager.
---@note: All globals get executed first but never ran. This gets set in the global environment of every future module after.
local ModuleManager = { modules = {}, globals = {} }
---@module Utility.Filesystem
local Filesystem = require("Utility/Filesystem")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.Timings.Action
local Action = require("Game/Timings/Action")
---@module Features.Combat.Objects.Task
local Task = require("Features/Combat/Objects/Task")
---@module Game.Timings.Timing
local Timing = require("Game/Timings/Timing")
---@module Utility.TaskSpawner
local TaskSpawner = require("Utility/TaskSpawner")
---@module Features.Combat.Targeting
local Targeting = require("Features/Combat/Targeting")
---@module Game.Timings.PartTiming
local PartTiming = require("Game/Timings/PartTiming")
---@module Features.Combat.Objects.HitboxOptions
local HitboxOptions = require("Features/Combat/Objects/HitboxOptions")
---@module Features.Combat.Objects.RepeatInfo
local RepeatInfo = require("Features/Combat/Objects/RepeatInfo")
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module Utility.Signal
local Signal = require("Utility/Signal")
-- Module filesystem.
local fs = Filesystem.new("Lycoris-Rewrite-TypeSoul-Modules")
local gfs = Filesystem.new(fs:append("Globals"))
-- Detach table.
local tdetach = {}
---Execute module function.
---@param lf function
---@param id string
---@param file string?
---@param global boolean
function ModuleManager.execute(lf, id, file, global)
---@module Features.Combat.Defense
---@note: For some reason, it broke lol. Returned nil.
-- Has to do with loadingPlaceholder issue. A very wide cyclic dependency where depdendencies rely on each other can break the bundler.
local Defense = require("Features/Combat/Defense")
-- Set function environment to allow for internal modules.
getfenv(lf).Timing = Timing
getfenv(lf).PartTiming = PartTiming
getfenv(lf).Defense = Defense
getfenv(lf).Action = Action
getfenv(lf).Task = Task
getfenv(lf).Maid = Maid
getfenv(lf).Signal = Signal
getfenv(lf).TaskSpawner = TaskSpawner
getfenv(lf).Targeting = Targeting
getfenv(lf).Logger = Logger
getfenv(lf).HitboxOptions = HitboxOptions
getfenv(lf).RepeatInfo = RepeatInfo
-- Load globals if we should.
for name, entry in next, (not global) and ModuleManager.globals or {} do
getfenv(lf)[name] = entry
end
-- Run executable function to initialize it.
local success, result = pcall(lf)
if not success then
return Logger.warn("Module '%s' failed to load due to error '%s' while executing.", file or id, result)
end
if global and typeof(result) ~= "table" then
return Logger.warn("Global module '%s' is invalid because it does not return a table.", file or id)
end
-- Output table.
local output = global and ModuleManager.globals or ModuleManager.modules
-- Get the result as a function.
output[id] = result
-- If this is a global, the result is a table, and it has a detach function, store it for later.
if typeof(result) == "table" and typeof(result.detach) == "function" then
tdetach[#tdetach + 1] = result.detach
end
end
---Load file modules from filesystem.
---@param tfs Filesystem The filesystem to load from.
---@param global boolean Whether we're loading global modules or not.
function ModuleManager.load(tfs, global)
for _, file in next, tfs:list(false) do
-- Check if it is .lua.
if string.sub(file, #file - 3, #file) ~= ".lua" then
continue
end
-- Get string to load.
local ls = tfs:read(file)
-- Get function that we can execute.
local lf, lr = loadstring(ls)
if not lf then
Logger.warn("Module file '%s' failed to load due to error '%s' while loading.", file, lr)
continue
end
ModuleManager.execute(lf, string.sub(file, 1, #file - 4), file, global)
end
end
---List loaded modules.
---@return string[]
function ModuleManager.loaded()
local out = {}
for file, _ in next, ModuleManager.modules do
table.insert(out, file)
end
return out
end
---Detach functions.
function ModuleManager.detach()
for _, detach in next, tdetach do
detach()
end
-- Clear detach table.
tdetach = {}
end
---Refresh ModuleManager.
function ModuleManager.refresh()
-- Detach all modules.
ModuleManager.detach()
-- Reset current list.
ModuleManager.modules = {}
ModuleManager.globals = {}
for id, lf in next, INTERNAL_GLOBALS do
ModuleManager.execute(lf, id, nil, true)
end
for id, lf in next, INTERNAL_MODULES do
ModuleManager.execute(lf, id, nil, false)
end
-- Load all globals in our filesystem.
ModuleManager.load(gfs, true)
-- Load all modules in our filesystem.
ModuleManager.load(fs, false)
end
-- Return ModuleManager module.
return ModuleManager
end)
-- MODULE: Game/Timings/PartTiming
defineModule("Game/Timings/PartTiming", function()
---@module Game.Timings.Timing
local Timing = require("Game/Timings/Timing")
---@class PartTiming: Timing
---@field pname string Part name.
---@field uhc boolean Use hitbox CFrame.
---@field pbrick string? Required BrickColor name.
---@field prgb table? Required color (RGB table).
---@field pchar string? Required LastLoadedChar value.
---@field exclusiveSeconds number? Ignore other matches for this timing for a duration.
local PartTiming = setmetatable({}, { __index = Timing })
PartTiming.__index = PartTiming
local function normalizePartKey(value)
return string.lower(tostring(value or "")):gsub("[^%w]", "")
end
local function rgbKey(prgb)
if type(prgb) ~= "table" then
return nil
end
local r = prgb.r or prgb.R or prgb[1]
local g = prgb.g or prgb.G or prgb[2]
local b = prgb.b or prgb.B or prgb[3]
if type(r) ~= "number" or type(g) ~= "number" or type(b) ~= "number" then
return nil
end
return string.format("%d,%d,%d", r, g, b)
end
---Timing ID.
---@return string
function PartTiming:id()
local base = self.pname or self.name or ""
local key = normalizePartKey(base)
if self.pbrick and self.pbrick ~= "" then
key = key .. "|brick:" .. normalizePartKey(self.pbrick)
end
local rgb = rgbKey(self.prgb)
if rgb then
key = key .. "|rgb:" .. rgb
end
if self.pchar and self.pchar ~= "" then
key = key .. "|char:" .. normalizePartKey(self.pchar)
end
return key
end
---Load from partial values.
---@param values table
function PartTiming:load(values)
Timing.load(self, values)
if typeof(values.pname) == "string" then
self.pname = values.pname
end
if typeof(values.uhc) == "boolean" then
self.uhc = values.uhc
end
if typeof(values.pbrick) == "string" then
self.pbrick = values.pbrick
end
if typeof(values.prgb) == "table" then
self.prgb = values.prgb
end
if typeof(values.pchar) == "string" then
self.pchar = values.pchar
end
if typeof(values.exclusiveSeconds) == "number" then
self.exclusiveSeconds = values.exclusiveSeconds
end
end
---Clone timing.
---@return PartTiming
function PartTiming:clone()
local clone = setmetatable(Timing.clone(self), PartTiming)
clone.pname = self.pname
clone.uhc = self.uhc
clone.pbrick = self.pbrick
clone.prgb = self.prgb
clone.pchar = self.pchar
clone.exclusiveSeconds = self.exclusiveSeconds
return clone
end
---Return a serializable table.
---@return PartTiming
function PartTiming:serialize()
local serializable = Timing.serialize(self)
serializable.pname = self.pname
serializable.uhc = self.uhc
serializable.pbrick = self.pbrick
serializable.prgb = self.prgb
serializable.pchar = self.pchar
serializable.exclusiveSeconds = self.exclusiveSeconds
return serializable
end
---Create a new part timing.
---@param values table?
---@return PartTiming
function PartTiming.new(values)
local self = setmetatable(Timing.new(), PartTiming)
self.pname = ""
self.uhc = false
self.pbrick = nil
self.prgb = nil
self.pchar = nil
self.exclusiveSeconds = 0
if values then
self:load(values)
end
return self
end
-- Return PartTiming module.
return PartTiming
end)
-- MODULE: Game/Timings/PlaybackData
defineModule("Game/Timings/PlaybackData", function()
---@class PlaybackData
---@field base number Timestamp of when the object was created.
---@field ash table<number, number> Animation speed history. The key is the timestamp delta and the value is the speed at that point.
---@field entity Model Entity to playback.
local PlaybackData = {}
PlaybackData.__index = PlaybackData
---Get last exceeded speed difference from a timestamp delta.
---@param from number
---@return number?, number?
function PlaybackData:last(from)
local latestExceededSpeed = nil
local latestExceededDelta = nil
for delta, speed in next, self.ash do
if from <= delta then
continue
end
if latestExceededDelta and delta <= latestExceededDelta then
continue
end
latestExceededSpeed = speed
latestExceededDelta = delta
end
return latestExceededSpeed, latestExceededDelta
end
---Track animation speed.
---@param speed number
function PlaybackData:astrack(speed)
local delta = os.clock() - self.base
if self:last(delta) == speed then
return
end
self.ash[delta] = speed
end
---Create new PlaybackData object.
---@param entity Model
---@return PlaybackData
function PlaybackData.new(entity)
local self = setmetatable({}, PlaybackData)
self.base = os.clock()
self.entity = entity
---@note: Timestamp delta is how many seconds need to pass before being able to reach this speed.
self.ash = {}
return self
end
-- Return PlaybackData module.
return PlaybackData
end)
-- MODULE: Game/Timings/SaveManager
defineModule("Game/Timings/SaveManager", function()
---@module Game.Timings.TimingSave
local TimingSave = require("Game/Timings/TimingSave")
---@module Game.Timings.TimingContainerPair
local TimingContainerPair = require("Game/Timings/TimingContainerPair")
---@module Game.Timings.TimingContainer
local TimingContainer = require("Game/Timings/TimingContainer")
---@module Game.Timings.AnimationTiming
local AnimationTiming = require("Game/Timings/AnimationTiming")
---@module Game.Timings.PartTiming
local PartTiming = require("Game/Timings/PartTiming")
---@module Game.Timings.SoundTiming
local SoundTiming = require("Game/Timings/SoundTiming")
---@module Utility.Maid
local Maid = require("Utility/Maid")
---@module Utility.Signal
local Signal = require("Utility/Signal")
-- SaveManager module.
local SaveManager = { llc = nil, llcn = nil, lct = nil }
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Utility.Filesystem
local Filesystem = require("Utility/Filesystem")
---@module Utility.Deserializer
local Deserializer = require("Utility/Deserializer")
---@module Utility.Serializer
local Serializer = require("Utility/Serializer")
---@module Utility.Logger
local Logger = require("Utility/Logger")
-- Manager filesystem.
local fs = Filesystem.new("CottomHub-Timings")
-- Current timing save.
local config = TimingSave.new()
local EMBEDDED_TIMINGS_TEXT = [=[
{
"part": [
{
"name": "Genki",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1200,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Genki",
"uhc": false
},
{
"name": "Genki_GohanTeen",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1400,
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Genki",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": "Gohan [Teen]",
"exclusiveSeconds": 1.4
},
{
"name": "Genki_GokuBuuSaga",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Genki",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": "Goku [Buu Saga]",
"exclusiveSeconds": 1.6
},
{
"name": "Genki_MediumBlue",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Jump",
"name": "Jump_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Jump",
"name": "Jump_2",
"when": 200,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Genki",
"uhc": false,
"pbrick": "Medium blue",
"prgb": null,
"pchar": null
},
{
"name": "GodFlash",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Jump",
"name": "Jump_1",
"when": 0,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Jump",
"name": "Jump_2",
"when": 200,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "GodFlash",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": null
},
{
"name": "Blast",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 44,
"Y": 46,
"Z": 46
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 10
},
"ihbc": false
}
],
"hitbox": {
"X": 44,
"Y": 46,
"Z": 46
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 10
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Blast",
"uhc": false
},
{
"name": "Blast_HotPink",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Blast",
"uhc": false,
"pbrick": "Hot pink",
"prgb": null,
"pchar": null
},
{
"name": "Coin",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1200,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Coin",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": null
},
{
"name": "Javelin",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Javelin",
"uhc": false,
"pbrick": "Toothpaste",
"prgb": null,
"pchar": null
},
{
"name": "EraserBlow",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 60,
"Y": 60,
"Z": 60
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "EraserBlow",
"uhc": false,
"pbrick": "Really black",
"prgb": null,
"pchar": null
},
{
"name": "Ball_Black",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 0,
"hitbox": {
"X": 100,
"Y": 100,
"Z": 100
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 100,
"Y": 100,
"Z": 100
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Ball",
"uhc": false,
"pbrick": "Really black",
"prgb": null,
"pchar": null
},
{
"name": "KiBlast_Black",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 40,
"Y": 40,
"Z": 40
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"pbrick": "Really black",
"prgb": null,
"pchar": null
},
{
"name": "KiBlast",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"exclusiveSeconds": 1.6
},
{
"name": "KiBlast_Persimmon",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"pbrick": "Persimmon",
"prgb": null,
"pchar": null,
"exclusiveSeconds": 1.6
},
{
"name": "KiBlast_Majin",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": "Vegeta [Majin]",
"exclusiveSeconds": 1.6
},
{
"name": "KiBlast_Piccolo",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Dash",
"name": "DashBack_2",
"when": 1400,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": "Piccolo",
"exclusiveSeconds": 1.2
},
{
"name": "KiBlast_Krillin",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 30,
"Y": 30,
"Z": 30
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "KiBlast",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": "Krillin",
"exclusiveSeconds": 1.6
},
{
"name": "Ring_Majin",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1200,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Ring",
"uhc": false,
"pbrick": null,
"prgb": {
"r": 203,
"g": 151,
"b": 61
},
"pchar": "Vegeta [Majin]",
"exclusiveSeconds": 1.4
},
{
"name": "Ring_Color",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1400,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "Ring",
"uhc": false,
"pbrick": null,
"prgb": {
"r": 203,
"g": 151,
"b": 61
},
"pchar": null,
"exclusiveSeconds": 1.4
},
{
"name": "laysiawhiteLawRock",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": true,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 45,
"Y": 45,
"Z": 45
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 45,
"Y": 45,
"Z": 45
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 2000,
"hitbox": {
"X": 45,
"Y": 45,
"Z": 45
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 45,
"Y": 45,
"Z": 45
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"pname": "laysiawhiteLawRock",
"uhc": false,
"pbrick": null,
"prgb": null,
"pchar": null
}
],
"sound": [],
"version": 1,
"animation": [
{
"name": "M1_1947243130",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1947243130",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1885684657",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1885684657",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470472673",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470472673",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470482438",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470482438",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1947173251",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1947173251",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1947196236",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1947196236",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1947219719",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1947219719",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1947230024",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1947230024",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1885679702",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1885679702",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1885690040",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1885690040",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1885693880",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1885693880",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1885667765",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1885667765",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1730640014",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1730640014",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1730629532",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1730629532",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1730618971",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1730618971",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1730606513",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1730606513",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1730596371",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1730596371",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_13631792671",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0.85,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 100
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 100
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 15,
"Y": 15,
"Z": 100
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://13631792671",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_8343484557",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 16,
"Y": 10,
"Z": 12
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1000,
"hitbox": {
"X": 16,
"Y": 10,
"Z": 12
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 16,
"Y": 10,
"Z": 12
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8343484557",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_13801810717",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 350,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "Dash",
"name": "DashBack_1",
"when": 350,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 2050,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://13801810717",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_72535824544679",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0.8,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 85
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1100,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 85
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 15,
"Y": 15,
"Z": 85
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://72535824544679",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_80128331690638",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 1.2,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 218,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 27
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1418,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 27
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 27
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://80128331690638",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_73498149701712",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 1.5,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 70
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1500,
"hitbox": {
"X": 15,
"Y": 15,
"Z": 70
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 15,
"Y": 15,
"Z": 70
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://73498149701712",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_132237726679338",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Jump",
"name": "Jump_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://132237726679338",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_17421950447",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0.4,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 328,
"hitbox": {
"X": 17,
"Y": 17,
"Z": 120
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 17,
"Y": 17,
"Z": 120
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17421950447",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": false,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17423591514",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17423591514",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17423592816",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17423592816",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17423594220",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 600,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17423594220",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17442363923",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 600,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17442363923",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17423597575",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 600,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17423597575",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8442976984",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8442976984",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8442979908",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8442979908",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8442981940",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8442981940",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461128166",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461128166",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461128859",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461128859",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461136273",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461136273",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461136875",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461136875",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461137417",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461137417",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461277837",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 37,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 37,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 37,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461277837",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461145506",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 18,
"Y": 19,
"Z": 23
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 18,
"Y": 19,
"Z": 23
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 18,
"Y": 19,
"Z": 23
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461145506",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1461127258",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 34,
"Y": 33,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1461127258",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_12927983921",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 388,
"hitbox": {
"X": 20,
"Y": 40,
"Z": 0
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 20,
"Y": 40,
"Z": 0
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://12927983921",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_12927306244",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashRight_1",
"when": 405,
"hitbox": {
"X": 15,
"Y": 17,
"Z": 170
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 15,
"Y": 17,
"Z": 170
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://12927306244",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17306010161",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17306010161",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17306012933",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17306012933",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17306015782",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17306015782",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17306019069",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17306019069",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_17306027379",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Ball",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://17306027379",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_116007627948983",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [],
"hitbox": {
"X": 0,
"Y": 0,
"Z": 0
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://116007627948983",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_112322786810921",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1600,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 10
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 10
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://112322786810921",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_92901308072582",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://92901308072582",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8320258247",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8320258247",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8319862463",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8319862463",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8321532463",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8321532463",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_8321564926",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://8321564926",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_99173747030368",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": false
}
],
"hitbox": {
"X": 24,
"Y": 24,
"Z": 24
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://99173747030368",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470422387",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470422387",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470439852",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470439852",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470449816",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470449816",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470447472",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470447472",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_1470454728",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://1470454728",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_3242973133",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 489,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1689,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://3242973133",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "Skill_3242845717",
"tag": "Skill",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 196,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 196,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 1396,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://3242845717",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_10005397081",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://10005397081",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_10005430027",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://10005430027",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_10005462854",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://10005462854",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_10005508646",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://10005508646",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_129856070992547",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://129856070992547",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_106548325298957",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://106548325298957",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_108198644371262",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://108198644371262",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_84395905295585",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://84395905295585",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_100538053697796",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://100538053697796",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_71830104436314",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://71830104436314",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_111366686942798",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://111366686942798",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_123396863449599",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Start Block",
"name": "StartBlock_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
},
{
"_type": "End Block",
"name": "EndBlock_1",
"when": 300,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://123396863449599",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_10005583738",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://10005583738",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
},
{
"name": "M1_139831323330863",
"tag": "M1",
"imdd": 0,
"imxd": 1000,
"duih": false,
"punishable": 0,
"after": 0,
"actions": [
{
"_type": "Dash",
"name": "DashBack_1",
"when": 0,
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"ihbc": true
}
],
"hitbox": {
"X": 1,
"Y": 1,
"Z": 1
},
"shape": "Box",
"offset": {
"X": 0,
"Y": 0,
"Z": 0
},
"umoa": false,
"smod": "N/A",
"smn": false,
"srpn": false,
"aatk": false,
"fhb": true,
"ndfb": false,
"scrambled": false,
"_id": "rbxassetid://139831323330863",
"rpue": false,
"rpd": 0,
"rsd": 0,
"ha": false,
"iae": false,
"ieae": false,
"mat": 2000,
"phd": false,
"pfh": true,
"phds": 0,
"pfht": 0.15,
"dp": false
}
]
}
]=]
-- Services.
local runService = game:GetService("RunService")
-- Maids.
local saveMaid = Maid.new()
---Get save files list.
---@return table
function SaveManager.list()
local list = fs:list(true)
local out = {}
for idx = 1, #list do
local file = list[idx]
if file:sub(-4) ~= ".txt" then
continue
end
local pos = file:find(".txt", 1, true)
local char = file:sub(pos, pos)
local start = pos
while char ~= "/" and char ~= "\\" and char ~= "" do
pos = pos - 1
char = file:sub(pos, pos)
end
if char == "/" or char == "\\" then
table.insert(out, file:sub(pos + 1, start - 1))
end
end
return out
end
---Merge with current config.
---@param name string
---@param type MergeType
function SaveManager.merge(name, type)
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
local success, result = pcall(fs.read, fs, name .. ".txt")
if not success then
Logger.longNotify("Failed to read config file %s.", name)
return Logger.warn("Timing manager ran into the error '%s' while attempting to read config %s.", result, name)
end
success, result = pcall(Deserializer.unmarshal_one, result)
if not success then
Logger.longNotify("Failed to deserialize config file %s.", name)
return Logger.warn(
"Timing manager ran into the error '%s' while attempting to deserialize config %s.",
result,
name
)
end
if typeof(result) ~= "table" then
Logger.longNotify("Failed to load config file %s.", name)
return Logger.warn("Timing manager failed to load config %s with result %s.", name, tostring(result))
end
config:merge(TimingSave.new(result), type)
Logger.notify("Config file %s has merged with the loaded one.", name)
end
---Refresh dropdown values with timing data.
---@param dropdown table
function SaveManager.refresh(dropdown)
dropdown:SetValues(SaveManager.list())
end
---Set config name as auto-load.
---@param name string
function SaveManager.autoload(name)
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
local success, result = pcall(fs.write, fs, "autoload.txt", name)
if not success then
Logger.longNotify("Failed to write autoload file %s.", name)
return Logger.warn(
"Timing manager ran into the error '%s' while attempting to write autoload file %s.",
result,
name
)
end
Logger.notify("Config file %s has set to auto-load.", name)
end
---Create timing as config name.
---@param name string
function SaveManager.create(name)
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
if fs:file(name .. ".txt") then
return Logger.longNotify("Config file %s already exists.", name)
end
SaveManager.write(name)
end
---Save timing as config name.
---@param name string
function SaveManager.save(name)
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
if not fs:file(name .. ".txt") then
return Logger.longNotify("Config file %s does not exist.", name)
end
SaveManager.write(name)
end
---Write timing as config name.
---@param name string
---@return number
function SaveManager.write(name)
if not name or #name <= 0 then
return -1, Logger.longNotify("Config name cannot be empty.")
end
local success, result = pcall(Serializer.marshal, config:serialize())
if not success then
Logger.longNotify("Failed to serialize config file %s.", name)
return -2,
Logger.warn("Timing manager ran into the error '%s' while attempting to serialize config %s.", result, name)
end
success, result = pcall(fs.write, fs, name .. ".txt", result)
if not success then
Logger.longNotify("Failed to write config file %s.", name)
return -3,
Logger.warn("Timing manager ran into the error '%s' while attempting to write config %s.", result, name)
end
Logger.notify("Config file %s has written to.", name)
return 0
end
---Clear config from config name.
---@param name string
function SaveManager.clear(name)
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
local success, result = pcall(Serializer.marshal, TimingSave.new():serialize())
if not success then
Logger.longNotify("Failed to serialize config file %s.", name)
return Logger.warn(
"Timing manager ran into the error '%s' while attempting to serialize config %s.",
result,
name
)
end
success, result = pcall(fs.write, fs, name .. ".txt", result)
if not success then
Logger.longNotify("Failed to write config file %s.", name)
return Logger.warn("Timing manager ran into the error '%s' while attempting to write config %s.", result, name)
end
Logger.notify("Config file %s has cleared.", name)
end
---Load timing from config name.
---@param name string
function SaveManager.load(name)
local timestamp = os.clock()
if not name or #name <= 0 then
return Logger.longNotify("Config name cannot be empty.")
end
if SaveManager.llcn and SaveManager.llcn == name and SaveManager.llc then
return Logger.notify("Config file %s is already loaded.", name)
end
local success, result = pcall(fs.read, fs, name .. ".txt")
if not success then
Logger.longNotify("Failed to read config file %s.", name)
return Logger.warn("Timing manager ran into the error '%s' while attempting to read config %s.", result, name)
end
success, result = pcall(Deserializer.unmarshal_one, result)
if not success then
Logger.longNotify("Failed to deserialize config file %s.", name)
return Logger.warn(
"Timing manager ran into the error '%s' while attempting to deserialize config %s.",
result,
name
)
end
if typeof(result) ~= "table" then
Logger.longNotify("Failed to process config file %s.", name)
return Logger.warn("Timing manager failed to process config %s with result %s.", name, tostring(result))
end
config:clear()
success, result = pcall(config.load, config, result)
if not success then
Logger.longNotify("Failed to load config file %s.", name)
return Logger.warn("Timing manager ran into the error '%s' while attempting to load config %s.", result, name)
end
Logger.notify(
"Config file %s has loaded with %i timings in %.2f seconds.",
name,
config:count(),
os.clock() - timestamp
)
SaveManager.llc = config:clone()
SaveManager.llcn = name
end
local function loadEmbeddedConfig()
if not EMBEDDED_TIMINGS_TEXT or #EMBEDDED_TIMINGS_TEXT == 0 then
return false
end
local success, result = pcall(Deserializer.unmarshal_one, EMBEDDED_TIMINGS_TEXT)
if not success or typeof(result) ~= "table" then
Logger.warn("Embedded config failed to deserialize: %s", tostring(result))
return false
end
config:clear()
local ok, err = pcall(config.load, config, result)
if not ok then
Logger.warn("Embedded config failed to load: %s", tostring(err))
return false
end
SaveManager.llc = config:clone()
SaveManager.llcn = "embedded"
Logger.notify("Embedded config loaded with %i timings.", config:count())
return true
end
---Initialize SaveManager.
function SaveManager.init()
local timestamp = os.clock()
local preRenderSignal = Signal.new(runService.PreRender)
-- Create internal timing containers.
local internalAnimationContainer = TimingContainer.new(AnimationTiming.new())
local internalPartContainer = TimingContainer.new(PartTiming.new())
local internalSoundContainer = TimingContainer.new(SoundTiming.new())
internalAnimationContainer:load({})
internalPartContainer:load({})
internalSoundContainer:load({})
-- Count up internal timings.
local internalCount = internalAnimationContainer:count()
+ internalPartContainer:count()
+ internalSoundContainer:count()
Logger.notify(
"Internal timings have loaded with %i timings in %.2f seconds.",
internalCount,
os.clock() - timestamp
)
-- Load embedded config first (prevents duplicate autoload).
local embeddedLoaded = loadEmbeddedConfig()
-- Attempt to read auto-load config if no embedded config was loaded.
if not embeddedLoaded then
local success, result = pcall(fs.read, fs, "autoload.txt")
if success and result then
SaveManager.load(result)
end
end
-- Animation stack.
SaveManager.as = TimingContainerPair.new(internalAnimationContainer, config:get().animation)
-- Part stack.
SaveManager.ps = TimingContainerPair.new(internalPartContainer, config:get().part)
-- Sound stack.
SaveManager.ss = TimingContainerPair.new(internalSoundContainer, config:get().sound)
-- Run auto save.
saveMaid:add(preRenderSignal:connect("SaveManager_AutoSave", function()
local llc = SaveManager.llc
if not llc then
return
end
local llcn = SaveManager.llcn
if not llcn then
return
end
if not Configuration.expectToggleValue("PeriodicAutoSave") then
return
end
if
SaveManager.lct
and os.clock() - SaveManager.lct < (Configuration.expectOptionValue("PeriodicAutoSaveInterval") or 60)
then
return
end
SaveManager.lct = os.clock()
if config:equals(llc) then
return
end
Logger.warn("Auto-saving timings to '%s' config file.", SaveManager.llcn)
SaveManager.write(SaveManager.llcn)
SaveManager.llc = config:clone()
Logger.notify("Timing auto-save has completed successfully.")
end))
end
---Detach SaveManager.
function SaveManager.detach()
saveMaid:clean()
end
-- Return SaveManager module.
return SaveManager
end)
-- MODULE: Game/Timings/SoundTiming
defineModule("Game/Timings/SoundTiming", function()
---@module Game.Timings.Timing
local Timing = require("Game/Timings/Timing")
---@class SoundTiming: Timing
---@field id string Sound ID.
---@field rpue boolean Repeat parry until end.
---@field _rsd number Repeat start delay in miliseconds. Never access directly.
---@field _rpd number Delay between each repeat parry in miliseconds. Never access directly.
local SoundTiming = setmetatable({}, { __index = Timing })
SoundTiming.__index = SoundTiming
---Timing ID.
---@return string
function SoundTiming:id()
return self._id
end
-- Getter for repeat start delay in seconds.
---@return number
function SoundTiming:rsd()
return PP_SCRAMBLE_NUM(self._rsd) / 1000
end
-- Getter for repeat start delay in seconds.
---@return number
function SoundTiming:rpd()
return PP_SCRAMBLE_NUM(self._rpd) / 1000
end
---Load from partial values.
---@param values table
function SoundTiming:load(values)
Timing.load(self, values)
if typeof(values._id) == "string" then
self._id = values._id
end
if type(values.rsd) == "number" then
self._rsd = values.rsd
end
if typeof(values.rpue) == "boolean" then
self.rpue = values.rpue
end
if typeof(values.rpd) == "number" then
self._rpd = values.rpd
end
end
---Clone timing.
---@return SoundTiming
function SoundTiming:clone()
local clone = setmetatable(Timing.clone(self), SoundTiming)
clone._rpd = self._rpd
clone.rpue = self.rpue
clone._rsd = self._rsd
clone._id = self._id
return clone
end
---Return a serializable table.
---@return SoundTiming
function SoundTiming:serialize()
local serializable = Timing.serialize(self)
serializable._id = self._id
serializable.rpue = self.rpue
serializable.rsd = self._rsd
serializable.rpd = self._rpd
return serializable
end
---Create a new sound timing.
---@param values table?
---@return SoundTiming
function SoundTiming.new(values)
local self = setmetatable(Timing.new(), SoundTiming)
self._id = ""
self.rpue = false
self._rsd = 0
self._rpd = 0
if values then
self:load(values)
end
return self
end
-- Return SoundTiming module.
return SoundTiming
end)
-- MODULE: Game/Timings/Timing
defineModule("Game/Timings/Timing", function()
---@module Game.Timings.ActionContainer
local ActionContainer = require("Game/Timings/ActionContainer")
---@class Timing
---@field name string
---@field tag string
---@field imdd number Initial minimum distance from position.
---@field imxd number Initial maximum distance from position.
---@field punishable number Punishable window in seconds.
---@field after number After window in seconds.
---@field duih boolean Delay until in hitbox.
---@field actions ActionContainer
---@field hitbox Vector3
---@field shape string The hitbox shape ("Box" or "Ball").
---@field offset Vector3 The hitbox offset.
---@field umoa boolean Use module over actions.
---@field smn boolean Skip module notification.
---@field srpn boolean Skip repeat notification.
---@field smod string Selected module string.
---@field aatk boolean Allow attacking.
---@field fhb boolean Hitbox facing offset.
---@field ndfb boolean No dash fallback.
---@field scrambled boolean Scrambled?
local Timing = {}
Timing.__index = Timing
---Timing ID. Override me.
---@return string
function Timing:id()
return self.name
end
---Set timing ID. Override me.
---@param id string
function Timing:set(id)
self.name = id
end
---Load from partial values.
---@param values table
function Timing:load(values)
if typeof(values.name) == "string" then
self.name = values.name
end
if typeof(values.tag) == "string" then
self.tag = values.tag
end
if typeof(values.imdd) == "number" then
self.imdd = values.imdd
end
if typeof(values.imxd) == "number" then
self.imxd = values.imxd
end
if typeof(values.duih) == "boolean" then
self.duih = values.duih
end
if typeof(values.punishable) == "number" then
self.punishable = values.punishable
end
if typeof(values.after) == "number" then
self.after = values.after
end
if typeof(values.actions) == "table" then
self.actions:load(values.actions)
end
if typeof(values.smn) == "boolean" then
self.smn = values.smn
end
if typeof(values.hitbox) == "table" then
self.hitbox = Vector3.new(values.hitbox.X or 0, values.hitbox.Y or 0, values.hitbox.Z or 0)
elseif typeof(values.hitbox) == "Vector3" then
self.hitbox = values.hitbox
end
if typeof(values.shape) == "string" then
self.shape = values.shape
end
if typeof(values.offset) == "table" then
self.offset = Vector3.new(values.offset.X or 0, values.offset.Y or 0, values.offset.Z or 0)
elseif typeof(values.offset) == "Vector3" then
self.offset = values.offset
end
if typeof(values.umoa) == "boolean" then
self.umoa = values.umoa
end
if typeof(values.srpn) == "boolean" then
self.srpn = values.srpn
end
if typeof(values.smod) == "string" then
self.smod = values.smod
end
if typeof(values.aatk) == "boolean" then
self.aatk = values.aatk
end
if typeof(values.fhb) == "boolean" then
self.fhb = values.fhb
end
if typeof(values.ndfb) == "boolean" then
self.ndfb = values.ndfb
end
if typeof(values.scrambled) == "boolean" then
self.scrambled = values.scrambled
end
end
---Equals check.
---@param other Timing
---@return boolean
function Timing:equals(other)
if self.name ~= other.name then
return false
end
if self.tag ~= other.tag then
return false
end
if self.imdd ~= other.imdd then
return false
end
if self.imxd ~= other.imxd then
return false
end
if self.duih ~= other.duih then
return false
end
if self.punishable ~= other.punishable then
return false
end
if self.after ~= other.after then
return false
end
if not self.actions:equals(other.actions) then
return false
end
if self.smn ~= other.smn then
return false
end
if self.hitbox ~= other.hitbox then
return false
end
if self.shape ~= other.shape then
return false
end
if self.offset ~= other.offset then
return false
end
if self.umoa ~= other.umoa then
return false
end
if self.srpn ~= other.srpn then
return false
end
if self.smod ~= other.smod then
return false
end
if self.aatk ~= other.aatk then
return false
end
if self.fhb ~= other.fhb then
return false
end
if self.ndfb ~= other.ndfb then
return false
end
if self.scrambled ~= other.scrambled then
return false
end
return true
end
---Clone timing.
---@return Timing
function Timing:clone()
local clone = Timing.new()
clone.name = self.name
clone.tag = self.tag
clone.duih = self.duih
clone.imdd = self.imdd
clone.imxd = self.imxd
clone.smn = self.smn
clone.punishable = self.punishable
clone.after = self.after
clone.actions = self.actions:clone()
clone.hitbox = self.hitbox
clone.shape = self.shape
clone.offset = self.offset
clone.umoa = self.umoa
clone.srpn = self.srpn
clone.smod = self.smod
clone.aatk = self.aatk
clone.fhb = self.fhb
clone.ndfb = self.ndfb
clone.scrambled = self.scrambled
return clone
end
---Return a serializable table.
---@return table
function Timing:serialize()
return {
name = self.name,
tag = self.tag,
imdd = self.imdd,
imxd = self.imxd,
duih = self.duih,
punishable = self.punishable,
smn = self.smn,
after = self.after,
actions = self.actions:serialize(),
hitbox = {
X = self.hitbox.X,
Y = self.hitbox.Y,
Z = self.hitbox.Z,
},
shape = self.shape,
offset = {
X = self.offset.X,
Y = self.offset.Y,
Z = self.offset.Z,
},
srpn = self.srpn,
umoa = self.umoa,
smod = self.smod,
aatk = self.aatk,
fhb = self.fhb,
ndfb = self.ndfb,
scrambled = self.scrambled,
phd = self.phd,
pfh = self.pfh,
}
end
---Create new Timing object.
---@param values table?
---@return Timing
function Timing.new(values)
local self = setmetatable({}, Timing)
self.tag = "Other"
self.name = "N/A"
self.imdd = 0
self.imxd = 0
self.smn = false
self.punishable = 0
self.after = 0
self.duih = false
self.actions = ActionContainer.new()
self.hitbox = Vector3.zero
self.shape = "Box"
self.offset = Vector3.zero
self.umoa = false
self.srpn = false
self.smod = "N/A"
self.aatk = false
self.fhb = true
self.ndfb = false
self.scrambled = false
if values then
self:load(values)
end
return self
end
-- Return Timing module.
return Timing
end)
-- MODULE: Game/Timings/TimingContainer
defineModule("Game/Timings/TimingContainer", function()
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@class TimingContainer
---@field timings table<string, Timing>
---@field module Timing
local TimingContainer = {}
TimingContainer.__index = TimingContainer
---Merge timing container.
---@param other TimingContainer
---@param type MergeType
function TimingContainer:merge(other, type)
local mergeType = type
if type == "Add New Timings" or type == "1" then
mergeType = 1
end
if type == "Overwrite and Add Everything" or type == "2" then
mergeType = 2
end
assert(mergeType == 1 or mergeType == 2, "Invalid timing table merge type")
for idx, timing in next, other.timings do
if mergeType == 1 and self.timings[idx] then
continue
end
self.timings[idx] = timing
end
end
---Find a timing from name.
---@param name string
---@return Timing?
function TimingContainer:find(name)
for _, timing in next, self.timings do
if timing.name ~= name then
continue
end
return timing
end
end
---Clone timing container.
---@return TimingContainer
function TimingContainer:clone()
local container = TimingContainer.new(self.module)
for _, timing in next, self.timings do
container:push(timing:clone())
end
return container
end
---List all timings.
---@return Timing[]
function TimingContainer:list()
local timings = {}
for _, timing in next, self.timings do
timings[#timings + 1] = timing
end
return timings
end
---Get names of all timings.
---@return string[]
function TimingContainer:names()
local names = {}
for _, timing in next, self.timings do
names[#names + 1] = timing.name
end
table.sort(names)
return names
end
---Remove a timing from the list.
---@param timing Timing
function TimingContainer:remove(timing)
local id = timing:id()
if not id then
return
end
self.timings[id] = nil
end
---Push a timing to the list.
---@param timing Timing
function TimingContainer:push(timing)
local id = timing:id()
if not id then
return
end
---@note: Timing array keys must all be unique.
if self.timings[id] then
return error(string.format("Timing identifier '%s' already exists in container.", id))
end
---@note: Every timing must have unique names.
if self:find(timing.name) then
return error(string.format("Timing name '%s' already exists in container.", timing.name))
end
self.timings[id] = timing
end
---Equals check.
---@param other TimingContainer
---@return boolean
function TimingContainer:equals(other)
if self:count() ~= other:count() then
return false
end
for id, timing in next, self.timings do
local otherTiming = other.timings[id]
if not otherTiming then
return false
end
if not timing:equals(otherTiming) then
return false
end
end
return true
end
---Clear all timings.
function TimingContainer:clear()
self.timings = {}
end
---Get timing count.
---@return number
function TimingContainer:count()
local count = 0
for _ in next, self.timings do
count = count + 1
end
return count
end
---Load from partial values.
---@param values table
function TimingContainer:load(values)
for _, value in next, values do
local timing = self.module.new(value)
if not timing then
continue
end
local id = timing:id()
if not id then
continue
end
---@note: Timing array keys must all be unique.
if self.timings[id] then
return error(string.format("Timing identifier '%s' already exists in container.", id))
end
---@note: Every timing must have unique names.
if self:find(timing.name) then
return error(string.format("Timing name '%s' already exists in container.", timing.name))
end
---@note: Why are the stored timing keys different from what's loaded?
--- Internally, all timings are stored by their identifiers.
--- This helps to quickly find a timing by its identifier. Example - an animation ID.
--- Although, this does not mean each identifier must have a meaning. It can be random.
self.timings[id] = timing
end
end
---Return a serializable table.
---@return table
function TimingContainer:serialize()
local out = {}
for _, timing in next, self.timings do
out[#out + 1] = timing:serialize()
end
return out
end
---Create new TimingContainer object.
---@param module Timing
---@return TimingContainer
function TimingContainer.new(module)
local self = setmetatable({}, TimingContainer)
self.timings = {}
self.module = module
return self
end
-- Return TimingContainer module.
return TimingContainer
end)
-- MODULE: Game/Timings/TimingContainerPair
defineModule("Game/Timings/TimingContainerPair", function()
---@class TimingContainerPair
---@note The configs are always prioritized over the internal timings.
---@field internal TimingContainer
---@field config TimingContainer
local TimingContainerPair = {}
TimingContainerPair.__index = TimingContainerPair
---Create new TimingContainerPair object.
---@param internal TimingContainer
---@param config TimingContainer
---@return TimingContainerPair
function TimingContainerPair.new(internal, config)
local self = setmetatable({}, TimingContainerPair)
self.internal = internal
self.config = config
return self
end
---Index timing container.
---@param key any?
---@return Timing?
function TimingContainerPair:index(key)
key = PP_SCRAMBLE_STR(key)
return self.config.timings[key] or self.internal.timings[key]
end
---Find timing from name.
---@param name string
---@return Timing?
function TimingContainerPair:find(name)
return self.config:find(name) or self.internal:find(name)
end
---List all timings.
---@return Timing[]
function TimingContainerPair:list()
local timings = {}
for _, timing in next, self.config:list() do
table.insert(timings, timing)
end
for _, timing in next, self.internal:list() do
table.insert(timings, timing)
end
return timings
end
-- Return TimingContainerStack module.
return TimingContainerPair
end)
-- MODULE: Game/Timings/TimingSave
defineModule("Game/Timings/TimingSave", function()
---@module Game.Timings.TimingContainer
local TimingContainer = require("Game/Timings/TimingContainer")
---@module Game.Timings.AnimationTiming
local AnimationTiming = require("Game/Timings/AnimationTiming")
---@module Game.Timings.PartTiming
local PartTiming = require("Game/Timings/PartTiming")
---@module Game.Timings.SoundTiming
local SoundTiming = require("Game/Timings/SoundTiming")
---@class TimingSave
---@field _data TimingContainer[]
local TimingSave = {}
TimingSave.__index = TimingSave
---Timing save version constant.
---@note: Increment me when the data structure changes and we need to add backwards compatibility.
local TIMING_SAVE_VERSION = 1
---@alias MergeType
---| '1' # Only add new timings
---| '2' # Overwrite and add everything
---Get timing save.
---@return TimingContainer[]
function TimingSave:get()
return self._data
end
---Clear timing containers.
function TimingSave:clear()
for _, container in next, self._data do
container:clear()
end
end
---Merge with another TimingSave object.
---@param save TimingSave The other save.
---@param type MergeType
function TimingSave:merge(save, type)
for idx, other in next, save._data do
local container = self._data[idx]
if not container then
continue
end
container:merge(other, type)
end
end
---Load from partial values.
---@param values table
function TimingSave:load(values)
local data = self._data
if typeof(values.animation) == "table" then
data.animation:load(values.animation)
end
if typeof(values.part) == "table" then
data.part:load(values.part)
end
if typeof(values.sound) == "table" then
data.sound:load(values.sound)
end
end
---Clone timing save.
---@return TimingSave
function TimingSave:clone()
local save = TimingSave.new()
for idx, container in next, self._data do
save._data[idx] = container:clone()
end
return save
end
---Equal timing saves.
---@param other TimingSave
---@return boolean
function TimingSave:equals(other)
if not other or typeof(other) ~= "table" then
return false
end
for idx, container in next, self._data do
local otherContainer = other._data[idx]
if not otherContainer then
return false
end
if not container:equals(otherContainer) then
return false
end
end
return true
end
---Get timing save count.
---@return number
function TimingSave:count()
local count = 0
for _, container in next, self._data do
count = count + container:count()
end
return count
end
---Return a serializable table.
---@return table
function TimingSave:serialize()
local data = self._data
return {
version = TIMING_SAVE_VERSION,
animation = data.animation:serialize(),
part = data.part:serialize(),
sound = data.sound:serialize(),
}
end
---Create new TimingSave object.
---@param values table?
---@return TimingSave
function TimingSave.new(values)
local self = setmetatable({}, TimingSave)
self._data = {
animation = TimingContainer.new(AnimationTiming),
part = TimingContainer.new(PartTiming),
sound = TimingContainer.new(SoundTiming),
}
if values then
self:load(values)
end
return self
end
-- Return TimingSave module.
return TimingSave
end)
-- MODULE: Menu/Objects/BuilderSection
defineModule("Menu/Objects/BuilderSection", function()
---@module Game.Timings.Action
local Action = require("Game/Timings/Action")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.Timings.Timing
local Timing = require("Game/Timings/Timing")
---@module Utility.Configuration
local Configuration = require("Utility/Configuration")
---@module Features.Combat.PositionHistory
local PositionHistory = require("Features/Combat/PositionHistory")
---@class BuilderSection
---@note: We assume that all elements will exist in callbacks. This is why they are not explicitly set in the constructor.
---@field tabbox table
---@field pair TimingContainerPair
---@field name string
---@field timingList table
---@field timingName table
---@field timingTag table
---@field hitboxLength table
---@field hitboxWidth table
---@field hitboxHeight table
---@field timingType table
---@field punishableWindow table
---@field afterWindow table
---@field delayUntilInHitbox table
---@field initialMinimumDistance table
---@field initialMaximumDistance table
---@field actionList table
---@field actionName table
---@field actionDelay table
---@field actionType table
local BuilderSection = {}
BuilderSection.__index = BuilderSection
-- Services.
local stats = game:GetService("Stats")
local players = game:GetService("Players")
local debrisService = game:GetService("Debris")
local PREVIEW_LIFETIME = 5.0
local PREVIEW_HIT_COLOR = Color3.new(0, 1, 0)
local PREVIEW_PREDICT_COLOR = Color3.new(1, 0, 1)
local PREVIEW_PAST_COLOR = Color3.new(0.839215, 0.976470, 0.537254)
local function mapTag(tag)
if tag == "Mantra" then
return "Skill"
end
if tag == "Critical" then
return "Counter"
end
if tag == "Undefined" then
return "Other"
end
return tag
end
local function resolvePreviewRoot()
local localPlayer = players.LocalPlayer
local character = localPlayer and localPlayer.Character
if not character then
return nil, "No character found."
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return nil, "No HumanoidRootPart found."
end
return root, nil
end
local function applyFacingOffset(cframe, size, useFacingOffset)
if not useFacingOffset then
return cframe
end
return cframe * CFrame.new(0, 0, -(size.Z / 2))
end
local function applyWorldOffset(cframe, offset)
if not offset or offset == Vector3.zero then
return cframe
end
local rotation = cframe - cframe.Position
local applied = (cframe.RightVector * offset.X)
+ (cframe.UpVector * offset.Y)
+ (cframe.LookVector * offset.Z)
return CFrame.new(cframe.Position + applied) * rotation
end
local function buildPreviewSize(width, height, length)
return Vector3.new(tonumber(width) or 0, tonumber(height) or 0, tonumber(length) or 0)
end
local function spawnPreviewPart(cframe, size, color, shape)
local part = Instance.new("Part")
local hitboxShape = shape == "Ball" and "Ball" or "Box"
local renderSize = size
if hitboxShape == "Ball" then
local radius = math.max(size.X, size.Y, size.Z) / 2
renderSize = Vector3.new(radius * 2, radius * 2, radius * 2)
part.Shape = Enum.PartType.Ball
else
part.Shape = Enum.PartType.Block
end
part.Parent = workspace
part.Anchored = true
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
part.Material = Enum.Material.ForceField
part.CastShadow = false
part.Size = renderSize
part.CFrame = cframe
part.Color = color
part.Name = "RW_PreviewHitbox"
part.Transparency = Configuration.expectToggleValue("EnableVisualizations") == false and 1 or 0.2
debrisService:AddItem(part, PREVIEW_LIFETIME)
end
---Create timing ID element. Override me.
---@param tab table
function BuilderSection:tide(tab) end
---Create extra elements. Override me.
---@param tab table
function BuilderSection:extra(tab) end
---Load the extra elements. Override me.
---@param timing Timing
function BuilderSection:exload(timing) end
---Load the extra action elements. Override me.
---@param action Action
function BuilderSection:exaload(action) end
function BuilderSection:previewHitbox(kind)
local root, reason = resolvePreviewRoot()
if not root then
return Logger.longNotify("Preview failed: %s", reason)
end
local size = nil
local shape = "Box"
local offset = Vector3.zero
if kind == "action" then
size = buildPreviewSize(self.hitboxWidth.Value, self.hitboxHeight.Value, self.hitboxLength.Value)
shape = self.actionHitboxShape and self.actionHitboxShape.Value or "Box"
offset = Vector3.new(
tonumber(self.actionHitboxOffsetX and self.actionHitboxOffsetX.Value) or 0,
tonumber(self.actionHitboxOffsetY and self.actionHitboxOffsetY.Value) or 0,
tonumber(self.actionHitboxOffsetZ and self.actionHitboxOffsetZ.Value) or 0
)
if self.delayUntilInHitbox and self.delayUntilInHitbox.Value then
Logger.longNotify("Delay Until In Hitbox is enabled; runtime uses timing hitbox.")
end
else
size = buildPreviewSize(self.timingHitboxWidth.Value, self.timingHitboxHeight.Value, self.timingHitboxLength.Value)
shape = self.timingHitboxShape and self.timingHitboxShape.Value or "Box"
offset = Vector3.new(
tonumber(self.timingHitboxOffsetX and self.timingHitboxOffsetX.Value) or 0,
tonumber(self.timingHitboxOffsetY and self.timingHitboxOffsetY.Value) or 0,
tonumber(self.timingHitboxOffsetZ and self.timingHitboxOffsetZ.Value) or 0
)
end
if size.Magnitude <= 0 then
return Logger.longNotify("Preview failed: hitbox size is zero.")
end
if Configuration.expectToggleValue("EnableVisualizations") == false then
Logger.longNotify("Enable Visualizations is off; preview will be invisible.")
end
local useFacingOffset = self.hitboxFacingOffset and self.hitboxFacingOffset.Value
local baseCFrame = applyFacingOffset(root.CFrame, size, useFacingOffset)
baseCFrame = applyWorldOffset(baseCFrame, offset)
spawnPreviewPart(baseCFrame, size, PREVIEW_HIT_COLOR, shape)
if self.predictFacingHitboxes and self.predictFacingHitboxes.Value then
local predictTime = tonumber(self.extrapolationTime and self.extrapolationTime.Value) or 0
local predicted = root.CFrame + (root.AssemblyLinearVelocity * predictTime)
predicted = applyFacingOffset(predicted, size, useFacingOffset)
predicted = applyWorldOffset(predicted, offset)
spawnPreviewPart(predicted, size, PREVIEW_PREDICT_COLOR, shape)
end
if self.pastHitboxDetection and self.pastHitboxDetection.Value then
local historySeconds = tonumber(self.historySeconds and self.historySeconds.Value) or 0
local history = PositionHistory.stepped(players.LocalPlayer, 5, historySeconds) or {}
for _, cframe in next, history do
local previewCFrame = applyFacingOffset(cframe, size, useFacingOffset)
previewCFrame = applyWorldOffset(previewCFrame, offset)
spawnPreviewPart(previewCFrame, size, PREVIEW_PAST_COLOR, shape)
end
end
end
---Action delay. Override me.
---@param base table
function BuilderSection:daction(base)
-- The user can accidently click this input through the dropdown and override the delay.
-- It has been moved and set to "Finished" to prevent this.
self.actionDelay = base:AddInput(nil, {
Text = "Action Delay",
Numeric = true,
Finished = true,
Callback = self:anc(function(action, value)
action._when = tonumber(value)
end),
})
end
---Reset elements. Extend me.
function BuilderSection:reset()
-- Reset timing elements.
self.timingName:SetRawValue("")
self.timingType:SetRawValue("Config")
self.timingTag:SetRawValue("Other")
self.initialMaximumDistance:SetRawValue(0)
self.punishableWindow:SetRawValue(0)
self.afterWindow:SetRawValue(0)
self.initialMinimumDistance:SetRawValue(0)
self.delayUntilInHitbox:SetRawValue(false)
self.timingHitboxHeight:SetRawValue(0)
self.timingHitboxLength:SetRawValue(0)
self.timingHitboxWidth:SetRawValue(0)
if self.timingHitboxLengthInput then
self.timingHitboxLengthInput:SetRawValue(0)
end
if self.timingHitboxWidthInput then
self.timingHitboxWidthInput:SetRawValue(0)
end
if self.timingHitboxHeightInput then
self.timingHitboxHeightInput:SetRawValue(0)
end
if self.timingHitboxShape then
self.timingHitboxShape:SetRawValue("Box")
end
if self.timingHitboxOffsetX then
self.timingHitboxOffsetX:SetRawValue(0)
end
if self.timingHitboxOffsetY then
self.timingHitboxOffsetY:SetRawValue(0)
end
if self.timingHitboxOffsetZ then
self.timingHitboxOffsetZ:SetRawValue(0)
end
self.skipRepeatNotification:SetRawValue(false)
self.noDashFallback:SetRawValue(false)
self.hitboxFacingOffset:SetRawValue(true)
-- Reset action list.
self:arefresh(nil)
-- Reset action elements.
self:raction()
end
---Check before creating new timing. Override me.
---@return boolean
function BuilderSection:check()
if not self.timingName.Value or #self.timingName.Value <= 0 then
return Logger.longNotify("Please enter a valid timing name.")
end
if self.pair:find(self.timingName.Value) then
return Logger.longNotify("The timing '%s' already exists in the list.", self.timingName.Value)
end
return true
end
---Check before creating new action. Override me.
---@param timing Timing
---@return boolean
function BuilderSection:acheck(timing)
if not self.actionName.Value or #self.actionName.Value <= 0 then
return Logger.longNotify("Please enter a valid action name.")
end
if timing.actions:find(self.actionName.Value) then
return Logger.longNotify("The action '%s' already exists in the list.", self.actionName.Value)
end
return true
end
---Set creation timing properties. Override me.
---@param timing Timing
function BuilderSection:cset(timing)
timing.name = self.timingName.Value
end
---Create new timing. Override me.
---@return Timing
function BuilderSection:create()
local timing = Timing.new()
self:cset(timing)
return timing
end
---Initialize action tab. Extend me.
function BuilderSection:action()
self:baction(self.tabbox:AddTab("Action"))
end
---Reset action elements.
function BuilderSection:raction()
self.actionName:SetRawValue("")
self.actionDelay:SetRawValue(0)
self.actionType:SetRawValue("Parry")
self.hitboxHeight:SetRawValue(0)
self.hitboxLength:SetRawValue(0)
self.hitboxWidth:SetRawValue(0)
if self.hitboxLengthInput then
self.hitboxLengthInput:SetRawValue(0)
end
if self.hitboxWidthInput then
self.hitboxWidthInput:SetRawValue(0)
end
if self.hitboxHeightInput then
self.hitboxHeightInput:SetRawValue(0)
end
if self.actionHitboxShape then
self.actionHitboxShape:SetRawValue("Box")
end
if self.actionHitboxOffsetX then
self.actionHitboxOffsetX:SetRawValue(0)
end
if self.actionHitboxOffsetY then
self.actionHitboxOffsetY:SetRawValue(0)
end
if self.actionHitboxOffsetZ then
self.actionHitboxOffsetZ:SetRawValue(0)
end
end
---Refresh timing list.
function BuilderSection:refresh()
local values = self.timingType.Value == "Internal" and self.pair.internal:names() or self.pair.config:names()
self.timingList:SetValues(values)
self.timingList:SetValue(nil)
self.timingList:Display()
end
---Refresh action list.
---@param timing Timing?
function BuilderSection:arefresh(timing)
self.actionList:SetValues(timing and timing.actions:names() or {})
self.actionList:SetValue(nil)
self.actionList:Display()
end
---Wrap a callback that needs a timing. This will check for internal timings.
---@param callback function(Timing, ...)
---@return function(...)
function BuilderSection:tnc(callback)
return function(...)
-- If no value, return.
if not self.timingList.Value then
return Logger.warn("No timing selected.")
end
-- Find timing.
local timing = self.pair:find(self.timingList.Value)
if not timing then
return Logger.longNotify("You must select a valid timing to perform this action.")
end
-- Check timing type.
if self.timingType.Value == "Internal" then
return Logger.longNotify("Internal timing. Changes not replicated. You must clone it to the config first.")
end
-- Fire callback.
callback(timing, ...)
end
end
---Wrap a callback that needs both an action and a timing.
---@note: This will check for internal timings.
---@param callback function(Timing, Action, ...)
---@return function
function BuilderSection:tanc(callback)
return function(...)
-- If no value, return.
if not self.timingList.Value then
return Logger.warn("No timing selected.")
end
-- Find timing.
local timing = self.pair:find(self.timingList.Value)
if not timing then
return Logger.longNotify("You must select a valid timing to perform this action.")
end
-- If no value, return.
if not self.actionList.Value then
return Logger.warn("No action selected.")
end
-- Find action.
local action = timing.actions:find(self.actionList.Value)
if not action then
return Logger.longNotify("You must select a valid action to perform this action.")
end
-- Check timing type.
if self.timingType.Value == "Internal" then
return Logger.longNotify("Internal timing. Changes not replicated. You must clone it to the config first.")
end
-- Fire callback.
callback(timing, action, ...)
end
end
---Wrap a callback that needs a action. This will check for internal timings.
---@param callback function(Action, ...)
---@return function
function BuilderSection:anc(callback)
return function(...)
-- If no value, return.
if not self.timingList.Value then
return Logger.warn("No timing selected.")
end
-- Find timing.
local timing = self.pair:find(self.timingList.Value)
if not timing then
return Logger.longNotify("You must select a valid timing to perform this action.")
end
-- If no value, return.
if not self.actionList.Value then
return Logger.warn("No action selected.")
end
-- Find action.
local action = timing.actions:find(self.actionList.Value)
if not action then
return Logger.longNotify("You must select a valid action to perform this action.")
end
-- Check timing type.
if self.timingType.Value == "Internal" then
return Logger.longNotify("Internal timing. Changes not replicated. You must clone it to the config first.")
end
-- Fire callback.
callback(action, ...)
end
end
---Initialize action base.
---@param base table
function BuilderSection:baction(base)
self.actionList = base:AddDropdown(nil, {
Text = "Action List",
Values = {},
AllowNull = true,
Callback = self:tnc(function(timing, value)
-- Reset action elements.
self:raction()
-- Check if value exists.
if not value then
return Logger.warn("No action value.")
end
-- Find action.
local action = timing.actions:find(value)
if not action then
return Logger.longNotify("The selected action '%s' does not exist in the list.", value)
end
-- Set action elements.
self.actionName:SetRawValue(action.name)
self.actionDelay:SetRawValue(action._when or 0)
self.actionType:SetRawValue(action._type)
self.hitboxWidth:SetRawValue(action.hitbox.X)
self.hitboxHeight:SetRawValue(action.hitbox.Y)
self.hitboxLength:SetRawValue(action.hitbox.Z)
if self.hitboxLengthInput then
self.hitboxLengthInput:SetRawValue(action.hitbox.Z)
end
if self.hitboxWidthInput then
self.hitboxWidthInput:SetRawValue(action.hitbox.X)
end
if self.hitboxHeightInput then
self.hitboxHeightInput:SetRawValue(action.hitbox.Y)
end
if self.actionHitboxShape then
self.actionHitboxShape:SetRawValue(action.shape or "Box")
end
if self.actionHitboxOffsetX then
local offset = action.offset or Vector3.zero
self.actionHitboxOffsetX:SetRawValue(offset.X)
self.actionHitboxOffsetY:SetRawValue(offset.Y)
self.actionHitboxOffsetZ:SetRawValue(offset.Z)
end
-- Load extra action elements.
self:exaload(action)
end),
})
self.actionType = base:AddDropdown(nil, {
Text = "Action Type",
Values = { "Parry", "Dash", "Jump", "Start Block", "End Block" },
Default = 1,
Callback = self:anc(function(action, value)
action._type = value
end),
})
self:daction(base)
self._syncingActionHitbox = false
self.actionHitboxShape = base:AddDropdown(nil, {
Text = "Hitbox Shape",
Values = { "Box", "Ball" },
Default = 1,
Callback = self:anc(function(action, value)
action.shape = value
end),
})
local function updateActionLength(value)
local num = tonumber(value) or 0
if self._syncingActionHitbox then
return
end
self._syncingActionHitbox = true
self.hitboxLength:SetRawValue(num)
if self.hitboxLengthInput then
self.hitboxLengthInput:SetRawValue(num)
end
self._syncingActionHitbox = false
self:anc(function(action, val)
action.hitbox = Vector3.new(action.hitbox.X, action.hitbox.Y, val)
end)(num)
end
local function updateActionWidth(value)
local num = tonumber(value) or 0
if self._syncingActionHitbox then
return
end
self._syncingActionHitbox = true
self.hitboxWidth:SetRawValue(num)
if self.hitboxWidthInput then
self.hitboxWidthInput:SetRawValue(num)
end
self._syncingActionHitbox = false
self:anc(function(action, val)
action.hitbox = Vector3.new(val, action.hitbox.Y, action.hitbox.Z)
end)(num)
end
local function updateActionHeight(value)
local num = tonumber(value) or 0
if self._syncingActionHitbox then
return
end
self._syncingActionHitbox = true
self.hitboxHeight:SetRawValue(num)
if self.hitboxHeightInput then
self.hitboxHeightInput:SetRawValue(num)
end
self._syncingActionHitbox = false
self:anc(function(action, val)
action.hitbox = Vector3.new(action.hitbox.X, val, action.hitbox.Z)
end)(num)
end
self.hitboxLength = base:AddSlider(nil, {
Text = "Hitbox Length",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateActionLength,
})
self.hitboxLengthInput = base:AddInput(nil, {
Text = "Hitbox Length (Input)",
Numeric = true,
Finished = false,
Callback = updateActionLength,
})
self.hitboxWidth = base:AddSlider(nil, {
Text = "Hitbox Width",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateActionWidth,
})
self.hitboxWidthInput = base:AddInput(nil, {
Text = "Hitbox Width (Input)",
Numeric = true,
Finished = false,
Callback = updateActionWidth,
})
self.hitboxHeight = base:AddSlider(nil, {
Text = "Hitbox Height",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateActionHeight,
})
self.hitboxHeightInput = base:AddInput(nil, {
Text = "Hitbox Height (Input)",
Numeric = true,
Finished = false,
Callback = updateActionHeight,
})
self.actionHitboxOffsetX = base:AddInput(nil, {
Text = "Hitbox Offset X",
Numeric = true,
Finished = true,
Callback = self:anc(function(action, value)
local x = tonumber(value) or 0
local y = tonumber(self.actionHitboxOffsetY and self.actionHitboxOffsetY.Value) or 0
local z = tonumber(self.actionHitboxOffsetZ and self.actionHitboxOffsetZ.Value) or 0
action.offset = Vector3.new(x, y, z)
end),
})
self.actionHitboxOffsetY = base:AddInput(nil, {
Text = "Hitbox Offset Y",
Numeric = true,
Finished = true,
Callback = self:anc(function(action, value)
local x = tonumber(self.actionHitboxOffsetX and self.actionHitboxOffsetX.Value) or 0
local y = tonumber(value) or 0
local z = tonumber(self.actionHitboxOffsetZ and self.actionHitboxOffsetZ.Value) or 0
action.offset = Vector3.new(x, y, z)
end),
})
self.actionHitboxOffsetZ = base:AddInput(nil, {
Text = "Hitbox Offset Z",
Numeric = true,
Finished = true,
Callback = self:anc(function(action, value)
local x = tonumber(self.actionHitboxOffsetX and self.actionHitboxOffsetX.Value) or 0
local y = tonumber(self.actionHitboxOffsetY and self.actionHitboxOffsetY.Value) or 0
local z = tonumber(value) or 0
action.offset = Vector3.new(x, y, z)
end),
})
base:AddButton("Preview Action Hitbox", function()
self:previewHitbox("action")
end)
base:AddDivider()
self.actionName = base:AddInput(nil, {
Text = "Action Name",
})
base:AddButton(
"Create New Action",
self:tnc(function(timing)
-- Fetch actions.
local actions = timing.actions
-- Check.
if not self:acheck(timing) then
return
end
-- Create new action.
local action = Action.new()
action.name = self.actionName.Value
action._type = "Parry"
-- Record ping for telemetry.
local network = stats:FindFirstChild("Network")
local serverStatsItem = network and network:FindFirstChild("ServerStatsItem")
local dataPingItem = serverStatsItem and serverStatsItem:FindFirstChild("Data Ping")
if dataPingItem then
local ok, value = pcall(function()
return dataPingItem:GetValue()
end)
if ok and type(value) == "number" then
action.ping = value
end
end
-- Push action.
actions:push(action)
-- Refresh action list.
self:arefresh(timing)
-- Set action list value.
self.actionList:SetValue(action.name)
self.actionList:Display()
end)
)
base:AddButton(
"Duplicate Selected Action",
self:tanc(function(timing, action)
-- Fetch actions.
local actions = timing.actions
-- Check.
if not self:acheck(timing) then
return
end
-- Create new action.
local newAction = action:clone()
newAction.name = self.actionName.Value
-- Record ping for telemetry.
local network = stats:FindFirstChild("Network")
local serverStatsItem = network and network:FindFirstChild("ServerStatsItem")
local dataPingItem = serverStatsItem and serverStatsItem:FindFirstChild("Data Ping")
if dataPingItem then
local ok, value = pcall(function()
return dataPingItem:GetValue()
end)
if ok and type(value) == "number" then
newAction.ping = value
end
end
-- Push action.
actions:push(newAction)
-- Refresh action list.
self:arefresh(timing)
-- Set action list value.
self.actionList:SetValue(newAction.name)
self.actionList:Display()
end)
)
base:AddButton(
"Remove Selected Action",
self:tnc(function(timing)
-- Get selected value.
local selected = self.actionList.Value
if not selected then
return Logger.longNotify("Please select an action to remove.")
end
-- Fetch actions.
local actions = timing.actions
-- Find action.
local action = actions:find(selected)
if not action then
return Logger.longNotify("The selected action '%s' does not exist in the list.", selected)
end
-- Remove action.
actions:remove(action)
-- Refresh action list.
self:arefresh(timing)
end)
)
end
---Initialize timing tab.
function BuilderSection:timing()
local tab = self.tabbox:AddTab("Timings")
self.timingType = tab:AddDropdown(nil, {
Text = "Timing Type",
Values = { "Config", "Internal" },
Default = 1,
Callback = function()
-- Refresh timing list.
self:refresh()
-- Reset elements.
self:reset()
end,
})
self.timingList = tab:AddDropdown(nil, {
Text = "Timing List",
Values = self.timingType.Value == "Internal" and self.pair.internal:names() or self.pair.config:names(),
AllowNull = true,
Callback = function(value)
-- Reset elements.
self:reset()
-- Check if value exists.
if not value then
return Logger.warn("No timing value.")
end
-- Fetch timing.
local found = self.pair:find(value)
if not found then
return Logger.longNotify("The selected timing '%s' does not exist in the list.", value)
end
-- Set timing elements.
self.timingName:SetRawValue(found.name)
self.timingTag:SetRawValue(mapTag(found.tag))
self.initialMaximumDistance:SetRawValue(found.imxd)
self.initialMinimumDistance:SetRawValue(found.imdd)
self.delayUntilInHitbox:SetRawValue(found.duih)
self.timingHitboxLength:SetRawValue(found.hitbox.Z)
self.timingHitboxWidth:SetRawValue(found.hitbox.X)
self.timingHitboxHeight:SetRawValue(found.hitbox.Y)
if self.timingHitboxLengthInput then
self.timingHitboxLengthInput:SetRawValue(found.hitbox.Z)
end
if self.timingHitboxWidthInput then
self.timingHitboxWidthInput:SetRawValue(found.hitbox.X)
end
if self.timingHitboxHeightInput then
self.timingHitboxHeightInput:SetRawValue(found.hitbox.Y)
end
if self.timingHitboxShape then
self.timingHitboxShape:SetRawValue(found.shape or "Box")
end
if self.timingHitboxOffsetX then
local offset = found.offset or Vector3.zero
self.timingHitboxOffsetX:SetRawValue(offset.X)
self.timingHitboxOffsetY:SetRawValue(offset.Y)
self.timingHitboxOffsetZ:SetRawValue(offset.Z)
end
self.punishableWindow:SetRawValue(found.punishable)
self.afterWindow:SetRawValue(found.after)
self.skipRepeatNotification:SetRawValue(found.srpn)
self.hitboxFacingOffset:SetRawValue(found.fhb)
self.noDashFallback:SetRawValue(found.ndfb)
-- Load extra elements.
self:exload(found)
-- Refresh action list.
self:arefresh(found)
end,
})
tab:AddDivider()
self.timingName = tab:AddInput(nil, {
Text = "Timing Name",
Finished = true,
})
self:tide(tab)
local configDepBox = tab:AddDependencyBox()
configDepBox:AddButton("Create New Timing", function()
-- Fetch config.
local config = self.pair.config
-- Check if we can successfully create a timing from the given data.
if not self:check() then
return
end
-- Create new timing.
local timing = self:create()
-- Push new timing.
config:push(timing)
-- Refresh timing list.
self:refresh()
-- Set timing list value.
self.timingList:SetValue(timing.name)
self.timingList:Display()
end)
configDepBox:AddButton(
"Duplicate Selected Timing",
self:tnc(function(found)
-- Fetch config.
local config = self.pair.config
-- Check if we can successfully create a timing from the given data.
if not self:check() then
return
end
-- Clone new timing.
local timing = found:clone()
-- Set creation properties.
self:cset(timing)
-- Push new timing.
config:push(timing)
-- Refresh timing list.
self:refresh()
-- Set timing list value.
self.timingList:SetValue(timing.name)
self.timingList:Display()
end)
)
local internalDepBox = tab:AddDependencyBox()
internalDepBox:AddButton("Clone To Config", function()
-- Fetch name.
local name = self.timingList.Value
if not name then
return Logger.longNotify("Please select a timing to clone.")
end
-- Fetch data.
local internal = self.pair.internal
local config = self.pair.config
-- Fetch the currently selected timing.
local found = internal:find(name)
if not found then
return Logger.longNotify("The selected timing '%s' does not exist in the list.", name)
end
-- Check for existing ID.
if config.timings[found:id()] then
return Logger.longNotify("The timing ID '%s' already exists in the config.", found:id())
end
-- Check for existing timing.
if config:find(found.name) then
return Logger.longNotify("The timing name '%s' already exists in the config.", found.name)
end
-- Clone timing.
---@note: No need to refresh after this. It's in the other timing list!
config:push(internal:clone(found))
end)
tab:AddButton("Remove Selected Timing", function()
-- Fetch name.
local name = self.timingList.Value
if not name then
return Logger.longNotify("Please select a timing to remove.")
end
-- Fetch data.
local internal = self.pair.internal
local config = self.pair.config
local found = config:find(name)
-- Check if internal.
---@todo: Implement functionality to remove internal timings.
if internal:find(name) then
return Logger.longNotify("You cannot remove internal timings, only override them.")
end
-- Check if found.
if not found then
return Logger.longNotify("The selected timing '%s' does not exist in the list.", name)
end
-- Remove timing.
config:remove(found)
-- Refresh timing list.
self:refresh()
end)
configDepBox:SetupDependencies({
{ self.timingType, "Config" },
})
internalDepBox:SetupDependencies({
{ self.timingType, "Internal" },
})
end
---Initialize builder tab.
function BuilderSection:builder()
local tab = self.tabbox:AddTab(string.format("%s", self.name))
self.timingTag = tab:AddDropdown(nil, {
Text = "Timing Tag",
Values = { "Other", "Counter", "Skill", "M1" },
Default = 1,
Callback = self:tnc(function(timing, value)
timing.tag = value
end),
})
self.initialMinimumDistance = tab:AddSlider(nil, {
Text = "Initial Minimum Distance",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = self:tnc(function(timing, value)
timing.imdd = value
end),
})
self.initialMaximumDistance = tab:AddSlider(nil, {
Text = "Initial Maximum Distance",
Min = 0,
Max = 2500,
Suffix = "s",
Default = 1000,
Rounding = 0,
Callback = self:tnc(function(timing, value)
timing.imxd = value
end),
})
self.punishableWindow = tab:AddSlider(nil, {
Text = "Punishable Window",
Min = 0,
Max = 2,
Default = 0.6,
Suffix = "s",
Rounding = 1,
Callback = self:tnc(function(timing, value)
timing.punishable = value
end),
})
self.afterWindow = tab:AddSlider(nil, {
Text = "After Window",
Min = 0,
Max = 1,
Default = 0.1,
Suffix = "s",
Rounding = 2,
Callback = self:tnc(function(timing, value)
timing.after = value
end),
})
self.delayUntilInHitbox = tab:AddToggle(nil, {
Text = "Delay Until In Hitbox",
Default = false,
Callback = self:tnc(function(timing, value)
timing.duih = value
end),
})
local duihDepBox = tab:AddDependencyBox()
self._syncingTimingHitbox = false
self.timingHitboxShape = duihDepBox:AddDropdown(nil, {
Text = "Hitbox Shape",
Values = { "Box", "Ball" },
Default = 1,
Callback = self:tnc(function(timing, value)
timing.shape = value
end),
})
local function updateTimingLength(value)
local num = tonumber(value) or 0
if self._syncingTimingHitbox then
return
end
self._syncingTimingHitbox = true
self.timingHitboxLength:SetRawValue(num)
if self.timingHitboxLengthInput then
self.timingHitboxLengthInput:SetRawValue(num)
end
self._syncingTimingHitbox = false
self:tnc(function(timing, val)
timing.hitbox = Vector3.new(timing.hitbox.X, timing.hitbox.Y, val)
end)(num)
end
local function updateTimingWidth(value)
local num = tonumber(value) or 0
if self._syncingTimingHitbox then
return
end
self._syncingTimingHitbox = true
self.timingHitboxWidth:SetRawValue(num)
if self.timingHitboxWidthInput then
self.timingHitboxWidthInput:SetRawValue(num)
end
self._syncingTimingHitbox = false
self:tnc(function(timing, val)
timing.hitbox = Vector3.new(val, timing.hitbox.Y, timing.hitbox.Z)
end)(num)
end
local function updateTimingHeight(value)
local num = tonumber(value) or 0
if self._syncingTimingHitbox then
return
end
self._syncingTimingHitbox = true
self.timingHitboxHeight:SetRawValue(num)
if self.timingHitboxHeightInput then
self.timingHitboxHeightInput:SetRawValue(num)
end
self._syncingTimingHitbox = false
self:tnc(function(timing, val)
timing.hitbox = Vector3.new(timing.hitbox.X, val, timing.hitbox.Z)
end)(num)
end
self.timingHitboxLength = duihDepBox:AddSlider(nil, {
Text = "Hitbox Length",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateTimingLength,
})
self.timingHitboxLengthInput = duihDepBox:AddInput(nil, {
Text = "Hitbox Length (Input)",
Numeric = true,
Finished = false,
Callback = updateTimingLength,
})
self.timingHitboxWidth = duihDepBox:AddSlider(nil, {
Text = "Hitbox Width",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateTimingWidth,
})
self.timingHitboxWidthInput = duihDepBox:AddInput(nil, {
Text = "Hitbox Width (Input)",
Numeric = true,
Finished = false,
Callback = updateTimingWidth,
})
self.timingHitboxHeight = duihDepBox:AddSlider(nil, {
Text = "Hitbox Height",
Min = 0,
Max = 300,
Suffix = "s",
Default = 0,
Rounding = 0,
Callback = updateTimingHeight,
})
self.timingHitboxHeightInput = duihDepBox:AddInput(nil, {
Text = "Hitbox Height (Input)",
Numeric = true,
Finished = false,
Callback = updateTimingHeight,
})
self.timingHitboxOffsetX = duihDepBox:AddInput(nil, {
Text = "Hitbox Offset X",
Numeric = true,
Finished = true,
Callback = self:tnc(function(timing, value)
local x = tonumber(value) or 0
local y = tonumber(self.timingHitboxOffsetY and self.timingHitboxOffsetY.Value) or 0
local z = tonumber(self.timingHitboxOffsetZ and self.timingHitboxOffsetZ.Value) or 0
timing.offset = Vector3.new(x, y, z)
end),
})
self.timingHitboxOffsetY = duihDepBox:AddInput(nil, {
Text = "Hitbox Offset Y",
Numeric = true,
Finished = true,
Callback = self:tnc(function(timing, value)
local x = tonumber(self.timingHitboxOffsetX and self.timingHitboxOffsetX.Value) or 0
local y = tonumber(value) or 0
local z = tonumber(self.timingHitboxOffsetZ and self.timingHitboxOffsetZ.Value) or 0
timing.offset = Vector3.new(x, y, z)
end),
})
self.timingHitboxOffsetZ = duihDepBox:AddInput(nil, {
Text = "Hitbox Offset Z",
Numeric = true,
Finished = true,
Callback = self:tnc(function(timing, value)
local x = tonumber(self.timingHitboxOffsetX and self.timingHitboxOffsetX.Value) or 0
local y = tonumber(self.timingHitboxOffsetY and self.timingHitboxOffsetY.Value) or 0
local z = tonumber(value) or 0
timing.offset = Vector3.new(x, y, z)
end),
})
duihDepBox:AddButton("Preview Timing Hitbox", function()
self:previewHitbox("timing")
end)
duihDepBox:SetupDependencies({
{ self.delayUntilInHitbox, true },
})
self.skipRepeatNotification = tab:AddToggle(nil, {
Text = "Skip Repeat Notification",
Default = false,
Callback = self:tnc(function(timing, value)
timing.srpn = value
end),
})
self.hitboxFacingOffset = tab:AddToggle(nil, {
Text = "Hitbox Facing Offset",
Tooltip = "Should the hitbox be offset towards the facing direction?",
Default = true,
Callback = self:tnc(function(timing, value)
timing.fhb = value
end),
})
self.noDashFallback = tab:AddToggle(nil, {
Text = "No Dash Fallback",
Tooltip = "If enabled, the timing will not fallback to a dash if the parry action is not available.",
Default = false,
Callback = self:tnc(function(timing, value)
timing.ndfb = value
end),
})
self:extra(tab)
end
---Initialize BuilderSection object.
function BuilderSection:init()
self:timing()
self:builder()
self:action()
end
---Create new BuilderSection object.
---@param name string
---@param tabbox table
---@param pair TimingContainerPair
---@param timing Timing
---@return BuilderSection
function BuilderSection.new(name, tabbox, pair, timing)
local self = setmetatable({}, BuilderSection)
self.name = name
self.tabbox = tabbox
self.pair = pair
return self
end
-- Return BuilderSection module.
return BuilderSection
end)
-- MODULE: Menu/Objects/AnimationBuilderSection
defineModule("Menu/Objects/AnimationBuilderSection", function()
---@module Menu.Objects.BuilderSection
local BuilderSection = require("Menu/Objects/BuilderSection")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.Timings.AnimationTiming
local AnimationTiming = require("Game/Timings/AnimationTiming")
---@class AnimationBuilderSection: BuilderSection
---@field animationId table
---@field repeatStartDelay table
---@field repeatUntilParryEnd table
---@field repeatParryDelay table
---@field timing AnimationTiming
local AnimationBuilderSection = setmetatable({}, { __index = BuilderSection })
AnimationBuilderSection.__index = AnimationBuilderSection
---Create timing ID element. Override me.
---@param tab table
function AnimationBuilderSection:tide(tab)
self.animationId = tab:AddInput(nil, {
Text = "Animation ID",
})
end
---Load the extra elements. Override me.
---@param timing AnimationTiming
function AnimationBuilderSection:exload(timing)
self.animationId:SetRawValue(timing._id)
self.repeatUntilParryEnd:SetRawValue(timing.rpue)
self.repeatStartDelay:SetRawValue(timing._rsd)
self.repeatParryDelay:SetRawValue(timing._rpd)
self.ignoreAnimationEnd:SetRawValue(timing.iae)
self.ignoreEarlyAnimationEnd:SetRawValue(timing.ieae)
self.maxAnimationTimeout:SetRawValue(timing.mat)
self.pastHitboxDetection:SetRawValue(timing.phd)
self.predictFacingHitboxes:SetRawValue(timing.pfh)
self.historySeconds:SetRawValue(timing.phds)
self.extrapolationTime:SetRawValue(timing.pfht)
end
---Reset the elements. Extend me.
function AnimationBuilderSection:reset()
BuilderSection.reset(self)
self.animationId:SetRawValue("")
self.repeatParryDelay:SetRawValue(0)
self.repeatStartDelay:SetRawValue(0)
self.repeatUntilParryEnd:SetRawValue(false)
self.hitboxFacingOffset:SetRawValue(true)
self.ignoreAnimationEnd:SetRawValue(false)
self.ignoreEarlyAnimationEnd:SetRawValue(false)
self.maxAnimationTimeout:SetRawValue(2000)
self.pastHitboxDetection:SetRawValue(false)
self.historySeconds:SetRawValue(0.5)
self.predictFacingHitboxes:SetRawValue(false)
self.extrapolationTime:SetRawValue(0.15)
end
---Check before creating new timing. Override me.
---@return boolean
function AnimationBuilderSection:check()
if not BuilderSection.check(self) then
return false
end
if not self.animationId.Value or #self.animationId.Value <= 0 then
return Logger.longNotify("Please enter a valid animation ID.")
end
local timing = self.pair:index(self.animationId.Value)
if timing then
return Logger.longNotify("The timing ID '%s' (%s) is already in the list.", self.animationId.Value, timing.name)
end
return true
end
---Set creation timing properties. Override me.
---@param timing AnimationTiming
function AnimationBuilderSection:cset(timing)
timing.name = self.timingName.Value
timing._id = self.animationId.Value
end
---Create new timing. Override me.
---@return Timing
function AnimationBuilderSection:create()
local timing = AnimationTiming.new()
self:cset(timing)
return timing
end
---Initialize extra tab.
---@param tab table
function AnimationBuilderSection:extra(tab)
self.ignoreAnimationEnd = tab:AddToggle(nil, {
Text = "Ignore Animation End",
Tooltip = "Should the timing ignore the end of the animation?",
Default = false,
Callback = self:tnc(function(timing, value)
timing.iae = value
end),
})
local depBoxEnd = tab:AddDependencyBox()
self.maxAnimationTimeout = depBoxEnd:AddInput(nil, {
Text = "Max Animation Timeout",
Tooltip = "The maximum time (in milliseconds) that the animation is allowed to run with no end check.",
Default = 2000,
Numeric = true,
Callback = self:tnc(function(timing, value)
timing.mat = tonumber(value)
end),
})
depBoxEnd:SetupDependencies({
{ self.ignoreAnimationEnd, true },
})
self.ignoreEarlyAnimationEnd = tab:AddToggle(nil, {
Text = "Ignore Early Animation End",
Tooltip = "Should the timing ignore the early end of the animation?",
Default = false,
Callback = self:tnc(function(timing, value)
timing.ieae = value
end),
})
self.pastHitboxDetection = tab:AddToggle(nil, {
Text = "Past Hitbox Detection",
Default = false,
Tooltip = "Should the hitbox detection track the past hitboxes too?",
Callback = self:tnc(function(timing, value)
timing.phd = value
end),
})
local pfdOffDepBox = tab:AddDependencyBox()
self.historySeconds = pfdOffDepBox:AddSlider(nil, {
Text = "History Seconds",
Tooltip = "How far back in seconds should we fetch history?",
Default = 0.5,
Min = 0,
Max = 3.0,
Rounding = 2,
Numeric = true,
Callback = self:tnc(function(timing, value)
timing.phds = tonumber(value) or 0
end),
})
pfdOffDepBox:SetupDependencies({
{ self.pastHitboxDetection, true },
})
self.predictFacingHitboxes = tab:AddToggle(nil, {
Text = "Predict Facing Hitboxes",
Default = false,
Tooltip = "Should we make a prediction on the facing direction and make a hitbox on that?",
Callback = self:tnc(function(timing, value)
timing.pfh = value
end),
})
self.disablePrediction = tab:AddToggle(nil, {
Text = "Disable Prediction",
Default = false,
Tooltip = "Should we disable prediction?",
Callback = self:tnc(function(timing, value)
timing.dp = value
end),
})
self.extrapolationTime = tab:AddSlider(nil, {
Text = "Extrapolation Time",
Tooltip = "The time (in seconds) to extrapolate by.",
Default = 0.15,
Min = 0,
Max = 2.0,
Rounding = 3,
Numeric = true,
Callback = self:tnc(function(timing, value)
timing.pfht = tonumber(value)
end),
})
end
---Initialize action tab.
function AnimationBuilderSection:action()
local tab = self.tabbox:AddTab("Action")
self.repeatUntilParryEnd = tab:AddToggle(nil, {
Text = "Repeat Parry Until End",
Default = false,
Callback = self:tnc(function(timing, value)
timing.rpue = value
end),
})
local depBoxOn = tab:AddDependencyBox()
self.repeatStartDelay = depBoxOn:AddInput(nil, {
Text = "Repeat Start Delay",
Numeric = true,
Finished = true,
Callback = self:tnc(function(timing, value)
timing._rsd = tonumber(value) or 0
end),
})
self.repeatParryDelay = depBoxOn:AddInput(nil, {
Text = "Repeat Parry Delay",
Numeric = true,
Finished = true,
Callback = self:tnc(function(timing, value)
timing._rpd = tonumber(value) or 0
end),
})
local depBoxOff = tab:AddDependencyBox()
self:baction(depBoxOff)
depBoxOn:SetupDependencies({
{ self.repeatUntilParryEnd, true },
})
depBoxOff:SetupDependencies({
{ self.repeatUntilParryEnd, false },
})
end
---Create new AnimationBuilderSection object.
---@param name string
---@param tabbox table
---@param pair TimingContainerPair
---@param timing AnimationTiming
---@return AnimationBuilderSection
function AnimationBuilderSection.new(name, tabbox, pair, timing)
return setmetatable(BuilderSection.new(name, tabbox, pair, timing), AnimationBuilderSection)
end
-- Return AnimationBuilderSection module.
return AnimationBuilderSection
end)
-- MODULE: Menu/Objects/PartBuilderSection
defineModule("Menu/Objects/PartBuilderSection", function()
---@module Menu.Objects.BuilderSection
local BuilderSection = require("Menu/Objects/BuilderSection")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.Timings.PartTiming
local PartTiming = require("Game/Timings/PartTiming")
---@class PartBuilderSection: BuilderSection
---@field partName table
---@field timingDelay table
---@field initialMinimumDistance table
---@field initialMaximumDistance table
---@field timing PartTiming
local PartBuilderSection = setmetatable({}, { __index = BuilderSection })
PartBuilderSection.__index = PartBuilderSection
---Check before writing.
---@return boolean
function PartBuilderSection:check()
if not BuilderSection.check(self) then
return false
end
if not self.partName.Value or #self.partName.Value <= 0 then
return Logger.longNotify("Please enter a valid part name.")
end
if self.pair:index(self.partName.Value) then
return Logger.longNotify("The timing ID '%s' is already in the list.", self.partName.Value)
end
return true
end
---Load the extra elements. Override me.
---@param timing Timing
function PartBuilderSection:exload(timing)
self.useHitboxCFrame:SetRawValue(timing.uhc)
self.partName:SetRawValue(timing.pname)
end
---Reset the elements. Extend me.
function PartBuilderSection:reset()
BuilderSection.reset(self)
self.partName:SetRawValue("")
end
---Set creation timing properties. Override me.
---@param timing PartTiming
function PartBuilderSection:cset(timing)
timing.name = self.timingName.Value
timing.pname = self.partName.Value
end
---Create new timing. Override me.
---@return PartTiming
function PartBuilderSection:create()
local timing = PartTiming.new()
self:cset(timing)
return timing
end
---Create timing ID element. Override me.
---@param tab table
function PartBuilderSection:tide(tab)
self.partName = tab:AddInput(nil, {
Text = "Part Name",
})
end
---Initialize extra tab.
---@param tab table
function PartBuilderSection:extra(tab)
self.useHitboxCFrame = tab:AddToggle(nil, {
Text = "Use Hitbox CFrame",
Tooltip = "Should the hitbox face where it was originally supposed to?",
Default = true,
Callback = self:tnc(function(timing, value)
timing.uhc = value
end),
})
end
---Initialize PartBuilderSection object.
function PartBuilderSection:init()
self:timing()
self:builder()
self:action()
end
---Create new PartBuilderSection object.
---@param name string
---@param tabbox table
---@param pair TimingContainerPair
---@param timing PartTiming
---@return PartBuilderSection
function PartBuilderSection.new(name, tabbox, pair, timing)
return setmetatable(BuilderSection.new(name, tabbox, pair, timing), PartBuilderSection)
end
-- Return PartBuilderSection module.
return PartBuilderSection
end)
-- MODULE: Menu/Objects/SoundBuilderSection
defineModule("Menu/Objects/SoundBuilderSection", function()
---@module Menu.Objects.BuilderSection
local BuilderSection = require("Menu/Objects/BuilderSection")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---@module Game.Timings.SoundTiming
local SoundTiming = require("Game/Timings/SoundTiming")
---@class SoundBuilderSection: BuilderSection
---@field soundId table
---@field repeatStartDelay table
---@field repeatUntilParryEnd table
---@field repeatParryDelay table
---@field timing SoundTiming
local SoundBuilderSection = setmetatable({}, { __index = BuilderSection })
SoundBuilderSection.__index = SoundBuilderSection
---Create timing ID element. Override me.
---@param tab table
function SoundBuilderSection:tide(tab)
self.soundId = tab:AddInput(nil, {
Text = "Sound ID",
})
end
---Load the extra elements. Override me.
---@param timing Timing
function SoundBuilderSection:exload(timing)
self.soundId:SetRawValue(timing._id)
self.repeatStartDelay:SetRawValue(timing._rsd)
self.repeatUntilParryEnd:SetRawValue(timing.rpue)
self.repeatParryDelay:SetRawValue(timing._rpd)
end
---Reset the elements. Extend me.
function SoundBuilderSection:reset()
BuilderSection.reset(self)
self.soundId:SetRawValue("")
self.repeatParryDelay:SetRawValue(0)
self.repeatStartDelay:SetRawValue(0)
self.repeatUntilParryEnd:SetRawValue(false)
end
---Check before creating new timing. Override me.
---@return boolean
function SoundBuilderSection:check()
if not BuilderSection.check(self) then
return false
end
if not self.soundId.Value or #self.soundId.Value <= 0 then
return Logger.longNotify("Please enter a valid sound ID.")
end
if self.pair:index(self.soundId.Value) then
return Logger.longNotify("The timing ID '%s' is already in the list.", self.soundId.Value)
end
return true
end
---Set creation timing properties. Override me.
---@param timing SoundTiming
function SoundBuilderSection:cset(timing)
timing.name = self.timingName.Value
timing._id = self.soundId.Value
end
---Create new timing. Override me.
---@return Timing
function SoundBuilderSection:create()
local timing = SoundTiming.new()
self:cset(timing)
return timing
end
---Initialize action tab.
function SoundBuilderSection:action()
local tab = self.tabbox:AddTab("Action")
self.repeatUntilParryEnd = tab:AddToggle(nil, {
Text = "Repeat Parry Until End",
Default = false,
Callback = self:tnc(function(timing, value)
timing.rpue = value
end),
})
local depBoxOn = tab:AddDependencyBox()
self.repeatStartDelay = depBoxOn:AddInput(nil, {
Text = "Repeat Start Delay",
Default = false,
Callback = self:tnc(function(timing, value)
timing._rsd = tonumber(value) or 0
end),
})
self.repeatParryDelay = depBoxOn:AddInput(nil, {
Text = "Repeat Parry Delay",
Numeric = true,
Callback = self:tnc(function(timing, value)
timing._rpd = tonumber(value) or 0
end),
})
local depBoxOff = tab:AddDependencyBox()
self:baction(depBoxOff)
depBoxOn:SetupDependencies({
{ self.repeatUntilParryEnd, true },
})
depBoxOff:SetupDependencies({
{ self.repeatUntilParryEnd, false },
})
end
---Create new SoundBuilderSection object.
---@param name string
---@param tabbox table
---@param pair TimingContainerPair
---@param timing SoundTiming
---@return SoundBuilderSection
function SoundBuilderSection.new(name, tabbox, pair, timing)
return setmetatable(BuilderSection.new(name, tabbox, pair, timing), SoundBuilderSection)
end
-- Return SoundBuilderSection module.
return SoundBuilderSection
end)
-- MODULE: GUI/Library
defineModule("GUI/Library", function()
local Profiler = require("Utility/Profiler")
local CoreGuiManager = require("Utility/CoreGuiManager")
return LPH_NO_VIRTUALIZE(function()
local InputService = game:GetService("UserInputService")
local TextService = game:GetService("TextService")
local Teams = game:GetService("Teams")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
repeat
task.wait()
until Players.LocalPlayer
local RenderStepped = RunService.RenderStepped
local LocalPlayer = Players.LocalPlayer
local Mouse = LocalPlayer:GetMouse()
local ProtectGui = protectgui or (syn and syn.protect_gui) or function() end
local ScreenGui = CoreGuiManager.imark(Instance.new("ScreenGui"))
ProtectGui(ScreenGui)
ScreenGui.ZIndexBehavior = Enum.ZIndexBehavior.Global
local Toggles = {}
local Options = {}
local ColorPickers = {}
local Entries = {}
local ContextMenus = {}
local Tooltips = {}
local ModeSelectFrames = {}
local UpdateTimestamp = os.clock()
local Toggled = false
local NeedsRefresh = false
pcall(function()
getgenv().Toggles = Toggles
getgenv().Options = Options
end)
local Library = {
Registry = {},
RegistryMap = {},
HudRegistry = {},
FontColor = Color3.fromRGB(255, 255, 255),
MainColor = Color3.fromRGB(28, 28, 28),
BackgroundColor = Color3.fromRGB(20, 20, 20),
AccentColor = Color3.fromRGB(0, 85, 255),
OutlineColor = Color3.fromRGB(50, 50, 50),
RiskColor = Color3.fromRGB(255, 50, 50),
Black = Color3.new(0, 0, 0),
Font = Font.fromEnum(Enum.Font.RobotoMono),
OpenedFrames = {},
DependencyBoxes = {},
Signals = {},
ScreenGui = ScreenGui,
OwnerTag = nil,
OwnerKeys = { toggles = {}, options = {} },
}
function Library:RegisterOwnerKey(kind, key)
if not self.OwnerTag or not key then
return
end
local bucket = self.OwnerKeys[kind]
if not bucket then
return
end
bucket[key] = true
end
local RainbowStep = 0
local Hue = 0
table.insert(
Library.Signals,
RenderStepped:Connect(function(Delta)
local showInfo = not (Toggles.ShowLoggerWindow and not Toggles.ShowLoggerWindow.Value)
local showEffect = not (Toggles.ShowEffectLoggerWindow and not Toggles.ShowEffectLoggerWindow.Value)
local showSkill = not (Toggles.ShowSkillLoggerWindow and not Toggles.ShowSkillLoggerWindow.Value)
if not showInfo and not showEffect and not showSkill then
Entries = {}
end
local NextIndex, NextEntry = next(Entries)
if NextIndex and NextEntry then
Entries[NextIndex] = nil
NextEntry()
end
RainbowStep = RainbowStep + Delta
if RainbowStep >= (1 / 60) then
RainbowStep = 0
Hue = Hue + (1 / 400)
if Hue > 1 then
Hue = 0
end
Library.CurrentRainbowHue = Hue
Library.CurrentRainbowColor = Color3.fromHSV(Hue, 0.8, 1)
for _, ColorPicker in next, ColorPickers do
if ColorPicker.Rainbow then
ColorPicker:Display()
end
end
end
local LocalPlayer = game:GetService("Players").LocalPlayer
local PlayerGui = LocalPlayer and LocalPlayer.PlayerGui
local CursorGui = PlayerGui and PlayerGui:FindFirstChild("CursorGui")
local Cursor = CursorGui and CursorGui:FindFirstChild("Cursor")
if Cursor then
Cursor.Visible = false
game:GetService("UserInputService").MouseIconEnabled = true
end
end)
)
local function GetPlayersString()
local PlayerList = Players:GetPlayers()
for i = 1, #PlayerList do
PlayerList[i] = PlayerList[i].Name
end
table.sort(PlayerList, function(str1, str2)
return str1 < str2
end)
return PlayerList
end
local function GetTeamsString()
local TeamList = Teams:GetTeams()
for i = 1, #TeamList do
TeamList[i] = TeamList[i].Name
end
table.sort(TeamList, function(str1, str2)
return str1 < str2
end)
return TeamList
end
function Library:SafeCallback(label, f, ...)
if not f then
return
end
xpcall(Profiler.wrap(label, f), function(err)
warn(string.format("Library:SafeCallback - failed on label %s - %s", label, err))
warn(debug.traceback())
end, ...)
end
function Library:AttemptSave()
if Library.SaveManager then
Library.SaveManager:Save()
end
end
function Library:Create(Class, Properties)
local _Instance = Class
if type(Class) == "string" then
_Instance = Instance.new(Class)
end
for Property, Value in next, Properties do
_Instance[Property] = Value
end
return _Instance
end
function Library:KeyBlacklists()
local tbl = {}
for key, val in next, Library.InfoLoggerData.KeyBlacklistList do
if not val then
continue
end
tbl[#tbl + 1] = key
end
return tbl
end
function Library:RefreshInfoLogger()
local CurrentTypeCycle = Library.InfoLoggerCycles[Library.InfoLoggerCycle]
local Blacklist = Library.InfoLoggerData.KeyBlacklistList
for Idx, Entry in next, Library.InfoLoggerData.MissingDataEntries do
if not Blacklist[Entry.Key] then
continue
end
table.remove(Library.InfoLoggerData.MissingDataEntries, Idx)
pcall(Entry.Label.Destroy, Entry.Label)
end
for Idx, Entry in next, Library.InfoLoggerData.MissingDataEntries do
Entry.Label.Parent = Entry.Type == CurrentTypeCycle and Library.InfoLoggerContainer or nil
Entry.Label.LayoutOrder = Idx
end
Library.InfoLoggerLabel.Text = string.format("Info Logger (%s)", CurrentTypeCycle)
local YSize = 0
local XSize = 0
for _, Entry in next, Library.InfoLoggerData.MissingDataEntries do
if not Entry.Label.Parent then
continue
end
YSize = YSize + Entry.Label.TextBounds.Y + 2
if Entry.Label.TextBounds.X <= XSize then
continue
end
XSize = Entry.Label.TextBounds.X
end
XSize = XSize + 20
YSize = YSize + 22
Library.InfoLoggerFrame.Size = UDim2.new(0, math.clamp(XSize, 210, 800), 0, math.clamp(YSize, 24, 180))
end
function Library:RefreshEffectLogger()
local ed = Library.EffectLoggerData
if not ed then
return
end
local entries = ed.Entries
local bl = Library.InfoLoggerData and Library.InfoLoggerData.KeyBlacklistList or {}
for Idx, Entry in next, entries do
if bl[Entry.Key] then
table.remove(entries, Idx)
pcall(Entry.Label.Destroy, Entry.Label)
end
end
for Idx, Entry in next, entries do
Entry.Label.Parent = Library.EffectLoggerContainer
Entry.Label.LayoutOrder = Idx
end
local YSize = 0
local XSize = 0
for _, Entry in next, entries do
if not Entry.Label.Parent then
continue
end
YSize = YSize + Entry.Label.TextBounds.Y + 2
if Entry.Label.TextBounds.X > XSize then
XSize = Entry.Label.TextBounds.X
end
end
XSize = XSize + 20
YSize = YSize + 22
if Library.EffectLoggerFrame then
Library.EffectLoggerFrame.Size = UDim2.new(0, math.clamp(XSize, 210, 800), 0, math.clamp(YSize, 24, 180))
end
end
function Library:RefreshSkillLogger()
local sd = Library.SkillLoggerData
if not sd then
return
end
local entries = sd.Entries
local bl = Library.InfoLoggerData and Library.InfoLoggerData.KeyBlacklistList or {}
for Idx, Entry in next, entries do
if bl[Entry.Key] then
table.remove(entries, Idx)
pcall(Entry.Label.Destroy, Entry.Label)
end
end
for Idx, Entry in next, entries do
Entry.Label.Parent = Library.SkillLoggerContainer
Entry.Label.LayoutOrder = Idx
end
local YSize = 0
local XSize = 0
for _, Entry in next, entries do
if not Entry.Label.Parent then
continue
end
YSize = YSize + Entry.Label.TextBounds.Y + 2
if Entry.Label.TextBounds.X > XSize then
XSize = Entry.Label.TextBounds.X
end
end
XSize = XSize + 20
YSize = YSize + 22
if Library.SkillLoggerFrame then
Library.SkillLoggerFrame.Size = UDim2.new(0, math.clamp(XSize, 210, 800), 0, math.clamp(YSize, 24, 180))
end
end
function Library:AddTelemetryEntry(str, ...)
local type = "Telemetry"
local lolll = string.format(str, ...)
local ts = os.clock()
local ifd = Library.InfoLoggerData
local mde = ifd.MissingDataEntries
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddTelemetryEntry")
local function getEntriesForThisType()
local entries = {}
for Idx, Entry in next, mde do
if Entry.Type == type then
table.insert(entries, { [1] = Entry, [2] = Idx })
end
end
return entries
end
-- Pop the last element if we're under 30 entries for this type.
-- Max of 30 entries per type; in total - 120 for all types.
local entries = getEntriesForThisType()
local last = entries[#entries]
if #entries > 30 and last then
last[1].Label:Destroy()
table.remove(mde, last[2])
end
-- Create a new label.
---@type TextLabel
local label = Library:CreateLabel({
Text = lolll,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
-- entry
local entry = { Timestamp = ts, Label = label, Key = tostring(math.random()), Type = type }
-- Copy & blacklist.
label.InputBegan:Connect(function(Input)
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify("Copied timestamp to clipboard.")
end
end)
-- Create a new entry for later destroying.
table.insert(mde, 1, entry)
-- Refresh.
Library:RefreshInfoLogger()
debug.profileend()
end)
end
function Library:AddUsingSkillEntry(playerName, skillValue, isLocal, state, durationSeconds, onCreated)
local entryType = "UsingSkill"
local ts = tick()
local who = tostring(playerName or "Unknown")
local displayValue = tostring(skillValue or "")
if displayValue == "" then
displayValue = "(cleared)"
end
local tag = isLocal and "Local" or "Other"
state = state or "Update"
if type(durationSeconds) ~= "number" then
durationSeconds = nil
end
local sd = Library.SkillLoggerData
if not sd then
return
end
local entries = sd.Entries
local ifd = Library.InfoLoggerData
local bl = ifd and ifd.KeyBlacklistList or {}
local key = "Skill:" .. tostring(skillValue or who)
if bl[key] then
return
end
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddUsingSkillEntry")
local last = entries[#entries]
if #entries > 30 and last then
last.Label:Destroy()
table.remove(entries, #entries)
end
local timeText = os.date("%H:%M:%S")
local durationText = string.format(" | at=%s", timeText)
if durationSeconds ~= nil then
durationText = durationText .. string.format(" | lasted=%.3fs", durationSeconds)
end
---@type TextLabel
local label = Library:CreateLabel({
Text = string.format("(%s) %s UsingSkill %s: %s%s", tag, who, state, displayValue, durationText),
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
local entry = { Timestamp = ts, Label = label, Key = key, Type = entryType }
label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
setclipboard(label.Text)
Library:Notify("Copied skill log to clipboard.")
end
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify("Copied timestamp to clipboard.")
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
ifd.KeyBlacklistList[key] = true
ifd.KeyBlacklistHistory[#ifd.KeyBlacklistHistory + 1] = key
Library:RefreshInfoLogger()
Library:RefreshEffectLogger()
Library:RefreshSkillLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Blacklisted key '%s' from list.", key))
end
end)
if type(onCreated) == "function" then
pcall(onCreated, entry)
end
table.insert(entries, 1, entry)
Library:RefreshSkillLogger()
debug.profileend()
end)
end
function Library:AddEffectEntry(effectName, source, category, payloadText)
local type = "Effect"
local ts = tick()
local key = tostring(effectName or "Unknown")
local src = tostring(source or "Effect")
local cat = tostring(category or "Other")
local payload = tostring(payloadText or key)
local ifd = Library.InfoLoggerData
local bl = ifd and ifd.KeyBlacklistList or {}
local ed = Library.EffectLoggerData
if not ed then
return
end
local entries = ed.Entries
if bl[key] then
return
end
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddEffectEntry")
local last = entries[#entries]
if #entries > 30 and last then
last.Label:Destroy()
table.remove(entries, #entries)
end
---@type TextLabel
local label = Library:CreateLabel({
Text = string.format("[%s][%s] %s", src, cat, payload),
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
local entry = { Timestamp = ts, Label = label, Key = key, Type = type }
label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
setclipboard(payload)
Library:Notify("Copied effect payload to clipboard.")
end
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify("Copied timestamp to clipboard.")
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
ifd.KeyBlacklistList[key] = true
ifd.KeyBlacklistHistory[#ifd.KeyBlacklistHistory + 1] = key
Library:RefreshInfoLogger()
Library:RefreshEffectLogger()
Library:RefreshSkillLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Blacklisted key '%s' from list.", key))
end
end)
table.insert(entries, 1, entry)
Library:RefreshEffectLogger()
debug.profileend()
end)
end
function Library:AddKeyFrameEntry(distance, key, name, position, flag)
local ifd = Library.InfoLoggerData
local mde = ifd.MissingDataEntries
local bl = ifd.KeyBlacklistList
local ts = tick()
if bl[key] then
return
end
local type = "Keyframe"
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddKeyFrameEntry")
local function getEntriesForThisType()
local entries = {}
for Idx, Entry in next, mde do
if Entry.Type == type then
table.insert(entries, { [1] = Entry, [2] = Idx })
end
end
return entries
end
-- Pop the last element if we're under 30 entries for this type.
-- Max of 30 entries per type; in total - 120 for all types.
local entries = getEntriesForThisType()
local last = entries[#entries]
if #entries > 30 and last then
last[1].Label:Destroy()
table.remove(mde, last[2])
end
local SaveManager = require("Game/Timings/SaveManager")
local asdf = SaveManager.as:index(key)
-- Create a new label.
---@type TextLabel
local label = Library:CreateLabel({
-- (52.4m away) (HitStart) Animation 'rbxassetid://124453535' reached keyframe at position 0.69.
Text = string.format(
"(%.2fm away) %s '%s' %s '%s' at '%.3f' time position.",
distance,
asdf and "Timing" or "Animation",
asdf and PP_SCRAMBLE_STR(asdf.name) or key,
flag and "will reach" or "reached",
name,
position
),
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
-- entry
local entry = { Timestamp = ts, Label = label, Key = key, Type = type }
-- Copy & blacklist.
label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
setclipboard(key)
Library:Notify(string.format("Copied key '%s' to clipboard.", key))
end
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify(string.format("Copied timestamp for '%s' to clipboard.", key))
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
ifd.KeyBlacklistList[key] = true
ifd.KeyBlacklistHistory[#ifd.KeyBlacklistHistory + 1] = key
Library:RefreshInfoLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Blacklisted key '%s' from list.", key))
end
end)
-- Create a new entry for later destroying.
table.insert(mde, 1, entry)
-- Refresh.
Library:RefreshInfoLogger()
debug.profileend()
end)
end
function Library:AddExistAnimEntry(name, distance, timing)
local ifd = Library.InfoLoggerData
local mde = ifd.MissingDataEntries
local bl = ifd.KeyBlacklistList
local ts = tick()
local key = timing.name
if bl[key] then
return
end
local type = "Existing Anim"
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddExistAnimEntry")
local function getEntriesForThisType()
local entries = {}
for Idx, Entry in next, mde do
if Entry.Type == type then
table.insert(entries, { [1] = Entry, [2] = Idx })
end
end
return entries
end
-- Pop the last element if we're under 30 entries for this type.
-- Max of 30 entries per type; in total - 120 for all types.
local entries = getEntriesForThisType()
local last = entries[#entries]
if #entries > 30 and last then
last[1].Label:Destroy()
table.remove(mde, last[2])
end
-- Create a new label.
---@type TextLabel
local label = Library:CreateLabel({
Text = string.format("(%.2fm away) Animation timing '%s' from '%s' was played.", distance, key, name),
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
-- entry
local entry = { Timestamp = ts, Label = label, Key = key, Type = type }
-- Copy & blacklist.
label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
setclipboard(key)
Library:Notify(string.format("Copied key '%s' to clipboard.", key))
end
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify(string.format("Copied timestamp for '%s' to clipboard.", key))
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
ifd.KeyBlacklistList[key] = true
ifd.KeyBlacklistHistory[#ifd.KeyBlacklistHistory + 1] = key
Library:RefreshInfoLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Blacklisted key '%s' from list.", key))
end
end)
-- Create a new entry for later destroying.
table.insert(mde, 1, entry)
-- Refresh.
Library:RefreshInfoLogger()
debug.profileend()
end)
end
function Library:AddMissEntry(type, key, name, distance, parent)
local ifd = Library.InfoLoggerData
local mde = ifd.MissingDataEntries
local bl = ifd.KeyBlacklistList
local ts = tick()
if bl[key] then
return
end
table.insert(Entries, 1, function()
debug.profilebegin("Library:AddMissEntry")
local function getEntriesForThisType()
local entries = {}
for Idx, Entry in next, mde do
if Entry.Type == type then
table.insert(entries, { [1] = Entry, [2] = Idx })
end
end
return entries
end
-- Pop the last element if we're under 30 entries for this type.
-- Max of 30 entries per type; in total - 120 for all types.
local entries = getEntriesForThisType()
local last = entries[#entries]
if #entries > 30 and last then
last[1].Label:Destroy()
table.remove(mde, last[2])
end
local asset = typeof(key) == "string" and tonumber(key:sub(14, 40)) or nil
-- Create a new label.
---@type TextLabel
local label = Library:CreateLabel({
Text = name and string.format("(%.2fm away) Key '%s' from '%s' is missing.", distance, key, name)
or string.format("(%.2fm away) Key '%s' is missing.", distance, key),
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 1,
TextSize = 12,
Visible = true,
ZIndex = 306,
Parent = nil,
}, true)
if parent then
label.Text = string.format("(%s) %s", parent, label.Text)
end
Library:AddToRegistry(label, {
TextColor3 = "FontColor",
}, true)
if asset then
task.spawn(function()
pcall(function()
local lol = game:GetService("MarketplaceService"):GetProductInfo(asset)
if not lol then
return
end
label.Text = string.format("(%s) %s", lol.Name, label.Text)
end)
end)
end
-- entry
local entry = { Timestamp = ts, Label = label, Key = key, Type = type }
-- Copy & blacklist.
label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
setclipboard(key)
Library:Notify(string.format("Copied key '%s' to clipboard.", key))
end
if Input.KeyCode == Enum.KeyCode.T then
setclipboard(tostring(entry.Timestamp))
Library:Notify(string.format("Copied timestamp for '%s' to clipboard.", key))
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
ifd.KeyBlacklistList[key] = true
ifd.KeyBlacklistHistory[#ifd.KeyBlacklistHistory + 1] = key
Library:RefreshInfoLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Blacklisted key '%s' from list.", key))
end
end)
-- Create a new entry for later destroying.
table.insert(mde, 1, entry)
-- Refresh.
Library:RefreshInfoLogger()
debug.profileend()
end)
end
function Library:ApplyTextStroke(Inst)
Inst.TextStrokeTransparency = 1
--[[
Library:Create("UIStroke", {
Color = Color3.new(0, 0, 0),
Thickness = 1,
LineJoinMode = Enum.LineJoinMode.Miter,
Parent = Inst,
})
]]
--
end
function Library:CreateLabel(Properties, IsHud)
local _Instance = Library:Create("TextLabel", {
BackgroundTransparency = 1,
FontFace = Library.Font,
TextColor3 = Library.FontColor,
TextSize = 16,
TextStrokeTransparency = 0,
})
Library:ApplyTextStroke(_Instance)
Library:AddToRegistry(_Instance, {
TextColor3 = "FontColor",
}, IsHud)
if Properties.TextSize then
Properties.TextSize = Properties.TextSize + 1
end
return Library:Create(_Instance, Properties)
end
function Library:MakeDraggable(Instance, Cutoff)
Instance.Active = true
Instance.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.MouseButton1
or Input.UserInputType == Enum.UserInputType.Touch
then
local ObjPos = Vector2.new(Mouse.X - Instance.AbsolutePosition.X, Mouse.Y - Instance.AbsolutePosition.Y)
if ObjPos.Y > (Cutoff or 40) then
return
end
while
InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
or InputService:IsMouseButtonPressed(Enum.UserInputType.Touch)
do
Instance.Position = UDim2.new(
0,
Mouse.X - ObjPos.X + (Instance.Size.X.Offset * Instance.AnchorPoint.X),
0,
Mouse.Y - ObjPos.Y + (Instance.Size.Y.Offset * Instance.AnchorPoint.Y)
)
RenderStepped:Wait()
end
end
end)
end
function Library:AddToolTip(InfoStr, HoverInstance)
local X, Y = Library:GetTextBounds(InfoStr, Library.Font, 14)
local Tooltip = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
Size = UDim2.fromOffset(X + 5, Y + 4),
ZIndex = 100,
Parent = Library.ScreenGui,
Visible = false,
})
local Label = Library:CreateLabel({
Position = UDim2.fromOffset(3, 1),
Size = UDim2.fromOffset(X, Y),
TextSize = 14,
Text = InfoStr,
TextColor3 = Library.FontColor,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = Tooltip.ZIndex + 1,
Parent = Tooltip,
})
Library:AddToRegistry(Tooltip, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
Library:AddToRegistry(Label, {
TextColor3 = "FontColor",
})
Tooltips[#Tooltips + 1] = Tooltip
local IsHovering = false
HoverInstance.MouseEnter:Connect(function()
if Library:MouseIsOverOpenedFrame() then
return
end
IsHovering = true
Tooltip.Position = UDim2.fromOffset(Mouse.X + 15, Mouse.Y + 12)
Tooltip.Visible = true
while IsHovering do
RunService.Heartbeat:Wait()
Tooltip.Position = UDim2.fromOffset(Mouse.X + 15, Mouse.Y + 12)
end
end)
HoverInstance.MouseLeave:Connect(function()
IsHovering = false
Tooltip.Visible = false
end)
end
function Library:OnHighlight(HighlightInstance, Instance, Properties, PropertiesDefault)
HighlightInstance.MouseEnter:Connect(function()
local Reg = Library.RegistryMap[Instance]
for Property, ColorIdx in next, Properties do
Instance[Property] = Library[ColorIdx] or ColorIdx
if Reg and Reg.Properties[Property] then
Reg.Properties[Property] = ColorIdx
end
end
end)
HighlightInstance.MouseLeave:Connect(function()
local Reg = Library.RegistryMap[Instance]
for Property, ColorIdx in next, PropertiesDefault do
Instance[Property] = Library[ColorIdx] or ColorIdx
if Reg and Reg.Properties[Property] then
Reg.Properties[Property] = ColorIdx
end
end
end)
end
function Library:MouseIsOverOpenedFrame()
for Frame, _ in next, Library.OpenedFrames do
local AbsPos, AbsSize = Frame.AbsolutePosition, Frame.AbsoluteSize
if
Mouse.X >= AbsPos.X
and Mouse.X <= AbsPos.X + AbsSize.X
and Mouse.Y >= AbsPos.Y
and Mouse.Y <= AbsPos.Y + AbsSize.Y
then
return true
end
end
end
function Library:IsMouseOverFrame(Frame)
local AbsPos, AbsSize = Frame.AbsolutePosition, Frame.AbsoluteSize
if
Mouse.X >= AbsPos.X
and Mouse.X <= AbsPos.X + AbsSize.X
and Mouse.Y >= AbsPos.Y
and Mouse.Y <= AbsPos.Y + AbsSize.Y
then
return true
end
end
function Library:UpdateDependencyBoxes()
for _, Depbox in next, Library.DependencyBoxes do
Depbox:Update()
end
end
function Library:MapValue(Value, MinA, MaxA, MinB, MaxB)
return (1 - ((Value - MinA) / (MaxA - MinA))) * MinB + ((Value - MinA) / (MaxA - MinA)) * MaxB
end
function Library:GetTextBounds(Text, Font, Size, Resolution)
local Bounds = TextService:GetTextSize(Text, Size, "RobotoMono", Resolution or Vector2.new(1920, 1080))
return Bounds.X, Bounds.Y
end
function Library:GetDarkerColor(Color)
local H, S, V = Color3.toHSV(Color)
return Color3.fromHSV(H, S, V / 1.5)
end
Library.AccentColorDark = Library:GetDarkerColor(Library.AccentColor)
function Library:AddToRegistry(Instance, Properties, IsHud)
local Idx = #Library.Registry + 1
local Data = {
Instance = Instance,
Properties = Properties,
Idx = Idx,
}
table.insert(Library.Registry, Data)
Library.RegistryMap[Instance] = Data
if IsHud then
table.insert(Library.HudRegistry, Data)
end
end
function Library:RemoveFromRegistry(Instance)
local Data = Library.RegistryMap[Instance]
if Data then
for Idx = #Library.Registry, 1, -1 do
if Library.Registry[Idx] == Data then
table.remove(Library.Registry, Idx)
end
end
for Idx = #Library.HudRegistry, 1, -1 do
if Library.HudRegistry[Idx] == Data then
table.remove(Library.HudRegistry, Idx)
end
end
Library.RegistryMap[Instance] = nil
end
end
function Library:UpdateColorsUsingRegistry()
-- TODO: Could have an 'active' list of objects
-- where the active list only contains Visible objects.
-- IMPL: Could setup .Changed events on the AddToRegistry function
-- that listens for the 'Visible' propert being changed.
-- Visible: true => Add to active list, and call UpdateColors function
-- Visible: false => Remove from active list.
-- The above would be especially efficient for a rainbow menu color or live color-changing.
for Idx, Object in next, Library.Registry do
for Property, ColorIdx in next, Object.Properties do
if type(ColorIdx) == "string" then
Object.Instance[Property] = Library[ColorIdx]
elseif type(ColorIdx) == "function" then
Object.Instance[Property] = ColorIdx()
end
end
end
end
function Library:GiveSignal(Signal)
-- Only used for signals not attached to library instances, as those should be cleaned up on object destruction by Roblox
table.insert(Library.Signals, Signal)
end
function Library:Unload()
-- Unload all of the signals
for Idx = #Library.Signals, 1, -1 do
local Connection = table.remove(Library.Signals, Idx)
Connection:Disconnect()
end
-- Call our unload callback, maybe to undo some hooks etc
if Library.OnUnload then
Library.OnUnload()
end
ScreenGui:Destroy()
end
function Library:OnUnload(Callback)
Library.OnUnload = Callback
end
Library:GiveSignal(ScreenGui.DescendantRemoving:Connect(function(Instance)
if Library.RegistryMap[Instance] then
Library:RemoveFromRegistry(Instance)
end
end))
local BaseAddons = {}
do
local Funcs = {}
function Funcs:AddColorPicker(Idx, Info)
local ToggleLabel = self.TextLabel
-- local Container = self.Container;
assert(Info.Default, "AddColorPicker: Missing default value.")
local ColorPicker = {
Value = Info.Default,
Transparency = Info.Transparency or 0,
Type = "ColorPicker",
Title = type(Info.Title) == "string" and Info.Title or "Color picker",
Callback = Info.Callback or function(Color) end,
Rainbow = Info.Rainbow or false,
}
function ColorPicker:SetHSVFromRGB(Color)
local H, S, V = Color3.toHSV(Color)
ColorPicker.Hue = H
ColorPicker.Sat = S
ColorPicker.Vib = V
end
ColorPicker:SetHSVFromRGB(ColorPicker.Value)
local DisplayFrame = Library:Create("Frame", {
BackgroundColor3 = ColorPicker.Value,
BorderColor3 = Library:GetDarkerColor(ColorPicker.Value),
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(0, 28, 0, 14),
ZIndex = 6,
Parent = ToggleLabel,
})
-- Transparency image taken from https://github.com/matas3535/SplixPrivateDrawingLibrary/blob/main/Library.lua cus i'm lazy
local CheckerFrame = Library:Create("ImageLabel", {
BorderSizePixel = 0,
Size = UDim2.new(0, 27, 0, 13),
ZIndex = 5,
Image = "http://www.roblox.com/asset/?id=12977615774",
Visible = not not Info.Transparency,
Parent = DisplayFrame,
})
-- 1/16/23
-- Rewrote this to be placed inside the Library ScreenGui
-- There was some issue which caused RelativeOffset to be way off
-- Thus the color picker would never show
local PickerFrameOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.fromOffset(DisplayFrame.AbsolutePosition.X, DisplayFrame.AbsolutePosition.Y + 18),
Size = UDim2.fromOffset(230, Info.Transparency and 271 or 253),
Visible = false,
ZIndex = 15,
Parent = ScreenGui,
})
DisplayFrame:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
PickerFrameOuter.Position =
UDim2.fromOffset(DisplayFrame.AbsolutePosition.X, DisplayFrame.AbsolutePosition.Y + 18)
end)
local PickerFrameInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 16,
Parent = PickerFrameOuter,
})
local Highlight = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 17,
Parent = PickerFrameInner,
})
local SatVibMapOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 4, 0, 25),
Size = UDim2.new(0, 200, 0, 200),
ZIndex = 17,
Parent = PickerFrameInner,
})
local SatVibMapInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 18,
Parent = SatVibMapOuter,
})
local SatVibMap = Library:Create("ImageLabel", {
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 18,
Image = "rbxassetid://4155801252",
Parent = SatVibMapInner,
})
local CursorOuter = Library:Create("ImageLabel", {
AnchorPoint = Vector2.new(0.5, 0.5),
Size = UDim2.new(0, 6, 0, 6),
BackgroundTransparency = 1,
Image = "http://www.roblox.com/asset/?id=9619665977",
ImageColor3 = Color3.new(0, 0, 0),
ZIndex = 19,
Parent = SatVibMap,
})
local CursorInner = Library:Create("ImageLabel", {
Size = UDim2.new(0, CursorOuter.Size.X.Offset - 2, 0, CursorOuter.Size.Y.Offset - 2),
Position = UDim2.new(0, 1, 0, 1),
BackgroundTransparency = 1,
Image = "http://www.roblox.com/asset/?id=9619665977",
ZIndex = 20,
Parent = CursorOuter,
})
local HueSelectorOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 208, 0, 25),
Size = UDim2.new(0, 15, 0, 200),
ZIndex = 17,
Parent = PickerFrameInner,
})
local HueSelectorInner = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 18,
Parent = HueSelectorOuter,
})
local HueCursor = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
AnchorPoint = Vector2.new(0, 0.5),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, 0, 0, 1),
ZIndex = 18,
Parent = HueSelectorInner,
})
local HueBoxOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.fromOffset(4, 228),
Size = UDim2.new(0.5, -6, 0, 20),
ZIndex = 18,
Parent = PickerFrameInner,
})
local HueBoxInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 18,
Parent = HueBoxOuter,
})
Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(212, 212, 212)),
}),
Rotation = 90,
Parent = HueBoxInner,
})
local HueBox = Library:Create("TextBox", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 5, 0, 0),
Size = UDim2.new(1, -5, 1, 0),
FontFace = Library.Font,
PlaceholderColor3 = Color3.fromRGB(190, 190, 190),
PlaceholderText = "Hex color",
Text = "#FFFFFF",
TextColor3 = Library.FontColor,
TextSize = 14,
TextStrokeTransparency = 0,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 20,
Parent = HueBoxInner,
})
Library:ApplyTextStroke(HueBox)
local RgbBoxBase = Library:Create(HueBoxOuter:Clone(), {
Position = UDim2.new(0.5, 2, 0, 228),
Size = UDim2.new(0.5, -6, 0, 20),
Parent = PickerFrameInner,
})
local RgbBox = Library:Create(RgbBoxBase.Frame:FindFirstChild("TextBox"), {
Text = "255, 255, 255",
PlaceholderText = "RGB color",
TextColor3 = Library.FontColor,
})
local TransparencyBoxOuter, TransparencyBoxInner, TransparencyCursor
if Info.Transparency then
TransparencyBoxOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.fromOffset(4, 251),
Size = UDim2.new(1, -8, 0, 15),
ZIndex = 19,
Parent = PickerFrameInner,
})
TransparencyBoxInner = Library:Create("Frame", {
BackgroundColor3 = ColorPicker.Value,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 19,
Parent = TransparencyBoxOuter,
})
Library:AddToRegistry(TransparencyBoxInner, { BorderColor3 = "OutlineColor" })
Library:Create("ImageLabel", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Image = "http://www.roblox.com/asset/?id=12978095818",
ZIndex = 20,
Parent = TransparencyBoxInner,
})
TransparencyCursor = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
AnchorPoint = Vector2.new(0.5, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(0, 1, 1, 0),
ZIndex = 21,
Parent = TransparencyBoxInner,
})
end
local DisplayLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 14),
Position = UDim2.fromOffset(5, 5),
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 14,
Text = ColorPicker.Title, --Info.Default;
TextWrapped = false,
ZIndex = 16,
Parent = PickerFrameInner,
})
local ContextMenu = {}
do
ContextMenu.Options = {}
ContextMenu.Container = Library:Create("Frame", {
BorderColor3 = Color3.new(),
ZIndex = 14,
Visible = false,
Parent = ScreenGui,
})
ContextMenu.Inner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.fromScale(1, 1),
ZIndex = 15,
Parent = ContextMenu.Container,
})
ContextMenus[#ContextMenus + 1] = ContextMenu
Library:Create("UIListLayout", {
Name = "Layout",
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = ContextMenu.Inner,
})
Library:Create("UIPadding", {
Name = "Padding",
PaddingLeft = UDim.new(0, 4),
Parent = ContextMenu.Inner,
})
local function updateMenuPosition()
ContextMenu.Container.Position = UDim2.fromOffset(
(DisplayFrame.AbsolutePosition.X + DisplayFrame.AbsoluteSize.X) + 4,
DisplayFrame.AbsolutePosition.Y + 1
)
end
local function updateMenuSize()
local menuWidth = 60
for i, label in next, ContextMenu.Inner:GetChildren() do
if label:IsA("TextLabel") then
menuWidth = math.max(menuWidth, label.TextBounds.X)
end
end
ContextMenu.Container.Size =
UDim2.fromOffset(menuWidth + 8, ContextMenu.Inner.Layout.AbsoluteContentSize.Y + 4)
end
DisplayFrame:GetPropertyChangedSignal("AbsolutePosition"):Connect(updateMenuPosition)
ContextMenu.Inner.Layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(updateMenuSize)
task.spawn(updateMenuPosition)
task.spawn(updateMenuSize)
Library:AddToRegistry(ContextMenu.Inner, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
function ContextMenu:Show()
self.Container.Visible = true
end
function ContextMenu:Hide()
self.Container.Visible = false
end
function ContextMenu:AddOption(Str, Callback)
if type(Callback) ~= "function" then
Callback = function() end
end
local Button = Library:CreateLabel({
Active = false,
Size = UDim2.new(1, 0, 0, 15),
TextSize = 13,
Text = Str,
ZIndex = 16,
Parent = self.Inner,
TextXAlignment = Enum.TextXAlignment.Left,
})
Library:OnHighlight(Button, Button, { TextColor3 = "AccentColor" }, { TextColor3 = "FontColor" })
Button.InputBegan:Connect(function(Input)
if
Input.UserInputType ~= Enum.UserInputType.Touch
and Input.UserInputType ~= Enum.UserInputType.MouseButton1
then
return
end
Callback()
end)
end
ContextMenu:AddOption("Rainbow toggle", function()
ColorPicker.Rainbow = not ColorPicker.Rainbow
ColorPicker:Display()
end)
ContextMenu:AddOption("Copy color", function()
Library.ColorClipboard = ColorPicker.Value
Library:Notify("Copied color!", 2)
end)
ContextMenu:AddOption("Paste color", function()
if not Library.ColorClipboard then
return Library:Notify("You have not copied a color!", 2)
end
ColorPicker:SetValueRGB(Library.ColorClipboard)
end)
ContextMenu:AddOption("Copy HEX", function()
pcall(setclipboard, ColorPicker.Value:ToHex())
Library:Notify("Copied hex code to clipboard!", 2)
end)
ContextMenu:AddOption("Copy RGB", function()
pcall(
setclipboard,
table.concat({
math.floor(ColorPicker.Value.R * 255),
math.floor(ColorPicker.Value.G * 255),
math.floor(ColorPicker.Value.B * 255),
}, ", ")
)
Library:Notify("Copied RGB values to clipboard!", 2)
end)
end
Library:AddToRegistry(
PickerFrameInner,
{ BackgroundColor3 = "BackgroundColor", BorderColor3 = "OutlineColor" }
)
Library:AddToRegistry(Highlight, { BackgroundColor3 = "AccentColor" })
Library:AddToRegistry(
SatVibMapInner,
{ BackgroundColor3 = "BackgroundColor", BorderColor3 = "OutlineColor" }
)
Library:AddToRegistry(HueBoxInner, { BackgroundColor3 = "MainColor", BorderColor3 = "OutlineColor" })
Library:AddToRegistry(RgbBoxBase.Frame, { BackgroundColor3 = "MainColor", BorderColor3 = "OutlineColor" })
Library:AddToRegistry(RgbBox, { TextColor3 = "FontColor" })
Library:AddToRegistry(HueBox, { TextColor3 = "FontColor" })
local SequenceTable = {}
for Hue = 0, 1, 0.1 do
table.insert(SequenceTable, ColorSequenceKeypoint.new(Hue, Color3.fromHSV(Hue, 1, 1)))
end
local HueSelectorGradient = Library:Create("UIGradient", {
Color = ColorSequence.new(SequenceTable),
Rotation = 90,
Parent = HueSelectorInner,
})
HueBox.FocusLost:Connect(function(enter)
if enter then
local success, result = pcall(Color3.fromHex, HueBox.Text)
if success and typeof(result) == "Color3" then
ColorPicker.Hue, ColorPicker.Sat, ColorPicker.Vib = Color3.toHSV(result)
end
end
ColorPicker:Display()
end)
RgbBox.FocusLost:Connect(function(enter)
if enter then
local r, g, b = RgbBox.Text:match("(%d+),%s*(%d+),%s*(%d+)")
if r and g and b then
ColorPicker.Hue, ColorPicker.Sat, ColorPicker.Vib = Color3.toHSV(Color3.fromRGB(r, g, b))
end
end
ColorPicker:Display()
end)
function ColorPicker:Display()
ColorPicker.Value = Color3.fromHSV(ColorPicker.Hue, ColorPicker.Sat, ColorPicker.Vib)
SatVibMap.BackgroundColor3 = Color3.fromHSV(ColorPicker.Hue, 1, 1)
if ColorPicker.Rainbow then
ColorPicker.Value = Library.CurrentRainbowColor
end
Library:Create(DisplayFrame, {
BackgroundColor3 = ColorPicker.Value,
BackgroundTransparency = ColorPicker.Transparency,
BorderColor3 = Library:GetDarkerColor(ColorPicker.Value),
})
if TransparencyBoxInner then
TransparencyBoxInner.BackgroundColor3 = ColorPicker.Value
TransparencyCursor.Position = UDim2.new(1 - ColorPicker.Transparency, 0, 0, 0)
end
CursorOuter.Position = UDim2.new(ColorPicker.Sat, 0, 1 - ColorPicker.Vib, 0)
HueCursor.Position = UDim2.new(0, 0, ColorPicker.Hue, 0)
HueBox.Text = "#" .. ColorPicker.Value:ToHex()
RgbBox.Text = table.concat({
math.floor(ColorPicker.Value.R * 255),
math.floor(ColorPicker.Value.G * 255),
math.floor(ColorPicker.Value.B * 255),
}, ", ")
Library:SafeCallback(
"ColorPicker_Callback" .. "_" .. (Idx or ""),
ColorPicker.Callback,
ColorPicker.Value
)
Library:SafeCallback(
"ColorPicker_Changed" .. "_" .. (Idx or ""),
ColorPicker.Changed,
ColorPicker.Value
)
end
function ColorPicker:OnChanged(Func)
ColorPicker.Changed = Func
Func(ColorPicker.Value)
end
function ColorPicker:Show()
for Frame, Val in next, Library.OpenedFrames do
if Frame.Name == "Color" then
Frame.Visible = false
Library.OpenedFrames[Frame] = nil
end
end
PickerFrameOuter.Visible = true
Library.OpenedFrames[PickerFrameOuter] = true
end
function ColorPicker:Hide()
PickerFrameOuter.Visible = false
Library.OpenedFrames[PickerFrameOuter] = nil
end
function ColorPicker:SetValue(HSV, Transparency)
local Color = Color3.fromHSV(HSV[1], HSV[2], HSV[3])
ColorPicker.Transparency = Transparency or 0
ColorPicker:SetHSVFromRGB(Color)
ColorPicker:Display()
end
function ColorPicker:SetValueRGB(Color, Transparency)
ColorPicker.Transparency = Transparency or 0
ColorPicker:SetHSVFromRGB(Color)
ColorPicker:Display()
end
SatVibMap.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
while
InputService:IsMouseButtonPressed(Enum.UserInputType.Touch)
or InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
do
local MinX = SatVibMap.AbsolutePosition.X
local MaxX = MinX + SatVibMap.AbsoluteSize.X
local MouseX = math.clamp(Mouse.X, MinX, MaxX)
local MinY = SatVibMap.AbsolutePosition.Y
local MaxY = MinY + SatVibMap.AbsoluteSize.Y
local MouseY = math.clamp(Mouse.Y, MinY, MaxY)
ColorPicker.Sat = (MouseX - MinX) / (MaxX - MinX)
ColorPicker.Vib = 1 - ((MouseY - MinY) / (MaxY - MinY))
ColorPicker:Display()
RenderStepped:Wait()
end
Library:AttemptSave()
end
end)
HueSelectorInner.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
while
InputService:IsMouseButtonPressed(Enum.UserInputType.Touch)
or InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
do
local MinY = HueSelectorInner.AbsolutePosition.Y
local MaxY = MinY + HueSelectorInner.AbsoluteSize.Y
local MouseY = math.clamp(Mouse.Y, MinY, MaxY)
ColorPicker.Hue = ((MouseY - MinY) / (MaxY - MinY))
ColorPicker:Display()
RenderStepped:Wait()
end
Library:AttemptSave()
end
end)
DisplayFrame.InputBegan:Connect(function(Input)
if
(
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
) and not Library:MouseIsOverOpenedFrame()
then
if PickerFrameOuter.Visible then
ColorPicker:Hide()
else
ContextMenu:Hide()
ColorPicker:Show()
end
elseif
(
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
) and not Library:MouseIsOverOpenedFrame()
then
ContextMenu:Show()
ColorPicker:Hide()
end
end)
if TransparencyBoxInner then
TransparencyBoxInner.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
while
InputService:IsMouseButtonPressed(Enum.UserInputType.Touch)
or InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
do
local MinX = TransparencyBoxInner.AbsolutePosition.X
local MaxX = MinX + TransparencyBoxInner.AbsoluteSize.X
local MouseX = math.clamp(Mouse.X, MinX, MaxX)
ColorPicker.Transparency = 1 - ((MouseX - MinX) / (MaxX - MinX))
ColorPicker:Display()
RenderStepped:Wait()
end
Library:AttemptSave()
end
end)
end
Library:GiveSignal(InputService.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
local AbsPos, AbsSize = PickerFrameOuter.AbsolutePosition, PickerFrameOuter.AbsoluteSize
if
Mouse.X < AbsPos.X
or Mouse.X > AbsPos.X + AbsSize.X
or Mouse.Y < (AbsPos.Y - 20 - 1)
or Mouse.Y > AbsPos.Y + AbsSize.Y
then
ColorPicker:Hide()
end
if not Library:IsMouseOverFrame(ContextMenu.Container) then
ContextMenu:Hide()
end
end
if Input.UserInputType == Enum.UserInputType.MouseButton2 and ContextMenu.Container.Visible then
if
not Library:IsMouseOverFrame(ContextMenu.Container)
and not Library:IsMouseOverFrame(DisplayFrame)
then
ContextMenu:Hide()
end
end
end))
ColorPicker:Display()
ColorPicker.DisplayFrame = DisplayFrame
if Idx then
Options[Idx] = ColorPicker
ColorPickers[Idx] = ColorPicker
Library:RegisterOwnerKey("options", Idx)
end
return self
end
function Funcs:AddKeyPicker(Idx, Info)
local ParentObj = self
local ToggleLabel = self.TextLabel
local Container = self.Container
assert(Info.Default, "AddKeyPicker: Missing default value.")
local KeyPicker = {
Value = Info.Default,
Toggled = false,
Mode = Info.Mode or "Toggle", -- Always, Toggle, Hold
Type = "KeyPicker",
Callback = Info.Callback or function(Value) end,
ChangedCallback = Info.ChangedCallback or function(New) end,
SyncToggleState = Info.SyncToggleState or false,
}
if KeyPicker.SyncToggleState then
Info.Modes = { "Toggle", "Hold" }
Info.Mode = "Toggle"
end
local PickOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(0, 28, 0, 15),
ZIndex = 6,
Parent = ToggleLabel,
})
local PickInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 7,
Parent = PickOuter,
})
Library:AddToRegistry(PickInner, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
local DisplayLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 13,
Text = Info.Default,
TextWrapped = true,
ZIndex = 8,
Parent = PickInner,
})
local ModeSelectOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.fromOffset(
ToggleLabel.AbsolutePosition.X + ToggleLabel.AbsoluteSize.X + 4,
ToggleLabel.AbsolutePosition.Y + 1
),
Size = UDim2.new(0, 60, 0, 60 + 2),
Visible = false,
ZIndex = 14,
Parent = ScreenGui,
})
ModeSelectFrames[#ModeSelectFrames + 1] = ModeSelectOuter
ToggleLabel:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
ModeSelectOuter.Position = UDim2.fromOffset(
ToggleLabel.AbsolutePosition.X + ToggleLabel.AbsoluteSize.X + 4,
ToggleLabel.AbsolutePosition.Y + 1
)
end)
local ModeSelectInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 15,
Parent = ModeSelectOuter,
})
Library:AddToRegistry(ModeSelectInner, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = ModeSelectInner,
})
local ContainerLabel = Library:CreateLabel({
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, 0, 0, 18),
TextSize = 13,
Visible = false,
ZIndex = 110,
Parent = Library.KeybindContainer,
}, true)
local Modes = Info.Modes or { "Always", "Toggle", "Hold", "Off" }
local ModeButtons = {}
function KeyPicker:DoClick()
if KeyPicker.Mode == "Toggle" and ParentObj.Type == "Toggle" and KeyPicker.SyncToggleState then
ParentObj:SetValue(not ParentObj.Value)
end
if KeyPicker.Mode == "Hold" and ParentObj.Type == "Toggle" and KeyPicker.SyncToggleState then
ParentObj:SetValue(KeyPicker.Toggled)
end
Library:SafeCallback("KeyPicker_Callback" .. "_" .. (Idx or ""), KeyPicker.Callback, KeyPicker.Toggled)
Library:SafeCallback("KeyPicker_Clicked" .. "_" .. (Idx or ""), KeyPicker.Clicked, KeyPicker.Toggled)
end
for Idx, Mode in next, Modes do
local ModeButton = {}
local Label = Library:CreateLabel({
Active = false,
Size = UDim2.new(1, 0, 0, 15),
TextSize = 13,
Text = Mode,
ZIndex = 16,
Parent = ModeSelectInner,
})
function ModeButton:Select()
for _, Button in next, ModeButtons do
Button:Deselect()
end
if Mode == "Always" then
KeyPicker.Toggled = true
KeyPicker:DoClick()
end
if Mode == "Off" then
KeyPicker.Toggled = false
KeyPicker:DoClick()
end
KeyPicker.Mode = Mode
Label.TextColor3 = Library.AccentColor
Library.RegistryMap[Label].Properties.TextColor3 = "AccentColor"
ModeSelectOuter.Visible = false
end
function ModeButton:Deselect()
KeyPicker.Mode = nil
Label.TextColor3 = Library.FontColor
Library.RegistryMap[Label].Properties.TextColor3 = "FontColor"
end
Label.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
ModeButton:Select()
Library:AttemptSave()
end
end)
if Mode == KeyPicker.Mode then
ModeButton:Select()
end
ModeButtons[Mode] = ModeButton
end
function KeyPicker:Update()
if Info.NoUI then
return
end
local State = KeyPicker:GetState()
ContainerLabel.Text = string.format("[%s] %s (%s)", KeyPicker.Value, Info.Text, KeyPicker.Mode)
ContainerLabel.Visible = true
ContainerLabel.TextColor3 = State and Library.AccentColor or Library.FontColor
Library.RegistryMap[ContainerLabel].Properties.TextColor3 = State and "AccentColor" or "FontColor"
local YSize = 0
local XSize = 0
for _, Label in next, Library.KeybindContainer:GetChildren() do
if Label:IsA("TextLabel") and Label.Visible then
YSize = YSize + 18
if Label.TextBounds.X > XSize then
XSize = Label.TextBounds.X
end
end
end
Library.KeybindFrame.Size = UDim2.new(0, math.max(XSize + 10, 210), 0, YSize + 23)
end
function KeyPicker:GetState()
if KeyPicker.Mode == "Always" then
return true
elseif KeyPicker.Mode == "Off" then
return false
elseif KeyPicker.Mode == "Hold" then
if KeyPicker.Value == "N/A" then
return false
end
local Key = KeyPicker.Value
if Key == "MB1" then
return InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
or InputService.TouchEnabled and #InputService.Touches > 0
elseif Key == "MB2" then
return InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2)
or InputService.TouchEnabled and #InputService.Touches > 1
else
return InputService:IsKeyDown(Enum.KeyCode[KeyPicker.Value])
end
else
return KeyPicker.Toggled
end
end
function KeyPicker:SetValue(Data)
local Key, Mode = Data[1], Data[2]
DisplayLabel.Text = Key
KeyPicker.Value = Key
ModeButtons[Mode]:Select()
KeyPicker:Update()
end
function KeyPicker:OnClick(Callback)
KeyPicker.Clicked = Callback
end
function KeyPicker:OnChanged(Callback)
KeyPicker.Changed = Callback
Callback(KeyPicker.Value)
end
if ParentObj.Addons then
table.insert(ParentObj.Addons, KeyPicker)
end
local Picking = false
PickOuter.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 and not Library:MouseIsOverOpenedFrame() then
Picking = true
DisplayLabel.Text = ""
local Break
local Text = ""
task.spawn(function()
while not Break do
if Text == "..." then
Text = ""
end
Text = Text .. "."
DisplayLabel.Text = Text
wait(0.4)
end
end)
wait(0.2)
local Event
Event = InputService.InputBegan:Connect(function(Input)
local Key
if
Input.UserInputType == Enum.UserInputType.Keyboard
or Input.UserInputType == Enum.UserInputType.Touch
then
Key = Input.KeyCode.Name
elseif Input.UserInputType == Enum.UserInputType.MouseButton1 then
Key = "MB1"
elseif Input.UserInputType == Enum.UserInputType.MouseButton2 then
Key = "MB2"
end
if Input.KeyCode == Enum.KeyCode.Escape or Input.KeyCode == Enum.KeyCode.Backspace then
Key = "N/A"
end
Break = true
Picking = false
DisplayLabel.Text = Key
KeyPicker.Value = Key
Library:SafeCallback(
"KeyPicker_ChangedCallback" .. "_" .. (Idx or ""),
KeyPicker.ChangedCallback,
Input.KeyCode or Input.UserInputType
)
Library:SafeCallback(
"KeyPicker_Changed" .. "_" .. (Idx or ""),
KeyPicker.Changed,
Input.KeyCode or Input.UserInputType
)
Library:AttemptSave()
Event:Disconnect()
end)
elseif
Input.UserInputType == Enum.UserInputType.MouseButton2 and not Library:MouseIsOverOpenedFrame()
then
ModeSelectOuter.Visible = true
end
end)
Library:GiveSignal(InputService.InputBegan:Connect(function(Input, ProcessedByGame)
local textChatService = game:GetService("TextChatService")
local userInputService = game:GetService("UserInputService")
local chatInputBarConfiguration = textChatService:FindFirstChildOfClass("ChatInputBarConfiguration")
if userInputService:GetFocusedTextBox() or (chatInputBarConfiguration and chatInputBarConfiguration.IsFocused) then
return
end
if not Picking then
if KeyPicker.Mode == "Toggle" then
local Key = KeyPicker.Value
if Key == "MB1" or Key == "MB2" then
if
Key == "MB1" and Input.UserInputType == Enum.UserInputType.MouseButton1
or Key == "MB2" and Input.UserInputType == Enum.UserInputType.MouseButton2
then
KeyPicker.Toggled = not KeyPicker.Toggled
KeyPicker:DoClick()
end
elseif Input.UserInputType == Enum.UserInputType.Keyboard then
if Input.KeyCode.Name == Key then
KeyPicker.Toggled = not KeyPicker.Toggled
KeyPicker:DoClick()
end
elseif Input.UserInputType == Enum.UserInputType.Touch then
if Input.KeyCode.Name == Key then
KeyPicker.Toggled = not KeyPicker.Toggled
KeyPicker:DoClick()
end
end
end
if KeyPicker.Mode == "Hold" then
pcall(function()
local Key = KeyPicker.Value
if Key == "MB1" then
KeyPicker.Toggled = InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
elseif Key == "MB2" then
KeyPicker.Toggled = InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2)
end
if Key == "MB1" or Key == "MB2" then
KeyPicker:DoClick()
else
KeyPicker.Toggled = InputService:IsKeyDown(Enum.KeyCode[Key])
KeyPicker:DoClick()
end
end)
end
KeyPicker:Update()
end
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
local AbsPos, AbsSize = ModeSelectOuter.AbsolutePosition, ModeSelectOuter.AbsoluteSize
if
Mouse.X < AbsPos.X
or Mouse.X > AbsPos.X + AbsSize.X
or Mouse.Y < (AbsPos.Y - 20 - 1)
or Mouse.Y > AbsPos.Y + AbsSize.Y
then
ModeSelectOuter.Visible = false
end
end
end))
Library:GiveSignal(InputService.InputEnded:Connect(function(Input, ProcessedByGame)
if not Picking then
if KeyPicker.Mode == "Hold" then
pcall(function()
local Key = KeyPicker.Value
if Key == "MB1" then
KeyPicker.Toggled = InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
elseif Key == "MB2" then
KeyPicker.Toggled = InputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton2)
end
if Key == "MB1" or Key == "MB2" then
KeyPicker:DoClick()
else
KeyPicker.Toggled = InputService:IsKeyDown(Enum.KeyCode[Key])
KeyPicker:DoClick()
end
end)
end
KeyPicker:Update()
end
end))
if Info.Mode == "Always" then
KeyPicker.Toggled = true
KeyPicker:DoClick()
end
if Info.Mode == "Off" then
KeyPicker.Toggled = false
KeyPicker:DoClick()
end
KeyPicker:Update()
if Idx then
Options[Idx] = KeyPicker
Library:RegisterOwnerKey("options", Idx)
end
return self
end
BaseAddons.__index = Funcs
BaseAddons.__namecall = function(Table, Key, ...)
return Funcs[Key](...)
end
end
local BaseGroupbox = {}
do
local Funcs = {}
function Funcs:AddBlank(Size)
local Groupbox = self
local Container = Groupbox.Container
Library:Create("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, Size),
ZIndex = 1,
Parent = Container,
})
end
function Funcs:AddLabel(Text, DoesWrap)
local Label = {}
local Groupbox = self
local Container = Groupbox.Container
local TextLabel = Library:CreateLabel({
Size = UDim2.new(1, -4, 0, 15),
TextSize = 14,
Text = Text,
TextWrapped = DoesWrap or false,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 5,
Parent = Container,
})
if DoesWrap then
local Y = select(
2,
Library:GetTextBounds(Text, Library.Font, 14, Vector2.new(TextLabel.AbsoluteSize.X, math.huge))
)
TextLabel.Size = UDim2.new(1, -4, 0, Y)
else
Library:Create("UIListLayout", {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = TextLabel,
})
end
Label.TextLabel = TextLabel
Label.Container = Container
function Label:SetText(Text)
TextLabel.Text = Text
if DoesWrap then
local Y = select(
2,
Library:GetTextBounds(Text, Library.Font, 14, Vector2.new(TextLabel.AbsoluteSize.X, math.huge))
)
TextLabel.Size = UDim2.new(1, -4, 0, Y)
end
Groupbox:Resize()
end
if not DoesWrap then
setmetatable(Label, BaseAddons)
end
Groupbox:AddBlank(5)
Groupbox:Resize()
return Label
end
function Funcs:AddButton(...)
-- TODO: Eventually redo this
local Button = {}
local function ProcessButtonParams(Class, Obj, ...)
local Props = select(1, ...)
if type(Props) == "table" then
Obj.Text = Props.Text
Obj.Func = Props.Func
Obj.DoubleClick = Props.DoubleClick
Obj.DoubleClickText = Props.DoubleClickText
Obj.Tooltip = Props.Tooltip
else
Obj.Text = select(1, ...)
Obj.Func = select(2, ...)
end
assert(type(Obj.Func) == "function", "AddButton: `Func` callback is missing.")
end
ProcessButtonParams("Button", Button, ...)
local Groupbox = self
local Container = Groupbox.Container
local function CreateBaseButton(Button)
local Outer = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, -4, 0, 20),
ZIndex = 5,
})
local Inner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = Outer,
})
local Label = Library:CreateLabel({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 14,
Text = Button.Text,
ZIndex = 6,
Parent = Inner,
})
Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(212, 212, 212)),
}),
Rotation = 90,
Parent = Inner,
})
Library:AddToRegistry(Outer, {
BorderColor3 = "Black",
})
Library:AddToRegistry(Inner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
Library:OnHighlight(Outer, Outer, { BorderColor3 = "AccentColor" }, { BorderColor3 = "Black" })
return Outer, Inner, Label
end
local function InitEvents(Button)
local function WaitForEvent(event, timeout, validator)
local bindable = Instance.new("BindableEvent")
local connection = event:Once(function(...)
if type(validator) == "function" and validator(...) then
bindable:Fire(true)
else
bindable:Fire(false)
end
end)
task.delay(timeout, function()
connection:disconnect()
bindable:Fire(false)
end)
return bindable.Event:Wait()
end
local function ValidateClick(Input)
if Library:MouseIsOverOpenedFrame() then
return false
end
if
Input.UserInputType ~= Enum.UserInputType.MouseButton1
and Input.UserInputType ~= Enum.UserInputType.Touch
then
return false
end
return true
end
Button.Outer.InputBegan:Connect(function(Input)
if not ValidateClick(Input) then
return
end
if Button.Locked then
return
end
if Button.DoubleClick then
Library:RemoveFromRegistry(Button.Label)
Library:AddToRegistry(Button.Label, { TextColor3 = "AccentColor" })
Button.Label.TextColor3 = Library.AccentColor
Button.Label.Text = Button.DoubleClickText or "Are you sure?"
Button.Locked = true
local clicked = WaitForEvent(Button.Outer.InputBegan, 2, ValidateClick)
Library:RemoveFromRegistry(Button.Label)
Library:AddToRegistry(Button.Label, { TextColor3 = "FontColor" })
Button.Label.TextColor3 = Library.FontColor
Button.Label.Text = Button.Text
task.defer(rawset, Button, "Locked", false)
if clicked then
Library:SafeCallback("Button" .. "_" .. Button.Label.Text, Button.Func)
end
return
end
Library:SafeCallback("Button" .. "_" .. Button.Label.Text, Button.Func)
end)
end
Button.Outer, Button.Inner, Button.Label = CreateBaseButton(Button)
Button.Outer.Parent = Container
InitEvents(Button)
function Button:AddTooltip(tooltip)
if type(tooltip) == "string" then
Library:AddToolTip(tooltip, self.Outer)
end
return self
end
function Button:AddButton(...)
local SubButton = {}
ProcessButtonParams("SubButton", SubButton, ...)
self.Outer.Size = UDim2.new(0.5, -2, 0, 20)
SubButton.Outer, SubButton.Inner, SubButton.Label = CreateBaseButton(SubButton)
SubButton.Outer.Position = UDim2.new(1, 3, 0, 0)
SubButton.Outer.Size = UDim2.fromOffset(self.Outer.AbsoluteSize.X - 2, self.Outer.AbsoluteSize.Y)
SubButton.Outer.Parent = self.Outer
function SubButton:AddTooltip(tooltip)
if type(tooltip) == "string" then
Library:AddToolTip(tooltip, self.Outer)
end
return SubButton
end
if type(SubButton.Tooltip) == "string" then
SubButton:AddTooltip(SubButton.Tooltip)
end
InitEvents(SubButton)
return SubButton
end
if type(Button.Tooltip) == "string" then
Button:AddTooltip(Button.Tooltip)
end
Groupbox:AddBlank(5)
Groupbox:Resize()
return Button
end
function Funcs:AddDivider()
local Groupbox = self
local Container = self.Container
local Divider = {
Type = "Divider",
}
Groupbox:AddBlank(2)
local DividerOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, -4, 0, 5),
ZIndex = 5,
Parent = Container,
})
local DividerInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = DividerOuter,
})
Library:AddToRegistry(DividerOuter, {
BorderColor3 = "Black",
})
Library:AddToRegistry(DividerInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
Groupbox:AddBlank(9)
Groupbox:Resize()
end
---Add input function.
---@param Idx string
---@param Info table
---@return any
function Funcs:AddInput(Idx, Info)
assert(Info.Text, "AddInput: Missing `Text` string.")
local Textbox = {
Value = Info.Default or "",
Numeric = Info.Numeric or false,
Finished = Info.Finished or false,
Type = "Input",
Callback = Info.Callback or function(Value) end,
}
local Groupbox = self
local Container = Groupbox.Container
local InputLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 15),
TextSize = 14,
Text = Info.Text,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 5,
Parent = Container,
})
Groupbox:AddBlank(1)
local TextBoxOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, -4, 0, 20),
ZIndex = 5,
Parent = Container,
})
local TextBoxInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = TextBoxOuter,
})
Library:AddToRegistry(TextBoxInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
Library:OnHighlight(
TextBoxOuter,
TextBoxOuter,
{ BorderColor3 = "AccentColor" },
{ BorderColor3 = "Black" }
)
if type(Info.Tooltip) == "string" then
Library:AddToolTip(Info.Tooltip, TextBoxOuter)
end
Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(212, 212, 212)),
}),
Rotation = 90,
Parent = TextBoxInner,
})
local Container = Library:Create("Frame", {
BackgroundTransparency = 1,
ClipsDescendants = true,
Position = UDim2.new(0, 5, 0, 0),
Size = UDim2.new(1, -5, 1, 0),
ZIndex = 7,
Parent = TextBoxInner,
})
local Box = Library:Create("TextBox", {
BackgroundTransparency = 1,
Position = UDim2.fromOffset(0, 0),
Size = UDim2.fromScale(5, 1),
FontFace = Library.Font,
PlaceholderColor3 = Color3.fromRGB(190, 190, 190),
PlaceholderText = Info.Placeholder or "",
Text = Info.Default or "",
TextColor3 = Library.FontColor,
TextSize = 14,
TextStrokeTransparency = 0,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 7,
Parent = Container,
})
Library:ApplyTextStroke(Box)
local Connection = nil
local function normalizeText(value)
if value == nil then
return ""
end
if type(value) ~= "string" then
return tostring(value)
end
return value
end
function Textbox:SetRawValue(Text)
Text = normalizeText(Text)
if Info.MaxLength and #Text > Info.MaxLength then
Text = Text:sub(1, Info.MaxLength)
end
if Textbox.Numeric then
if (not tonumber(Text)) and Text:len() > 0 then
Text = Textbox.Value
end
end
Textbox.Value = Text
Box.Text = Text
end
function Textbox:SetValue(Text)
Text = normalizeText(Text)
if Info.MaxLength and #Text > Info.MaxLength then
Text = Text:sub(1, Info.MaxLength)
end
if Textbox.Numeric then
if (not tonumber(Text)) and Text:len() > 0 then
Text = Textbox.Value
end
end
Textbox.Value = Text
Box.Text = Text
Library:SafeCallback("Textbox_Callback" .. "_" .. (Idx or ""), Textbox.Callback, Textbox.Value)
Library:SafeCallback("Textbox_Changed" .. "_" .. (Idx or ""), Textbox.Changed, Textbox.Value)
end
if Textbox.Finished then
Connection = Box.FocusLost:Connect(function(enter)
if not enter then
return
end
Textbox:SetValue(Box.Text)
Library:AttemptSave()
end)
else
Connection = Box:GetPropertyChangedSignal("Text"):Connect(function()
Textbox:SetValue(Box.Text)
Library:AttemptSave()
end)
end
-- https://devforum.roblox.com/t/how-to-make-textboxes-follow-current-cursor-position/1368429/6
-- thank you nicemike40 :)
local function Update()
local PADDING = 2
local reveal = Container.AbsoluteSize.X
if not Box:IsFocused() or Box.TextBounds.X <= reveal - 2 * PADDING then
-- we aren't focused, or we fit so be normal
Box.Position = UDim2.new(0, PADDING, 0, 0)
else
-- we are focused and don't fit, so adjust position
local cursor = Box.CursorPosition
if cursor ~= -1 then
-- calculate pixel width of text from start to cursor
local subtext = string.sub(Box.Text, 1, cursor - 1)
local width = TextService:GetTextSize(
subtext,
Box.TextSize,
Box.Font,
Vector2.new(math.huge, math.huge)
).X
-- check if we're inside the box with the cursor
local currentCursorPos = Box.Position.X.Offset + width
-- adjust if necessary
if currentCursorPos < PADDING then
Box.Position = UDim2.fromOffset(PADDING - width, 0)
elseif currentCursorPos > reveal - PADDING - 1 then
Box.Position = UDim2.fromOffset(reveal - width - PADDING - 1, 0)
end
end
end
end
task.spawn(Update)
Box:GetPropertyChangedSignal("Text"):Connect(Update)
Box:GetPropertyChangedSignal("CursorPosition"):Connect(Update)
Box.FocusLost:Connect(Update)
Box.Focused:Connect(Update)
Box.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton2 then
Library:Notify("Text copied to clipboard!", 2.5)
setclipboard(Box.Text)
end
end)
Library:AddToRegistry(Box, {
TextColor3 = "FontColor",
})
function Textbox:OnChanged(Func)
Textbox.Changed = Func
Func(Textbox.Value)
end
Groupbox:AddBlank(5)
Groupbox:Resize()
if Idx then
Options[Idx] = Textbox
Library:RegisterOwnerKey("options", Idx)
end
return Textbox
end
function Funcs:AddToggle(Idx, Info)
assert(Info.Text, "AddInput: Missing `Text` string.")
local Toggle = {
Value = Info.Default or false,
Type = "Toggle",
Callback = Info.Callback or function(Value) end,
Addons = {},
Risky = Info.Risky,
}
local Groupbox = self
local Container = Groupbox.Container
local ToggleOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(0, 13, 0, 13),
ZIndex = 5,
Parent = Container,
})
Library:AddToRegistry(ToggleOuter, {
BorderColor3 = "Black",
})
local ToggleInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = ToggleOuter,
})
Library:AddToRegistry(ToggleInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
local ToggleLabel = Library:CreateLabel({
Size = UDim2.new(0, 216, 1, 0),
Position = UDim2.new(1, 6, 0, 0),
TextSize = 14,
Text = Info.Text,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 6,
Parent = ToggleInner,
})
Library:Create("UIListLayout", {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = ToggleLabel,
})
local ToggleRegion = Library:Create("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 170, 1, 0),
ZIndex = 8,
Parent = ToggleOuter,
})
Library:OnHighlight(ToggleRegion, ToggleOuter, { BorderColor3 = "AccentColor" }, { BorderColor3 = "Black" })
function Toggle:UpdateColors()
Toggle:Display()
end
if type(Info.Tooltip) == "string" then
Library:AddToolTip(Info.Tooltip, ToggleRegion)
end
function Toggle:Display()
ToggleInner.BackgroundColor3 = Toggle.Value and Library.AccentColor or Library.MainColor
ToggleInner.BorderColor3 = Toggle.Value and Library.AccentColorDark or Library.OutlineColor
Library.RegistryMap[ToggleInner].Properties.BackgroundColor3 = Toggle.Value and "AccentColor"
or "MainColor"
Library.RegistryMap[ToggleInner].Properties.BorderColor3 = Toggle.Value and "AccentColorDark"
or "OutlineColor"
end
function Toggle:OnChanged(Func)
Toggle.Changed = Func
Func(Toggle.Value)
end
function Toggle:SetRawValue(Bool)
Bool = not not Bool
Toggle.Value = Bool
Toggle:Display()
for _, Addon in next, Toggle.Addons do
if Addon.Type == "KeyPicker" and Addon.SyncToggleState then
Addon.Toggled = Bool
Addon:Update()
end
end
Library:UpdateDependencyBoxes()
end
function Toggle:SetValue(Bool)
Bool = not not Bool
Toggle.Value = Bool
Toggle:Display()
for _, Addon in next, Toggle.Addons do
if Addon.Type == "KeyPicker" and Addon.SyncToggleState then
Addon.Toggled = Bool
Addon:Update()
end
end
Library:SafeCallback("Toggle_Callback" .. "_" .. (Idx or ""), Toggle.Callback, Toggle.Value)
Library:SafeCallback("Toggle_Changed" .. "_" .. (Idx or ""), Toggle.Changed, Toggle.Value)
Library:UpdateDependencyBoxes()
end
ToggleRegion.InputBegan:Connect(function(Input)
if
(
Input.UserInputType == Enum.UserInputType.MouseButton1
or Input.UserInputType == Enum.UserInputType.Touch
) and not Library:MouseIsOverOpenedFrame()
then
Toggle:SetValue(not Toggle.Value) -- Why was it not like this from the start?
Library:AttemptSave()
end
end)
if Toggle.Risky then
Library:RemoveFromRegistry(ToggleLabel)
ToggleLabel.TextColor3 = Library.RiskColor
Library:AddToRegistry(ToggleLabel, { TextColor3 = "RiskColor" })
end
Toggle:Display()
Groupbox:AddBlank(Info.BlankSize or 5 + 2)
Groupbox:Resize()
Toggle.TextLabel = ToggleLabel
Toggle.Container = Container
setmetatable(Toggle, BaseAddons)
if Idx then
Toggles[Idx] = Toggle
Library:RegisterOwnerKey("toggles", Idx)
end
Library:UpdateDependencyBoxes()
return Toggle
end
function Funcs:AddSlider(Idx, Info)
assert(Info.Default, "AddSlider: Missing default value.")
assert(Info.Text, "AddSlider: Missing slider text.")
assert(Info.Min, "AddSlider: Missing minimum value.")
assert(Info.Max, "AddSlider: Missing maximum value.")
assert(Info.Rounding, "AddSlider: Missing rounding value.")
local Slider = {
Value = Info.Default,
Min = Info.Min,
Max = Info.Max,
Rounding = Info.Rounding,
MaxSize = 232,
Type = "Slider",
Callback = Info.Callback or function(Value) end,
}
local Groupbox = self
local Container = Groupbox.Container
if not Info.Compact then
Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 10),
TextSize = 14,
Text = Info.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
ZIndex = 5,
Parent = Container,
})
Groupbox:AddBlank(3)
end
local SliderOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, -4, 0, 13),
ZIndex = 5,
Parent = Container,
})
Library:AddToRegistry(SliderOuter, {
BorderColor3 = "Black",
})
local SliderInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = SliderOuter,
})
Library:AddToRegistry(SliderInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
local Fill = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderColor3 = Library.AccentColorDark,
Size = UDim2.new(0, 0, 1, 0),
ZIndex = 7,
Parent = SliderInner,
})
Library:AddToRegistry(Fill, {
BackgroundColor3 = "AccentColor",
BorderColor3 = "AccentColorDark",
})
local HideBorderRight = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Position = UDim2.new(1, 0, 0, 0),
Size = UDim2.new(0, 1, 1, 0),
ZIndex = 8,
Parent = Fill,
})
Library:AddToRegistry(HideBorderRight, {
BackgroundColor3 = "AccentColor",
})
local DisplayLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 14,
Text = "Infinite",
ZIndex = 9,
Parent = SliderInner,
})
Library:OnHighlight(SliderOuter, SliderOuter, { BorderColor3 = "AccentColor" }, { BorderColor3 = "Black" })
if type(Info.Tooltip) == "string" then
Library:AddToolTip(Info.Tooltip, SliderOuter)
end
function Slider:UpdateColors()
Fill.BackgroundColor3 = Library.AccentColor
Fill.BorderColor3 = Library.AccentColorDark
end
function Slider:Display()
local Suffix = Info.Suffix or ""
if Info.Compact then
DisplayLabel.Text = Info.Text .. ": " .. Slider.Value .. Suffix
elseif Info.HideMax then
DisplayLabel.Text = string.format("%s", Slider.Value .. Suffix)
else
DisplayLabel.Text = string.format("%s/%s", Slider.Value .. Suffix, Slider.Max .. Suffix)
end
local X = math.ceil(Library:MapValue(Slider.Value, Slider.Min, Slider.Max, 0, Slider.MaxSize))
Fill.Size = UDim2.new(0, X, 1, 0)
HideBorderRight.Visible = not (X == Slider.MaxSize or X == 0)
end
function Slider:OnChanged(Func)
Slider.Changed = Func
Func(Slider.Value)
end
local function Round(Value)
if Slider.Rounding == 0 then
return math.floor(Value)
end
return tonumber(string.format("%." .. Slider.Rounding .. "f", Value))
end
function Slider:GetValueFromXOffset(X)
return Round(Library:MapValue(X, 0, Slider.MaxSize, Slider.Min, Slider.Max))
end
function Slider:SetRawValue(Value)
local Num = tonumber(Value)
if not Num then
return
end
Num = math.clamp(Num, Slider.Min, Slider.Max)
Slider.Value = Num
Slider:Display()
end
function Slider:SetValue(Str)
local Num = tonumber(Str)
if not Num then
return
end
Num = math.clamp(Num, Slider.Min, Slider.Max)
Slider.Value = Num
Slider:Display()
Library:SafeCallback("Slider_Callback" .. "_" .. (Idx or ""), Slider.Callback, Slider.Value)
Library:SafeCallback("Slider_Changed" .. "_" .. (Idx or ""), Slider.Changed, Slider.Value)
end
local CurrentAmount = 0.01
local isInputChangedConnected = true
local isInputEndedConnected = false
SliderInner.InputBegan:Connect(function(Input)
isInputEndedConnected = false
if
(
Input.UserInputType == Enum.UserInputType.MouseButton1
or Input.UserInputType == Enum.UserInputType.Touch
) and not Library:MouseIsOverOpenedFrame()
then
local isTouch = Input.UserInputType == Enum.UserInputType.Touch
local startPos = isTouch and Input.Position.X or Mouse.X
local startFillPos = Fill.Size.X.Offset
local diff = startPos - (Fill.AbsolutePosition.X + startFillPos)
while isInputChangedConnected and not isInputEndedConnected do
local newPos = isTouch and Input.Position.X or Mouse.X
local newX = math.clamp(startFillPos + (newPos - startPos) + diff, 0, Slider.MaxSize)
local newValue = Slider:GetValueFromXOffset(newX)
local oldValue = Slider.Value
Slider.Value = newValue
Slider:Display()
if newValue ~= oldValue then
Library:SafeCallback("Slider_Callback" .. "_" .. (Idx or ""), Slider.Callback, Slider.Value)
Library:SafeCallback("Slider_Changed" .. "_" .. (Idx or ""), Slider.Changed, Slider.Value)
end
RenderStepped:Wait()
end
Library:AttemptSave()
end
if Input.KeyCode == Enum.KeyCode.Minus then
CurrentAmount = math.max(CurrentAmount / 10, 0.00001)
end
if Input.KeyCode == Enum.KeyCode.Equals and (CurrentAmount * 10) <= Slider.Max then
CurrentAmount = CurrentAmount * 10
end
if Input.KeyCode == Enum.KeyCode.Right then
Slider:SetValue(Slider.Value + CurrentAmount)
end
if Input.KeyCode == Enum.KeyCode.Left then
Slider:SetValue(Slider.Value - CurrentAmount)
end
end)
SliderInner.InputEnded:Connect(function()
isInputEndedConnected = true
end)
Slider:Display()
Groupbox:AddBlank(Info.BlankSize or 6)
Groupbox:Resize()
if Idx then
Options[Idx] = Slider
Library:RegisterOwnerKey("options", Idx)
end
return Slider
end
function Funcs:AddDropdown(Idx, Info)
if Info.SpecialType == "Player" then
Info.Values = GetPlayersString()
Info.AllowNull = true
elseif Info.SpecialType == "Team" then
Info.Values = GetTeamsString()
Info.AllowNull = true
end
assert(Info.Values, "AddDropdown: Missing dropdown value list.")
assert(
Info.AllowNull or Info.Default,
"AddDropdown: Missing default value. Pass `AllowNull` as true if this was intentional."
)
if not Info.Text then
Info.Compact = true
end
local Dropdown = {
Values = Info.Values,
Value = Info.Multi and {},
SaveValues = Info.SaveValues or false,
Multi = Info.Multi,
Type = "Dropdown",
SpecialType = Info.SpecialType, -- can be either 'Player' or 'Team'
Callback = Info.Callback or function(Value) end,
}
local Groupbox = self
local Container = Groupbox.Container
local RelativeOffset = 0
if not Info.Compact then
local DropdownLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 10),
TextSize = 14,
Text = Info.Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Bottom,
ZIndex = 5,
Parent = Container,
})
Groupbox:AddBlank(3)
end
for _, Element in next, Container:GetChildren() do
if not Element:IsA("UIListLayout") then
RelativeOffset = RelativeOffset + Element.Size.Y.Offset
end
end
local DropdownOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(1, -4, 0, 20),
ZIndex = 5,
Parent = Container,
})
Library:AddToRegistry(DropdownOuter, {
BorderColor3 = "Black",
})
local DropdownInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 6,
Parent = DropdownOuter,
})
Library:AddToRegistry(DropdownInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Color3.new(1, 1, 1)),
ColorSequenceKeypoint.new(1, Color3.fromRGB(212, 212, 212)),
}),
Rotation = 90,
Parent = DropdownInner,
})
local DropdownArrow = Library:Create("ImageLabel", {
AnchorPoint = Vector2.new(0, 0.5),
BackgroundTransparency = 1,
Position = UDim2.new(1, -16, 0.5, 0),
Size = UDim2.new(0, 12, 0, 12),
Image = "http://www.roblox.com/asset/?id=6282522798",
ZIndex = 8,
Parent = DropdownInner,
})
local ItemList = Library:CreateLabel({
Position = UDim2.new(0, 5, 0, 0),
Size = UDim2.new(1, -5, 1, 0),
TextSize = 14,
Text = "--",
TextXAlignment = Enum.TextXAlignment.Left,
TextWrapped = true,
ZIndex = 7,
Parent = DropdownInner,
})
Library:OnHighlight(
DropdownOuter,
DropdownOuter,
{ BorderColor3 = "AccentColor" },
{ BorderColor3 = "Black" }
)
if type(Info.Tooltip) == "string" then
Library:AddToolTip(Info.Tooltip, DropdownOuter)
end
local MAX_DROPDOWN_ITEMS = 8
local ListOuter = Library:Create("Frame", {
BackgroundColor3 = Color3.new(0, 0, 0),
BorderColor3 = Color3.new(0, 0, 0),
ZIndex = 20,
Visible = false,
Name = "ListOuter",
Parent = ScreenGui,
})
local function RecalculateListPosition()
ListOuter.Position = UDim2.fromOffset(
DropdownOuter.AbsolutePosition.X,
DropdownOuter.AbsolutePosition.Y + DropdownOuter.Size.Y.Offset + 1
)
end
local function RecalculateListSize(YSize)
ListOuter.Size = UDim2.fromOffset(DropdownOuter.AbsoluteSize.X, YSize or (MAX_DROPDOWN_ITEMS * 20 + 2))
end
RecalculateListPosition()
RecalculateListSize()
DropdownOuter:GetPropertyChangedSignal("AbsolutePosition"):Connect(RecalculateListPosition)
local ListInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 21,
Parent = ListOuter,
})
Library:AddToRegistry(ListInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
local Scrolling = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
CanvasSize = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 21,
Parent = ListInner,
TopImage = "rbxasset://textures/ui/Scroll/scroll-middle.png",
BottomImage = "rbxasset://textures/ui/Scroll/scroll-middle.png",
ScrollBarThickness = 3,
ScrollBarImageColor3 = Library.AccentColor,
})
Library:AddToRegistry(Scrolling, {
ScrollBarImageColor3 = "AccentColor",
})
Library:Create("UIListLayout", {
Padding = UDim.new(0, 0),
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = Scrolling,
})
function Dropdown:Display()
local Values = Dropdown.Values
local Str = ""
if Info.Multi then
for Idx, Value in next, Values do
if Dropdown.Value[Value] then
Str = Str .. Value .. ", "
end
end
Str = Str:sub(1, #Str - 2)
else
Str = Dropdown.Value or ""
end
ItemList.Text = (Str == "" and "--" or Str)
end
function Dropdown:GetActiveValues()
if Info.Multi then
local T = {}
for Value, Bool in next, Dropdown.Value do
table.insert(T, Value)
end
return T
else
return Dropdown.Value and 1 or 0
end
end
function Dropdown:BuildDropdownList()
local Values = Dropdown.Values
local Buttons = {}
for _, Element in next, Scrolling:GetChildren() do
if not Element:IsA("UIListLayout") then
Element:Destroy()
end
end
local Count = 0
for Idx, Value in next, Values do
local Table = {}
Count = Count + 1
local Button = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Middle,
Size = UDim2.new(1, -1, 0, 20),
ZIndex = 23,
Active = true,
Parent = Scrolling,
})
Library:AddToRegistry(Button, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
local ButtonLabel = Library:CreateLabel({
Active = false,
Size = UDim2.new(1, -6, 1, 0),
Position = UDim2.new(0, 6, 0, 0),
TextSize = 14,
Text = Value,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 25,
Parent = Button,
})
Library:OnHighlight(
Button,
Button,
{ BorderColor3 = "AccentColor", ZIndex = 24 },
{ BorderColor3 = "OutlineColor", ZIndex = 23 }
)
local Selected
if Info.Multi then
Selected = Dropdown.Value[Value]
else
Selected = Dropdown.Value == Value
end
function Table:UpdateButton()
if Info.Multi then
Selected = Dropdown.Value[Value]
else
Selected = Dropdown.Value == Value
end
ButtonLabel.TextColor3 = Selected and Library.AccentColor or Library.FontColor
Library.RegistryMap[ButtonLabel].Properties.TextColor3 = Selected and "AccentColor"
or "FontColor"
end
ButtonLabel.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.MouseButton1
or Input.UserInputType == Enum.UserInputType.Touch
then
local Try = not Selected
if Dropdown:GetActiveValues() == 1 and not Try and not Info.AllowNull then
else
if Info.Multi then
Selected = Try
if Selected then
Dropdown.Value[Value] = true
else
Dropdown.Value[Value] = nil
end
else
Selected = Try
if Selected then
Dropdown.Value = Value
else
Dropdown.Value = nil
end
for _, OtherButton in next, Buttons do
OtherButton:UpdateButton()
end
Library:UpdateDependencyBoxes()
end
Table:UpdateButton()
Dropdown:Display()
Library:SafeCallback(
"Dropdown_Callback" .. "_" .. (Idx or ""),
Dropdown.Callback,
Dropdown.Value
)
Library:SafeCallback(
"Dropdown_Changed" .. "_" .. (Idx or ""),
Dropdown.Changed,
Dropdown.Value
)
Library:AttemptSave()
end
end
end)
Table:UpdateButton()
Dropdown:Display()
Buttons[Button] = Table
end
Scrolling.CanvasSize = UDim2.fromOffset(0, (Count * 20) + 1)
local Y = math.clamp(Count * 20, 0, MAX_DROPDOWN_ITEMS * 20) + 1
RecalculateListSize(Y)
end
function Dropdown:SetValues(NewValues)
if NewValues then
Dropdown.Values = NewValues
end
Dropdown:BuildDropdownList()
end
function Dropdown:OpenDropdown()
ListOuter.Visible = true
Library.OpenedFrames[ListOuter] = true
DropdownArrow.Rotation = 180
end
function Dropdown:CloseDropdown()
ListOuter.Visible = false
Library.OpenedFrames[ListOuter] = nil
DropdownArrow.Rotation = 0
end
function Dropdown:OnChanged(Func)
Dropdown.Changed = Func
Func(Dropdown.Value)
end
function Dropdown:SetRawValue(Val)
if Dropdown.Multi then
local nTable = {}
for Value, Bool in next, Val do
if table.find(Dropdown.Values, Value) then
nTable[Value] = true
end
end
Dropdown.Value = nTable
else
if not Val then
Dropdown.Value = nil
elseif table.find(Dropdown.Values, Val) then
Dropdown.Value = Val
end
end
Dropdown:BuildDropdownList()
end
function Dropdown:SetValue(Val)
if Dropdown.Multi then
local nTable = {}
for Value, Bool in next, Val do
if table.find(Dropdown.Values, Value) then
nTable[Value] = true
end
end
Dropdown.Value = nTable
else
if not Val then
Dropdown.Value = nil
elseif table.find(Dropdown.Values, Val) then
Dropdown.Value = Val
end
end
Dropdown:BuildDropdownList()
Library:SafeCallback("Dropdown_Callback" .. "_" .. (Idx or ""), Dropdown.Callback, Dropdown.Value)
Library:SafeCallback("Dropdown_Changed" .. "_" .. (Idx or ""), Dropdown.Changed, Dropdown.Value)
end
DropdownOuter.InputBegan:Connect(function(Input)
if
(
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
) and not Library:MouseIsOverOpenedFrame()
then
if ListOuter.Visible then
Dropdown:CloseDropdown()
else
Dropdown:OpenDropdown()
end
end
end)
InputService.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
local AbsPos, AbsSize = ListOuter.AbsolutePosition, ListOuter.AbsoluteSize
if
Mouse.X < AbsPos.X
or Mouse.X > AbsPos.X + AbsSize.X
or Mouse.Y < (AbsPos.Y - 20 - 1)
or Mouse.Y > AbsPos.Y + AbsSize.Y
then
Dropdown:CloseDropdown()
end
end
end)
Dropdown:BuildDropdownList()
Dropdown:Display()
local Defaults = {}
if type(Info.Default) == "string" then
local Idx = table.find(Dropdown.Values, Info.Default)
if Idx then
table.insert(Defaults, Idx)
end
elseif type(Info.Default) == "table" then
for _, Value in next, Info.Default do
local Idx = table.find(Dropdown.Values, Value)
if Idx then
table.insert(Defaults, Idx)
end
end
elseif type(Info.Default) == "number" and Dropdown.Values[Info.Default] ~= nil then
table.insert(Defaults, Info.Default)
end
if next(Defaults) then
for i = 1, #Defaults do
local Index = Defaults[i]
if Info.Multi then
Dropdown.Value[Dropdown.Values[Index]] = true
else
Dropdown.Value = Dropdown.Values[Index]
end
if not Info.Multi then
break
end
end
Dropdown:BuildDropdownList()
Dropdown:Display()
end
Groupbox:AddBlank(Info.BlankSize or 5)
Groupbox:Resize()
if Idx then
Options[Idx] = Dropdown
Library:RegisterOwnerKey("options", Idx)
end
return Dropdown
end
function Funcs:AddDependencyBox()
local Depbox = {
Dependencies = {},
}
local Groupbox = self
local Container = Groupbox.Container
local Holder = Library:Create("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 0, 0),
Visible = false,
Parent = Container,
})
local Frame = Library:Create("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Visible = true,
Parent = Holder,
})
local Layout = Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = Frame,
})
function Depbox:Resize()
Holder.Size = UDim2.new(1, 0, 0, Layout.AbsoluteContentSize.Y)
Groupbox:Resize()
end
Layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
Depbox:Resize()
end)
Holder:GetPropertyChangedSignal("Visible"):Connect(function()
Depbox:Resize()
end)
function Depbox:Update()
for _, Dependency in next, Depbox.Dependencies do
local Elem = Dependency[1]
local Value = Dependency[2]
if Elem.Type == "Toggle" and Elem.Value ~= Value then
Holder.Visible = false
Depbox:Resize()
return
end
if Elem.Type == "Dropdown" and Elem.Value ~= Value then
Holder.Visible = false
Depbox:Resize()
return
end
end
Holder.Visible = true
Depbox:Resize()
end
function Depbox:SetupDependencies(Dependencies)
for _, Dependency in next, Dependencies do
assert(type(Dependency) == "table", "SetupDependencies: Dependency is not of type `table`.")
assert(Dependency[1], "SetupDependencies: Dependency is missing element argument.")
assert(Dependency[2] ~= nil, "SetupDependencies: Dependency is missing value argument.")
end
Depbox.Dependencies = Dependencies
Depbox:Update()
end
Depbox.Container = Frame
setmetatable(Depbox, BaseGroupbox)
table.insert(Library.DependencyBoxes, Depbox)
return Depbox
end
BaseGroupbox.__index = Funcs
BaseGroupbox.__namecall = function(Table, Key, ...)
return Funcs[Key](...)
end
end
-- < Create other UI elements >
do
Library.NotificationArea = Library:Create("Frame", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 40),
Size = UDim2.new(0, 300, 0, 200),
ZIndex = 100,
Parent = ScreenGui,
})
Library:Create("UIListLayout", {
Padding = UDim.new(0, 4),
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = Library.NotificationArea,
})
local WatermarkOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 100, 0, -25),
Size = UDim2.new(0, 213, 0, 20),
ZIndex = 200,
Visible = false,
Parent = ScreenGui,
})
local WatermarkInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 201,
Parent = WatermarkOuter,
})
Library:AddToRegistry(WatermarkInner, {
BorderColor3 = "OutlineColor",
})
local ColorFrame = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 204,
Parent = WatermarkInner,
})
Library:AddToRegistry(ColorFrame, {
BackgroundColor3 = "AccentColor",
}, true)
local InnerFrame = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Position = UDim2.new(0, 1, 0, 1),
Size = UDim2.new(1, -2, 1, -2),
ZIndex = 202,
Parent = WatermarkInner,
})
local Gradient = Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
}),
Rotation = -90,
Parent = InnerFrame,
})
Library:AddToRegistry(Gradient, {
Color = function()
return ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
})
end,
})
local WatermarkLabel = Library:CreateLabel({
Position = UDim2.new(0, 5, 0, 1),
Size = UDim2.new(1, -4, 1, 0),
TextColor3 = Library.AccentColor,
TextSize = 14,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 203,
Parent = InnerFrame,
})
Library:AddToRegistry(WatermarkLabel, {
TextColor3 = "AccentColor",
}, true)
Library.Watermark = WatermarkOuter
Library.Watermark.Visible = false
Library.WatermarkText = WatermarkLabel
Library:MakeDraggable(Library.Watermark)
local InfoLoggerOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 15, 0.5, 0),
Size = UDim2.new(0, 210, 0, 20),
Visible = false,
ZIndex = 287,
Parent = ScreenGui,
})
local InfoLoggerInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 288,
Parent = InfoLoggerOuter,
})
Library:AddToRegistry(InfoLoggerInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local InfoColorFrame = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 299,
Parent = InfoLoggerInner,
})
Library:AddToRegistry(InfoColorFrame, {
BackgroundColor3 = "AccentColor",
}, true)
local InfoLoggerLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 20),
Position = UDim2.fromOffset(5, 2),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = Library.AccentColor,
Text = "Info Logger",
TextSize = 14,
ZIndex = 300,
Parent = InfoLoggerInner,
})
Library:AddToRegistry(InfoLoggerLabel, {
TextColor3 = "AccentColor",
}, true)
local InfoLoggerContainer = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -20),
Position = UDim2.new(0, 0, 0, 20),
ZIndex = 1,
ScrollBarThickness = 0,
Parent = InfoLoggerInner,
})
local InfoUIListLayout = Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = InfoLoggerContainer,
})
InfoUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
InfoLoggerContainer.CanvasSize = UDim2.fromOffset(0, InfoUIListLayout.AbsoluteContentSize.Y)
end)
Library:Create("UIPadding", {
PaddingLeft = UDim.new(0, 5),
Parent = InfoLoggerContainer,
})
---@param InputObject InputObject
Library:GiveSignal(InfoLoggerOuter.InputBegan:Connect(function(InputObject)
if InputObject.UserInputType ~= Enum.UserInputType.Keyboard then
return
end
if
InputObject.KeyCode == Enum.KeyCode.Z
and game:GetService("UserInputService"):IsKeyDown(Enum.KeyCode.LeftControl)
then
local kbh = Library.InfoLoggerData.KeyBlacklistHistory
local kbl = Library.InfoLoggerData.KeyBlacklistList
local front = kbh[1]
if not front then
return
end
kbl[front] = nil
table.remove(kbh, 1)
Library:RefreshInfoLogger()
Library:RefreshEffectLogger()
Library:RefreshSkillLogger()
if Options and Options.BlacklistedKeys then
Options.BlacklistedKeys:SetValues(Library:KeyBlacklists())
end
Library:Notify(string.format("Re-whitelisted key '%s' into list.", front))
end
if InputObject.KeyCode == Enum.KeyCode.Q then
Library.InfoLoggerCycle = math.max(Library.InfoLoggerCycle - 1, 1)
Library:RefreshInfoLogger()
end
if InputObject.KeyCode == Enum.KeyCode.E then
Library.InfoLoggerCycle = math.min(Library.InfoLoggerCycle + 1, #Library.InfoLoggerCycles)
Library:RefreshInfoLogger()
end
end))
-- default cycle is animation.
Library.InfoLoggerLabel = InfoLoggerLabel
Library.InfoLoggerFrame = InfoLoggerOuter
Library.InfoLoggerContainer = InfoLoggerContainer
Library.InfoLoggerCycle = 1
Library.InfoLoggerCycles = {
"Animation",
"Existing Anim",
"Keyframe",
"Telemetry",
"UsingSkill",
"Part",
"Sound",
}
Library.InfoLoggerData = {
MissingDataEntries = {},
KeyBlacklistHistory = {},
KeyBlacklistList = {},
}
Library:MakeDraggable(InfoLoggerOuter)
Library:RefreshInfoLogger()
local EffectLoggerOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 15, 0.5, 200),
Size = UDim2.new(0, 210, 0, 20),
Visible = false,
ZIndex = 287,
Parent = ScreenGui,
})
local EffectLoggerInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 288,
Parent = EffectLoggerOuter,
})
Library:AddToRegistry(EffectLoggerInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local EffectColorFrame = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 299,
Parent = EffectLoggerInner,
})
Library:AddToRegistry(EffectColorFrame, {
BackgroundColor3 = "AccentColor",
}, true)
local EffectLoggerLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 20),
Position = UDim2.fromOffset(5, 2),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = Library.AccentColor,
Text = "Effect Logger",
TextSize = 14,
ZIndex = 300,
Parent = EffectLoggerInner,
})
Library:AddToRegistry(EffectLoggerLabel, {
TextColor3 = "AccentColor",
}, true)
local EffectLoggerContainer = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -20),
Position = UDim2.new(0, 0, 0, 20),
ZIndex = 1,
ScrollBarThickness = 0,
Parent = EffectLoggerInner,
})
local EffectUIListLayout = Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = EffectLoggerContainer,
})
EffectUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
EffectLoggerContainer.CanvasSize = UDim2.fromOffset(0, EffectUIListLayout.AbsoluteContentSize.Y)
end)
Library:Create("UIPadding", {
PaddingLeft = UDim.new(0, 5),
Parent = EffectLoggerContainer,
})
Library.EffectLoggerLabel = EffectLoggerLabel
Library.EffectLoggerFrame = EffectLoggerOuter
Library.EffectLoggerContainer = EffectLoggerContainer
Library.EffectLoggerData = {
Entries = {},
}
Library:MakeDraggable(EffectLoggerOuter)
Library:RefreshEffectLogger()
local SkillLoggerOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 15, 0.5, 400),
Size = UDim2.new(0, 210, 0, 20),
Visible = false,
ZIndex = 287,
Parent = ScreenGui,
})
local SkillLoggerInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 288,
Parent = SkillLoggerOuter,
})
Library:AddToRegistry(SkillLoggerInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local SkillColorFrame = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 299,
Parent = SkillLoggerInner,
})
Library:AddToRegistry(SkillColorFrame, {
BackgroundColor3 = "AccentColor",
}, true)
local SkillLoggerLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 20),
Position = UDim2.fromOffset(5, 2),
TextXAlignment = Enum.TextXAlignment.Left,
TextColor3 = Library.AccentColor,
Text = "Skill Logger",
TextSize = 14,
ZIndex = 300,
Parent = SkillLoggerInner,
})
Library:AddToRegistry(SkillLoggerLabel, {
TextColor3 = "AccentColor",
}, true)
local SkillLoggerContainer = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -20),
Position = UDim2.new(0, 0, 0, 20),
ZIndex = 1,
ScrollBarThickness = 0,
Parent = SkillLoggerInner,
})
local SkillUIListLayout = Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = SkillLoggerContainer,
})
SkillUIListLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
SkillLoggerContainer.CanvasSize = UDim2.fromOffset(0, SkillUIListLayout.AbsoluteContentSize.Y)
end)
Library:Create("UIPadding", {
PaddingLeft = UDim.new(0, 5),
Parent = SkillLoggerContainer,
})
Library.SkillLoggerLabel = SkillLoggerLabel
Library.SkillLoggerFrame = SkillLoggerOuter
Library.SkillLoggerContainer = SkillLoggerContainer
Library.SkillLoggerData = {
Entries = {},
}
Library:MakeDraggable(SkillLoggerOuter)
Library:RefreshSkillLogger()
local KeybindOuter = Library:Create("Frame", {
AnchorPoint = Vector2.new(0, 0.5),
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 10, 0.5, 0),
Size = UDim2.new(0, 210, 0, 20),
Visible = false,
ZIndex = 100,
Parent = ScreenGui,
})
local KeybindInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 101,
Parent = KeybindOuter,
})
Library:AddToRegistry(KeybindInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local ColorFrame = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 102,
Parent = KeybindInner,
})
Library:AddToRegistry(ColorFrame, {
BackgroundColor3 = "AccentColor",
}, true)
local KeybindLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 20),
Position = UDim2.fromOffset(5, 2),
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 14,
TextColor3 = Library.AccentColor,
Text = "Keybind List",
ZIndex = 104,
Parent = KeybindInner,
})
Library:AddToRegistry(KeybindLabel, {
TextColor3 = "AccentColor",
}, true)
local KeybindContainer = Library:Create("Frame", {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, -20),
Position = UDim2.new(0, 0, 0, 20),
ZIndex = 1,
Parent = KeybindInner,
})
Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = KeybindContainer,
})
Library:Create("UIPadding", {
PaddingLeft = UDim.new(0, 5),
Parent = KeybindContainer,
})
Library.KeybindFrame = KeybindOuter
Library.KeybindFrame.Visible = false
Library.KeybindContainer = KeybindContainer
Library:MakeDraggable(KeybindOuter)
end
function Library:SetWatermarkVisibility(Bool)
Library.Watermark.Visible = Bool
end
function Library:SetWatermark(Text)
local X, Y = Library:GetTextBounds(Text, Library.Font, 14)
Library.WatermarkText.Text = Text
Library.Watermark.Size = UDim2.new(0, X + 15, 0, (Y * 1.5) + 3)
end
function Library:ManuallyManagedNotify(Text)
if shared.Lycoris.silent then
return
end
local XSize, YSize = Library:GetTextBounds(Text, Library.Font, 14)
YSize = YSize + 7
local NotifyOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 100, 0, 10),
Size = UDim2.new(0, 0, 0, YSize),
ClipsDescendants = true,
ZIndex = 100,
Parent = Library.NotificationArea,
})
local NotifyInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 101,
Parent = NotifyOuter,
})
Library:AddToRegistry(NotifyInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local InnerFrame = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Position = UDim2.new(0, 1, 0, 1),
Size = UDim2.new(1, -2, 1, -2),
ZIndex = 102,
Parent = NotifyInner,
})
local Gradient = Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
}),
Rotation = -90,
Parent = InnerFrame,
})
Library:AddToRegistry(Gradient, {
Color = function()
return ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
})
end,
})
local NotifyLabel = Library:CreateLabel({
Position = UDim2.new(0, 4, 0, 0),
Size = UDim2.new(1, -4, 1, 0),
Text = Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 14,
ZIndex = 103,
Parent = InnerFrame,
})
local LeftColor = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Position = UDim2.new(0, -1, 0, -1),
Size = UDim2.new(0, 3, 1, 2),
ZIndex = 104,
Parent = NotifyOuter,
})
Library:AddToRegistry(LeftColor, {
BackgroundColor3 = "AccentColor",
}, true)
pcall(NotifyOuter.TweenSize, NotifyOuter, UDim2.new(0, XSize + 8 + 4, 0, YSize), "Out", "Quad", 0.4, true)
local TweenOutCalled = false
local function TweenOut()
if TweenOutCalled then
return
end
TweenOutCalled = true
pcall(NotifyOuter.TweenSize, NotifyOuter, UDim2.new(0, 0, 0, YSize), "Out", "Quad", 0.4, true)
task.wait(0.4)
NotifyOuter:Destroy()
end
local Connection = nil
local Connection2 = nil
Connection = InnerFrame.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
TweenOut()
Connection:Disconnect()
end
end)
Connection2 = InnerFrame.MouseEnter:Connect(function()
if game:GetService("UserInputService"):IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
TweenOut()
Connection2:Disconnect()
end
end)
return TweenOut
end
function Library:Notify(Text, Time)
if shared.Lycoris.silent then
return
end
local XSize, YSize = Library:GetTextBounds(Text, Library.Font, 14)
YSize = YSize + 7
local NotifyOuter = Library:Create("Frame", {
BorderColor3 = Color3.new(0, 0, 0),
Position = UDim2.new(0, 100, 0, 10),
Size = UDim2.new(0, 0, 0, YSize),
ClipsDescendants = true,
ZIndex = 100,
Parent = Library.NotificationArea,
})
local NotifyInner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 101,
Parent = NotifyOuter,
})
Library:AddToRegistry(NotifyInner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
}, true)
local InnerFrame = Library:Create("Frame", {
BackgroundColor3 = Color3.new(1, 1, 1),
BorderSizePixel = 0,
Position = UDim2.new(0, 1, 0, 1),
Size = UDim2.new(1, -2, 1, -2),
ZIndex = 102,
Parent = NotifyInner,
})
local Gradient = Library:Create("UIGradient", {
Color = ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
}),
Rotation = -90,
Parent = InnerFrame,
})
Library:AddToRegistry(Gradient, {
Color = function()
return ColorSequence.new({
ColorSequenceKeypoint.new(0, Library:GetDarkerColor(Library.MainColor)),
ColorSequenceKeypoint.new(1, Library.MainColor),
})
end,
})
local NotifyLabel = Library:CreateLabel({
Position = UDim2.new(0, 4, 0, 0),
Size = UDim2.new(1, -4, 1, 0),
Text = Text,
TextXAlignment = Enum.TextXAlignment.Left,
TextSize = 14,
ZIndex = 103,
Parent = InnerFrame,
})
local LeftColor = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Position = UDim2.new(0, -1, 0, -1),
Size = UDim2.new(0, 3, 1, 2),
ZIndex = 104,
Parent = NotifyOuter,
})
Library:AddToRegistry(LeftColor, {
BackgroundColor3 = "AccentColor",
}, true)
pcall(NotifyOuter.TweenSize, NotifyOuter, UDim2.new(0, XSize + 8 + 4, 0, YSize), "Out", "Quad", 0.4, true)
local function TweenOut()
pcall(NotifyOuter.TweenSize, NotifyOuter, UDim2.new(0, 0, 0, YSize), "Out", "Quad", 0.4, true)
task.wait(0.4)
NotifyOuter:Destroy()
end
local Connection = nil
local Connection2 = nil
Connection = InnerFrame.InputBegan:Connect(function(Input)
if Input.UserInputType == Enum.UserInputType.MouseButton1 then
TweenOut()
Connection:Disconnect()
end
end)
Connection2 = InnerFrame.MouseEnter:Connect(function()
if game:GetService("UserInputService"):IsMouseButtonPressed(Enum.UserInputType.MouseButton1) then
TweenOut()
Connection2:Disconnect()
end
end)
task.spawn(function()
task.wait(Time or 5)
TweenOut()
end)
end
function Library:CreateWindow(...)
local Arguments = { ... }
local Config = { AnchorPoint = Vector2.zero }
if type(...) == "table" then
Config = ...
else
Config.Title = Arguments[1]
Config.AutoShow = Arguments[2] or false
end
if type(Config.Title) ~= "string" then
Config.Title = "No title"
end
if type(Config.TabPadding) ~= "number" then
Config.TabPadding = 0
end
if type(Config.MenuFadeTime) ~= "number" then
Config.MenuFadeTime = 0.2
end
if typeof(Config.Position) ~= "UDim2" then
Config.Position = UDim2.fromOffset(175, 50)
end
if typeof(Config.Size) ~= "UDim2" then
Config.Size = UDim2.fromOffset(550, 600)
end
if Config.Center then
Config.AnchorPoint = Vector2.new(0.5, 0.5)
Config.Position = UDim2.fromScale(0.5, 0.5)
end
local Window = {
Tabs = {},
}
local Outer = Library:Create("Frame", {
AnchorPoint = Config.AnchorPoint,
BackgroundColor3 = Color3.new(0, 0, 0),
BorderSizePixel = 0,
Position = Config.Position,
Size = Config.Size,
Visible = false,
ZIndex = 1,
Parent = ScreenGui,
})
Library:MakeDraggable(Outer, 25)
local Inner = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.AccentColor,
BorderMode = Enum.BorderMode.Inset,
Position = UDim2.new(0, 1, 0, 1),
Size = UDim2.new(1, -2, 1, -2),
ZIndex = 1,
Parent = Outer,
})
Library:AddToRegistry(Inner, {
BackgroundColor3 = "MainColor",
BorderColor3 = "AccentColor",
})
local WindowLabel = Library:CreateLabel({
Position = UDim2.new(0, 7, 0, 0),
Size = UDim2.new(0, 0, 0, 25),
Text = Config.Title or "",
TextColor3 = Library.AccentColor,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 1,
Parent = Inner,
})
Library:AddToRegistry(WindowLabel, {
TextColor3 = "AccentColor",
})
local MainSectionOuter = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
Position = UDim2.new(0, 8, 0, 25),
Size = UDim2.new(1, -16, 1, -33),
ZIndex = 1,
Parent = Inner,
})
Library:AddToRegistry(MainSectionOuter, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
local MainSectionInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Color3.new(0, 0, 0),
BorderMode = Enum.BorderMode.Inset,
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
ZIndex = 1,
Parent = MainSectionOuter,
})
Library:AddToRegistry(MainSectionInner, {
BackgroundColor3 = "BackgroundColor",
})
local TabArea = Library:Create("Frame", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 8, 0, 8),
Size = UDim2.new(1, -16, 0, 21),
ZIndex = 1,
Parent = MainSectionInner,
})
local TabListLayout = Library:Create("UIListLayout", {
Padding = UDim.new(0, Config.TabPadding),
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = TabArea,
})
local TabContainer = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Library.OutlineColor,
Position = UDim2.new(0, 8, 0, 30),
Size = UDim2.new(1, -16, 1, -38),
ZIndex = 2,
Parent = MainSectionInner,
})
Library:AddToRegistry(TabContainer, {
BackgroundColor3 = "MainColor",
BorderColor3 = "OutlineColor",
})
function Window:SetWindowTitle(Title)
WindowLabel.Text = Title
end
---Add a tab to the window.
---@param Name string
---@return table
function Window:AddTab(Name)
local Tab = {
GroupboxCount = 0,
TabboxCount = 0,
Groupboxes = {},
Tabboxes = {},
}
local TabButtonWidth = Library:GetTextBounds(Name, Library.Font, 16)
local TabButton = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
Size = UDim2.new(0, TabButtonWidth + 8 + 4, 1, 0),
ZIndex = 1,
Parent = TabArea,
})
Library:AddToRegistry(TabButton, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
local TabButtonLabel = Library:CreateLabel({
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, -1),
Text = Name,
ZIndex = 1,
Parent = TabButton,
})
local Blocker = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderSizePixel = 0,
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, 0, 0, 1),
BackgroundTransparency = 1,
ZIndex = 3,
Parent = TabButton,
})
Library:AddToRegistry(Blocker, {
BackgroundColor3 = "MainColor",
})
local TabFrame = Library:Create("Frame", {
Name = "TabFrame",
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 0),
Size = UDim2.new(1, 0, 1, 0),
Visible = false,
ZIndex = 2,
Parent = TabContainer,
})
local LeftSide = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = UDim2.new(0, 8 - 1, 0, 8 - 1),
Size = UDim2.new(0.5, -12 + 2, 0, 507 + 2),
CanvasSize = UDim2.new(0, 0, 0, 0),
BottomImage = "",
TopImage = "",
ScrollBarThickness = 0,
ZIndex = 2,
Parent = TabFrame,
})
local RightSide = Library:Create("ScrollingFrame", {
BackgroundTransparency = 1,
BorderSizePixel = 0,
Position = UDim2.new(0.5, 4 + 1, 0, 8 - 1),
Size = UDim2.new(0.5, -12 + 2, 0, 507 + 2),
CanvasSize = UDim2.new(0, 0, 0, 0),
BottomImage = "",
TopImage = "",
ScrollBarThickness = 0,
ZIndex = 2,
Parent = TabFrame,
})
Library:Create("UIListLayout", {
Padding = UDim.new(0, 8),
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Parent = LeftSide,
})
Library:Create("UIListLayout", {
Padding = UDim.new(0, 8),
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
Parent = RightSide,
})
for _, Side in next, { LeftSide, RightSide } do
Side:WaitForChild("UIListLayout"):GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
Side.CanvasSize = UDim2.fromOffset(0, Side.UIListLayout.AbsoluteContentSize.Y)
end)
end
function Tab:ShowTab()
for _, Tab in next, Window.Tabs do
Tab:HideTab()
end
Blocker.BackgroundTransparency = 0
TabButton.BackgroundColor3 = Library.MainColor
Library.RegistryMap[TabButton].Properties.BackgroundColor3 = "MainColor"
TabFrame.Visible = true
end
function Tab:HideTab()
Blocker.BackgroundTransparency = 1
TabButton.BackgroundColor3 = Library.BackgroundColor
Library.RegistryMap[TabButton].Properties.BackgroundColor3 = "BackgroundColor"
TabFrame.Visible = false
end
function Tab:SetLayoutOrder(Position)
TabButton.LayoutOrder = Position
TabListLayout:ApplyLayout()
end
function Tab:AddGroupbox(Info)
local Groupbox = { Name = Info.Name }
local BoxOuter = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 0, 507 + 2),
ZIndex = 2,
Parent = Info.Side == 1 and LeftSide or RightSide,
})
Library:AddToRegistry(BoxOuter, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
local BoxInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Color3.new(0, 0, 0),
-- BorderMode = Enum.BorderMode.Inset;
Size = UDim2.new(1, -2, 1, -2),
Position = UDim2.new(0, 1, 0, 1),
ZIndex = 4,
Parent = BoxOuter,
})
Library:AddToRegistry(BoxInner, {
BackgroundColor3 = "BackgroundColor",
})
local Highlight = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 5,
Parent = BoxInner,
})
Library:AddToRegistry(Highlight, {
BackgroundColor3 = "AccentColor",
})
local GroupboxLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 0, 18),
Position = UDim2.new(0, 4, 0, 2),
TextSize = 14,
Text = Info.Name,
TextXAlignment = Enum.TextXAlignment.Left,
ZIndex = 5,
Parent = BoxInner,
})
local Container = Library:Create("Frame", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 4, 0, 20),
Size = UDim2.new(1, -4, 1, -20),
ZIndex = 1,
Parent = BoxInner,
})
Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = Container,
})
function Groupbox:Resize()
local Size = 0
for _, Element in next, Groupbox.Container:GetChildren() do
if (not Element:IsA("UIListLayout")) and Element.Visible then
Size = Size + Element.Size.Y.Offset
end
end
BoxOuter.Size = UDim2.new(1, 0, 0, 20 + Size + 2 + 2)
end
Groupbox.Container = Container
setmetatable(Groupbox, BaseGroupbox)
Groupbox:AddBlank(3)
Groupbox:Resize()
Tab.GroupboxCount = Tab.GroupboxCount + 1
Tab.Groupboxes[Info.Name] = Groupbox
return Groupbox
end
function Tab:AddDynamicGroupbox(Name)
if (Tab.GroupboxCount + Tab.TabboxCount) % 2 == 0 then
return Tab:AddLeftGroupbox(Name)
else
return Tab:AddRightGroupbox(Name)
end
end
function Tab:AddLeftGroupbox(Name)
return Tab:AddGroupbox({ Side = 1, Name = Name })
end
function Tab:AddRightGroupbox(Name)
return Tab:AddGroupbox({ Side = 2, Name = Name })
end
function Tab:AddTabbox(Info)
local Tabbox = {
Tabs = {},
}
local BoxOuter = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Library.OutlineColor,
BorderMode = Enum.BorderMode.Inset,
Size = UDim2.new(1, 0, 0, 0),
ZIndex = 2,
Parent = Info.Side == 1 and LeftSide or RightSide,
})
Library:AddToRegistry(BoxOuter, {
BackgroundColor3 = "BackgroundColor",
BorderColor3 = "OutlineColor",
})
local BoxInner = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderColor3 = Color3.new(0, 0, 0),
-- BorderMode = Enum.BorderMode.Inset;
Size = UDim2.new(1, -2, 1, -2),
Position = UDim2.new(0, 1, 0, 1),
ZIndex = 4,
Parent = BoxOuter,
})
Library:AddToRegistry(BoxInner, {
BackgroundColor3 = "BackgroundColor",
})
local Highlight = Library:Create("Frame", {
BackgroundColor3 = Library.AccentColor,
BorderSizePixel = 0,
Size = UDim2.new(1, 0, 0, 2),
ZIndex = 10,
Parent = BoxInner,
})
Library:AddToRegistry(Highlight, {
BackgroundColor3 = "AccentColor",
})
local TabboxButtons = Library:Create("Frame", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 0, 0, 1),
Size = UDim2.new(1, 0, 0, 18),
ZIndex = 5,
Parent = BoxInner,
})
Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = TabboxButtons,
})
function Tabbox:AddTab(Name)
local Tab = {}
local Button = Library:Create("Frame", {
BackgroundColor3 = Library.MainColor,
BorderColor3 = Color3.new(0, 0, 0),
Size = UDim2.new(0.5, 0, 1, 0),
ZIndex = 6,
Parent = TabboxButtons,
})
Library:AddToRegistry(Button, {
BackgroundColor3 = "MainColor",
})
local ButtonLabel = Library:CreateLabel({
Size = UDim2.new(1, 0, 1, 0),
TextSize = 14,
Text = Name,
TextXAlignment = Enum.TextXAlignment.Center,
ZIndex = 7,
Parent = Button,
})
local Block = Library:Create("Frame", {
BackgroundColor3 = Library.BackgroundColor,
BorderSizePixel = 0,
Position = UDim2.new(0, 0, 1, 0),
Size = UDim2.new(1, 0, 0, 1),
Visible = false,
ZIndex = 9,
Parent = Button,
})
Library:AddToRegistry(Block, {
BackgroundColor3 = "BackgroundColor",
})
local Container = Library:Create("Frame", {
BackgroundTransparency = 1,
Position = UDim2.new(0, 4, 0, 20),
Size = UDim2.new(1, -4, 1, -20),
ZIndex = 1,
Visible = false,
Parent = BoxInner,
})
Library:Create("UIListLayout", {
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Parent = Container,
})
function Tab:Show()
for _, Tab in next, Tabbox.Tabs do
Tab:Hide()
end
Container.Visible = true
Block.Visible = true
Button.BackgroundColor3 = Library.BackgroundColor
Library.RegistryMap[Button].Properties.BackgroundColor3 = "BackgroundColor"
Tab:Resize()
end
function Tab:Hide()
Container.Visible = false
Block.Visible = false
Button.BackgroundColor3 = Library.MainColor
Library.RegistryMap[Button].Properties.BackgroundColor3 = "MainColor"
end
function Tab:Resize()
local TabCount = 0
for _, Tab in next, Tabbox.Tabs do
TabCount = TabCount + 1
end
for _, Button in next, TabboxButtons:GetChildren() do
if not Button:IsA("UIListLayout") then
Button.Size = UDim2.new(1 / TabCount, 0, 1, 0)
end
end
if not Container.Visible then
return
end
local Size = 0
for _, Element in next, Tab.Container:GetChildren() do
if (not Element:IsA("UIListLayout")) and Element.Visible then
Size = Size + Element.Size.Y.Offset
end
end
BoxOuter.Size = UDim2.new(1, 0, 0, 20 + Size + 2 + 2)
end
Button.InputBegan:Connect(function(Input)
if
(
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
) and not Library:MouseIsOverOpenedFrame()
then
Tab:Show()
Tab:Resize()
end
end)
Tab.Container = Container
Tabbox.Tabs[Name] = Tab
setmetatable(Tab, BaseGroupbox)
Tab:AddBlank(3)
Tab:Resize()
-- Show first tab (number is 2 cus of the UIListLayout that also sits in that instance)
if #TabboxButtons:GetChildren() == 2 then
Tab:Show()
end
return Tab
end
Tab.Tabboxes[Info.Name or ""] = Tabbox
Tab.TabboxCount = Tab.TabboxCount + 1
return Tabbox
end
function Tab:AddLeftTabbox(Name)
return Tab:AddTabbox({ Name = Name, Side = 1 })
end
function Tab:AddRightTabbox(Name)
return Tab:AddTabbox({ Name = Name, Side = 2 })
end
function Tab:AddDynamicTabbox(Name)
if (Tab.GroupboxCount + Tab.TabboxCount) % 2 == 0 then
return Tab:AddLeftTabbox(Name)
else
return Tab:AddRightTabbox(Name)
end
end
TabButton.InputBegan:Connect(function(Input)
if
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.MouseButton1
then
Tab:ShowTab()
end
end)
-- This was the first tab added, so we show it by default.
if #TabContainer:GetChildren() == 1 then
Tab:ShowTab()
end
Window.Tabs[Name] = Tab
return Tab
end
local ModalElement = Library:Create("TextButton", {
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 0, 0),
Visible = true,
Text = "",
Modal = false,
Parent = ScreenGui,
})
local TransparencyCache = {}
local Fading = false
local FirstTime = false
function Library:Toggle()
if Fading then
return
end
local FadeTime = Config.MenuFadeTime
local ShouldFade = FadeTime > 0.01
if ShouldFade then
Fading = true
end
Toggled = not Toggled
ModalElement.Modal = Toggled
if Toggled then
Outer.Visible = true
end
if not Toggled then
for _, ColorPicker in next, ColorPickers do
ColorPicker:Hide()
end
for _, ContextMenu in next, ContextMenus do
ContextMenu:Hide()
end
for _, Tooltip in next, Tooltips do
Tooltip.Visible = false
end
for _, ModeSelectFrame in next, ModeSelectFrames do
ModeSelectFrame.Visible = false
end
end
if ShouldFade or not FirstTime then
for _, Desc in next, Outer:GetDescendants() do
local Properties = {}
if Desc:IsA("ImageLabel") then
table.insert(Properties, "ImageTransparency")
table.insert(Properties, "BackgroundTransparency")
elseif Desc:IsA("TextLabel") or Desc:IsA("TextBox") then
table.insert(Properties, "TextTransparency")
elseif Desc:IsA("Frame") or Desc:IsA("ScrollingFrame") then
table.insert(Properties, "BackgroundTransparency")
elseif Desc:IsA("UIStroke") then
table.insert(Properties, "Transparency")
end
local Cache = TransparencyCache[Desc]
if not Cache then
Cache = {}
TransparencyCache[Desc] = Cache
end
for _, Prop in next, Properties do
if not Cache[Prop] then
Cache[Prop] = Desc[Prop]
end
if Cache[Prop] == 1 then
continue
end
TweenService:Create(
Desc,
TweenInfo.new(FadeTime, Enum.EasingStyle.Linear),
{ [Prop] = Toggled and Cache[Prop] or 1 }
):Play()
end
end
task.wait(FadeTime)
FirstTime = true
end
Outer.Visible = Toggled
Fading = false
end
Library:GiveSignal(InputService.InputBegan:Connect(function(Input, Processed)
if type(Library.ToggleKeybind) == "table" and Library.ToggleKeybind.Type == "KeyPicker" then
if
(
Input.UserInputType == Enum.UserInputType.Touch
or Input.UserInputType == Enum.UserInputType.Keyboard
) and Input.KeyCode.Name == Library.ToggleKeybind.Value
then
task.spawn(Library.Toggle)
end
elseif
Input.KeyCode == Enum.KeyCode.RightControl
or (Input.KeyCode == Enum.KeyCode.RightShift and not Processed)
then
task.spawn(Library.Toggle)
end
end))
if Config.AutoShow then
task.spawn(Library.Toggle)
end
Library.KeybindFrame.Visible = not shared.Lycoris.silent
Library.Watermark.Visible = not shared.Lycoris.silent
Window.Holder = Outer
return Window
end
local function OnPlayerChange()
local PlayerList = GetPlayersString()
for _, Value in next, Options do
if Value.Type == "Dropdown" and Value.SpecialType == "Player" then
Value:SetValues(PlayerList)
end
end
end
Players.PlayerAdded:Connect(OnPlayerChange)
Players.PlayerRemoving:Connect(OnPlayerChange)
return Library
end)()
end)
-- MODULE: Utility/Configuration
defineModule("Utility/Configuration", function()
-- Safe configuration getter methods.
-- The menu is the last thing initialized in the script.
local Configuration = {}
---Expect toggle value.
---@param key string
---@return any?
Configuration.expectToggleValue = LPH_NO_VIRTUALIZE(function(key)
if not Toggles then
return nil
end
local toggle = Toggles[key]
if not toggle then
return nil
end
return toggle.Value
end)
---Expect option value.
---@param key string
---@return any?
Configuration.expectOptionValue = LPH_NO_VIRTUALIZE(function(key)
if not Options then
return nil
end
local option = Options[key]
if not option then
return nil
end
return option.Value
end)
---Identify element.
---@param identifier string
---@param topLevelIdentifier string
---@return string
Configuration.identify = LPH_NO_VIRTUALIZE(function(identifier, topLevelIdentifier)
return identifier .. topLevelIdentifier
end)
---Fetch toggle value.
---@param identifier string
---@param topLevelIdentifier string
---@return any
Configuration.idToggleValue = LPH_NO_VIRTUALIZE(function(identifier, topLevelIdentifier)
if not Toggles then
return nil
end
local toggle = Toggles[identifier .. topLevelIdentifier]
if not toggle then
return nil
end
return toggle.Value
end)
---Fetch option value.
---@param identifier string
---@param topLevelIdentifier string
---@return any
Configuration.idOptionValue = LPH_NO_VIRTUALIZE(function(identifier, topLevelIdentifier)
if not Options then
return nil
end
local option = Options[identifier .. topLevelIdentifier]
if not option then
return nil
end
return option.Value
end)
---Fetch option values.
---@param identifier string
---@param topLevelIdentifier string
---@return any
Configuration.idOptionValues = LPH_NO_VIRTUALIZE(function(identifier, topLevelIdentifier)
if not Options then
return nil
end
local option = Options[identifier .. topLevelIdentifier]
if not option then
return nil
end
return option.Values
end)
-- Return Configuration module.
return Configuration
end)
-- MODULE: Utility/CoreGuiManager
defineModule("Utility/CoreGuiManager", function()
---@note: We need to be careful where we use CoreGui because exploits have this weird permission issue. We need consistent setting of the parent.
---@note: All scripts that must access this module should require it at the top of the file where it gets loaded.
local CoreGuiManager = {}
-- Instance list.
local instances = {}
---Mark an instance to be parented to CoreGui at initialization.
---@param instance Instance
---@return Instance
function CoreGuiManager.imark(instance)
instances[#instances + 1] = instance
return instance
end
---Consistently and safely parent(s) all instances to CoreGui.
---@return table
function CoreGuiManager.set()
local coreGui = game:GetService("CoreGui")
for _, instance in next, instances do
instance.Parent = coreGui
end
return instances
end
---Remove all stored instances.
function CoreGuiManager.clear()
for _, instance in next, instances do
instance:Destroy()
end
instances = {}
end
---Return CoreGuiManager module.
return CoreGuiManager
end)
-- MODULE: Utility/Deserializer
defineModule("Utility/Deserializer", function()
-- Deserializer module.
local Deserializer = {}
---@module Utility.DeserializerStream
local DeserializerStream = require("Utility/DeserializerStream")
---@module Utility.String
local String = require("Utility/String")
-- Services.
local httpService = game:GetService("HttpService")
local function isJsonString(value)
if type(value) ~= "string" then
return false
end
local first = value:match("%S")
return first == "{" or first == "["
end
-- Deserialization data map.
local byteToDataMap = {
[0xc0] = nil,
[0xc2] = false,
[0xc3] = true,
[0xc4] = DeserializerStream.byte,
[0xc5] = DeserializerStream.short,
[0xc6] = DeserializerStream.int,
[0xca] = DeserializerStream.float,
[0xcb] = DeserializerStream.double,
[0xcc] = DeserializerStream.byte,
[0xcd] = DeserializerStream.unsignedShort,
[0xce] = DeserializerStream.unsignedInt,
[0xcf] = DeserializerStream.unsignedLong,
[0xd0] = DeserializerStream.byte,
[0xd1] = DeserializerStream.short,
[0xd2] = DeserializerStream.int,
[0xd3] = DeserializerStream.long,
[0xd9] = DeserializerStream.byte,
[0xda] = DeserializerStream.unsignedShort,
[0xdb] = DeserializerStream.unsignedInt,
[0xdc] = DeserializerStream.unsignedShort,
[0xdd] = DeserializerStream.unsignedInt,
[0xde] = DeserializerStream.unsignedShort,
[0xdf] = DeserializerStream.unsignedInt,
}
---Decode array with a specific length and recursively read.
---@param stream DeserializerStream
---@param length number
---@return table
local function decodeArray(stream, length)
local elements = {}
for i = 1, length do
elements[i] = Deserializer.at(stream)
end
return elements
end
---Decode map with a specific length and recursively read.
---@param stream DeserializerStream
---@param length number
---@return table, number
local function decodeMap(stream, length)
local elements = {}
for _ = 1, length do
elements[Deserializer.at(stream)] = Deserializer.at(stream)
end
return elements
end
---Deserialize the data at a specific position.
---@param stream DeserializerStream
---@return any
function Deserializer.at(stream)
local byte = stream:byte()
local byteData = byteToDataMap[byte] or function()
error("Unhandled byte data: " .. byte)
end
if byte == 0xde or byte == 0xdf then
return decodeMap(stream, byteData(stream))
end
if byte >= 0x80 and byte <= 0x8f then
return decodeMap(stream, byte - 0x80)
end
if byte >= 0x90 and byte <= 0x9f then
return decodeArray(stream, byte - 0x90)
end
if byte == 0xdc or byte == 0xdd then
return decodeArray(stream, byteData(stream))
end
if byte == 0xc4 or byte == 0xc5 or byte == 0xc6 then
return stream:leReadBytes(byteData(stream))
end
if byte == 0xd9 or byte == 0xda or byte == 0xdb then
return stream:string(byteData(stream))
end
if byte >= 0xa0 and byte <= 0xbf then
return stream:string(byte - 0xa0)
end
if byte == 0xc0 or byte == 0xc1 or byte == 0xc2 then
return byteToDataMap[byte]
end
if byte >= 0x00 and byte <= 0x7f then
return byte
end
if byte >= 0xe0 and byte <= 0xff then
return -32 + (byte - 0xe0)
end
return typeof(byteData) == "function" and byteData(stream) or byteData
end
---Starts recursively deserializing the data from the first index one time.
---@param data table
---@return any
function Deserializer.unmarshal_one(data)
if type(data) == "string" then
if isJsonString(data) then
return httpService:JSONDecode(data)
end
data = String.tba(data)
end
return Deserializer.at(DeserializerStream.new(data))
end
-- Return Deseralizer module.
return Deserializer
end)
-- MODULE: Utility/DeserializerStream
defineModule("Utility/DeserializerStream", function()
---@class DeserializerStream
---@field source table
---@field index number
local DeserializerStream = {}
DeserializerStream.__index = DeserializerStream
---Read bytes in little endian.
---@param len number
---@return number[]
function DeserializerStream:leReadBytes(len)
local bytes = {}
for idx = self.index + 1, self.index + len do
bytes[#bytes + 1] = self.source[idx]
end
self.index = self.index + len
if self.index > #self.source then
return error("leReadBytes - read overflow")
end
return bytes
end
---Read bytes in big endianess format.
---@param len number
---@return number[]
function DeserializerStream:beReadBytes(len)
local bytes = {}
for idx = self.index + len, self.index + 1, -1 do
bytes[#bytes + 1] = self.source[idx]
end
self.index = self.index + len
if self.index > #self.source then
return error("beReadBytes - read overflow")
end
return bytes
end
---Read string.
---@param len number
---@return string
function DeserializerStream:string(len)
local src = self.source
local buf = buffer.create(len)
for idx = self.index + 1, self.index + len do
buffer.writeu8(buf, idx - self.index - 1, src[idx])
end
self.index = self.index + len
---@note: Inlined leReadBytes.
if self.index > #self.source then
return error("string - read overflow")
end
return buffer.readstring(buf, 0, len)
end
---Read unsigned long.
---@return number
function DeserializerStream:unsignedLong()
local bytes = self:beReadBytes(8)
local p1 = bit32.bor(bytes[1], bit32.lshift(bytes[2], 8), bit32.lshift(bytes[3], 16), bit32.lshift(bytes[4], 24))
local p2 = bit32.bor(bytes[5], bit32.lshift(bytes[6], 8), bit32.lshift(bytes[7], 16), bit32.lshift(bytes[8], 24))
return bit32.bor(p1, bit32.lshift(p2, 32))
end
---Read unsigned int.
---@return number
function DeserializerStream:unsignedInt()
local bytes = self:beReadBytes(4)
return bit32.bor(bytes[1], bit32.lshift(bytes[2], 8), bit32.lshift(bytes[3], 16), bit32.lshift(bytes[4], 24))
end
---Read unsigned short.
---@return number
function DeserializerStream:unsignedShort()
local bytes = self:beReadBytes(2)
return bit32.bor(bytes[1], bit32.lshift(bytes[2], 8))
end
---Read float.
---@return number
function DeserializerStream:float()
local bytes = self:beReadBytes(4)
local sign = (-1) ^ bit32.rshift(bytes[4], 7)
local exp = bit32.rshift(bytes[3], 7) + bit32.lshift(bit32.band(bytes[4], 0x7F), 1)
local frac = bytes[1] + bit32.lshift(bytes[2], 8) + bit32.lshift(bit32.band(bytes[3], 0x7F), 16)
local normal = 1
if exp == 0 then
if frac == 0 then
return sign * 0
else
normal = 0
exp = 1
end
elseif exp == 0x7F then
if frac == 0 then
return sign * (1 / 0)
else
return sign * (0 / 0)
end
end
return sign * 2 ^ (exp - 127) * (1 + normal / 2 ^ 23)
end
---Read double.
---@return number
function DeserializerStream:double()
local bytes = self:beReadBytes(8)
local sign = (-1) ^ bit32.rshift(bytes[8], 7)
local exp = bit32.lshift(bit32.band(bytes[8], 0x7F), 4) + bit32.rshift(bytes[7], 4)
local frac = bit32.band(bytes[7], 0x0F) * 2 ^ 48
local normal = 1
frac = frac
+ (bytes[6] * 2 ^ 40)
+ (bytes[5] * 2 ^ 32)
+ (bytes[4] * 2 ^ 24)
+ (bytes[3] * 2 ^ 16)
+ (bytes[2] * 2 ^ 8)
+ bytes[1]
if exp == 0 then
if frac == 0 then
return sign * 0
else
normal = 0
exp = 1
end
elseif exp == 0x7FF then
if frac == 0 then
return sign * (1 / 0)
else
return sign * (0 / 0)
end
end
return sign * 2 ^ (exp - 1023) * (normal + frac / 2 ^ 52)
end
---Read long.
---@return number
function DeserializerStream:long()
local value = self:unsignedLong()
if bit32.band(value, 0x8000000000000000) ~= 0x0 then
value = value - 0x800000000000000
end
return value
end
---Read int.
---@return number
function DeserializerStream:int()
local value = self:unsignedInt()
if bit32.band(value, 0x80000000) ~= 0 then
value = value - 0x100000000
end
return value
end
---Read short.
---@return number
function DeserializerStream:short()
local value = self:unsignedShort()
if bit32.band(value, 0x8000) ~= 0 then
value = value - 0x10000
end
return value
end
---Read byte.
---@return number
function DeserializerStream:byte()
local bytes = self:leReadBytes(1)
return bytes[1]
end
---Create new DeserializerStream object.
---@param source table
---@return DeserializerStream
function DeserializerStream.new(source)
local self = setmetatable({}, DeserializerStream)
self.source = source
self.index = 0
return self
end
-- Return DeserializerStream module.
return DeserializerStream
end)
-- MODULE: Utility/Filesystem
defineModule("Utility/Filesystem", function()
return LPH_NO_VIRTUALIZE(function()
---@class Filesystem
---@field _path string
local Filesystem = {}
Filesystem.__index = Filesystem
---Create and get the current path.
---@return string
function Filesystem:path()
if not isfolder(self._path) then
makefolder(self._path)
end
return self._path
end
---Append path to current path.
---@param path string
---@return string
function Filesystem:append(path)
return self:path() .. "\\" .. path
end
---Check if filename is a file.
---@param filename string
---@return boolean
function Filesystem:file(filename)
return isfile(self:append(filename))
end
---Read file from path.
---@param filename string
---@return string
function Filesystem:read(filename)
if not self:file(filename) then
return error("File does not exist or is a folder.", 2)
end
return readfile(self:append(filename))
end
---Delete file.
---@param filename string
---@return string
function Filesystem:delete(filename)
if not self:file(filename) then
return error("File does not exist or is a folder.", 2)
end
return delfile(self:append(filename))
end
---Write file to workspace folder.
---@param filename string
---@param contents string?
function Filesystem:write(filename, contents)
writefile(self:append(filename), contents and contents or "")
end
---List files.
---@param raw boolean?
---@return table
function Filesystem:list(raw)
local list = listfiles(self:path())
if not list then
return error("File list does not exist.", 2)
end
local new = {}
for idx, path in next, list do
---@note: Solara returns full paths.
--- C:/Users/brean/Downloads/Workspace/(path_here)/(file_here)
--- We must get rid of the C:/Users/brean/Downloads/Workspace and have that be fully dynamic and not break
if getexecutorname and getexecutorname():match("Solara") then
path = string.sub(path, #listfiles()[1] + 2, #path)
end
---@note: Non-raw weird behavior where the path is never detected in the string. Let's manually index remove it.
new[idx] = raw and path or string.sub(path, #(self:path() .. "\\") + 1, #path)
end
return new
end
---Create new Filesystem object.
---@param path string
---@return Filesystem
function Filesystem.new(path)
local self = setmetatable({}, Filesystem)
self._path = path
return self
end
-- Return Filesystem module.
return Filesystem
end)()
end)
-- MODULE: Utility/Logger
defineModule("Utility/Logger", function()
return LPH_NO_VIRTUALIZE(function()
-- Logger module.
local Logger = {}
Logger.__index = Logger
---@module GUI.Library
local Library = require("GUI/Library")
---Build a string with a prefix.
---@param str string
---@return string
local function buildPrefixString(str)
return string.format("[%s %s] [Cottom hub AB]: %s", os.date("%x"), os.date("%X"), str)
end
---Create a manually managed notification.
---@param str string
---@return function
function Logger.mnnotify(str, ...)
return Library:ManuallyManagedNotify(string.format(str, ...))
end
---Notify message with a default short cooldown to create consistent cooldowns between files.
---@param str string
function Logger.notify(str, ...)
Library:Notify(string.format(str, ...), 3.0)
end
---Notify message with a default long cooldown to create consistent cooldowns between files.
---@param str string
function Logger.longNotify(str, ...)
Library:Notify(string.format(str, ...), 30.0)
end
---Warn message.
---@param str string
function Logger.warn(str, ...)
if shared.Lycoris.silent then
return
end
warn(string.format(buildPrefixString(str), ...))
end
---Trace & warn message.
---@param str string
function Logger.trace(str, ...)
if shared.Lycoris.silent then
return
end
Logger.warn(str, ...)
warn(debug.traceback(2))
end
-- Return Logger module.
return Logger
end)()
end)
-- MODULE: Utility/Maid
defineModule("Utility/Maid", function()
-- https://github.com/Quenty/NevermoreEngine/blob/version2/Modules/Shared/Events/Maid.lua
---@class Maid
local Maid = {}
Maid.__type = "maid"
---Create new Maid object.
---@return Maid
Maid.new = LPH_NO_VIRTUALIZE(function()
return setmetatable({
_tasks = {},
}, Maid)
end)
---Return maid[key] - if not, it's not apart of the maid metatable - so we return the relevant task.
-- @return value
Maid.__index = LPH_NO_VIRTUALIZE(function(self, index)
if Maid[index] then
return Maid[index]
else
return self._tasks[index]
end
end)
---Clean or add a task with a specific key.
---@param index any
---@param newTask any
Maid.__newindex = LPH_NO_VIRTUALIZE(function(self, index, newTask)
if Maid[index] ~= nil then
return warn(("'%s' is reserved"):format(tostring(index)), 2)
end
local tasks = self._tasks
local oldTask = tasks[index]
if oldTask == newTask then
return
end
tasks[index] = newTask
if oldTask then
if typeof(oldTask) == "thread" then
return coroutine.status(oldTask) == "suspended" and task.cancel(oldTask) or nil
end
if type(oldTask) == "function" then
oldTask()
elseif typeof(oldTask) == "RBXScriptConnection" then
oldTask:Disconnect()
elseif typeof(oldTask) == "Instance" and oldTask:IsA("Tween") then
oldTask:Pause()
oldTask:Cancel()
oldTask:Destroy()
elseif oldTask.Destroy then
oldTask:Destroy()
elseif oldTask.detach then
oldTask:detach()
end
end
end)
---Add a task without a specific ID and return the task.
---@param task any
---@return any
Maid.mark = LPH_NO_VIRTUALIZE(function(self, task)
self:add(task)
return task
end)
---Get a unique ID for a task.
---@return number
Maid.uid = LPH_NO_VIRTUALIZE(function(self)
return #self._tasks + 1
end)
---Add a task without a specific ID.
---@param task any
---@return number
Maid.add = LPH_NO_VIRTUALIZE(function(self, task)
if not task then
return error("task cannot be false or nil", 2)
end
local taskId = self:uid()
self[taskId] = task
return taskId
end)
---Remove task without cleaning it.
---@param taskId number
Maid.removeTask = LPH_NO_VIRTUALIZE(function(self, taskId)
local tasks = self._tasks
tasks[taskId] = nil
end)
---Clean up all tasks.
Maid.clean = LPH_NO_VIRTUALIZE(function(self)
local tasks = self._tasks
-- Disconnect all events first - as we know this is safe.
for index, task in pairs(tasks) do
if typeof(task) == "RBXScriptConnection" then
tasks[index] = nil
task:Disconnect()
end
end
-- Clear out tasks table completely, even if clean up tasks add more tasks to the maid.
local index, _task = next(tasks)
while _task ~= nil do
tasks[index] = nil
if typeof(_task) == "thread" then
if coroutine.status(_task) == "suspended" then
task.cancel(_task)
end
else
if type(_task) == "function" then
_task()
elseif typeof(_task) == "RBXScriptConnection" then
_task:Disconnect()
elseif typeof(_task) == "Instance" and _task:IsA("Tween") then
_task:Pause()
_task:Cancel()
_task:Destroy()
elseif typeof(_task) == "Instance" and _task:IsA("AnimationTrack") then
_task:Stop()
elseif _task.Destroy then
_task:Destroy()
elseif _task.detach then
_task:detach()
end
end
index, _task = next(tasks)
end
end)
-- Return Maid module.
return Maid
end)
-- MODULE: Utility/OriginalStore
defineModule("Utility/OriginalStore", function()
---@class OriginalStore
---@param data any
---@param index any
---@param value any
---@field stored boolean
local OriginalStore = {}
OriginalStore.__index = OriginalStore
---Get stored data value.
---@return any
OriginalStore.get = LPH_NO_VIRTUALIZE(function(self)
if not self.stored then
return nil
end
return self.value
end)
---Set something, run a callback, and then restore.
---@param data table|Instance
---@param index any
---@param value any
---@param callback fun(): any
OriginalStore.run = LPH_NO_VIRTUALIZE(function(self, data, index, value, callback)
self:set(data, index, value)
callback()
self:restore()
end)
---Mark data value.
---@param data table|Instance
---@param index any
OriginalStore.mark = LPH_NO_VIRTUALIZE(function(self, data, index)
if self.stored and self.data ~= data then
self:restore()
end
if not self.stored then
self.data = data
self.index = index
self.value = data[index]
self.stored = true
end
end)
---Set data value.
---@param data table|Instance
---@param index any
---@param value any
OriginalStore.set = LPH_NO_VIRTUALIZE(function(self, data, index, value)
self:mark(data, index)
data[index] = value
end)
---Restore data value.
OriginalStore.restore = LPH_NO_VIRTUALIZE(function(self)
if not self.stored then
return
end
pcall(function()
self.data[self.index] = self.value
end)
self.stored = false
end)
---Detach OriginalStore object.
OriginalStore.detach = LPH_NO_VIRTUALIZE(function(self)
self:restore()
self.data = nil
self.index = nil
self.value = nil
self.stored = false
end)
---Create new OriginalStore object.
---@return OriginalStore
OriginalStore.new = LPH_NO_VIRTUALIZE(function()
local self = setmetatable({}, OriginalStore)
self.data = nil
self.index = nil
self.value = nil
self.stored = false
return self
end)
-- Return OriginalStore module.
return OriginalStore
end)
-- MODULE: Utility/Profiler
defineModule("Utility/Profiler", function()
return LPH_NO_VIRTUALIZE(function()
-- Profile code time.
-- Determine what parts of our script are lagging us through the microprofiler.
local Profiler = {}
---Runs a function with a specified profiler label.
---@param label string
---@param functionToProfile function
function Profiler.run(label, functionToProfile, ...)
-- Profile under label.
debug.profilebegin(label)
-- Call function to profile.
local ret_values = table.pack(functionToProfile(...))
-- End most recent profiling.
debug.profileend()
-- Return values.
return unpack(ret_values)
end
---Wrap function in a profiler statement with label.
---@param label string
---@param functionToProfile function
---@return function
function Profiler.wrap(label, functionToProfile)
return function(...)
return Profiler.run(label, functionToProfile, ...)
end
end
-- Return profiler module.
return Profiler
end)()
end)
-- MODULE: Utility/Serializer
defineModule("Utility/Serializer", function()
--[[
* MessagePack serializer / decode (0.6.1) written in pure Lua 5.3 / Lua 5.4
* written by Sebastian Steinhauer <s.steinhauer@yahoo.de>
* modified by the Lycoris Team <discord.gg/lyc>
*
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* For more information, please refer to <http://unlicense.org/>
]]
-- Serializer module.
local Serializer = {}
-- Services.
local httpService = game:GetService("HttpService")
---Does a specified table match the layout of an array.
---@param tbl table
---@return boolean
local function isAnArray(tbl)
local expected = 1
for k in next, tbl do
if k ~= expected then
return false
end
expected = expected + 1
end
return true
end
---Serialize number to a float.
---@param value number
---@return string
local function serializeFloat(value)
local serializedFloat = string.unpack("f", string.pack("f", value))
if serializedFloat == value then
return string.pack(">Bf", 0xca, value)
end
return string.pack(">Bd", 0xcb, value)
end
---Serialize number to a signed int.
---@param value number
---@return string
local function serializeSignedInt(value)
if value < 128 then
return string.pack("B", value)
elseif value <= 0xff then
return string.pack("BB", 0xcc, value)
elseif value <= 0xffff then
return string.pack(">BI2", 0xcd, value)
elseif value <= 0xffffffff then
return string.pack(">BI4", 0xce, value)
end
return string.pack(">BI8", 0xcf, value)
end
---Serialize number to a unsigned int.
---@param value number
---@return string
local function serializeUnsignedInt(value)
if value >= -32 then
return string.pack("B", 0xe0 + (value + 32))
elseif value >= -128 then
return string.pack("Bb", 0xd0, value)
elseif value >= -32768 then
return string.pack(">Bi2", 0xd1, value)
elseif value >= -2147483648 then
return string.pack(">Bi4", 0xd2, value)
end
return string.pack(">Bi8", 0xd3, value)
end
---Serialize string to a UTF8 string.
---@param value string
---@return string
local function serializeUtf8(value)
local len = #value
if len < 32 then
return string.pack("B", 0xa0 + len) .. value
elseif len < 256 then
return string.pack(">Bs1", 0xd9, value)
elseif len < 65536 then
return string.pack(">Bs2", 0xda, value)
end
return string.pack(">Bs4", 0xdb, value)
end
---Serialize string to a string of bytes.
---@param value string
---@return string
local function serializeStringBytes(value)
local len = #value
if len < 256 then
return string.pack(">Bs1", 0xc4, value)
elseif len < 65536 then
return string.pack(">Bs2", 0xc5, value)
end
return string.pack(">Bs4", 0xc6, value)
end
---Serialize table to a array.
---@param value table
---@return string
local function serializeArray(value)
local elements = {}
for i, v in pairs(value) do
if type(v) ~= "function" and type(v) ~= "thread" and type(v) ~= "userdata" then
elements[i] = Serializer.marshal(v)
end
end
local result = table.concat(elements)
local length = #elements
if length < 16 then
return string.pack(">B", 0x90 + length) .. result
elseif length < 65536 then
return string.pack(">BI2", 0xdc, length) .. result
end
return string.pack(">BI4", 0xdd, length) .. result
end
---Serialize table to a map.
---@param value table
---@return string
local function serializeMap(value)
local elements = {}
for k, v in pairs(value) do
if type(v) ~= "function" and type(v) ~= "thread" and type(v) ~= "userdata" then
elements[#elements + 1] = Serializer.marshal(k)
elements[#elements + 1] = Serializer.marshal(v)
end
end
local length = math.floor(#elements / 2)
if length < 16 then
return string.pack(">B", 0x80 + length) .. table.concat(elements)
elseif length < 65536 then
return string.pack(">BI2", 0xde, length) .. table.concat(elements)
end
return string.pack(">BI4", 0xdf, length) .. table.concat(elements)
end
---Serialize nil to a binary string.
---@return string
local function serializeNil()
return string.pack("B", 0xc0)
end
---serialize table to a binary string
---@param value table
---@return string
local function serializeTable(value)
return isAnArray(value) and serializeArray(value) or serializeMap(value)
end
---serialize boolean to a binary string
---@param value boolean
---@return string
local function serializeBoolean(value)
return string.pack("B", value and 0xc3 or 0xc2)
end
---serialize int to a binary string
---@param value number
---@return string
local function serializeInt(value)
return value >= 0 and serializeSignedInt(value) or serializeUnsignedInt(value)
end
---serialize number to a binary string
---@param value number
---@return string
local function serializeNumber(value)
return value % 1 == 0 and serializeInt(value) or serializeFloat(value)
end
---serialize string to a binary string
---@param value number
---@return string
local function serializeString(value)
return utf8.len(value) and serializeUtf8(value) or serializeStringBytes(value)
end
-- Types mapping to functions that serialize it.
local typeToSerializeMap = {
["nil"] = serializeNil,
["boolean"] = serializeBoolean,
["number"] = serializeNumber,
["string"] = serializeString,
["table"] = serializeTable,
}
---Marshal a value into a binary string.
---@param value any
---@return string
function Serializer.marshal(value)
return httpService:JSONEncode(value)
end
-- Return Serializer module.
return Serializer
end)
-- MODULE: Utility/Signal
defineModule("Utility/Signal", function()
-- Wrapper for Roblox's signals for safe connections to signals.
-- Automatically profiles signals & wraps them in a safe alternative.
---@class Signal
---@field signal RBXScriptSignal Underlying roblox script signal
local Signal = {}
Signal.__index = Signal
---@module Utility.Profiler
local Profiler = require("Utility/Profiler")
---@module Utility.Logger
local Logger = require("Utility/Logger")
---Safely connect to Roblox's signal.
---@param label string
---@param eventFunction function
---@return RBXScriptConnection
function Signal:connect(label, eventFunction)
---Log event errors.
---@param error string
local function onEventFunctionError(error)
Logger.trace("onEventFunctionError - (%s) - %s", label, error)
end
-- Connect to signal. Wrap function with profiler and error handling.
local connection = self.signal:Connect(Profiler.wrap(
label,
LPH_NO_VIRTUALIZE(function(...)
return xpcall(eventFunction, onEventFunctionError, ...)
end)
))
-- Return connection.
return connection
end
---Create new wrapper signal object.
---@param robloxSignal RBXScriptSignal
---@return Signal
function Signal.new(robloxSignal)
-- Create new wrapper signal object.
local self = setmetatable({}, Signal)
self.signal = robloxSignal
-- Return new wrapper signal object.
return self
end
-- Return Signal module.
return Signal
end)
-- MODULE: Utility/String
defineModule("Utility/String", function()
local String = {}
-- Generate mapping.
local charByteMap = {}
for idx = 0, 255 do
charByteMap[string.char(idx)] = idx
end
---String to byte array.
---@param str string
---@return table
function String.tba(str)
local chars = {}
local idx = 1
if #str == 0 then
return {}
end
repeat
chars[idx] = charByteMap[str:sub(idx, idx)]
idx = idx + 1
until idx == #str + 1
return chars
end
-- Return String module.
return String
end)
-- MODULE: Utility/Table
defineModule("Utility/Table", function()
return LPH_NO_VIRTUALIZE(function()
-- Table utility functions.
local Table = {}
---Take a chunk out of an array into a new array.
---@param input any[]
---@param start number
---@param stop number
---@return any[]
function Table.slice(input, start, stop)
local out = {}
if start == nil then
start = 1
elseif start < 0 then
start = #input + start + 1
end
if stop == nil then
stop = #input
elseif stop < 0 then
stop = #input + stop + 1
end
for idx = start, stop do
table.insert(out, input[idx])
end
return out
end
-- Return Table module.
return Table
end)()
end)
-- MODULE: Utility/TaskSpawner
defineModule("Utility/TaskSpawner", function()
-- Task spawner module.
local TaskSpawner = {}
---@module Utility.Profiler
local Profiler = require("Utility/Profiler")
---@module Utility.Logger
local Logger = require("Utility/Logger")
-- Services.
local RunService = game:GetService("RunService")
---Spawn delayed task where the delay can be variable.
---@param label string
---@param delay function
---@param callback function
---@vararg any
function TaskSpawner.delay(label, delay, callback, ...)
---Log task errors.
---@param error string
local function onTaskFunctionError(error)
Logger.trace("onTaskFunctionError - (%s) - %s", label, error)
end
-- Wrap callback in profiler and error handling and delay handling.
local taskFunction = Profiler.wrap(
label,
LPH_NO_VIRTUALIZE(function(...)
local timestamp = os.clock()
while os.clock() - timestamp < delay() do
RunService.RenderStepped:Wait()
end
return xpcall(callback, onTaskFunctionError, ...)
end)
)
return task.spawn(taskFunction, ...)
end
---Spawn task.
---@param label string
---@param callback function
---@vararg any
function TaskSpawner.spawn(label, callback, ...)
---Log task errors.
---@param error string
local function onTaskFunctionError(error)
Logger.trace("onTaskFunctionError - (%s) - %s", label, error)
end
-- Wrap callback in profiler and error handling.
local taskFunction = Profiler.wrap(
label,
LPH_NO_VIRTUALIZE(function(...)
return xpcall(callback, onTaskFunctionError, ...)
end)
)
-- Return reference.
return task.spawn(taskFunction, ...)
end
-- Return TaskSpawner module.
return TaskSpawner
end)
local CoreGuiManager = require("Utility/CoreGuiManager")
local Library = require("GUI/Library")
local Logger = require("Utility/Logger")
local SaveManager = require("Game/Timings/SaveManager")
local AttributeListener = require("Features/Combat/AttributeListener")
local Defense = require("Features/Combat/Defense")
local InputClient = require("Game/InputClient")
local AutoCombo = require("Features/Combat/AutoCombo")
local Players = game:GetService("Players")
local userInputService = game:GetService("UserInputService")
local Configuration = require("Utility/Configuration")
local TaskSpawner = require("Utility/TaskSpawner")
CoreGuiManager.set()
SaveManager.init()
local window = Library:CreateWindow({ Title = "Cottom hub AB", AutoShow = true })
Library.ToggleKeybind = { Type = "KeyPicker", Value = "M" }
Library.OwnerTag = "Cottom"
Library.OwnerKeys = { toggles = {}, options = {} }
do
local loggerTab = window:AddTab("Logger")
local loggerWindowGroup = loggerTab:AddLeftGroupbox("Logger Window")
local showLoggerToggle = loggerWindowGroup:AddToggle("ShowLoggerWindow", {
Text = "Show Logger Window",
Default = true,
})
showLoggerToggle:AddKeyPicker(
"ShowLoggerWindowKeyBind",
{ Default = "N/A", SyncToggleState = true, Text = "Logger Window" }
)
showLoggerToggle:OnChanged(function(value)
if Library.InfoLoggerFrame then
Library.InfoLoggerFrame.Visible = value
end
end)
local animVisToggle = loggerWindowGroup:AddToggle("ShowAnimationVisualizer", {
Text = "Show Animation Visualizer",
Default = false,
})
animVisToggle:AddKeyPicker(
"AnimationVisualizerKeyBind",
{ Default = "N/A", SyncToggleState = true, Text = "Animation Visualizer" }
)
loggerWindowGroup:AddButton("Test Jump", function()
if not InputClient.jump() then
Logger.longNotify("Test Jump failed: VIM unavailable or blocked.")
end
end)
loggerWindowGroup:AddToggle("LogAllAnimations", {
Text = "Log All Animations (Everywhere)",
Default = false,
})
loggerWindowGroup:AddToggle("LogStandAnimations", {
Text = "Log Stand Animations",
Default = false,
})
loggerWindowGroup:AddToggle("LogLocalStandAnimations", {
Text = "Log Local Stand Animations",
Default = false,
})
local timingDefaultsGroup = loggerTab:AddLeftGroupbox("Timing Defaults")
timingDefaultsGroup:AddSlider("DefaultPunishableWindow", {
Text = "Default Punishable Window",
Min = 0,
Max = 2,
Default = 0.7,
Suffix = "s",
Rounding = 2,
})
timingDefaultsGroup:AddSlider("DefaultAfterWindow", {
Text = "Default After Window",
Min = 0,
Max = 2,
Default = 0.1,
Suffix = "s",
Rounding = 2,
})
local skillLoggerGroup = loggerTab:AddLeftGroupbox("Skill Logger")
skillLoggerGroup:AddToggle("LogUsingSkillLocal", {
Text = "Log UsingSkill (Local)",
Default = false,
})
skillLoggerGroup:AddToggle("LogUsingSkillOthers", {
Text = "Log UsingSkill (Others)",
Default = false,
})
local showSkillLoggerToggle = skillLoggerGroup:AddToggle("ShowSkillLoggerWindow", {
Text = "Show Skill Logger Window",
Default = true,
})
showSkillLoggerToggle:AddKeyPicker(
"ShowSkillLoggerWindowKeyBind",
{ Default = "N/A", SyncToggleState = true, Text = "Skill Logger" }
)
showSkillLoggerToggle:OnChanged(function(value)
if Library.SkillLoggerFrame then
Library.SkillLoggerFrame.Visible = value
end
end)
local effectLoggerGroup = loggerTab:AddLeftGroupbox("Effect Logger")
effectLoggerGroup:AddToggle("LogEffects", {
Text = "Log Effects",
Default = false,
})
local showEffectLoggerToggle = effectLoggerGroup:AddToggle("ShowEffectLoggerWindow", {
Text = "Show Effect Logger Window",
Default = true,
})
showEffectLoggerToggle:AddKeyPicker(
"ShowEffectLoggerWindowKeyBind",
{ Default = "N/A", SyncToggleState = true, Text = "Effect Logger" }
)
showEffectLoggerToggle:OnChanged(function(value)
if Library.EffectLoggerFrame then
Library.EffectLoggerFrame.Visible = value
end
end)
effectLoggerGroup:AddDropdown("EffectLoggerFilter", {
Text = "Effect Filter",
Values = { "All", "Players", "NPCs", "Local", "Other" },
Default = "All",
})
local loggerRangeGroup = loggerTab:AddRightGroupbox("Logger Range")
loggerRangeGroup:AddSlider("MinimumLoggerDistance", {
Text = "Minimum Logger Distance",
Min = 0,
Max = 1000,
Default = 0,
Suffix = "m",
Rounding = 0,
})
loggerRangeGroup:AddSlider("MaximumLoggerDistance", {
Text = "Maximum Logger Distance",
Min = 0,
Max = 1000,
Default = 1000,
Suffix = "m",
Rounding = 0,
})
local blacklistGroup = loggerTab:AddRightGroupbox("Key Blacklist")
local blacklistedKeys = blacklistGroup:AddDropdown("BlacklistedKeys", {
Text = "Blacklisted Keys",
Default = {},
Values = Library:KeyBlacklists(),
Multi = true,
AllowNull = true,
})
blacklistGroup:AddButton("Remove Selected Keys", function()
if not blacklistedKeys.Value then
return
end
for selected, _ in next, blacklistedKeys.Value do
Library.InfoLoggerData.KeyBlacklistList[selected] = nil
end
blacklistedKeys:SetValues(Library:KeyBlacklists())
blacklistedKeys:SetValue({})
blacklistedKeys:Display()
end)
local animationPreviewGroup = loggerTab:AddRightGroupbox("Animation Preview")
local previewAnimationId = animationPreviewGroup:AddInput("PreviewAnimationId", {
Text = "Animation ID",
Placeholder = "rbxassetid://123 or 123",
})
animationPreviewGroup:AddLabel("Preview Target: Dummy")
local previewSpawnButton = animationPreviewGroup:AddButton("Spawn Preview Dummy", function() end)
local previewDespawnButton = animationPreviewGroup:AddButton("Despawn Preview Dummy", function() end)
local previewDurationLabel = animationPreviewGroup:AddLabel("Duration: 0.000s")
local previewTimeLabel = animationPreviewGroup:AddLabel("Time: 0.000s / 0.000s (0.0%)")
local previewCamDistance = animationPreviewGroup:AddSlider("PreviewCameraDistance", {
Text = "Camera Distance",
Min = 3,
Max = 80,
Default = 14,
Suffix = "m",
Rounding = 1,
})
local previewCamHeight = animationPreviewGroup:AddSlider("PreviewCameraHeight", {
Text = "Camera Height",
Min = -10,
Max = 30,
Default = 3,
Suffix = "m",
Rounding = 1,
})
local previewCamFollow = animationPreviewGroup:AddToggle("PreviewCameraFollow", {
Text = "Follow Dummy",
Default = true,
})
local previewSpeedSlider = animationPreviewGroup:AddSlider("PreviewTrackSpeed", {
Text = "Track Speed",
Min = 0,
Max = 3,
Default = 1,
Suffix = "x",
Rounding = 2,
})
local previewTimeSlider = animationPreviewGroup:AddSlider("PreviewTrackTime", {
Text = "Time Position",
Min = 0,
Max = 1,
Default = 0,
Suffix = "s",
Rounding = 3,
})
local previewViewGroup = loggerTab:AddRightGroupbox("Preview Camera Views")
local previewViewFrontButton = previewViewGroup:AddButton("View Front", function() end)
local previewViewBackButton = previewViewGroup:AddButton("View Back", function() end)
local previewViewLeftButton = previewViewGroup:AddButton("View Left", function() end)
local previewViewRightButton = previewViewGroup:AddButton("View Right", function() end)
local previewViewTopButton = previewViewGroup:AddButton("View Top", function() end)
local previewViewResetButton = previewViewGroup:AddButton("Reset Camera", function() end)
local RunService = game:GetService("RunService")
local workspaceService = game:GetService("Workspace")
local previewState = {
track = nil,
animation = nil,
length = 0,
id = nil,
dummyModel = nil,
dummyBase = nil,
dummyRoot = nil,
dummyHumanoid = nil,
cameraActive = false,
cameraMode = nil,
cameraConn = nil,
cameraType = nil,
cameraSubject = nil,
cameraCFrame = nil,
paused = true,
speed = 1,
}
local function parsePreviewAnimationId(raw)
if type(raw) ~= "string" then
return nil
end
local digits = raw:match("%d+")
if not digits then
return nil
end
return "rbxassetid://" .. digits
end
local function resolvePreviewHumanoid()
if previewState.dummyHumanoid and previewState.dummyHumanoid.Parent then
return previewState.dummyHumanoid
end
return nil
end
local function clearPreviewDummy()
if previewState.dummyModel then
previewState.dummyModel:Destroy()
end
if previewState.dummyBase then
previewState.dummyBase:Destroy()
end
previewState.dummyModel = nil
previewState.dummyBase = nil
previewState.dummyRoot = nil
previewState.dummyHumanoid = nil
end
local function clearPreviewTrack()
if previewState.track then
pcall(previewState.track.Stop, previewState.track)
pcall(previewState.track.Destroy, previewState.track)
end
if previewState.animation then
pcall(previewState.animation.Destroy, previewState.animation)
end
previewState.track = nil
previewState.animation = nil
previewState.length = 0
previewState.id = nil
end
local function resetPreviewCamera()
local cam = workspaceService.CurrentCamera
if cam and previewState.cameraActive then
cam.CameraType = previewState.cameraType or Enum.CameraType.Custom
if previewState.cameraSubject then
cam.CameraSubject = previewState.cameraSubject
end
if previewState.cameraCFrame then
cam.CFrame = previewState.cameraCFrame
end
end
previewState.cameraActive = false
previewState.cameraMode = nil
if previewState.cameraConn then
previewState.cameraConn:Disconnect()
previewState.cameraConn = nil
end
end
local function spawnPreviewDummy()
local localPlayer = Players.LocalPlayer
local character = localPlayer and localPlayer.Character
if not character then
return Logger.longNotify("Cannot spawn preview dummy; local character not found.")
end
clearPreviewTrack()
clearPreviewDummy()
local cam = workspaceService.CurrentCamera
local basePos = cam and cam.CFrame.Position or Vector3.new(0, 0, 0)
local forward = cam and cam.CFrame.LookVector or Vector3.new(0, 0, -1)
basePos = basePos + forward * 40 + Vector3.new(0, 150, 0)
local base = Instance.new("Part")
base.Name = "CottomPreviewCircle"
base.Shape = Enum.PartType.Cylinder
base.Size = Vector3.new(0.2, 40, 40)
base.Anchored = true
base.CanCollide = false
base.CanTouch = false
base.CanQuery = false
base.Transparency = 0.35
base.Material = Enum.Material.Neon
base.Color = Color3.fromRGB(0, 85, 255)
base.CFrame = CFrame.new(basePos) * CFrame.Angles(0, 0, math.rad(90))
base.Parent = workspaceService
local dummy = character:Clone()
dummy.Name = "CottomPreviewDummy"
for _, inst in ipairs(dummy:GetDescendants()) do
if inst:IsA("Script") or inst:IsA("LocalScript") or inst:IsA("ModuleScript") then
inst:Destroy()
elseif inst:IsA("BasePart") then
inst.Anchored = false
inst.CanCollide = false
inst.CanTouch = false
inst.CanQuery = false
elseif inst:IsA("Humanoid") then
inst.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
inst.BreakJointsOnDeath = false
end
end
for _, inst in ipairs(dummy:GetChildren()) do
if inst:IsA("Tool") then
inst:Destroy()
end
end
dummy.Parent = workspaceService
local root = dummy:FindFirstChild("HumanoidRootPart") or dummy:FindFirstChild("Torso")
if root then
root.Anchored = true
root.CanCollide = false
root.CanTouch = false
root.CanQuery = false
dummy.PrimaryPart = root
dummy:PivotTo(CFrame.new(basePos + Vector3.new(0, 3, 0)))
end
previewState.dummyModel = dummy
previewState.dummyBase = base
previewState.dummyRoot = root
previewState.dummyHumanoid = dummy:FindFirstChildOfClass("Humanoid")
end
local function updatePreviewCamera()
if not previewState.cameraActive or not previewState.cameraMode then
return
end
local cam = workspaceService.CurrentCamera
if not cam or not previewState.dummyRoot then
return
end
local root = previewState.dummyRoot
local distance = previewCamDistance.Value or 14
local height = previewCamHeight.Value or 0
local focus = root.Position + Vector3.new(0, height, 0)
local dir = root.CFrame.LookVector
local right = root.CFrame.RightVector
local pos = focus
if previewState.cameraMode == "front" then
pos = focus + dir * distance
elseif previewState.cameraMode == "back" then
pos = focus - dir * distance
elseif previewState.cameraMode == "left" then
pos = focus - right * distance
elseif previewState.cameraMode == "right" then
pos = focus + right * distance
elseif previewState.cameraMode == "top" then
pos = focus + Vector3.new(0, distance, 0)
end
cam.CFrame = CFrame.new(pos, focus)
end
local function setPreviewCameraMode(mode)
if not previewState.dummyRoot then
return Logger.longNotify("Spawn the preview dummy first.")
end
local cam = workspaceService.CurrentCamera
if not cam then
return
end
if not previewState.cameraActive then
previewState.cameraType = cam.CameraType
previewState.cameraSubject = cam.CameraSubject
previewState.cameraCFrame = cam.CFrame
end
previewState.cameraActive = true
previewState.cameraMode = mode
cam.CameraType = Enum.CameraType.Scriptable
cam.CameraSubject = previewState.dummyRoot
updatePreviewCamera()
if not previewState.cameraConn then
previewState.cameraConn = RunService.RenderStepped:Connect(function()
if previewState.cameraActive and previewCamFollow.Value then
updatePreviewCamera()
end
end)
Library:GiveSignal(previewState.cameraConn)
end
end
local function updatePreviewLabels(timeOverride)
local length = previewState.length or 0
local timePos = timeOverride
if timePos == nil then
timePos = previewState.track and previewState.track.TimePosition or 0
end
local pct = length > 0 and (timePos / length) * 100 or 0
previewDurationLabel:SetText(string.format("Duration: %.3fs", length))
previewTimeLabel:SetText(string.format("Time: %.3fs / %.3fs (%.1f%%)", timePos, length, pct))
end
local function setPreviewTime(value)
if not previewState.track then
return
end
local length = previewState.length or 0
local pos = math.clamp(value or 0, 0, length)
previewState.track.TimePosition = pos
updatePreviewLabels(pos)
end
local function setPreviewSpeed(value)
previewState.speed = value
if previewState.track and not previewState.paused then
previewState.track:AdjustSpeed(value)
end
end
local function loadPreviewAnimation()
local animId = parsePreviewAnimationId(previewAnimationId.Value or "")
if not animId then
return Logger.longNotify("Please enter a valid animation ID.")
end
local humanoid = resolvePreviewHumanoid()
if not humanoid then
return Logger.longNotify("Cannot load animation; preview dummy not spawned.")
end
clearPreviewTrack()
local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
animator = Instance.new("Animator")
animator.Parent = humanoid
end
local animation = Instance.new("Animation")
animation.AnimationId = animId
local ok, track = pcall(function()
return animator:LoadAnimation(animation)
end)
if not ok or not track then
animation:Destroy()
return Logger.longNotify("Failed to load animation '%s'.", animId)
end
track.Looped = true
track:Play(0, 1, 0)
previewState.track = track
previewState.animation = animation
previewState.id = animId
previewState.length = track.Length
previewState.paused = true
previewState.speed = previewSpeedSlider.Value or 1
local start = os.clock()
while previewState.length <= 0 and os.clock() - start < 2 do
task.wait()
if not previewState.track then
break
end
previewState.length = previewState.track.Length
end
if previewState.length <= 0 then
previewState.length = 0
end
previewTimeSlider.Max = math.max(previewState.length, 0.001)
previewTimeSlider:SetRawValue(0)
updatePreviewLabels(0)
previewState.track.TimePosition = 0
previewState.track:AdjustSpeed(0)
end
previewSpeedSlider:OnChanged(function(value)
setPreviewSpeed(value)
end)
previewTimeSlider:OnChanged(function(value)
setPreviewTime(value)
end)
previewCamDistance:OnChanged(function()
updatePreviewCamera()
end)
previewCamHeight:OnChanged(function()
updatePreviewCamera()
end)
previewCamFollow:OnChanged(function(value)
if not value then
updatePreviewCamera()
end
end)
previewSpawnButton.Func = function()
spawnPreviewDummy()
setPreviewCameraMode("front")
end
previewDespawnButton.Func = function()
clearPreviewTrack()
clearPreviewDummy()
resetPreviewCamera()
previewState.paused = true
updatePreviewLabels(0)
previewTimeSlider:SetRawValue(0)
end
previewViewFrontButton.Func = function()
setPreviewCameraMode("front")
end
previewViewBackButton.Func = function()
setPreviewCameraMode("back")
end
previewViewLeftButton.Func = function()
setPreviewCameraMode("left")
end
previewViewRightButton.Func = function()
setPreviewCameraMode("right")
end
previewViewTopButton.Func = function()
setPreviewCameraMode("top")
end
previewViewResetButton.Func = function()
resetPreviewCamera()
end
animationPreviewGroup:AddButton("Load Animation", loadPreviewAnimation)
animationPreviewGroup:AddButton("Play", function()
if previewState.track then
local speed = previewState.speed or previewSpeedSlider.Value or 1
if speed <= 0 then
speed = 1
previewSpeedSlider:SetRawValue(speed)
end
previewState.paused = false
previewState.track:AdjustSpeed(speed)
end
end)
animationPreviewGroup:AddButton("Pause", function()
if previewState.track then
previewState.paused = true
previewState.track:AdjustSpeed(0)
end
end)
animationPreviewGroup:AddButton("Stop", function()
clearPreviewTrack()
previewState.paused = true
updatePreviewLabels(0)
previewTimeSlider:SetRawValue(0)
end)
animationPreviewGroup:AddButton("Log Timestamp", function()
local timePos = previewState.track and previewState.track.TimePosition or 0
local length = previewState.length or 0
Library:AddTelemetryEntry("Anim preview %s at %.3fs / %.3fs.", previewState.id or "N/A", timePos, length)
end)
local combatTab = window:AddTab("Combat")
local autoDefenseGroup = combatTab:AddLeftGroupbox("Auto Defense")
local autoDefenseToggle = autoDefenseGroup:AddToggle("EnableAutoDefense", {
Text = "Enable Auto Defense",
Default = true,
})
autoDefenseToggle:AddKeyPicker(
"EnableAutoDefenseKeybind",
{ Default = "N/A", SyncToggleState = true, Text = "Auto Defense" }
)
autoDefenseToggle:OnChanged(function(value)
if not value then
InputClient.block(false)
end
end)
local autoDefenseDepBox = autoDefenseGroup:AddDependencyBox()
autoDefenseDepBox:AddToggle("EnableNotifications", {
Text = "Enable Notifications",
Default = false,
})
autoDefenseDepBox:AddToggle("EnableVisualizations", {
Text = "Enable Visualizations",
Default = false,
})
autoDefenseDepBox:AddToggle("UseControllerM1", {
Text = "Use Controller M1 (VIM)",
Default = false,
Tooltip = "Send controller M1 (ButtonR2) instead of mouse clicks.",
})
autoDefenseDepBox:AddDropdown("DefaultDashDirection", {
Text = "Default Dash Direction",
Values = { "W", "A", "S", "D", "Random" },
Default = "S",
Tooltip = "Dash direction when no movement key is held.",
})
autoDefenseDepBox:AddSlider("BlockHoldSeconds", {
Text = "Block Hold Seconds",
Min = 0,
Max = 2,
Default = 0.4,
Suffix = "s",
Rounding = 2,
})
autoDefenseDepBox:SetupDependencies({
{ autoDefenseToggle, true },
})
local m1TradeTab = window:AddTab("M1 Auto Trade")
local m1TradeGroup = m1TradeTab:AddLeftGroupbox("M1 Auto Trade")
local m1TradeToggle = m1TradeGroup:AddToggle("EnableM1AutoTrade", {
Text = "Enable M1 Auto Trade",
Default = false,
})
m1TradeToggle:AddKeyPicker(
"EnableM1AutoTradeKeybind",
{ Default = "N/A", SyncToggleState = true, Text = "M1 Auto Trade" }
)
m1TradeToggle:OnChanged(function(value)
if value and Toggles["EnableAutoDefense"] then
Toggles["EnableAutoDefense"]:SetValue(false)
end
end)
local m1TradeDep = m1TradeGroup:AddDependencyBox()
m1TradeDep:AddSlider("M1TradeBlockHoldMs", {
Text = "Block Hold (ms)",
Min = 10,
Max = 500,
Default = 56,
Suffix = "ms",
Rounding = 0,
Tooltip = "How long to hold block before M1 chain.",
})
m1TradeDep:AddSlider("M1TradeBlockWaitMs", {
Text = "Block Wait (ms)",
Min = 0,
Max = 1000,
Default = 99,
Suffix = "ms",
Rounding = 0,
Tooltip = "Delay before tapping block.",
})
m1TradeDep:AddSlider("M1TradeDistanceHit", {
Text = "Distance To Hit",
Min = 0,
Max = 50,
Default = 9.05,
Suffix = "meters",
Rounding = 2,
})
m1TradeDep:AddSlider("M1TradeMaxTargets", {
Text = "How Many Targets",
Min = 1,
Max = 8,
Default = 1,
Rounding = 0,
})
m1TradeDep:AddSlider("M1TradeClickGapMs", {
Text = "M1 Click Gap (ms)",
Min = 10,
Max = 200,
Default = 10,
Suffix = "ms",
Rounding = 0,
Tooltip = "Delay between auto M1 clicks.",
})
m1TradeDep:AddSlider("M1TradeClickHoldMs", {
Text = "M1 Click Hold (ms)",
Min = 10,
Max = 200,
Default = 10,
Suffix = "ms",
Rounding = 0,
Tooltip = "How long each auto M1 press is held.",
})
m1TradeDep:AddToggle("UseControllerM1", {
Text = "Use Controller M1 (VIM)",
Default = false,
Tooltip = "Send controller M1 (ButtonR2) instead of mouse clicks.",
})
m1TradeDep:AddToggle("M1TradeManualOnly", {
Text = "Manual M1 Only (Allow Hold M1)",
Default = false,
Tooltip = "Only tap block; skip auto M1 clicks.",
})
m1TradeDep:AddToggle("M1TradeOneAutoM1", {
Text = "Do One Auto M1",
Default = false,
Tooltip = "Fire a single auto M1 instead of a full chain.",
})
m1TradeDep:AddDropdown("M1TradeSelectionType", {
Text = "Target Selection",
Values = {
"Closest In Distance",
"Closest To Crosshair",
"Least Health",
},
Default = "Closest In Distance",
})
m1TradeDep:SetupDependencies({
{ m1TradeToggle, true },
})
autoDefenseToggle:OnChanged(function(value)
if value and Toggles["EnableM1AutoTrade"] then
Toggles["EnableM1AutoTrade"]:SetValue(false)
end
end)
local targetingTabbox = combatTab:AddRightTabbox("Targeting")
local targetingTab = targetingTabbox:AddTab("Targeting")
targetingTab:AddDropdown("PlayerSelectionType", {
Text = "Player Selection Type",
Values = {
"Closest In Distance",
"Closest To Crosshair",
"Least Health",
},
Default = "Closest In Distance",
})
targetingTab:AddSlider("DistanceLimit", {
Text = "Distance Limit",
Min = 0,
Max = 100000,
Default = 99999,
Suffix = "studs",
Rounding = 0,
})
targetingTab:AddSlider("MaxTargets", {
Text = "Max Targets",
Min = 1,
Max = 64,
Default = 1,
Rounding = 0,
})
targetingTab:AddToggle("IgnorePlayers", {
Text = "Ignore Players",
Default = false,
})
targetingTab:AddToggle("IgnoreMobs", {
Text = "Ignore Mobs",
Default = false,
})
targetingTab:AddToggle("IgnoreAllies", {
Text = "Ignore Allies",
Default = false,
})
local whitelistTab = targetingTabbox:AddTab("Whitelisting")
local usernameList = whitelistTab:AddDropdown("UsernameList", {
Text = "Username List",
Values = {},
SaveValues = true,
Multi = true,
AllowNull = true,
Default = {},
})
local usernameInput = whitelistTab:AddInput("UsernameInput", {
Text = "Username Input",
Placeholder = "Display name or username.",
})
whitelistTab:AddButton("Add Username To List", function()
local username = usernameInput.Value or ""
if #username <= 0 then
return Logger.longNotify("Please enter a valid username.")
end
local values = usernameList.Values
if not table.find(values, username) then
table.insert(values, username)
end
usernameList:SetValues(values)
usernameList:SetValue({})
usernameList:Display()
end)
whitelistTab:AddButton("Remove Selected Usernames", function()
if not usernameList.Value then
return
end
local values = usernameList.Values
for selected, _ in next, usernameList.Value do
local index = table.find(values, selected)
if index then
table.remove(values, index)
end
end
usernameList:SetValues(values)
usernameList:SetValue({})
usernameList:Display()
end)
local miscTab = window:AddTab("Misc")
local predictionGroup = miscTab:AddLeftGroupbox("Prediction")
predictionGroup:AddSlider("PredictionExtraSeconds", {
Text = "Prediction Extra Seconds",
Min = 0,
Max = 0.5,
Default = 0,
Suffix = "s",
Rounding = 3,
Tooltip = "Extra lead time added to base prediction.",
})
local miscMovementGroup = miscTab:AddRightGroupbox("Movement")
miscMovementGroup:AddToggle("AutoJumpOnEnemyJump", {
Text = "Auto Jump (Enemy Jump)",
Default = false,
Tooltip = "Jump immediately when an enemy jumps.",
})
local desyncTab = window:AddTab("Desync")
local desyncGroup = desyncTab:AddLeftGroupbox("Desync")
local desyncToggle = desyncGroup:AddToggle("EnableDesync", {
Text = "Enable Desync",
Default = false,
})
desyncToggle:AddKeyPicker("EnableDesyncKeybind", { Default = "N/A", SyncToggleState = true, Text = "Desync" })
local LocalPlayer = Players.LocalPlayer
local desyncState = {
conn = nil,
localRoot = nil,
localHumanoid = nil,
warned = false,
}
local function resolveLocalRoot(character)
if not character then
return nil
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local root = humanoid.RootPart
if root and root.Parent then
return root
end
end
local root = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChild("Torso")
if root and root.Parent then
return root
end
return nil
end
local function clearDesyncRoot(root)
if not root then
return
end
if type(sethiddenproperty) ~= "function" then
return
end
pcall(sethiddenproperty, root, "PhysicsRepRootPart", nil)
end
local function getNearestLiveRoot()
local live = (workspaceService and workspaceService:FindFirstChild("Live")) or workspace:FindFirstChild("Live")
if not live then
return nil
end
local localRoot = desyncState.localRoot
if not localRoot then
return nil
end
local nearest, dist = nil, math.huge
for _, model in ipairs(live:GetChildren()) do
if model:IsA("Model") and model.Name ~= (LocalPlayer and LocalPlayer.Name or "") then
local hum = model:FindFirstChildOfClass("Humanoid")
local root = hum and hum.RootPart
if not root then
root = model:FindFirstChild("HumanoidRootPart") or model:FindFirstChild("Torso")
end
if root then
local d = (localRoot.Position - root.Position).Magnitude
if d < dist then
dist = d
nearest = root
end
end
end
end
return nearest
end
local function startDesync()
if desyncState.conn then
return true
end
if type(sethiddenproperty) ~= "function" then
if not desyncState.warned then
desyncState.warned = true
Logger.longNotify("Desync unavailable: sethiddenproperty missing.")
end
return false
end
desyncState.conn = RunService.Heartbeat:Connect(function()
if not desyncState.localRoot or not desyncState.localRoot.Parent then
local character = LocalPlayer and LocalPlayer.Character
desyncState.localHumanoid = character and character:FindFirstChildOfClass("Humanoid") or nil
desyncState.localRoot = resolveLocalRoot(character)
end
local root = desyncState.localRoot
if not root then
return
end
local target = getNearestLiveRoot()
pcall(sethiddenproperty, root, "PhysicsRepRootPart", target)
end)
if Library and Library.GiveSignal then
Library:GiveSignal(desyncState.conn)
end
return true
end
local function stopDesync()
if desyncState.conn then
desyncState.conn:Disconnect()
desyncState.conn = nil
end
clearDesyncRoot(desyncState.localRoot)
end
desyncToggle:OnChanged(function(value)
if value then
if not startDesync() then
desyncToggle:SetValue(false)
end
else
stopDesync()
end
end)
if LocalPlayer then
LocalPlayer.CharacterAdded:Connect(function(character)
desyncState.localHumanoid = character and character:FindFirstChildOfClass("Humanoid") or nil
desyncState.localRoot = resolveLocalRoot(character)
if not Configuration.expectToggleValue("EnableDesync") then
clearDesyncRoot(desyncState.localRoot)
end
end)
LocalPlayer.CharacterRemoving:Connect(function()
desyncState.localHumanoid = nil
desyncState.localRoot = nil
end)
if LocalPlayer.Character then
desyncState.localHumanoid = LocalPlayer.Character:FindFirstChildOfClass("Humanoid")
desyncState.localRoot = resolveLocalRoot(LocalPlayer.Character)
if not Configuration.expectToggleValue("EnableDesync") then
clearDesyncRoot(desyncState.localRoot)
end
end
end
do
local autoComboTab = window:AddTab("Auto-Combo")
local autoComboControl = autoComboTab:AddLeftGroupbox("Control")
local autoComboToggle = autoComboControl:AddToggle("EnableAutoCombo", {
Text = "Enable Auto-Combo",
Default = false,
})
autoComboToggle:AddKeyPicker("AutoComboKeybind", {
Default = "N/A",
Mode = "Hold",
Modes = { "Hold" },
Text = "Auto-Combo",
})
local autoComboOnM1Toggle = autoComboControl:AddToggle("AutoComboOnM1Hit", {
Text = "Auto-Combo On Local M1 Hit",
Default = true,
Tooltip = "Start Auto-Combo when your local M1 hits (LastHit + window).",
})
local autoComboOnM1Dep = autoComboControl:AddDependencyBox()
autoComboOnM1Dep:AddSlider("AutoComboOnM1HoldMs", {
Text = "M1 Hit Window",
Min = 50,
Max = 1000,
Default = 250,
Suffix = "ms",
Rounding = 0,
Tooltip = "Time window after local M1 animation to match LastHit.",
})
autoComboOnM1Dep:SetupDependencies({
{ autoComboOnM1Toggle, true },
})
local autoComboStopToggle = autoComboControl:AddToggle("AutoComboStopKeybindEnabled", {
Text = "Stop Auto-Combo Keybind",
Default = true,
})
autoComboStopToggle:AddKeyPicker("AutoComboStopKeybind", {
Default = "N/A",
Mode = "Hold",
Modes = { "Hold" },
Text = "Stop Auto-Combo",
})
autoComboControl:AddSlider("AutoComboDefaultStepDelayMs", {
Text = "Default Step Delay",
Min = 0,
Max = 5000,
Default = 100,
Suffix = "ms",
Rounding = 0,
})
autoComboControl:AddToggle("AutoComboLockTarget", {
Text = "Lock Target",
Default = true,
})
autoComboControl:AddToggle("AutoComboRetargetOnInvalid", {
Text = "Retarget On Invalid",
Default = true,
})
autoComboControl:AddSlider("AutoComboTweenMaxDistance", {
Text = "Tween Max Distance",
Min = 0,
Max = 50,
Default = 15,
Suffix = "studs",
Rounding = 1,
})
autoComboControl:AddSlider("AutoComboTweenSpeed", {
Text = "Tween Speed",
Min = 1,
Max = 200,
Default = 60,
Suffix = "studs/s",
Rounding = 0,
})
autoComboControl:AddSlider("AutoComboMoveToDurationSeconds", {
Text = "MoveTo Duration",
Min = 0,
Max = 10,
Default = 1.2,
Suffix = "s",
Rounding = 2,
})
autoComboControl:AddSlider("AutoComboMoveToStopDistance", {
Text = "MoveTo Stop Distance",
Min = 0,
Max = 20,
Default = 4,
Suffix = "studs",
Rounding = 1,
})
autoComboControl:AddSlider("AutoComboSprintHoldMs", {
Text = "Sprint Hold",
Min = 0,
Max = 2000,
Default = 350,
Suffix = "ms",
Rounding = 0,
})
autoComboControl:AddSlider("AutoComboUptiltJumpHoldMs", {
Text = "Uptilt Jump Hold",
Min = 0,
Max = 600,
Default = 120,
Suffix = "ms",
Rounding = 0,
})
autoComboControl:AddSlider("AutoComboUptiltM1HoldMs", {
Text = "Uptilt M1 Hold",
Min = 0,
Max = 300,
Default = 60,
Suffix = "ms",
Rounding = 0,
})
local autoComboProfileGroup = autoComboTab:AddRightGroupbox("Profiles")
local autoComboProfileList = autoComboProfileGroup:AddDropdown("AutoComboProfile", {
Text = "Profile",
Values = AutoCombo.listProfiles(),
AllowNull = true,
})
local autoComboProfileName = autoComboProfileGroup:AddInput("AutoComboProfileName", {
Text = "Profile Name",
Placeholder = "Enter profile name.",
})
local autoComboStepsGroup = autoComboTab:AddLeftGroupbox("Steps")
local autoComboStepList = autoComboStepsGroup:AddDropdown("AutoComboStepList", {
Text = "Steps",
Values = {},
AllowNull = true,
})
local autoComboEditorGroup = autoComboTab:AddRightGroupbox("Step Editor")
local autoComboStepType = autoComboEditorGroup:AddDropdown("AutoComboStepType", {
Text = "Step Type",
Values = { "Skill", "Wait", "M1", "Uptilt", "Upfling", "JumpHold", "Jump", "TweenToHead", "MoveToTarget", "Dash" },
Default = "Skill",
})
local autoComboParallel = autoComboEditorGroup:AddToggle("AutoComboStepParallel", {
Text = "Parallel (Non-Blocking)",
Default = false,
Tooltip = "Run this step in the background and continue to the next step immediately.",
})
local skillDep = autoComboEditorGroup:AddDependencyBox()
local skillSlot = skillDep:AddSlider("AutoComboStepSkillSlot", {
Text = "Skill Slot",
Min = 1,
Max = 4,
Default = 1,
Rounding = 0,
})
local skillToolName = skillDep:AddInput("AutoComboStepSkillToolName", {
Text = "Tool Name",
Placeholder = "Optional tool name.",
})
local skillDelayAfter = skillDep:AddInput("AutoComboStepSkillDelayAfter", {
Text = "Delay After (ms)",
Numeric = true,
Placeholder = "Optional delay.",
})
skillDep:SetupDependencies({
{ autoComboStepType, "Skill" },
})
local waitDep = autoComboEditorGroup:AddDependencyBox()
local waitDuration = waitDep:AddInput("AutoComboStepWaitDuration", {
Text = "Duration (ms)",
Numeric = true,
Placeholder = "e.g. 30000",
})
waitDep:SetupDependencies({
{ autoComboStepType, "Wait" },
})
local m1Dep = autoComboEditorGroup:AddDependencyBox()
local m1Hold = m1Dep:AddInput("AutoComboStepM1Hold", {
Text = "Hold (ms)",
Numeric = true,
})
local m1Repeat = m1Dep:AddInput("AutoComboStepM1Repeat", {
Text = "Repeat",
Numeric = true,
})
local m1Gap = m1Dep:AddInput("AutoComboStepM1Gap", {
Text = "Gap (ms)",
Numeric = true,
})
m1Dep:SetupDependencies({
{ autoComboStepType, "M1" },
})
local uptiltDep = autoComboEditorGroup:AddDependencyBox()
local uptiltJumpHold = uptiltDep:AddInput("AutoComboStepUptiltJumpHold", {
Text = "Jump Hold (ms)",
Numeric = true,
})
local uptiltM1Hold = uptiltDep:AddInput("AutoComboStepUptiltM1Hold", {
Text = "M1 Hold (ms)",
Numeric = true,
})
local uptiltDelay = uptiltDep:AddInput("AutoComboStepUptiltDelay", {
Text = "Delay Before M1 (ms)",
Numeric = true,
})
uptiltDep:SetupDependencies({
{ autoComboStepType, "Uptilt" },
})
local upflingDep = autoComboEditorGroup:AddDependencyBox()
local upflingJumpHold = upflingDep:AddInput("AutoComboStepUpflingJumpHold", {
Text = "Jump Hold (ms)",
Numeric = true,
})
local upflingM1Hold = upflingDep:AddInput("AutoComboStepUpflingM1Hold", {
Text = "M1 Hold (ms)",
Numeric = true,
})
upflingDep:SetupDependencies({
{ autoComboStepType, "Upfling" },
})
local jumpHoldDep = autoComboEditorGroup:AddDependencyBox()
local jumpHoldMs = jumpHoldDep:AddInput("AutoComboStepJumpHold", {
Text = "Hold (ms)",
Numeric = true,
})
jumpHoldDep:SetupDependencies({
{ autoComboStepType, "JumpHold" },
})
local jumpDep = autoComboEditorGroup:AddDependencyBox()
local jumpCount = jumpDep:AddInput("AutoComboStepJumpCount", {
Text = "Jump Count",
Numeric = true,
})
local jumpHoldTap = jumpDep:AddInput("AutoComboStepJumpHoldTap", {
Text = "Jump Hold (ms)",
Numeric = true,
})
local jumpGap = jumpDep:AddInput("AutoComboStepJumpGap", {
Text = "Jump Gap (ms)",
Numeric = true,
})
jumpDep:SetupDependencies({
{ autoComboStepType, "Jump" },
})
local tweenDep = autoComboEditorGroup:AddDependencyBox()
local tweenMaxDistance = tweenDep:AddInput("AutoComboStepTweenMaxDistance", {
Text = "Max Distance",
Numeric = true,
})
local tweenDuration = tweenDep:AddInput("AutoComboStepTweenDuration", {
Text = "Duration (s)",
Numeric = true,
})
local tweenAlignFeet = tweenDep:AddToggle("AutoComboStepTweenAlignFeet", {
Text = "Align Feet On Head",
Default = false,
})
local tweenSpeed = tweenDep:AddInput("AutoComboStepTweenSpeed", {
Text = "Tween Speed (studs/s)",
Numeric = true,
})
local tweenOffsetX = tweenDep:AddInput("AutoComboStepTweenOffsetX", {
Text = "Offset X",
Numeric = true,
})
local tweenOffsetY = tweenDep:AddInput("AutoComboStepTweenOffsetY", {
Text = "Offset Y",
Numeric = true,
})
local tweenOffsetZ = tweenDep:AddInput("AutoComboStepTweenOffsetZ", {
Text = "Offset Z",
Numeric = true,
})
tweenDep:SetupDependencies({
{ autoComboStepType, "TweenToHead" },
})
local moveDep = autoComboEditorGroup:AddDependencyBox()
local moveDuration = moveDep:AddInput("AutoComboStepMoveDuration", {
Text = "Duration (s)",
Numeric = true,
})
local moveStopDistance = moveDep:AddInput("AutoComboStepMoveStopDistance", {
Text = "Stop Distance",
Numeric = true,
})
local moveDelayAfter = moveDep:AddInput("AutoComboStepMoveDelayAfter", {
Text = "Delay After (ms)",
Numeric = true,
})
local moveUseSprint = moveDep:AddToggle("AutoComboStepMoveUseSprint", {
Text = "Use Sprint",
Default = true,
})
moveDep:SetupDependencies({
{ autoComboStepType, "MoveToTarget" },
})
local dashDep = autoComboEditorGroup:AddDependencyBox()
local dashDir = dashDep:AddDropdown("AutoComboStepDashDir", {
Text = "Direction",
Values = { "left", "right", "forward", "back" },
Default = "right",
})
dashDep:SetupDependencies({
{ autoComboStepType, "Dash" },
})
local autoComboState = {
profile = { name = "", steps = {} },
stepIndex = nil,
stepIndexMap = {},
updating = false,
}
local function cloneStep(step)
local copy = {}
for key, value in pairs(step) do
if type(value) == "table" then
local inner = {}
for k, v in pairs(value) do
inner[k] = v
end
copy[key] = inner
else
copy[key] = value
end
end
return copy
end
local function defaultStep(stepType)
if stepType == "Skill" then
return { type = "Skill", slot = 1, toolName = "", delayAfterMs = nil, parallel = false }
end
if stepType == "Wait" then
return { type = "Wait", durationMs = 1000, parallel = false }
end
if stepType == "M1" then
return { type = "M1", holdMs = 60, ["repeat"] = 1, gapMs = 60, parallel = false }
end
if stepType == "Uptilt" then
return {
type = "Uptilt",
jumpHoldMs = Options["AutoComboUptiltJumpHoldMs"] and Options["AutoComboUptiltJumpHoldMs"].Value or 120,
m1HoldMs = Options["AutoComboUptiltM1HoldMs"] and Options["AutoComboUptiltM1HoldMs"].Value or 60,
delayBeforeM1Ms = 30,
parallel = false,
}
end
if stepType == "Upfling" then
return { type = "Upfling", jumpHoldMs = 400, m1HoldMs = 230, parallel = false }
end
if stepType == "JumpHold" then
return { type = "JumpHold", holdMs = 400, parallel = false }
end
if stepType == "Jump" then
return { type = "Jump", count = 2, holdMs = 50, gapMs = 120, parallel = false }
end
if stepType == "TweenToHead" then
return {
type = "TweenToHead",
maxDistance = Options["AutoComboTweenMaxDistance"] and Options["AutoComboTweenMaxDistance"].Value or 15,
durationSeconds = nil,
alignFeet = false,
speed = Options["AutoComboTweenSpeed"] and Options["AutoComboTweenSpeed"].Value or 60,
offset = { x = 0, y = 0, z = 0 },
parallel = false,
}
end
if stepType == "MoveToTarget" then
return {
type = "MoveToTarget",
durationSeconds = Options["AutoComboMoveToDurationSeconds"]
and Options["AutoComboMoveToDurationSeconds"].Value
or 1.2,
stopDistance = Options["AutoComboMoveToStopDistance"] and Options["AutoComboMoveToStopDistance"].Value or 4,
delayAfterMs = nil,
useSprint = true,
parallel = false,
}
end
if stepType == "Dash" then
return { type = "Dash", dir = "right", parallel = false }
end
return { type = stepType }
end
local function stepSummary(step, index)
if not step then
return string.format("%02d: <none>", index)
end
local stype = step.type or "Unknown"
local prefix = step.parallel and "[P] " or ""
if stype == "Skill" then
return string.format("%02d: %sSkill slot %s", index, prefix, tostring(step.slot or 1))
elseif stype == "Wait" then
return string.format("%02d: %sWait %sms", index, prefix, tostring(step.durationMs or 0))
elseif stype == "M1" then
return string.format("%02d: %sM1 x%s", index, prefix, tostring(step["repeat"] or 1))
elseif stype == "Uptilt" then
return string.format("%02d: %sUptilt", index, prefix)
elseif stype == "Upfling" then
return string.format("%02d: %sUpfling", index, prefix)
elseif stype == "JumpHold" then
return string.format("%02d: %sJumpHold %sms", index, prefix, tostring(step.holdMs or 0))
elseif stype == "Jump" then
return string.format("%02d: %sJump x%s", index, prefix, tostring(step.count or 1))
elseif stype == "TweenToHead" then
return string.format("%02d: %sTweenToHead", index, prefix)
elseif stype == "MoveToTarget" then
local dur = step.durationSeconds or Options["AutoComboMoveToDurationSeconds"] and Options["AutoComboMoveToDurationSeconds"].Value
if dur and tonumber(dur) then
return string.format("%02d: %sMoveTo %.2fs", index, prefix, tonumber(dur))
end
return string.format("%02d: %sMoveToTarget", index, prefix)
elseif stype == "Dash" then
return string.format("%02d: %sDash %s", index, prefix, tostring(step.dir or "right"))
end
return string.format("%02d: %s%s", index, prefix, tostring(stype))
end
local function refreshStepList(selectIndex)
autoComboState.updating = true
autoComboState.stepIndexMap = {}
local values = {}
for i, step in ipairs(autoComboState.profile.steps or {}) do
local label = stepSummary(step, i)
table.insert(values, label)
autoComboState.stepIndexMap[label] = i
end
autoComboStepList:SetValues(values)
if selectIndex and values[selectIndex] then
autoComboStepList:SetValue(values[selectIndex])
elseif #values > 0 then
autoComboStepList:SetValue(values[1])
else
autoComboStepList:SetValue(nil)
end
autoComboStepList:Display()
autoComboState.updating = false
end
local function currentStep()
if not autoComboState.stepIndex then
return nil
end
return autoComboState.profile.steps and autoComboState.profile.steps[autoComboState.stepIndex]
end
local function loadStepToEditor(step)
autoComboState.updating = true
if not step then
autoComboStepType:SetValue("Skill")
autoComboParallel:SetValue(false)
skillSlot:SetRawValue(1)
skillToolName:SetRawValue("")
skillDelayAfter:SetRawValue("")
waitDuration:SetRawValue("")
m1Hold:SetRawValue("")
m1Repeat:SetRawValue("")
m1Gap:SetRawValue("")
uptiltJumpHold:SetRawValue("")
uptiltM1Hold:SetRawValue("")
uptiltDelay:SetRawValue("")
upflingJumpHold:SetRawValue("")
upflingM1Hold:SetRawValue("")
jumpHoldMs:SetRawValue("")
jumpCount:SetRawValue("")
jumpHoldTap:SetRawValue("")
jumpGap:SetRawValue("")
tweenDuration:SetRawValue("")
tweenAlignFeet:SetValue(false)
tweenMaxDistance:SetRawValue("")
tweenSpeed:SetRawValue("")
tweenOffsetX:SetRawValue("")
tweenOffsetY:SetRawValue("")
tweenOffsetZ:SetRawValue("")
moveDuration:SetRawValue("")
moveStopDistance:SetRawValue("")
moveDelayAfter:SetRawValue("")
moveUseSprint:SetValue(false)
dashDir:SetValue("right")
Library:UpdateDependencyBoxes()
autoComboState.updating = false
return
end
autoComboStepType:SetValue(step.type or "Skill")
autoComboParallel:SetValue(step.parallel == true)
skillSlot:SetRawValue(step.slot or 1)
skillToolName:SetRawValue(step.toolName or "")
skillDelayAfter:SetRawValue(step.delayAfterMs or "")
waitDuration:SetRawValue(step.durationMs or "")
m1Hold:SetRawValue(step.holdMs or "")
m1Repeat:SetRawValue(step["repeat"] or "")
m1Gap:SetRawValue(step.gapMs or "")
uptiltJumpHold:SetRawValue(step.jumpHoldMs or "")
uptiltM1Hold:SetRawValue(step.m1HoldMs or "")
uptiltDelay:SetRawValue(step.delayBeforeM1Ms or "")
upflingJumpHold:SetRawValue(step.jumpHoldMs or "")
upflingM1Hold:SetRawValue(step.m1HoldMs or "")
jumpHoldMs:SetRawValue(step.holdMs or "")
jumpCount:SetRawValue(step.count or "")
jumpHoldTap:SetRawValue(step.holdMs or "")
jumpGap:SetRawValue(step.gapMs or "")
tweenDuration:SetRawValue(step.durationSeconds or "")
tweenAlignFeet:SetValue(step.alignFeet == true)
tweenMaxDistance:SetRawValue(step.maxDistance or "")
local defaultSpeed = Options["AutoComboTweenSpeed"] and Options["AutoComboTweenSpeed"].Value or 60
tweenSpeed:SetRawValue(step.speed or defaultSpeed)
local offset = step.offset or {}
tweenOffsetX:SetRawValue(offset.x or 0)
tweenOffsetY:SetRawValue(offset.y or 0)
tweenOffsetZ:SetRawValue(offset.z or 0)
moveDuration:SetRawValue(step.durationSeconds or "")
moveStopDistance:SetRawValue(step.stopDistance or "")
moveDelayAfter:SetRawValue(step.delayAfterMs or "")
moveUseSprint:SetValue(step.useSprint == true)
dashDir:SetValue(step.dir or "right")
Library:UpdateDependencyBoxes()
autoComboState.updating = false
end
local function selectStep(index)
if not autoComboState.profile.steps then
return
end
autoComboState.stepIndex = index
loadStepToEditor(currentStep())
refreshStepList(index)
end
local function refreshProfiles(selectName)
local list = AutoCombo.listProfiles()
autoComboProfileList:SetValues(list)
if selectName then
autoComboProfileList:SetValue(selectName)
elseif list[1] then
autoComboProfileList:SetValue(list[1])
else
autoComboProfileList:SetValue(nil)
end
autoComboProfileList:Display()
end
autoComboProfileName:OnChanged(function(value)
if autoComboState.updating then
return
end
autoComboState.profile.name = value or ""
end)
autoComboProfileList:OnChanged(function(value)
if autoComboState.updating then
return
end
if value then
autoComboProfileName:SetRawValue(value)
end
end)
autoComboStepList:OnChanged(function(value)
if autoComboState.updating then
return
end
local idx = value and autoComboState.stepIndexMap[value] or nil
if not idx then
return
end
selectStep(idx)
end)
autoComboStepType:OnChanged(function(value)
if autoComboState.updating then
return
end
local step = currentStep()
if not step then
return
end
if step.type == value then
return
end
local defaults = defaultStep(value)
for key, val in pairs(defaults) do
step[key] = val
end
step.type = value
refreshStepList(autoComboState.stepIndex)
loadStepToEditor(step)
end)
local function updateStepField(field, value, refreshList)
if autoComboState.updating then
return
end
local step = currentStep()
if not step then
return
end
step[field] = value
if refreshList then
refreshStepList(autoComboState.stepIndex)
end
end
skillSlot:OnChanged(function(value)
updateStepField("slot", tonumber(value) or 1, true)
end)
skillToolName:OnChanged(function(value)
updateStepField("toolName", value or "")
end)
skillDelayAfter:OnChanged(function(value)
local num = tonumber(value)
updateStepField("delayAfterMs", num)
end)
waitDuration:OnChanged(function(value)
updateStepField("durationMs", tonumber(value) or 0, true)
end)
m1Hold:OnChanged(function(value)
updateStepField("holdMs", tonumber(value) or 60)
end)
m1Repeat:OnChanged(function(value)
updateStepField("repeat", tonumber(value) or 1, true)
end)
m1Gap:OnChanged(function(value)
updateStepField("gapMs", tonumber(value) or 60)
end)
uptiltJumpHold:OnChanged(function(value)
updateStepField("jumpHoldMs", tonumber(value) or 0)
end)
uptiltM1Hold:OnChanged(function(value)
updateStepField("m1HoldMs", tonumber(value) or 0)
end)
uptiltDelay:OnChanged(function(value)
updateStepField("delayBeforeM1Ms", tonumber(value) or 0)
end)
upflingJumpHold:OnChanged(function(value)
updateStepField("jumpHoldMs", tonumber(value) or 0)
end)
upflingM1Hold:OnChanged(function(value)
updateStepField("m1HoldMs", tonumber(value) or 0)
end)
jumpHoldMs:OnChanged(function(value)
updateStepField("holdMs", tonumber(value) or 0)
end)
jumpCount:OnChanged(function(value)
updateStepField("count", tonumber(value) or 1, true)
end)
jumpHoldTap:OnChanged(function(value)
updateStepField("holdMs", tonumber(value) or 0)
end)
jumpGap:OnChanged(function(value)
updateStepField("gapMs", tonumber(value) or 0)
end)
tweenMaxDistance:OnChanged(function(value)
updateStepField("maxDistance", tonumber(value) or 0)
end)
tweenDuration:OnChanged(function(value)
updateStepField("durationSeconds", tonumber(value) or 0)
end)
tweenAlignFeet:OnChanged(function(value)
updateStepField("alignFeet", value == true)
end)
tweenSpeed:OnChanged(function(value)
updateStepField("speed", tonumber(value) or 0)
end)
tweenOffsetX:OnChanged(function(value)
if autoComboState.updating then
return
end
local step = currentStep()
if not step then
return
end
step.offset = step.offset or {}
step.offset.x = tonumber(value) or 0
end)
tweenOffsetY:OnChanged(function(value)
if autoComboState.updating then
return
end
local step = currentStep()
if not step then
return
end
step.offset = step.offset or {}
step.offset.y = tonumber(value) or 0
end)
tweenOffsetZ:OnChanged(function(value)
if autoComboState.updating then
return
end
local step = currentStep()
if not step then
return
end
step.offset = step.offset or {}
step.offset.z = tonumber(value) or 0
end)
moveStopDistance:OnChanged(function(value)
updateStepField("stopDistance", tonumber(value) or 0)
end)
moveDuration:OnChanged(function(value)
updateStepField("durationSeconds", tonumber(value) or 0)
end)
moveDelayAfter:OnChanged(function(value)
updateStepField("delayAfterMs", tonumber(value) or 0)
end)
moveUseSprint:OnChanged(function(value)
updateStepField("useSprint", value == true)
end)
dashDir:OnChanged(function(value)
updateStepField("dir", value or "right", true)
end)
autoComboParallel:OnChanged(function(value)
updateStepField("parallel", value == true, true)
end)
autoComboStepsGroup:AddButton("Add Step", function()
local stepType = autoComboStepType.Value or "Skill"
local step = defaultStep(stepType)
table.insert(autoComboState.profile.steps, step)
selectStep(#autoComboState.profile.steps)
end)
autoComboStepsGroup:AddButton("Remove Step", function()
if not autoComboState.stepIndex then
return
end
table.remove(autoComboState.profile.steps, autoComboState.stepIndex)
if #autoComboState.profile.steps == 0 then
autoComboState.stepIndex = nil
loadStepToEditor(nil)
refreshStepList(nil)
return
end
autoComboState.stepIndex = math.clamp(autoComboState.stepIndex, 1, #autoComboState.profile.steps)
selectStep(autoComboState.stepIndex)
end)
autoComboStepsGroup:AddButton("Move Step Up", function()
local idx = autoComboState.stepIndex
if not idx or idx <= 1 then
return
end
local steps = autoComboState.profile.steps
steps[idx], steps[idx - 1] = steps[idx - 1], steps[idx]
selectStep(idx - 1)
end)
autoComboStepsGroup:AddButton("Move Step Down", function()
local idx = autoComboState.stepIndex
local steps = autoComboState.profile.steps
if not idx or idx >= #steps then
return
end
steps[idx], steps[idx + 1] = steps[idx + 1], steps[idx]
selectStep(idx + 1)
end)
autoComboStepsGroup:AddButton("Duplicate Step", function()
local step = currentStep()
if not step then
return
end
local copy = cloneStep(step)
table.insert(autoComboState.profile.steps, copy)
selectStep(#autoComboState.profile.steps)
end)
autoComboProfileGroup:AddButton("Create Profile", function()
local name = autoComboProfileName.Value or ""
if name == "" then
return Logger.longNotify("Please enter a profile name.")
end
autoComboState.profile = { name = name, steps = {} }
autoComboState.stepIndex = nil
loadStepToEditor(nil)
refreshStepList(nil)
local ok, err = AutoCombo.saveProfile(name, autoComboState.profile)
if not ok then
Logger.longNotify("Failed to create profile '%s' (%s).", name, tostring(err))
end
refreshProfiles(name)
end)
autoComboProfileGroup:AddButton("Save Profile", function()
local name = autoComboProfileName.Value or autoComboProfileList.Value or ""
if name == "" then
return Logger.longNotify("Please enter a profile name.")
end
autoComboState.profile.name = name
local ok, err = AutoCombo.saveProfile(name, autoComboState.profile)
if not ok then
return Logger.longNotify("Failed to save profile '%s' (%s).", name, tostring(err))
end
Logger.notify("Auto-Combo profile '%s' saved.", name)
refreshProfiles(name)
end)
autoComboProfileGroup:AddButton("Load Profile", function()
local name = autoComboProfileList.Value
if not name or name == "" then
return Logger.longNotify("Please select a profile to load.")
end
local profile, err = AutoCombo.loadProfile(name)
if not profile then
return Logger.longNotify("Failed to load profile '%s' (%s).", name, tostring(err))
end
autoComboState.profile = profile
autoComboState.stepIndex = (#profile.steps > 0) and 1 or nil
autoComboState.updating = true
autoComboProfileName:SetRawValue(profile.name)
autoComboState.updating = false
refreshStepList(autoComboState.stepIndex)
loadStepToEditor(currentStep())
end)
autoComboProfileGroup:AddButton("Delete Profile", function()
local name = autoComboProfileList.Value
if not name or name == "" then
return Logger.longNotify("Please select a profile to delete.")
end
local ok, err = AutoCombo.deleteProfile(name)
if not ok then
return Logger.longNotify("Failed to delete profile '%s' (%s).", name, tostring(err))
end
Logger.notify("Auto-Combo profile '%s' deleted.", name)
if autoComboState.profile.name == name then
autoComboState.profile = { name = "", steps = {} }
autoComboState.stepIndex = nil
loadStepToEditor(nil)
refreshStepList(nil)
end
refreshProfiles(nil)
end)
autoComboProfileGroup:AddButton("Duplicate Profile", function()
local source = autoComboProfileList.Value
local newName = autoComboProfileName.Value
if not source or source == "" then
return Logger.longNotify("Please select a profile to duplicate.")
end
if not newName or newName == "" then
return Logger.longNotify("Please enter a new profile name.")
end
local profile, err = AutoCombo.loadProfile(source)
if not profile then
return Logger.longNotify("Failed to load profile '%s' (%s).", source, tostring(err))
end
profile.name = newName
local ok, saveErr = AutoCombo.saveProfile(newName, profile)
if not ok then
return Logger.longNotify("Failed to save profile '%s' (%s).", newName, tostring(saveErr))
end
refreshProfiles(newName)
end)
autoComboToggle:OnChanged(function(value)
if not value then
AutoCombo.stop()
end
end)
refreshProfiles(nil)
refreshStepList(nil)
loadStepToEditor(nil)
end
end
do
local autoCounterTab = window:AddTab("Auto-Counter")
local autoCounterGroup = autoCounterTab:AddLeftGroupbox("Keybind Press")
autoCounterGroup:AddDropdown("AutoCounterKey", {
Text = "Auto Counter Key",
Values = { "1", "2", "3", "4" },
Default = "1",
})
local autoCounterKeybindToggle = autoCounterGroup:AddToggle("AutoCounterKeybindPress", {
Text = "Keybind Press",
Default = false,
Tooltip = "Press the selected key immediately when the keybind is pressed.",
})
autoCounterKeybindToggle:AddKeyPicker("AutoCounterKeybind", {
Default = "V",
Mode = "Hold",
Modes = { "Hold" },
Text = "Keybind Press",
})
local autoCounterOptions = autoCounterTab:AddRightGroupbox("Options")
autoCounterOptions:AddToggle("SkillAutoCounter", {
Text = "Skill Auto Counter",
Default = false,
Tooltip = "Enable skill auto counter behavior.",
})
autoCounterOptions:AddToggle("M1AutoCounter", {
Text = "M1 Auto Counter",
Default = false,
Tooltip = "Enable M1 auto counter behavior.",
})
autoCounterOptions:AddToggle("PartAutoCounter", {
Text = "Part Auto Counter",
Default = false,
Tooltip = "Enable part-based auto counter behavior.",
})
autoCounterOptions:AddSlider("M1AutoCounterDistance", {
Text = "M1 Counter Distance",
Min = 1,
Max = 60,
Default = 11.1,
Suffix = "m",
Rounding = 1,
})
autoCounterOptions:AddSlider("SkillAutoCounterDistance", {
Text = "Skill Counter Distance",
Min = 1,
Max = 80,
Default = 30,
Suffix = "m",
Rounding = 1,
})
autoCounterOptions:AddSlider("SkillAutoCounterDelaySeconds", {
Text = "Skill Counter Delay",
Min = 0,
Max = 2,
Default = 0,
Suffix = "s",
Rounding = 2,
Tooltip = "Delay before pressing the key for skill counters.",
})
autoCounterOptions:AddSlider("PartAutoCounterDistance", {
Text = "Part Counter Distance",
Min = 1,
Max = 80,
Default = 25,
Suffix = "m",
Rounding = 1,
})
-- UI Config system removed.
local AnimationBuilderSection = require("Menu/Objects/AnimationBuilderSection")
local PartBuilderSection = require("Menu/Objects/PartBuilderSection")
local SoundBuilderSection = require("Menu/Objects/SoundBuilderSection")
local AnimationTiming = require("Game/Timings/AnimationTiming")
local PartTiming = require("Game/Timings/PartTiming")
local SoundTiming = require("Game/Timings/SoundTiming")
local animationBuilder = nil
local partBuilder = nil
local soundBuilder = nil
local function refreshBuilders()
if animationBuilder then
animationBuilder:reset()
animationBuilder:refresh()
end
if partBuilder then
partBuilder:reset()
partBuilder:refresh()
end
if soundBuilder then
soundBuilder:reset()
soundBuilder:refresh()
end
end
local builderTab = window:AddTab("Builder")
local saveGroup = builderTab:AddDynamicGroupbox("Save Manager")
local autoSaveToggle = saveGroup:AddToggle("PeriodicAutoSave", {
Text = "Auto Save Periodically",
Default = false,
})
local autoSaveDepBox = saveGroup:AddDependencyBox()
autoSaveDepBox:AddSlider("PeriodicAutoSaveInterval", {
Text = "Auto Save Interval",
Min = 5,
Max = 300,
Default = 60,
Suffix = "s",
Rounding = 0,
})
autoSaveDepBox:SetupDependencies({
{ autoSaveToggle, true },
})
local configName = saveGroup:AddInput("ConfigName", {
Text = "Config Name",
})
local configList = saveGroup:AddDropdown("ConfigList", {
Text = "Config List",
Values = SaveManager.list(),
AllowNull = true,
})
local mergeConfigList = nil
saveGroup
:AddButton("Create Config", function()
SaveManager.create(configName.Value)
SaveManager.refresh(configList)
end)
:AddButton({
Text = "Load Config",
DoubleClick = true,
Func = function()
SaveManager.load(configList.Value)
refreshBuilders()
end,
})
saveGroup:AddButton({
Text = "Overwrite Config",
DoubleClick = true,
Func = function()
SaveManager.save(configList.Value)
end,
})
saveGroup:AddButton({
Text = "Clear Config",
DoubleClick = true,
Func = function()
SaveManager.clear(configList.Value)
end,
})
saveGroup:AddButton("Refresh List", function()
SaveManager.refresh(configList)
if mergeConfigList then
SaveManager.refresh(mergeConfigList)
end
refreshBuilders()
end)
saveGroup:AddButton("Set To Auto Load", function()
SaveManager.autoload(configList.Value)
end)
local mergeGroup = builderTab:AddDynamicGroupbox("Merge Manager")
mergeConfigList = mergeGroup:AddDropdown("MergeConfigList", {
Text = "Config List",
Values = SaveManager.list(),
AllowNull = true,
})
local mergeConfigType = mergeGroup:AddDropdown("MergeConfigType", {
Text = "Merge Type",
Values = { "Add New Timings", "Overwrite and Add Everything" },
Default = 1,
})
mergeGroup:AddButton({
Text = "Merge With Current Config",
DoubleClick = true,
Func = function()
SaveManager.merge(mergeConfigList.Value, mergeConfigType.Value)
refreshBuilders()
end,
})
partBuilder = PartBuilderSection.new("Part", builderTab:AddDynamicTabbox(), SaveManager.ps, PartTiming.new())
animationBuilder = AnimationBuilderSection.new(
"Animation",
builderTab:AddDynamicTabbox(),
SaveManager.as,
AnimationTiming.new()
)
soundBuilder = SoundBuilderSection.new("Sound", builderTab:AddDynamicTabbox(), SaveManager.ss, SoundTiming.new())
partBuilder:init()
animationBuilder:init()
soundBuilder:init()
refreshBuilders()
Library.OwnerTag = nil
Library:Toggle()
AttributeListener.init()
Defense.init()
local UsingSkillWatcher = require("Features/Combat/UsingSkillWatcher")
local M1AutoTrade = require("Features/Combat/M1AutoTrade")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
M1AutoTrade.init()
local EFFECT_MAX_DEPTH = 6
local EFFECT_MAX_TABLE_ENTRIES = 60
local EFFECT_MAX_STRING_LEN = 256
local EFFECT_MAX_TOTAL_LEN = 12000
local function formatEffectNumber(value)
if type(value) ~= "number" then
return tostring(value)
end
if math.abs(value - math.floor(value)) < 0.0001 then
return tostring(math.floor(value))
end
return string.format("%.4f", value)
end
local function serializeEffectValue(value, depth, seen)
if depth > EFFECT_MAX_DEPTH then
return "<max depth>"
end
local valueType = typeof(value)
if valueType == "Instance" then
local fullName = value.Name
if value.GetFullName then
local ok, name = pcall(value.GetFullName, value)
if ok and name and name ~= "" then
fullName = name
end
end
local out = string.format("Instance(%s:%s)", value.ClassName, fullName)
if value:IsA("ValueBase") then
out = out .. "=" .. serializeEffectValue(value.Value, depth + 1, seen)
end
return out
elseif valueType == "Vector3" then
return string.format(
"Vector3(%s,%s,%s)",
formatEffectNumber(value.X),
formatEffectNumber(value.Y),
formatEffectNumber(value.Z)
)
elseif valueType == "Vector2" then
return string.format("Vector2(%s,%s)", formatEffectNumber(value.X), formatEffectNumber(value.Y))
elseif valueType == "CFrame" then
local comps = { value:GetComponents() }
for i, v in ipairs(comps) do
comps[i] = formatEffectNumber(v)
end
return "CFrame(" .. table.concat(comps, ",") .. ")"
elseif valueType == "Color3" then
return string.format(
"Color3(%s,%s,%s)",
formatEffectNumber(value.R),
formatEffectNumber(value.G),
formatEffectNumber(value.B)
)
elseif valueType == "BrickColor" then
return "BrickColor(" .. tostring(value) .. ")"
elseif valueType == "EnumItem" then
return tostring(value)
elseif valueType == "NumberRange" then
return string.format("NumberRange(%s,%s)", formatEffectNumber(value.Min), formatEffectNumber(value.Max))
elseif valueType == "NumberSequence" or valueType == "ColorSequence" then
return tostring(value)
elseif valueType == "table" then
if seen[value] then
return "<cycle>"
end
seen[value] = true
local parts = {}
local count = 0
for k, v in next, value do
count = count + 1
if count > EFFECT_MAX_TABLE_ENTRIES then
parts[#parts + 1] = "..."
break
end
parts[#parts + 1] = string.format(
"[%s]=%s",
serializeEffectValue(k, depth + 1, seen),
serializeEffectValue(v, depth + 1, seen)
)
end
seen[value] = nil
return "{" .. table.concat(parts, ", ") .. "}"
elseif valueType == "string" then
local str = value
if #str > EFFECT_MAX_STRING_LEN then
str = str:sub(1, EFFECT_MAX_STRING_LEN - 3) .. "..."
end
return string.format("%q", str)
elseif valueType == "boolean" then
return tostring(value)
elseif valueType == "nil" then
return "nil"
end
return tostring(value)
end
local function formatEffectPayload(args)
local effectName = tostring(args[1] or "Unknown")
local seen = {}
local parts = {}
for i = 2, args.n do
parts[#parts + 1] = string.format("arg%d=%s", i - 1, serializeEffectValue(args[i], 0, seen))
end
local payload = effectName
if #parts > 0 then
payload = payload .. " | " .. table.concat(parts, " ")
end
if #payload > EFFECT_MAX_TOTAL_LEN then
payload = payload:sub(1, EFFECT_MAX_TOTAL_LEN - 3) .. "..."
end
return payload
end
local function classifyEffectCategory(args)
local live = workspace:FindFirstChild("Live")
for i = 1, args.n do
local value = args[i]
if typeof(value) == "Instance" then
local model = nil
if value:IsA("Model") then
model = value
else
model = value:FindFirstAncestorWhichIsA("Model")
end
if model then
local player = Players:GetPlayerFromCharacter(model)
if player then
if player == Players.LocalPlayer then
return "Local"
end
return "Players"
end
if model:FindFirstChildOfClass("Humanoid") then
return "NPCs"
end
if live and model:IsDescendantOf(live) then
return "NPCs"
end
end
end
end
return "Other"
end
local function logEffect(source, ...)
if not Configuration.expectToggleValue("LogEffects") then
return
end
local args = table.pack(...)
local category = classifyEffectCategory(args)
local filter = Options["EffectLoggerFilter"] and Options["EffectLoggerFilter"].Value or "All"
if filter ~= "All" and filter ~= category then
return
end
local payload = formatEffectPayload(args)
if Library and Library.AddEffectEntry then
Library:AddEffectEntry(tostring(args[1] or "Unknown"), source, category, payload)
end
end
local function resolveEffectReturnsCallback(remote)
local getter = getcallbackvalue or (syn and syn.getcallbackvalue) or rawget(_G, "getcallbackvalue")
if type(getter) ~= "function" then
return nil
end
local ok, cb = pcall(getter, remote, "OnClientInvoke")
if ok and type(cb) == "function" then
return cb
end
return nil
end
do
local effect = ReplicatedStorage:WaitForChild("Effect")
effect.OnClientEvent:Connect(function(...)
logEffect("Effect", ...)
end)
local effectReliable = ReplicatedStorage:WaitForChild("EffectReliable")
effectReliable.OnClientEvent:Connect(function(...)
logEffect("EffectReliable", ...)
end)
local pEffect = ReplicatedStorage:WaitForChild("PEffect")
pEffect.OnClientEvent:Connect(function(...)
logEffect("PEffect", ...)
end)
local effectBindable = ReplicatedStorage:WaitForChild("EffectBindable")
if effectBindable and effectBindable.Event then
effectBindable.Event:Connect(function(...)
logEffect("EffectBindable", ...)
end)
end
local effectReturns = ReplicatedStorage:WaitForChild("EffectReturns")
if effectReturns then
local prev = resolveEffectReturnsCallback(effectReturns)
if prev then
effectReturns.OnClientInvoke = function(...)
logEffect("EffectReturns", ...)
return prev(...)
end
else
local ok, logger = pcall(require, "Utility/Logger")
if ok and logger and logger.warn then
logger.warn("EffectReturns logging skipped: OnClientInvoke callback unavailable.")
end
end
end
end
local autoJumpState = {
playerConns = {},
charConns = {},
spaceConns = {},
}
local AUTOCOUNTER_METERS_TO_STUDS = 3.57
local autoCounterHotbarRef = nil
local function resolveAutoCounterHotbar()
if autoCounterHotbarRef and autoCounterHotbarRef.Parent then
return autoCounterHotbarRef
end
local playerGui = Players.LocalPlayer and Players.LocalPlayer:FindFirstChild("PlayerGui")
if not playerGui then
return nil
end
local hud = playerGui:FindFirstChild("HUD")
local backpackGui = hud and hud:FindFirstChild("Backpack")
local hotbar = backpackGui and backpackGui:FindFirstChild("Hotbar")
if not hotbar then
for _, inst in ipairs(playerGui:GetDescendants()) do
if inst.Name == "Hotbar" then
hotbar = inst
break
end
end
end
autoCounterHotbarRef = hotbar
return hotbar
end
local function resolveAutoCounterToolName(slot)
local hotbar = resolveAutoCounterHotbar()
if not hotbar then
return nil
end
local slotFrame = hotbar:FindFirstChild(tostring(slot))
if not slotFrame then
return nil
end
local toolNameObj = slotFrame:FindFirstChild("ToolName") or slotFrame:FindFirstChild("Name")
if not toolNameObj then
return nil
end
if toolNameObj:IsA("TextLabel") or toolNameObj:IsA("TextButton") then
return toolNameObj.Text
end
if toolNameObj:IsA("StringValue") then
return toolNameObj.Value
end
return nil
end
local function resolveAutoCounterTool(slot)
local backpack = Players.LocalPlayer and Players.LocalPlayer:FindFirstChild("Backpack")
if backpack then
for _, tool in ipairs(backpack:GetChildren()) do
if tool:IsA("Tool") then
local attrSlot = tool:GetAttribute("Slot") or tool:GetAttribute("HotbarSlot") or tool:GetAttribute("Index")
if attrSlot and tonumber(attrSlot) == slot then
return tool
end
local slotValue = tool:FindFirstChild("Slot") or tool:FindFirstChild("Index")
if slotValue and tonumber(slotValue.Value) == slot then
return tool
end
end
end
end
local character = Players.LocalPlayer and Players.LocalPlayer.Character
return character and character:FindFirstChildOfClass("Tool") or nil
end
local function tryFireAutoCounter()
if not InputClient or not InputClient.fireTool then
return false
end
local selected = Options["AutoCounterKey"] and Options["AutoCounterKey"].Value or "1"
local slot = tonumber(selected)
if not slot then
return false
end
local toolName = resolveAutoCounterToolName(slot)
local tool = resolveAutoCounterTool(slot)
if tool then
return InputClient.fireToolObject and InputClient.fireToolObject(tool) or InputClient.fireTool(tool.Name)
end
if toolName and toolName ~= "" then
return InputClient.fireTool(toolName)
end
return false
end
local function resolveAutoCounterKeyCode()
local selected = Options["AutoCounterKey"] and Options["AutoCounterKey"].Value or "1"
if selected == "1" then
return Enum.KeyCode.One
elseif selected == "2" then
return Enum.KeyCode.Two
elseif selected == "3" then
return Enum.KeyCode.Three
elseif selected == "4" then
return Enum.KeyCode.Four
end
return nil
end
local function pressAutoCounterKey(force)
if not force and not Configuration.expectToggleValue("AutoCounterKeybindPress") then
return
end
tryFireAutoCounter()
end
handleAutoCounterTiming = function(timing, source, distance)
if not timing or timing.tag == "Counter" or timing.tag == "Critical" then
return
end
if type(distance) ~= "number" then
return
end
if source == "Part" and not Configuration.expectToggleValue("PartAutoCounter") then
return
end
if source == "Part" then
local maxMeters = Configuration.expectOptionValue("PartAutoCounterDistance") or 25
if distance > maxMeters * AUTOCOUNTER_METERS_TO_STUDS then
return
end
end
if timing.tag == "M1" then
if Configuration.expectToggleValue("M1AutoCounter") then
local maxMeters = Configuration.expectOptionValue("M1AutoCounterDistance") or 11.1
if distance <= maxMeters * AUTOCOUNTER_METERS_TO_STUDS then
pressAutoCounterKey(true)
end
end
return
end
if timing.tag == "Skill" or timing.tag == "Mantra" then
if Configuration.expectToggleValue("SkillAutoCounter") then
local maxMeters = Configuration.expectOptionValue("SkillAutoCounterDistance") or 30
if distance <= maxMeters * AUTOCOUNTER_METERS_TO_STUDS then
local delaySeconds = Configuration.expectOptionValue("SkillAutoCounterDelaySeconds") or 0
if delaySeconds > 0 then
task.delay(delaySeconds, function()
pressAutoCounterKey(true)
end)
else
pressAutoCounterKey(true)
end
end
end
end
end
local function shouldAutoJump()
return Configuration.expectToggleValue("AutoJumpOnEnemyJump") == true
end
local function tryAutoJump()
if not shouldAutoJump() then
return
end
InputClient.jump()
end
local function hasJumpPenalty(character)
if not character then
return false
end
local penalty = character:FindFirstChild("JumpPenalty")
if not penalty then
return false
end
if penalty:IsA("BoolValue") then
return penalty.Value == true
end
return true
end
local function detachSpaceBool(spaceValue)
local conn = spaceValue and autoJumpState.spaceConns[spaceValue]
if conn then
conn:Disconnect()
end
autoJumpState.spaceConns[spaceValue] = nil
end
local function attachSpaceBool(spaceValue, character)
if not spaceValue or autoJumpState.spaceConns[spaceValue] then
return
end
local lastValue = spaceValue.Value
autoJumpState.spaceConns[spaceValue] = spaceValue:GetPropertyChangedSignal("Value"):Connect(function()
local current = spaceValue.Value
if current == true and lastValue == false then
if not hasJumpPenalty(character) then
tryAutoJump()
end
end
lastValue = current
end)
end
local function attachCharacterJumpWatcher(character)
if not character or autoJumpState.charConns[character] then
return
end
local function tryAttachSpace(descendant)
if descendant and descendant:IsA("BoolValue") and descendant.Name == "Space" then
attachSpaceBool(descendant, character)
return
end
end
local childAdded = character.ChildAdded:Connect(tryAttachSpace)
local childRemoving = character.ChildRemoved:Connect(function(child)
if child and child:IsA("BoolValue") and child.Name == "Space" then
detachSpaceBool(child)
end
end)
autoJumpState.charConns[character] = { childAdded, childRemoving }
for _, child in ipairs(character:GetChildren()) do
tryAttachSpace(child)
end
end
local function detachCharacterJumpWatcher(character)
local conns = character and autoJumpState.charConns[character]
if conns then
for _, conn in ipairs(conns) do
if conn then
conn:Disconnect()
end
end
autoJumpState.charConns[character] = nil
end
for spaceValue, _ in pairs(autoJumpState.spaceConns) do
if spaceValue and spaceValue.Parent == character then
detachSpaceBool(spaceValue)
end
end
end
local function attachPlayerJumpWatcher(player)
if not player or player == Players.LocalPlayer then
return
end
if autoJumpState.playerConns[player] then
return
end
local function onCharacterAdded(character)
if not character then
return
end
attachCharacterJumpWatcher(character)
end
local function onCharacterRemoving(character)
if not character then
return
end
detachCharacterJumpWatcher(character)
end
local addedConn = player.CharacterAdded:Connect(onCharacterAdded)
local removingConn = player.CharacterRemoving:Connect(onCharacterRemoving)
autoJumpState.playerConns[player] = { addedConn, removingConn }
if player.Character then
onCharacterAdded(player.Character)
end
end
local function detachPlayerJumpWatcher(player)
local conns = player and autoJumpState.playerConns[player]
if not conns then
return
end
if player.Character then
detachCharacterJumpWatcher(player.Character)
end
for _, conn in ipairs(conns) do
if conn then
conn:Disconnect()
end
end
autoJumpState.playerConns[player] = nil
end
for _, player in ipairs(Players:GetPlayers()) do
attachPlayerJumpWatcher(player)
end
Players.PlayerAdded:Connect(attachPlayerJumpWatcher)
Players.PlayerRemoving:Connect(detachPlayerJumpWatcher)
if Options["AutoCounterKeybind"] then
Options["AutoCounterKeybind"]:OnClick(function(toggled)
if toggled then
pressAutoCounterKey(false)
end
end)
end
local autoComboKeyHeld = false
if Options["AutoComboKeybind"] then
Options["AutoComboKeybind"]:OnClick(function(toggled)
if not toggled then
autoComboKeyHeld = false
return
end
if autoComboKeyHeld then
return
end
autoComboKeyHeld = true
if not Configuration.expectToggleValue("EnableAutoCombo") then
return
end
local profileName = Options["AutoComboProfile"] and Options["AutoComboProfile"].Value or ""
if profileName == "" then
return Logger.longNotify("Please select an Auto-Combo profile.")
end
AutoCombo.run(profileName)
end)
end
local autoComboStopHeld = false
if Options["AutoComboStopKeybind"] then
Options["AutoComboStopKeybind"]:OnClick(function(toggled)
if not toggled then
autoComboStopHeld = false
return
end
if autoComboStopHeld then
return
end
autoComboStopHeld = true
if not Configuration.expectToggleValue("AutoComboStopKeybindEnabled") then
return
end
AutoCombo.stop()
end)
end
activeSkillDef = nil
local function chooseDashDir(def, dir)
if not def or not dir then
return dir
end
local character = def.character
local localChar = Players.LocalPlayer and Players.LocalPlayer.Character
if not character or not localChar then
return dir
end
local targetRoot = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChild("Torso")
local localRoot = localChar:FindFirstChild("HumanoidRootPart") or localChar:FindFirstChild("Torso")
if not targetRoot or not localRoot then
return dir
end
local toTarget = (targetRoot.Position - localRoot.Position)
if toTarget.Magnitude < 0.01 then
return dir
end
local rightDot = localRoot.CFrame.RightVector:Dot(toTarget.Unit)
local forwardDot = localRoot.CFrame.LookVector:Dot(toTarget.Unit)
if dir == "right" and rightDot < 0 then
return "left"
end
if dir == "left" and rightDot > 0 then
return "right"
end
if dir == "forward" and forwardDot < 0 then
return "back"
end
if dir == "back" and forwardDot > 0 then
return "forward"
end
return dir
end
local blockFor
local defenseActionState = {
blockIssued = 0,
dashAttempt = 0,
dashOk = true,
}
local function dash(dir)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return false
end
local def = activeSkillDef
if type(dir) == "table" then
def = dir
dir = nil
end
local unresolved = type(dir) ~= "string" or dir == ""
if def and dir then
dir = chooseDashDir(def, dir)
unresolved = type(dir) ~= "string" or dir == ""
end
defenseActionState.dashAttempt = (defenseActionState.dashAttempt or 0) + 1
local ok = InputClient.dash(dir)
defenseActionState.dashOk = (ok == true) and not unresolved
return ok
end
local function dashAfter(seconds, dir)
local delaySeconds = seconds or 0
if delaySeconds <= 0 then
return dash(dir)
end
task.delay(delaySeconds, function()
dash(dir)
end)
return true
end
local function blockAfter(seconds, duration)
local delaySeconds = seconds or 0
if delaySeconds <= 0 then
return blockFor(duration)
end
task.delay(delaySeconds, function()
blockFor(duration)
end)
return true
end
local function jumpTwice(delaySeconds)
local gap = delaySeconds or 0.2
InputClient.jump()
task.delay(gap, function()
InputClient.jump()
end)
end
executeDefensiveAction = function(def, rule, actionFn)
if type(actionFn) ~= "function" then
return nil
end
local _ = def
local beforeBlock = defenseActionState.blockIssued or 0
local beforeDash = defenseActionState.dashAttempt or 0
local result = actionFn()
local afterBlock = defenseActionState.blockIssued or 0
local afterDash = defenseActionState.dashAttempt or 0
local minBlock = (rule and rule.minBlockSeconds) or NO_HITBOX_MIN_BLOCK_SECONDS
local needsBlockFallback = (afterBlock <= beforeBlock)
or (afterDash > beforeDash and defenseActionState.dashOk == false)
if needsBlockFallback then
blockFor(math.max(minBlock, 0.05))
end
return result
end
local function trackSignalWindow(def, key, rule, delaySeconds, windowSeconds, intervalSeconds, callback)
local delay = delaySeconds or 0
local window = windowSeconds or 0
local _ = intervalSeconds
if window <= 0 then
return false
end
local info = def.stateInfo and def.stateInfo[key]
if not info then
return false
end
info.trackToken = (info.trackToken or 0) + 1
local token = info.trackToken
local extraDelay = 0
if type(skillDelaySeconds) == "function" then
extraDelay = skillDelaySeconds()
else
local defenderModule = require("Features/Combat/Objects/Defender")
local baseDelay = type(defenderModule) == "table" and type(defenderModule.rdelay) == "function" and defenderModule.rdelay()
or 0
extraDelay = math.clamp(math.max(0, baseDelay + NO_HITBOX_REACTION_BIAS_SECONDS), 0, 0.06)
end
local delayWithPing = delay + extraDelay
task.delay(delayWithPing, LPH_NO_VIRTUALIZE(function()
if not Configuration.expectToggleValue("EnableAutoDefense") then
return
end
if info.trackToken ~= token then
return
end
if UsingSkillWatcher._allowActions and not UsingSkillWatcher._allowActions("Skill") then
return
end
if UsingSkillWatcher._passesDistance and not UsingSkillWatcher._passesDistance(def, rule) then
return
end
if type(callback) == "function" then
local ok, err = pcall(function()
executeDefensiveAction(def, rule, callback)
end)
if not ok then
if Logger and Logger.warn then
Logger.warn("SignalWindow callback failed: %s", tostring(err))
else
warn(string.format("[SignalWindow] callback failed: %s", tostring(err)))
end
end
end
end))
return true
end
local function getWorkspaceService()
if workspaceService then
return workspaceService
end
local ok, resolved = pcall(game.GetService, game, "Workspace")
if ok and resolved then
workspaceService = resolved
return resolved
end
return workspace
end
local function iterGenkiCgaBrown()
local results = {}
local ws = getWorkspaceService()
local thrown = ws and ws:FindFirstChild("Thrown")
if thrown then
for _, child in ipairs(thrown:GetChildren()) do
if child:IsA("BasePart") and child.Name == "Genki" then
if child.BrickColor and child.BrickColor.Name == "CGA brown" then
results[#results + 1] = child
end
end
end
end
if #results > 0 then
return results
end
local fallback = ws and ws:FindFirstChild("Genki", true)
if fallback and fallback:IsA("BasePart") then
if fallback.BrickColor and fallback.BrickColor.Name == "CGA brown" then
results[#results + 1] = fallback
end
end
return results
end
local PART_METERS_TO_STUDS = 3.57
local partPulseActive = false
local partPulseTask = nil
local partLastActionAt = {}
local PART_ACTION_COOLDOWN = 0.25
local babylonGateCounts = {}
local BABYLON_M1_BLOCK_SECONDS = 0.3
local BABYLON_M1_RESET_SECONDS = 3.0
local BABYLON_M1_TRIGGER_COUNT = 5
local BABYLON_MULTIPLE_THRESHOLD = 2
local function noHitboxPartPollSeconds()
return NO_HITBOX_PART_POLL_SECONDS
end
local function localRootPart()
local character = LocalPlayer and LocalPlayer.Character
if not character then
return nil
end
return character:FindFirstChild("HumanoidRootPart") or character:FindFirstChild("Torso")
end
local function distanceToPartMeters(part)
local root = localRootPart()
if not root or not part then
return nil
end
local studs = (part.Position - root.Position).Magnitude
return studs / PART_METERS_TO_STUDS
end
local function isPartComingToUs(part)
local root = localRootPart()
if not root or not part then
return false
end
local velocity = part.AssemblyLinearVelocity
if not velocity or velocity.Magnitude < 1 then
return false
end
local toUs = (root.Position - part.Position)
if toUs.Magnitude <= 0.01 then
return false
end
local dot = velocity.Unit:Dot(toUs.Unit)
return dot > 0.35
end
local function canPulsePart(part)
local last = partLastActionAt[part]
local now = os.clock()
if last and now - last < PART_ACTION_COOLDOWN then
return false
end
partLastActionAt[part] = now
return true
end
local safeLower
do
local lowerFn = string and string.lower
safeLower = function(value)
if lowerFn then
return lowerFn(tostring(value or ""))
end
return tostring(value or "")
end
end
local function isCharacterUsingSkill(character)
if not character then
return false
end
local state = character:GetAttribute("CurrentState")
if state == "Skill" then
return true
end
local skillValue = character:FindFirstChild("UsingSkill", true)
if skillValue and skillValue:IsA("ValueBase") then
local raw = skillValue.Value
if type(raw) == "string" then
local value = safeLower(raw)
if value ~= "" and value ~= "none" and value ~= "idle" then
return true
end
end
end
return false
end
local function resolvePartOwnerPlayer(part)
if not part then
return nil
end
local model = part:FindFirstAncestorWhichIsA("Model")
local owner = model and Players:GetPlayerFromCharacter(model) or nil
local creator = part:FindFirstChild("creator") or part:FindFirstChild("Creator")
if not owner and creator and creator.Value and creator.Value:IsA("Player") then
owner = creator.Value
end
local ownerAttr = part:GetAttribute("Owner")
if not owner and typeof(ownerAttr) == "Instance" and ownerAttr:IsA("Player") then
owner = ownerAttr
end
local ownerId = part:GetAttribute("OwnerUserId")
if not owner and type(ownerId) == "number" then
owner = Players:GetPlayerByUserId(ownerId)
end
if not owner then
local closest = nil
local closestDist = math.huge
for _, plr in ipairs(Players:GetPlayers()) do
if plr == Players.LocalPlayer then
continue
end
local char = plr.Character
local root = char and (char:FindFirstChild("HumanoidRootPart") or char:FindFirstChild("Torso"))
if root then
local dist = (root.Position - part.Position).Magnitude
if dist < closestDist then
closestDist = dist
closest = plr
end
end
end
owner = closest
end
if owner == Players.LocalPlayer then
return nil
end
return owner
end
local function countBabylonGates()
local count = 0
local ws = getWorkspaceService()
local thrown = ws and ws:FindFirstChild("Thrown")
if thrown then
for _, child in ipairs(thrown:GetChildren()) do
if child:IsA("BasePart") and safeLower(child.Name) == "babylongate" then
count = count + 1
end
end
end
return count
end
local function handleBallPart(part)
if not part or not part:IsA("BasePart") then
return
end
if safeLower(part.Name) ~= "ball" then
return
end
if allowActions and not allowActions("Skill") then
return
end
if not canPulsePart(part) then
return
end
local brick = part.BrickColor and part.BrickColor.Name or ""
local dist = distanceToPartMeters(part) or math.huge
if safeLower(brick) == safeLower("CGA brown") then
if not isPartComingToUs(part) then
return
end
if dist <= 40 then
blockFor(1.3)
elseif dist <= 80 then
blockFor(1.7)
else
blockFor(2.0)
end
return
end
if safeLower(brick) == safeLower("Medium blue") then
if dist >= 80 then
dashAfter(0.5, "right")
else
dash("right")
end
return
end
dash("right")
end
local function handleBabylonGatePart(part)
if not part or not part:IsA("BasePart") then
return
end
if safeLower(part.Name) ~= "babylongate" then
return
end
if allowActions and not allowActions("Skill") then
return
end
if not canPulsePart(part) then
return
end
local owner = resolvePartOwnerPlayer(part)
if not owner then
return
end
local character = owner.Character
if not character or isCharacterUsingSkill(character) then
babylonGateCounts[owner] = nil
return
end
local now = os.clock()
local state = babylonGateCounts[owner]
if not state or (now - (state.lastAt or 0)) > BABYLON_M1_RESET_SECONDS then
state = { count = 0, lastAt = 0 }
end
state.count = state.count + 1
state.lastAt = now
babylonGateCounts[owner] = state
blockFor(BABYLON_M1_BLOCK_SECONDS)
if state.count >= BABYLON_M1_TRIGGER_COUNT then
local gateCount = countBabylonGates()
if gateCount >= BABYLON_MULTIPLE_THRESHOLD and not isCharacterUsingSkill(character) then
dash("back")
end
state.count = 0
end
end
local function handleCutPastelBlueGreen(part)
if not part or not part:IsA("BasePart") then
return
end
if safeLower(part.Name) ~= "cut" then
return
end
local brick = part.BrickColor and part.BrickColor.Name or ""
if safeLower(brick) ~= safeLower("Pastel blue-green") then
return
end
if allowActions and not allowActions("Skill") then
return
end
if not canPulsePart(part) then
return
end
local dist = distanceToPartMeters(part)
if dist and dist <= 40 then
blockFor(1.4)
end
end
local function startPartPulse()
if partPulseActive then
return
end
partPulseActive = true
partPulseTask = task.spawn(LPH_NO_VIRTUALIZE(function()
while partPulseActive do
task.wait(NO_HITBOX_ONLY and noHitboxPartPollSeconds() or 0.12)
if not Configuration.expectToggleValue("EnableAutoDefense") then
continue
end
local ok, err = pcall(function()
local ws = getWorkspaceService()
local thrown = ws and ws:FindFirstChild("Thrown")
if thrown then
for _, child in ipairs(thrown:GetChildren()) do
handleBabylonGatePart(child)
handleBallPart(child)
handleCutPastelBlueGreen(child)
end
end
end)
if not ok then
if Logger and Logger.warn then
Logger.warn("Part pulse tick failed: %s", tostring(err))
else
warn(string.format("[PartPulse] tick failed: %s", tostring(err)))
end
end
end
end))
end
local DASH_BLOCK_DELAY_SECONDS = 0.05
blockFor = function(seconds)
if not Configuration.expectToggleValue("EnableAutoDefense") then
return false
end
local hold = seconds or 0
if hold <= 0 then
hold = NO_HITBOX_MIN_BLOCK_SECONDS
end
defenseActionState.blockIssued = (defenseActionState.blockIssued or 0) + 1
return UsingSkillWatcher.blockHoldSeconds(hold)
end
local function dashThenBlock(dir, seconds, parallel)
local holdSeconds = seconds or 0
if parallel then
task.defer(dash, dir)
else
dash(dir)
end
if holdSeconds <= 0 then
return false
end
task.delay(DASH_BLOCK_DELAY_SECONDS, function()
blockFor(holdSeconds)
end)
return true
end
local function normalizeCharName(value)
return string.lower(tostring(value or "")):gsub("[^%w]", "")
end
local function getDefLastLoadedChar(def)
local plr = def and def.player
if plr then
local raw = plr:GetAttribute("LastLoadedChar")
if raw ~= nil then
return raw
end
end
local character = def and def.character
if character then
local raw = character:GetAttribute("LastLoadedChar")
if raw ~= nil then
return raw
end
end
return nil
end
local function isChar(def, name)
local raw = getDefLastLoadedChar(def)
if not raw then
return false
end
return normalizeCharName(raw) == normalizeCharName(name)
end
local function hasKaioken(def)
local character = def and def.character
if not character then
return false
end
return character:FindFirstChild("KAIOKEN") ~= nil or character:FindFirstChild("Kaioken") ~= nil
end
local function distanceToDef(def)
local character = def and def.character
if not character then
return nil
end
local root = character:FindFirstChild("HumanoidRootPart") or character:FindFirstChild("Torso")
if not root then
return nil
end
local localChar = LocalPlayer and LocalPlayer.Character
if not localChar then
return nil
end
local localRoot = localChar:FindFirstChild("HumanoidRootPart") or localChar:FindFirstChild("Torso")
if not localRoot then
return nil
end
return (root.Position - localRoot.Position).Magnitude
end
local function rushBlockSecondsForDistance(distance)
if not distance or distance <= 40 then
return 1.2
end
if distance <= 80 then
return 1.45
end
return 1.8
end
UsingSkillWatcher.setRules({
Kamehameha = {
token = "kamehameha",
shape = "Box",
hitbox = Vector3.new(11, 12.5, 200),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Kamehameha", UsingSkillWatcher.rules.Kamehameha, 0, 0.6, 0.05, function()
InputClient.jump()
dash("right")
end)
end,
},
RushKaioken = {
token = "rush",
match = function(def)
return hasKaioken(def)
end,
fhb = true,
onStart = function(def)
local seconds = rushBlockSecondsForDistance(distanceToDef(def))
blockFor(seconds)
dash("back")
dashAfter(1.2, "back")
end,
},
Rush = {
token = "rush",
match = function(def)
return not hasKaioken(def)
end,
fhb = true,
onStart = function(def)
local seconds = rushBlockSecondsForDistance(distanceToDef(def))
dashThenBlock("right", seconds, true)
end,
},
GetsugaSlash = {
token = "getsuga",
spacedToken = "getsuga slash",
fhb = true,
ihbc = true,
onStart = function()
dash("right")
end,
},
MultiCut = {
token = "multicut",
spacedToken = "multi cut",
fhb = true,
ihbc = true,
onStart = function()
task.defer(dash, "back")
blockFor(1.4)
end,
},
Lunge = {
token = "lunge",
fhb = true,
ihbc = true,
onStart = function()
blockAfter(0.216, 1.2)
end,
},
FireflyLight = {
token = "fireflylight",
spacedToken = "firefly light",
fhb = true,
ihbc = true,
onStart = function(def)
InputClient.jump()
local dist = distanceToDef(def)
if dist and dist >= 40 then
dash("forward")
else
dash("back")
end
end,
},
DragonCombo = {
token = "dragoncombo",
spacedToken = "dragon combo",
shape = "Box",
hitbox = Vector3.new(60, 60, 60),
fhb = true,
onStart = function(def)
local holdSeconds = hasKaioken(def) and 2.2 or 1.8
dashThenBlock("back", holdSeconds, true)
end,
},
CrossKicks = {
token = "crosskicks",
spacedToken = "cross kicks",
shape = "Box",
hitbox = Vector3.new(24, 17, 24),
fhb = true,
onStart = function()
task.defer(dash, "back")
blockFor(0.9)
end,
},
SpecialBeamCannon = {
token = "specialbeamcannon",
spacedToken = "special beam cannon",
shape = "Box",
hitbox = Vector3.new(10, 10, 150),
fhb = true,
onStart = function()
dash("right")
end,
},
GutKick = {
token = "gutkick",
spacedToken = "gut kick",
shape = "Box",
hitbox = Vector3.new(10, 10, 10),
fhb = true,
onStart = function()
blockFor(1.2)
end,
},
Uppercut = {
token = "uppercut",
shape = "Box",
hitbox = Vector3.new(35, 35, 35),
fhb = true,
onStart = function()
blockFor(1.2)
dash("back")
dashAfter(1.2, "back")
end,
},
BusterCannon = {
token = "bustercannon",
spacedToken = "buster cannon",
shape = "Box",
hitbox = Vector3.new(15, 20, 180),
fhb = true,
onStart = function()
dash("right")
end,
},
RapidSwordStream = {
token = "rapidswordstream",
spacedToken = "rapid sword stream",
shape = "Ball",
hitbox = Vector3.new(17, 22, 14),
fhb = true,
onStart = function()
dashThenBlock("back", 1.2, false)
end,
},
BlazingRush = {
token = "blazingrush",
spacedToken = "blazing rush",
shape = "Box",
hitbox = Vector3.new(10, 10, 70),
ihbc = true,
fhb = true,
onStart = function()
blockFor(0.9)
end,
},
FlashFist = {
token = "flashfist",
spacedToken = "flash fist",
shape = "Box",
hitbox = Vector3.new(15, 15, 100),
fhb = true,
onStart = function()
blockFor(1.4)
end,
},
RetirementKick = {
token = "retirementkick",
spacedToken = "retirement kick",
shape = "Box",
hitbox = Vector3.new(30, 30, 30),
fhb = true,
onStart = function()
dashThenBlock("back", 1.6, true)
end,
},
KiBlitz = {
token = "kiblitz",
spacedToken = "ki blitz",
shape = "Box",
hitbox = Vector3.new(15, 15, 150),
fhb = true,
onStart = function()
blockFor(2.1)
end,
},
KiTraps = {
token = "kitraps",
spacedToken = "ki traps",
shape = "Box",
hitbox = Vector3.new(15, 15, 150),
fhb = true,
onStart = function()
blockFor(1.7)
end,
},
ScatterShot = {
token = "scattershot",
spacedToken = "scatter shot",
shape = "Box",
hitbox = Vector3.new(30, 30, 150),
fhb = true,
onStart = function()
blockFor(0.9)
InputClient.jump()
end,
},
ComboBlitz = {
token = "comboblitz",
spacedToken = "combo blitz",
shape = "Box",
hitbox = Vector3.new(15, 15, 30),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "ComboBlitz", UsingSkillWatcher.rules.ComboBlitz, 0, 2.0, 0.05, function()
blockFor(2.9)
end)
end,
},
ExplosiveField = {
token = "explosivefield",
spacedToken = "explosive field",
shape = "Ball",
hitbox = Vector3.new(45, 45, 45),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "ExplosiveField", UsingSkillWatcher.rules.ExplosiveField, 0.35, 0.8, 0.05, function()
dash("back")
end)
end,
},
SuperVolley = {
token = "supervolley",
spacedToken = "super volley",
ihbc = true,
fhb = true,
onStart = function()
blockFor(1.9)
end,
},
HellzoneGrenade = {
token = "hellzonegrenade",
spacedToken = "hellzone grenade",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.9, "back")
end,
},
DestructoDisk = {
token = "destructodisk",
spacedToken = "destructo disk",
ihbc = true,
fhb = true,
onStart = function()
jumpTwice()
task.defer(dash, "right")
end,
},
SuperBomber = {
token = "superbomber",
spacedToken = "super bomber",
shape = "Box",
hitbox = Vector3.new(50, 50, 150),
fhb = true,
onStart = function()
blockFor(1.45)
end,
},
EnergyFist = {
token = "energyfist",
spacedToken = "energy fist",
ihbc = true,
fhb = true,
onStart = function()
blockFor(1.25)
end,
},
KiBlast = {
token = "kiblast",
spacedToken = "ki blast",
ihbc = true,
fhb = true,
onStart = function()
blockFor(1.6)
end,
},
ElbowKick = {
token = "elbowkick",
spacedToken = "elbow kick",
shape = "Box",
hitbox = Vector3.new(45, 45, 45),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "ElbowKick", UsingSkillWatcher.rules.ElbowKick, 0, 1.7, 0.05, function()
blockFor(1.4)
end)
end,
},
KameBlast = {
token = "kameblast",
spacedToken = "kame blast",
shape = "Box",
hitbox = Vector3.new(15, 16, 120),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "KameBlast", UsingSkillWatcher.rules.KameBlast, 0, 2.0, 0.05, function()
InputClient.jump()
task.defer(dash, "right")
task.delay(0.3, function()
blockFor(0.45)
end)
end)
end,
},
RapidKickRush = {
token = "rapidkickrush",
spacedToken = "rapid kick rush",
shape = "Box",
hitbox = Vector3.new(17, 24, 20),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "RapidKickRush", UsingSkillWatcher.rules.RapidKickRush, 0.297, 1.0, 0.05, function()
jumpTwice()
task.defer(dash, "back")
end)
end,
},
CosmicRush = {
token = "cosmicrush",
spacedToken = "cosmic rush",
shape = "Box",
hitbox = Vector3.new(18, 20, 180),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "CosmicRush", UsingSkillWatcher.rules.CosmicRush, 0, 1.2, 0.05, function()
blockFor(0.95)
end)
end,
},
Grab = {
token = "grab",
shape = "Box",
hitbox = Vector3.new(15, 15, 120),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Grab", UsingSkillWatcher.rules.Grab, 0.455, 1.3, 0.05, function()
blockFor(1.1)
end)
end,
},
PunchOnslaught = {
token = "punchonslaught",
spacedToken = "punch onslaught",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "PunchOnslaught", UsingSkillWatcher.rules.PunchOnslaught, 0, 1.7, 0.05, function()
dash("back")
task.delay(0.4, function()
blockFor(1.125)
end)
end)
end,
},
FastFlashAttack = {
token = "fastflashattack",
spacedToken = "fast flash attack",
shape = "Box",
hitbox = Vector3.new(15, 15, 100),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "FastFlashAttack", UsingSkillWatcher.rules.FastFlashAttack, 0, 0.3, 0.05, function()
blockFor(0.9)
end)
end,
},
FinalStrike = {
token = "finalstrike",
spacedToken = "final strike",
shape = "Box",
hitbox = Vector3.new(25, 15, 120),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "FinalStrike", UsingSkillWatcher.rules.FinalStrike, 0.371, 1.2, 0.05, function()
dash("right")
end)
end,
},
PunisherDrive = {
token = "punisherdrive",
spacedToken = "punisher drive",
shape = "Box",
hitbox = Vector3.new(12, 12, 45),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "PunisherDrive", UsingSkillWatcher.rules.PunisherDrive, 0.07, 0.3, 0.05, function()
blockFor(1.2)
end)
end,
},
EnergyWave = {
token = "energywave",
spacedToken = "energy wave",
ihbc = true,
fhb = true,
onStart = function()
jumpTwice()
task.defer(dash, "right")
end,
},
SpiralingJustice = {
token = "spiralingjustice",
spacedToken = "spiraling justice",
ihbc = true,
fhb = true,
onStart = function()
task.delay(0.195, function()
blockFor(0.8)
end)
end,
},
MadDance = {
token = "maddance",
spacedToken = "mad dance",
ihbc = true,
fhb = true,
onStart = function()
dash("back")
end,
},
HeavyBlow = {
token = "heavyblow",
spacedToken = "heavy blow",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "HeavyBlow", UsingSkillWatcher.rules.HeavyBlow, 0, 0.9, 0.05, function()
dash("back")
end)
end,
},
PowerRoar = {
token = "powerroar",
spacedToken = "power roar",
shape = "Box",
hitbox = Vector3.new(15, 15, 45),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "PowerRoar", UsingSkillWatcher.rules.PowerRoar, 0, 0.8, 0.05, function()
jumpTwice()
task.defer(dash, "right")
end)
end,
},
WraithfulCrash = {
token = "wraithfulcrash",
spacedToken = "wraithful crash",
shape = "Box",
hitbox = Vector3.new(15, 15, 60),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "WraithfulCrash", UsingSkillWatcher.rules.WraithfulCrash, 0, 1.2, 0.05, function()
blockFor(1.2)
end)
end,
},
Skill_1731404034 = {
token = "1731404034",
shape = "Box",
hitbox = Vector3.new(40, 40, 80),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Skill_1731404034", UsingSkillWatcher.rules.Skill_1731404034, 0.14, 0.6, 0.05, function()
blockFor(1.2)
end)
end,
},
Skill_92993354909172 = {
token = "92993354909172",
shape = "Box",
hitbox = Vector3.new(15, 15, 60),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Skill_92993354909172", UsingSkillWatcher.rules.Skill_92993354909172, 0.26, 1.2, 0.05, function()
blockFor(1.2)
end)
end,
},
Skill_1886062099 = {
token = "1886062099",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Skill_1886062099", UsingSkillWatcher.rules.Skill_1886062099, 0, 0.9, 0.05, function()
dash("back")
end)
end,
},
FritAssorti = {
token = "fritassorti",
spacedToken = "frit assorti",
shape = "Box",
hitbox = Vector3.new(24, 24, 40),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "FritAssorti", UsingSkillWatcher.rules.FritAssorti, 0, 1.2, 0.05, function()
task.delay(0.6, function()
dash("right")
end)
end)
end,
},
Spectre = {
token = "spectre",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Spectre", UsingSkillWatcher.rules.Spectre, 0, 1.25, 0.05, function()
task.defer(dash, "back")
blockFor(1.6)
end)
end,
},
TableParty = {
token = "tableparty",
spacedToken = "table party",
shape = "Ball",
hitbox = Vector3.new(45, 45, 45),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "TableParty", UsingSkillWatcher.rules.TableParty, 0, 1.5, 0.05, function()
blockFor(1.4)
end)
end,
},
Skill_10006551970 = {
token = "10006551970",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Skill_10006551970", UsingSkillWatcher.rules.Skill_10006551970, 0.307, 1.1, 0.05, function()
jumpTwice()
task.defer(dash, "right")
end)
end,
},
CounterShock = {
token = "countershock",
spacedToken = "counter shock",
ihbc = true,
fhb = true,
onStart = function()
jumpTwice()
task.defer(dash, "back")
end,
},
GammaKnife = {
token = "gammaknife",
spacedToken = "gamma knife",
ihbc = true,
fhb = true,
onStart = function()
jumpTwice()
task.defer(dash, "back")
end,
},
Gatling = {
token = "gatling",
shape = "Box",
hitbox = Vector3.new(15, 15, 15),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Gatling", UsingSkillWatcher.rules.Gatling, 0, 1.5, 0.05, function()
task.defer(dash, "back")
blockFor(2.2)
end)
end,
},
BusoGatling = {
token = "busogatling",
spacedToken = "buso gatling",
fhb = true,
ihbc = true,
onStart = function()
task.defer(dash, "back")
blockFor(2.2)
end,
},
Skill_1731336080 = {
token = "1731336080",
shape = "Box",
hitbox = Vector3.new(15, 15, 35),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Skill_1731336080", UsingSkillWatcher.rules.Skill_1731336080, 0.159, 1.0, 0.05, function()
dash("right")
end)
end,
},
Bazooka = {
token = "bazooka",
shape = "Box",
hitbox = Vector3.new(15, 15, 120),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Bazooka", UsingSkillWatcher.rules.Bazooka, 0, 1.2, 0.05, function()
dashAfter(0.645, "right")
end)
end,
},
Rocket = {
token = "rocket",
ihbc = true,
fhb = true,
onStart = function(def)
local distance = distanceToDef(def)
local seconds = 1.2
if distance and distance > 80 then
seconds = 2.0
elseif distance and distance > 40 then
seconds = 1.7
end
blockFor(seconds)
end,
},
Pistol = {
token = "pistol",
ihbc = true,
fhb = true,
onStart = function(def)
local distance = distanceToDef(def)
local seconds = 1.2
if distance and distance > 80 then
seconds = 2.0
elseif distance and distance > 40 then
seconds = 1.7
end
blockFor(seconds)
end,
},
Saikuru = {
token = "saikuru",
shape = "Ball",
hitbox = Vector3.new(50, 20, 0),
offset = Vector3.new(0, 0, 20),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "Saikuru", UsingSkillWatcher.rules.Saikuru, 0, 0.4, 0.05, function()
dash("back")
end)
end,
},
SpinningMeteor = {
token = "spinningmeteor",
spacedToken = "spinning meteor",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
onStart = function(def)
trackSignalWindow(def, "SpinningMeteor", UsingSkillWatcher.rules.SpinningMeteor, 0, 1.2, 0.05, function()
dash("back")
end)
end,
},
KickAwaySledgehammer = {
token = "sledgehammer",
spacedToken = "kick away",
ihbc = true,
fhb = true,
onStart = function()
task.delay(0.085, function()
blockFor(0.7)
end)
end,
},
Begone = {
token = "begone",
ihbc = true,
fhb = true,
onStart = function()
dash("right")
end,
},
RedHawk = {
token = "redhawk",
spacedToken = "red hawk",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.384, 1.3)
end,
},
HellMemories = {
token = "hellmemories",
spacedToken = "hell memories",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.6, "right")
end,
},
GrillShot = {
token = "grillshot",
spacedToken = "grill shot",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.3, 1.4)
end,
},
ViceGrip = {
token = "vicegrip",
spacedToken = "vice grip",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.4, 0.7)
end,
},
LigerBomb = {
token = "ligerbomb",
spacedToken = "liger bomb",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.1, 0.2)
end,
},
Lariat = {
token = "lariat",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.33, "right")
end,
},
GuillotineDrop = {
token = "guillotinedrop",
spacedToken = "guillotine drop",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.35, "right")
end,
},
RaigaBomb = {
token = "raigabomb",
spacedToken = "raiga bomb",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.1, 0.2)
end,
},
LightningStraight = {
token = "lightningstraight",
spacedToken = "lightning straight",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.1, 3.7)
end,
},
WhirlwindThrow = {
token = "whirlwindthrow",
spacedToken = "whirlwind throw",
ihbc = true,
fhb = true,
onStart = function()
blockAfter(0.1, 0.2)
end,
},
JetPistol = {
token = "jetpistol",
spacedToken = "jet pistol",
ihbc = true,
fhb = true,
onStart = function()
task.defer(dash, "back")
blockFor(1.4)
end,
},
GrizzlyMagnum = {
token = "grizzlymagnum",
spacedToken = "grizzly magnum",
ihbc = true,
fhb = true,
onStart = function(def)
task.delay(0.687, function()
local dist = distanceToDef(def)
if dist and dist <= 20 then
dash("forward")
else
dash("back")
end
end)
end,
},
SanzenSekai = {
token = "sanzensekai",
spacedToken = "sanzen sekai",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.38, "right")
end,
},
TigerHunt = {
token = "tigerhunt",
spacedToken = "tiger hunt",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.196, "right")
end,
},
BurstEnergy = {
token = "burstenergy",
spacedToken = "burst energy",
shape = "Box",
hitbox = Vector3.new(15, 20, 120),
fhb = true,
onStart = function()
dash("right")
end,
},
VanishKick = {
token = "vanishkick",
spacedToken = "vanish kick",
shape = "Box",
hitbox = Vector3.new(18, 15, 20),
fhb = true,
deferHitboxSeconds = 0.7,
onStart = function(def)
UsingSkillWatcher.deferHitbox(def, "VanishKick", 0.7, function()
dash("back")
end)
end,
},
DashBarrage = {
token = "dashbarrage",
spacedToken = "dash barrage",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
fhb = true,
deferHitboxSeconds = 0.388,
onStart = function(def)
UsingSkillWatcher.deferHitbox(def, "DashBarrage", 0.388, function()
dash("right")
end)
end,
},
Punish = {
token = "punish",
shape = "Box",
hitbox = Vector3.new(20, 15, 24),
fhb = true,
onStart = function()
dash("right")
end,
},
SpiritSword = {
token = "spiritsword",
spacedToken = "spirit sword",
shape = "Box",
hitbox = Vector3.new(33, 6, 65),
fhb = true,
onStart = function()
blockFor(1.4)
task.delay(1.4, function()
blockFor(1.6)
end)
end,
},
SavageStrike = {
token = "savagestrike",
spacedToken = "savage strike",
ihbc = true,
fhb = true,
onStart = function()
dashAfter(0.37, "back")
end,
},
DeathBeam = {
token = "deathbeam",
spacedToken = "death beam",
shape = "Box",
hitbox = Vector3.new(3, 3, 100),
offset = Vector3.new(-1, 0, 0),
ihbc = true,
fhb = true,
onStart = function(def)
trackSignalWindow(def, "DeathBeam", UsingSkillWatcher.rules.DeathBeam, 0, 0.8, 0.05, function()
InputClient.jump()
task.defer(dash, "right")
end)
end,
},
TailSlam = {
token = "tailslam",
spacedToken = "tail slam",
shape = "Box",
hitbox = Vector3.new(24, 24, 24),
ihbc = true,
fhb = true,
onStart = function(def)
trackSignalWindow(def, "TailSlam", UsingSkillWatcher.rules.TailSlam, 0.4, 0.9, 0.05, function()
blockFor(1.2)
end)
end,
},
PlanetDestroyer = {
token = "planetdestroyer",
spacedToken = "planet destroyer",
ihbc = true,
fhb = true,
onStart = function(def)
local info = def.stateInfo and def.stateInfo.PlanetDestroyer
if not info then
return
end
info.genkiBlockUntil = 0
end,
onTick = function(def)
local info = def.stateInfo and def.stateInfo.PlanetDestroyer
if not info then
return
end
if info.genkiBlockUntil and os.clock() < info.genkiBlockUntil then
return
end
local root = localRootPart()
if not root then
return
end
for _, part in ipairs(iterGenkiCgaBrown()) do
local radius = math.max(17, math.max(part.Size.X, part.Size.Y, part.Size.Z) * 0.5)
if (part.Position - root.Position).Magnitude <= (radius + 4) then
blockFor(1.2)
info.genkiBlockUntil = os.clock() + 1.2
return
end
end
end,
},
})
UsingSkillWatcher.init()
startPartPulse()
end
local function standDistanceMeters(model)
if not model or not model:IsA("Model") then
return 0
end
local localCharacter = Players.LocalPlayer and Players.LocalPlayer.Character
if not localCharacter then
return 0
end
local localRoot = localCharacter:FindFirstChild("HumanoidRootPart")
if not localRoot then
return 0
end
local root = model:FindFirstChild("HumanoidRootPart")
if not root then
return 0
end
return (root.Position - localRoot.Position).Magnitude
end
local standOwnerCache = {}
standOwnerCache.logLast = standOwnerCache.logLast or {}
standOwnerCache.logCooldown = standOwnerCache.logCooldown or 0.35
function standOwnerCache.shouldLogStand(key)
local now = os.clock()
local last = standOwnerCache.logLast[key]
if last and now - last < standOwnerCache.logCooldown then
return false
end
standOwnerCache.logLast[key] = now
return true
end
standOwnerCache.standM1FallbackTiming = standOwnerCache.standM1FallbackTiming or {
tag = "M1",
actions = {
_data = {
{ _type = "Start Block" },
{ _type = "End Block", _when = 300 },
},
},
}
function standOwnerCache.resolveStandOwnerForScan(live, standModel)
local cached = standOwnerCache[standModel]
if cached and cached.Parent then
return cached
end
standOwnerCache[standModel] = nil
if not live then
return nil
end
for _, entity in next, live:GetChildren() do
local standValue = entity:FindFirstChild("STANDOBJ")
if standValue and standValue.Value == standModel then
standOwnerCache[standModel] = entity
return entity
end
end
return nil
end
function standOwnerCache.resolveStandTimingForScan(fullId, track)
local timingStore = SaveManager.as
if not timingStore then
return nil
end
local timing = timingStore:index(fullId)
if timing then
return timing
end
local trackName = track and track.Name
if trackName and trackName ~= "" then
timing = timingStore:find(trackName)
if timing then
return timing
end
end
local animName = track and track.Animation and track.Animation.Name
if animName and animName ~= "" then
return timingStore:find(animName)
end
return nil
end
function standOwnerCache.standAnimIsLikelyM1(name)
local lowered = string.lower(tostring(name or ""))
if lowered == "" then
return false
end
if lowered == "m1" then
return true
end
if
lowered:find("m1_")
or lowered:find("_m1")
or lowered:find(" m1")
or lowered:find("m1 ")
or lowered:find("-m1")
or lowered:find("m1-")
then
return true
end
if lowered:find("light") or lowered:find("basic") or lowered:find("jab") then
return true
end
return false
end
function standOwnerCache.isLikelyStandM1ForScan(track, timing)
if timing and timing.tag == "M1" then
return true
end
if timing and standOwnerCache.standAnimIsLikelyM1(timing.name) then
return true
end
local trackName = track and track.Name
if standOwnerCache.standAnimIsLikelyM1(trackName) then
return true
end
local animName = track and track.Animation and track.Animation.Name
if standOwnerCache.standAnimIsLikelyM1(animName) then
return true
end
return false
end
function standOwnerCache.collectStandModelsForScan(standsFolder, live, localStand)
local standModels = {}
local function addStandModel(standModel)
if standModel and standModel:IsA("Model") then
standModels[standModel] = true
end
end
if standsFolder then
for _, descendant in next, standsFolder:GetDescendants() do
local standModel = descendant:FindFirstAncestorWhichIsA("Model")
if standModel then
addStandModel(standModel)
end
end
end
if live then
for _, entity in next, live:GetChildren() do
local standValue = entity:FindFirstChild("STANDOBJ")
if standValue and standValue.Value and standValue.Value:IsA("Model") then
addStandModel(standValue.Value)
end
end
end
if localStand then
addStandModel(localStand)
end
return standModels
end
function standOwnerCache.processStandTracksForScan(standModel, tracks, live, localStand, flags)
if not standModel or not tracks then
return
end
local owner = standOwnerCache.resolveStandOwnerForScan(live, standModel)
local ownerName = owner and owner.Name or standModel.Name
local ownerDistance = owner and standDistanceMeters(owner) or standDistanceMeters(standModel)
for _, track in next, tracks do
if not track then
continue
end
local aid = (track.Animation and track.Animation.AnimationId) or track.AnimationId
local aidStr = tostring(aid or "")
if aidStr == "" then
continue
end
local numericId = aidStr:match("%d+")
local fullId = numericId and ("rbxassetid://" .. numericId) or aidStr
local timing = standOwnerCache.resolveStandTimingForScan(fullId, track)
if flags.m1Trade then
if timing and timing.tag == "M1" then
M1AutoTrade.handleEntity(timing, owner or standModel, ownerDistance)
elseif standOwnerCache.isLikelyStandM1ForScan(track, timing) then
M1AutoTrade.handleEntity(standOwnerCache.standM1FallbackTiming, owner or standModel, ownerDistance)
end
end
if flags.logStand or flags.logAll or (flags.logLocal and localStand and standModel == localStand) then
local key = standModel:GetFullName() .. "|" .. fullId
if standOwnerCache.shouldLogStand(key) then
local distance = ownerDistance
if timing then
Library:AddExistAnimEntry(ownerName, distance, timing)
else
Library:AddMissEntry("Stand Anim", fullId, ownerName, distance, "Stand")
end
end
end
end
end
function standOwnerCache.scanStandModelForScan(standModel, live, localStand, flags)
if not standModel then
return
end
for _, desc in next, standModel:GetDescendants() do
if desc:IsA("Animator") then
standOwnerCache.processStandTracksForScan(standModel, desc:GetPlayingAnimationTracks(), live, localStand, flags)
elseif desc:IsA("Humanoid") then
local animator = desc:FindFirstChildOfClass("Animator")
if animator then
standOwnerCache.processStandTracksForScan(
standModel,
animator:GetPlayingAnimationTracks(),
live,
localStand,
flags
)
end
elseif desc:IsA("AnimationController") then
standOwnerCache.processStandTracksForScan(standModel, desc:GetPlayingAnimationTracks(), live, localStand, flags)
end
end
end
function standOwnerCache.shouldProcessStandModelForScan(standModel, localStand, flags)
if localStand and standModel == localStand then
return flags.logLocal or flags.logAll or flags.m1Trade
end
return flags.logStand or flags.logAll or flags.m1Trade
end
standOwnerCache.scanStandAnimations = function()
if not standOwnerCache.standsFolderRef or not standOwnerCache.standsFolderRef.Parent then
standOwnerCache.standsFolderRef = workspace:FindFirstChild("Stands")
end
local standsFolder = standOwnerCache.standsFolderRef
local live = workspace:FindFirstChild("Live")
local localStand = nil
if Players.LocalPlayer and Players.LocalPlayer.Character then
local standValue = Players.LocalPlayer.Character:FindFirstChild("STANDOBJ")
if standValue and standValue.Value and standValue.Value:IsA("Model") then
localStand = standValue.Value
end
end
local logStand = Configuration.expectToggleValue("LogStandAnimations")
local logAll = Configuration.expectToggleValue("LogAllAnimations")
local logLocal = Configuration.expectToggleValue("LogLocalStandAnimations")
local m1Trade = Configuration.expectToggleValue("EnableM1AutoTrade")
if not (logStand or logAll or logLocal or m1Trade) then
return
end
local flags = {
logStand = logStand,
logAll = logAll,
logLocal = logLocal,
m1Trade = m1Trade,
}
local standModels = standOwnerCache.collectStandModelsForScan(standsFolder, live, localStand)
for standModel in next, standModels do
if standOwnerCache.shouldProcessStandModelForScan(standModel, localStand, flags) then
standOwnerCache.scanStandModelForScan(standModel, live, localStand, flags)
end
end
end
do
(game:GetService("RunService").PostSimulation or game:GetService("RunService").Heartbeat):Connect(LPH_NO_VIRTUALIZE(function()
if Configuration.expectToggleValue("LogAllAnimations")
or Configuration.expectToggleValue("LogStandAnimations")
or Configuration.expectToggleValue("LogLocalStandAnimations")
or Configuration.expectToggleValue("EnableM1AutoTrade")
then
standOwnerCache.scanStandAnimations()
end
end))
end



This is not the real Cottom Hub and the picture shown is clickbait. Do not use this it is potentially malicious and could get you banned on ABA or even your account hacked.
@RocketDaMoon this is the real cottom hub and it is not malicous.
@RocketDaMoon , this is open source