pulse-rb 1.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +225 -0
  2. package/adapters/linoria.lua +233 -0
  3. package/adapters/windui.llms.txt +366 -0
  4. package/adapters/windui.lua +505 -0
  5. package/bin/rb.js +2 -0
  6. package/dist/index.js +3285 -0
  7. package/package.json +59 -0
  8. package/pulse/dev/debuggui.lua +1206 -0
  9. package/pulse/dev/devconfig.lua +81 -0
  10. package/pulse/dev/ui/9_DevPanel.lua +384 -0
  11. package/pulse/helpers/aim.lua +193 -0
  12. package/pulse/helpers/cache.lua +68 -0
  13. package/pulse/helpers/cleaner.lua +110 -0
  14. package/pulse/helpers/conn.lua +33 -0
  15. package/pulse/helpers/cooldown.lua +47 -0
  16. package/pulse/helpers/draw.lua +122 -0
  17. package/pulse/helpers/hitbox.lua +63 -0
  18. package/pulse/helpers/input.lua +24 -0
  19. package/pulse/helpers/log.lua +228 -0
  20. package/pulse/helpers/loop.lua +58 -0
  21. package/pulse/helpers/memory.lua +48 -0
  22. package/pulse/helpers/narrate.lua +160 -0
  23. package/pulse/helpers/notify.lua +51 -0
  24. package/pulse/helpers/perf.lua +48 -0
  25. package/pulse/helpers/remote.lua +128 -0
  26. package/pulse/helpers/restore.lua +59 -0
  27. package/pulse/helpers/store.lua +39 -0
  28. package/pulse/helpers/team.lua +83 -0
  29. package/pulse/helpers/testmode.lua +80 -0
  30. package/pulse/helpers/trace.lua +111 -0
  31. package/pulse/helpers/track.lua +85 -0
  32. package/pulse/helpers/vec.lua +52 -0
  33. package/pulse/helpers/world.lua +51 -0
  34. package/pulse/runtime.lua +343 -0
  35. package/pulse/ui/linoria_settings.lua +55 -0
  36. package/pulse/ui/windui_settings.lua +87 -0
  37. package/templates/AGENTS.md +177 -0
  38. package/templates/CLAUDE.md +424 -0
  39. package/templates/deploy_config.example +17 -0
  40. package/templates/example_esp.rblua +69 -0
  41. package/templates/example_fov.rblua +20 -0
  42. package/templates/example_speed.rblua +25 -0
  43. package/templates/gitignore +4 -0
  44. package/templates/globals.lua +66 -0
  45. package/templates/layout.rblua +28 -0
  46. package/templates/module.lua +7 -0
  47. package/templates/module.rblua +32 -0
  48. package/templates/page_home.rblua +9 -0
  49. package/templates/remote.lua +6 -0
  50. package/templates/remotes.lua +14 -0
  51. package/vscode/language-configuration.json +35 -0
  52. package/vscode/package.json +35 -0
  53. package/vscode/src/extension.js +397 -0
  54. package/vscode/syntaxes/rblua.tmLanguage.json +126 -0
@@ -0,0 +1,81 @@
1
+ -- ═══════════════════════════════════════════════════════════════════
2
+ -- DEVELOPER CONFIGURATION — the only file you edit per debug session
3
+ --
4
+ -- Scenarios and which tools to use:
5
+ --
6
+ -- "Feature randomly stopped working"
7
+ -- → Pulse.Log.watchSignal(Components.Aimbot.enabled, "aimbot", "enabled")
8
+ -- Logs the exact moment and value every time the toggle changes.
9
+ --
10
+ -- "I don't know if the feature is even running"
11
+ -- → Pulse.Monitor.tick("aimbot") inside the loop body
12
+ -- Counter in monitor bar stops climbing = loop is dead.
13
+ --
14
+ -- "What data does the function receive / return?"
15
+ -- → Pulse.Trace.instrument(func, "globals")
16
+ -- Every func.* call logs actual argument values and return values.
17
+ --
18
+ -- "Something threw but I don't know where"
19
+ -- → Framework errors (Signal, Event, Component) now route to Pulse.Log.error
20
+ -- automatically — check the debug UI, filter level=ERR.
21
+ --
22
+ -- "Narrow to one feature so I'm not overwhelmed"
23
+ -- → Pulse.TestMode.configure({ target = "FeatureName" })
24
+ -- All other toggles start disabled.
25
+ -- ═══════════════════════════════════════════════════════════════════
26
+
27
+ -- ── 1. Logging ────────────────────────────────────────────────────
28
+ Pulse.Log.configure({
29
+ level = "debug", -- trace | debug | info | warn | error
30
+ console = true,
31
+ file = "pulse_dev.log",
32
+
33
+ -- Restrict to specific tags (whitelist):
34
+ -- tags = { "aimbot", "globals" },
35
+
36
+ -- Or exclude noisy tags:
37
+ -- except = { "component", "loop" },
38
+ })
39
+
40
+ -- ── 2. Test mode ──────────────────────────────────────────────────
41
+ -- Change target = "FeatureName" to start with only that feature on.
42
+ Pulse.TestMode.configure({
43
+ all = true, -- ← change to: target = "ShifterPartsCutter"
44
+ })
45
+
46
+ -- ── 3. Signal watching — "why did this feature turn off?" ─────────
47
+ -- Logs every value change on the signal, tagged so you can search it.
48
+ -- Uncomment and change to whatever component you're investigating.
49
+ --
50
+ -- Pulse.Log.watchSignal(Components.ShifterPartsCutter.enabled, "cutter", "enabled")
51
+ -- Pulse.Log.watchSignal(Components.Aimbot.enabled, "aimbot", "enabled")
52
+
53
+ -- ── 4. Function tracing — "what data does it receive/return?" ─────
54
+ -- Shows actual argument values and return values for every func.* call.
55
+ -- Can be noisy — use tags filter to narrow (e.g. tags = { "globals" }).
56
+ -- Uncomment when you need to see data flowing through helpers.
57
+ --
58
+ -- Pulse.Trace.instrument(func, "globals")
59
+ --
60
+ -- Or trace just one function:
61
+ -- func.IsWarrior = Pulse.Trace.wrap(func.IsWarrior, "globals", "IsWarrior")
62
+
63
+ -- ── 5. Execution probe — "is this loop even running?" ─────────────
64
+ -- Add Pulse.Monitor.tick("featureName") inside any loop body.
65
+ -- The monitor bar shows the counter — stops increasing = loop is dead.
66
+ -- (Done in your component code, not here — this is just a reminder)
67
+
68
+ -- ── 6. Narration ──────────────────────────────────────────────────
69
+ -- Auto-announces warn+error entries as on-screen toasts + sound.
70
+ Pulse.Narrate.configure({
71
+ sound = true,
72
+ volume = 0.4,
73
+ autoLevel = "warn", -- "error" for less noise
74
+ })
75
+
76
+ -- ── 7. Initial monitors ───────────────────────────────────────────
77
+ Pulse.Monitor.set("titans", 0)
78
+ Pulse.Monitor.set("shifters", 0)
79
+ Pulse.Monitor.set("players", 0)
80
+
81
+ Pulse.Log.info("devlog", "dev build active")
@@ -0,0 +1,384 @@
1
+ -- [pulse/dev/ui/9_DevPanel.lua]
2
+ -- WindUI Developer tab — injected in the UI phase of --dev builds only.
3
+ -- Runs AFTER layout.rblua has created the WindUI window, so _windWindow is live.
4
+ -- Adds Tools (Hydroxide, Dex) and Scanner (context-optimised game intelligence).
5
+ -- Linoria builds: _windWindow is nil → early return, no-op.
6
+
7
+ if not _windWindow then return end
8
+
9
+ -- ── Clipboard ─────────────────────────────────────────────────────────────────
10
+ local function _devCopy(text)
11
+ local ok = false
12
+ if not ok then pcall(function() setclipboard(text); ok = true end) end
13
+ if not ok then pcall(function() toclipboard(text); ok = true end) end
14
+ if not ok then pcall(function() writeclipboard(text); ok = true end) end
15
+ return ok
16
+ end
17
+
18
+ -- ── Path compression ──────────────────────────────────────────────────────────
19
+ -- Longest prefix first so LP. doesn't match before PG.
20
+ local _ALIASES = {
21
+ { "Players.LocalPlayer.PlayerGui.", "PG." },
22
+ { "Players.LocalPlayer.", "LP." },
23
+ { "ReplicatedStorage.", "RS." },
24
+ { "ReplicatedFirst.", "RF." },
25
+ { "workspace.", "WS." },
26
+ { "StarterGui.", "SG." },
27
+ { "ServerScriptService.", "SSS." },
28
+ }
29
+
30
+ local function _compress(obj)
31
+ local parts = {}
32
+ local cur = obj
33
+ while cur and cur ~= game do
34
+ table.insert(parts, 1, cur.Name)
35
+ cur = cur.Parent
36
+ end
37
+ local s = table.concat(parts, ".")
38
+ for _, pair in ipairs(_ALIASES) do
39
+ if s:sub(1, #pair[1]) == pair[1] then
40
+ return pair[2] .. s:sub(#pair[1] + 1)
41
+ end
42
+ end
43
+ return s
44
+ end
45
+
46
+ -- ── Scanner ───────────────────────────────────────────────────────────────────
47
+ -- Returns: fullText, remoteCount, scriptCount, instanceCount, valueCount
48
+ local function _runScan()
49
+ local RS = game:GetService("ReplicatedStorage")
50
+ local PG = game:GetService("Players").LocalPlayer.PlayerGui
51
+ local RF = game:GetService("ReplicatedFirst")
52
+ local WS = workspace
53
+ local out = {}
54
+
55
+ -- Header
56
+ local gameName = "?"
57
+ pcall(function()
58
+ gameName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
59
+ end)
60
+ table.insert(out, ("[%s] pid:%d %s"):format(gameName, game.PlaceId, os.date("%Y-%m-%d %H:%M")))
61
+
62
+ -- ── Remotes ───────────────────────────────────────────────────────────────
63
+ local remotes = {}
64
+ local function _scanRemotes(inst, depth)
65
+ if depth > 8 then return end
66
+ local ok, ch = pcall(function() return inst:GetChildren() end)
67
+ if not ok then return end
68
+ for _, c in ipairs(ch) do
69
+ local cls = c.ClassName
70
+ if cls == "RemoteEvent" or cls == "RemoteFunction" then
71
+ table.insert(remotes, { p = _compress(c), t = cls == "RemoteEvent" and "RE" or "RF" })
72
+ end
73
+ if c:IsA("Folder") or c:IsA("Configuration") or c:IsA("Model") then
74
+ _scanRemotes(c, depth + 1)
75
+ end
76
+ end
77
+ end
78
+ pcall(_scanRemotes, RS, 0)
79
+ table.sort(remotes, function(a, b) return a.p < b.p end)
80
+
81
+ table.insert(out, ("\nREMOTES (%d)"):format(#remotes))
82
+ for i, r in ipairs(remotes) do
83
+ if i > 40 then
84
+ table.insert(out, (" ...+%d more"):format(#remotes - 40))
85
+ break
86
+ end
87
+ table.insert(out, (" %-52s %s"):format(r.p, r.t))
88
+ end
89
+
90
+ -- ── LocalScripts ──────────────────────────────────────────────────────────
91
+ local scripts = {}
92
+ local function _scanScripts(inst)
93
+ local ok, ch = pcall(function() return inst:GetChildren() end)
94
+ if not ok then return end
95
+ for _, c in ipairs(ch) do
96
+ if c:IsA("LocalScript") and c.Name ~= "RobloxPromptGui" then
97
+ table.insert(scripts, _compress(c))
98
+ end
99
+ local ok2, gc = pcall(function() return c:GetChildren() end)
100
+ if ok2 then
101
+ for _, g in ipairs(gc) do
102
+ if g:IsA("LocalScript") then
103
+ table.insert(scripts, _compress(g))
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ pcall(_scanScripts, PG)
110
+ pcall(_scanScripts, RF)
111
+ table.sort(scripts)
112
+
113
+ table.insert(out, ("\nSCRIPTS (%d)"):format(#scripts))
114
+ for i, s in ipairs(scripts) do
115
+ if i > 15 then
116
+ table.insert(out, (" ...+%d more"):format(#scripts - 15))
117
+ break
118
+ end
119
+ table.insert(out, " " .. s)
120
+ end
121
+
122
+ -- ── Key instances ─────────────────────────────────────────────────────────
123
+ local instances = {}
124
+ local ok1, wsKids = pcall(function() return WS:GetChildren() end)
125
+ local ok2, rsKids = pcall(function() return RS:GetChildren() end)
126
+ if ok1 then
127
+ for _, c in ipairs(wsKids) do
128
+ if c:IsA("Folder") or c:IsA("Model") then
129
+ local n = 0
130
+ pcall(function() n = #c:GetChildren() end)
131
+ if n > 0 then
132
+ table.insert(instances, { p = _compress(c), cls = c.ClassName, n = n })
133
+ end
134
+ end
135
+ end
136
+ end
137
+ if ok2 then
138
+ for _, c in ipairs(rsKids) do
139
+ if c:IsA("Folder") then
140
+ local n = 0
141
+ pcall(function() n = #c:GetChildren() end)
142
+ if n > 0 then
143
+ table.insert(instances, { p = _compress(c), cls = "Folder", n = n })
144
+ end
145
+ end
146
+ end
147
+ end
148
+ table.sort(instances, function(a, b) return a.p < b.p end)
149
+
150
+ table.insert(out, ("\nKEY INSTANCES (%d)"):format(#instances))
151
+ for i, inst in ipairs(instances) do
152
+ if i > 20 then
153
+ table.insert(out, (" ...+%d more"):format(#instances - 20))
154
+ break
155
+ end
156
+ table.insert(out, (" %-48s %s(%d)"):format(
157
+ inst.p,
158
+ inst.cls == "Folder" and "Folder " or "Model ",
159
+ inst.n
160
+ ))
161
+ end
162
+
163
+ -- ── Notable values ────────────────────────────────────────────────────────
164
+ local values = {}
165
+ if ok2 then
166
+ for _, c in ipairs(rsKids) do
167
+ local cls = c.ClassName
168
+ if cls == "StringValue" or cls == "IntValue" or cls == "BoolValue" or cls == "NumberValue" then
169
+ local v = "?"
170
+ pcall(function() v = tostring(c.Value) end)
171
+ table.insert(values, { p = _compress(c), v = v })
172
+ end
173
+ end
174
+ end
175
+
176
+ table.insert(out, ("\nVALUES (%d)"):format(#values))
177
+ for i, val in ipairs(values) do
178
+ if i > 12 then
179
+ table.insert(out, (" ...+%d more"):format(#values - 12))
180
+ break
181
+ end
182
+ table.insert(out, (" %-48s %s"):format(val.p, val.v))
183
+ end
184
+
185
+ return table.concat(out, "\n"), #remotes, #scripts, #instances, #values
186
+ end
187
+
188
+ -- ── State ─────────────────────────────────────────────────────────────────────
189
+ local _lastScanText = ""
190
+ local _scanRunning = false
191
+ local _scanStatusPara = nil -- assigned after UI is built
192
+
193
+ -- Forward-declared so button callbacks can reference it before the body is defined.
194
+ local _doScan
195
+
196
+ -- ── Build WindUI Dev tab ───────────────────────────────────────────────────────
197
+ local _devTab
198
+ pcall(function()
199
+ _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" })
200
+ end)
201
+
202
+ if not _devTab then
203
+ Pulse.Log.warn("dev-panel", "Dev tab skipped — _windWindow:Tab() failed")
204
+ return
205
+ end
206
+
207
+ -- Single-column VStack (no 2-column split needed for dev tools)
208
+ local _col
209
+ pcall(function() _col = _devTab:VStack({}) end)
210
+ _col = _col or _devTab
211
+
212
+ -- ── Tools section ─────────────────────────────────────────────────────────────
213
+ local _toolSect
214
+ pcall(function()
215
+ _toolSect = _col:Section({
216
+ Title = "Tools",
217
+ Icon = "wrench",
218
+ Box = true,
219
+ BoxBorder = true,
220
+ Opened = true,
221
+ })
222
+ end)
223
+
224
+ local _TOOLS = {
225
+ {
226
+ name = "Hydroxide",
227
+ desc = "Remote spy with web interface",
228
+ -- Hydroxide is modular — needs two imports from the 'revision' branch.
229
+ load = function()
230
+ local owner, branch = "Upbolt", "revision"
231
+ local function webImport(file)
232
+ loadstring(
233
+ game:HttpGetAsync(("https://raw.githubusercontent.com/%s/Hydroxide/%s/%s.lua"):format(owner, branch, file)),
234
+ file .. ".lua"
235
+ )()
236
+ end
237
+ webImport("init")
238
+ webImport("ui/main")
239
+ end,
240
+ },
241
+ {
242
+ name = "Infinite Yield",
243
+ desc = "Admin / exploit command panel",
244
+ url = "https://raw.githubusercontent.com/DarkNetworks/Infinite-Yield/main/latest.lua",
245
+ },
246
+ {
247
+ name = "Dex Explorer",
248
+ desc = "Browse and inspect the live game tree",
249
+ url = "https://raw.githubusercontent.com/infyiff/backup/main/dex.lua",
250
+ },
251
+ }
252
+
253
+ if _toolSect then
254
+ for _, tool in ipairs(_TOOLS) do
255
+ local _launched = false
256
+ local _btn
257
+ pcall(function()
258
+ _btn = _toolSect:Button({
259
+ Title = tool.name,
260
+ Desc = tool.desc,
261
+ Callback = function()
262
+ if _launched then
263
+ _PulseNotify("Already running: " .. tool.name, 2)
264
+ return
265
+ end
266
+ pcall(function() _btn:SetTitle(tool.name .. " · loading…") end)
267
+ task.spawn(function()
268
+ local ok, err = pcall(function()
269
+ if tool.load then
270
+ tool.load()
271
+ else
272
+ local src = game:HttpGet(tool.url)
273
+ local fn, ce = loadstring(src)
274
+ if not fn then error(ce) end
275
+ fn()
276
+ end
277
+ end)
278
+ if ok then
279
+ _launched = true
280
+ pcall(function() _btn:SetTitle("✓ " .. tool.name) end)
281
+ _PulseNotify(tool.name .. " launched", 3)
282
+ Pulse.Log.info("dev-tools", tool.name .. " launched")
283
+ else
284
+ pcall(function() _btn:SetTitle(tool.name .. " · failed") end)
285
+ _PulseNotify(tool.name .. " failed — see LOG", 3)
286
+ Pulse.Log.error("dev-tools", tool.name .. ": " .. tostring(err))
287
+ task.delay(3, function()
288
+ if not _launched then
289
+ pcall(function() _btn:SetTitle(tool.name) end)
290
+ end
291
+ end)
292
+ end
293
+ end)
294
+ end,
295
+ })
296
+ end)
297
+ end
298
+ end
299
+
300
+ -- ── Scanner section ───────────────────────────────────────────────────────────
301
+ local _scanSect
302
+ pcall(function()
303
+ _scanSect = _col:Section({
304
+ Title = "Scanner",
305
+ Icon = "scan",
306
+ Box = true,
307
+ BoxBorder = true,
308
+ Opened = true,
309
+ })
310
+ end)
311
+
312
+ if _scanSect then
313
+ pcall(function()
314
+ _scanStatusPara = _scanSect:Paragraph({
315
+ Title = "Game Scanner",
316
+ Desc = "Auto-scan runs on inject. Results copied to clipboard.",
317
+ })
318
+ end)
319
+ pcall(function()
320
+ _scanSect:Button({
321
+ Title = "Scan Now",
322
+ Desc = "Re-scan and copy results to clipboard",
323
+ Callback = function() _doScan() end,
324
+ })
325
+ end)
326
+ pcall(function()
327
+ _scanSect:Button({
328
+ Title = "Copy Last Results",
329
+ Desc = "Re-copy the most recent scan to clipboard",
330
+ Callback = function()
331
+ if _lastScanText == "" then
332
+ _PulseNotify("No scan yet — run Scan Now first", 3)
333
+ return
334
+ end
335
+ local ok = _devCopy(_lastScanText)
336
+ _PulseNotify(ok and "Scan results copied" or "No clipboard function found", 3)
337
+ end,
338
+ })
339
+ end)
340
+ end
341
+
342
+ -- ── Scan logic ────────────────────────────────────────────────────────────────
343
+ _doScan = function()
344
+ if _scanRunning then return end
345
+ _scanRunning = true
346
+ pcall(function()
347
+ _scanStatusPara:SetTitle("Game Scanner")
348
+ _scanStatusPara:SetDesc("Scanning…")
349
+ end)
350
+ task.spawn(function()
351
+ local ok, txt, nRem, nScr, nInst, nVal = pcall(_runScan)
352
+ _scanRunning = false
353
+ if ok then
354
+ _lastScanText = txt
355
+ local summary = ("%d remotes · %d scripts · %d instances · %d values"):format(
356
+ nRem, nScr, nInst, nVal)
357
+ local copied = _devCopy(txt)
358
+ pcall(function()
359
+ _scanStatusPara:SetTitle("Scan " .. os.date("%H:%M:%S"))
360
+ _scanStatusPara:SetDesc(summary .. (copied and " · copied to clipboard" or ""))
361
+ end)
362
+ Pulse.Log.info("dev-scanner", summary)
363
+ if copied then
364
+ _PulseNotify("Game scan complete · results copied", 4)
365
+ end
366
+ else
367
+ pcall(function()
368
+ _scanStatusPara:SetTitle("Scan failed")
369
+ _scanStatusPara:SetDesc("Check LOG tab › dev-scanner tag for details.")
370
+ end)
371
+ Pulse.Log.error("dev-scanner", tostring(txt))
372
+ end
373
+ end)
374
+ end
375
+
376
+ -- ── Auto-scan on inject ────────────────────────────────────────────────────────
377
+ -- Fires 2 s after game is loaded so services are fully populated.
378
+ task.spawn(function()
379
+ if not game:IsLoaded() then game.Loaded:Wait() end
380
+ task.wait(2)
381
+ _doScan()
382
+ end)
383
+
384
+ Pulse.Log.debug("dev-panel", "WindUI Dev tab ready")
@@ -0,0 +1,193 @@
1
+ -- ── Pulse.Aim ────────────────────────────────────────────────────────────────
2
+ -- Camera targeting helpers for aimbot-style features.
3
+ -- Usage:
4
+ -- if Pulse.Aim.inFOV(nape.Position, radius()) then ... end
5
+ -- local dist = Pulse.Aim.screenDist(nape.Position)
6
+ -- Pulse.Aim.lookAt(nape.Position) -- snap
7
+ -- Pulse.Aim.smoothTo(nape.Position, 0.95) -- smooth lerp
8
+ -- local nearest = Pulse.Aim.findNearest(titans, { fovRadius = 300 })
9
+ --
10
+ -- Stateful locker (for aimbot components):
11
+ -- local lock = Pulse.Aim.locker({
12
+ -- getPos = function(e) return e.Nape.Position end,
13
+ -- validate = function(e) return e.Parent ~= nil end,
14
+ -- })
15
+ -- lock:lock(entity) -- set target
16
+ -- lock:release() -- clear
17
+ -- lock:getTarget() -- current entity or nil
18
+ -- lock:isValid() -- validates current target via opts.validate
19
+ -- lock:aim() -- smooth camera move; returns false if target became invalid
20
+ -- lock:aim("snap") -- snap mode (uses Pulse.Aim.lookAt)
21
+
22
+ Pulse.Aim = (function()
23
+ local A = {}
24
+
25
+ -- True if worldPos projects within pixelRadius pixels of screen centre.
26
+ function A.inFOV(worldPos, pixelRadius)
27
+ local cam = workspace.CurrentCamera
28
+ if not cam then return false end
29
+ local sp, onScreen = cam:WorldToViewportPoint(worldPos)
30
+ if not onScreen then return false end
31
+ local sz = cam.ViewportSize
32
+ local dx = sp.X - sz.X * 0.5
33
+ local dy = sp.Y - sz.Y * 0.5
34
+ return (dx * dx + dy * dy) <= (pixelRadius * pixelRadius)
35
+ end
36
+
37
+ -- Pixel distance from screen centre to worldPos (math.huge if off-screen).
38
+ function A.screenDist(worldPos)
39
+ local cam = workspace.CurrentCamera
40
+ if not cam then return math.huge end
41
+ local sp, onScreen = cam:WorldToViewportPoint(worldPos)
42
+ if not onScreen then return math.huge end
43
+ local sz = cam.ViewportSize
44
+ local dx = sp.X - sz.X * 0.5
45
+ local dy = sp.Y - sz.Y * 0.5
46
+ return math.sqrt(dx * dx + dy * dy)
47
+ end
48
+
49
+ -- Lock the camera to look at a world position.
50
+ -- Syncs the VirtualInputManager cursor so the game's camera script agrees,
51
+ -- then triple-sets CFrame to override any interpolation.
52
+ function A.lookAt(pos)
53
+ local cam = workspace.CurrentCamera
54
+ if not cam then return end
55
+ local sp, onScreen = cam:WorldToViewportPoint(pos)
56
+ if onScreen then
57
+ pcall(function() _VIM:SendMouseMoveEvent(sp.X, sp.Y, game) end)
58
+ end
59
+ local cf = CFrame.lookAt(cam.CFrame.Position, pos)
60
+ cam.CFrame = cf
61
+ pcall(function() cam.CoordinateFrame = cf end)
62
+ cam.CFrame = cf
63
+ if onScreen then
64
+ pcall(function() _VIM:SendMouseMoveEvent(sp.X, sp.Y, game) end)
65
+ end
66
+ end
67
+
68
+ -- Return the nearest entity from candidates.
69
+ -- opts:
70
+ -- origin — Vector3 (defaults to local HRP position)
71
+ -- fovRadius — pixel radius; skips entities outside FOV (optional)
72
+ -- maxDist — world-space distance cap (optional)
73
+ -- filter — function(entity) → bool; return false to skip
74
+ -- getRoot — function(entity) → BasePart (defaults to HumanoidRootPart)
75
+ function A.findNearest(candidates, opts)
76
+ opts = opts or {}
77
+ local hrp = _PulseGetHRP()
78
+ local origin = opts.origin or (hrp and hrp.Position)
79
+ if not origin then return nil end
80
+
81
+ local getRoot = opts.getRoot or function(e)
82
+ return e:FindFirstChild("HumanoidRootPart")
83
+ end
84
+
85
+ local nearest = nil
86
+ local bestDist = opts.maxDist or math.huge
87
+ local _sk = { filter = 0, root = 0, fov = 0, dist = 0 }
88
+
89
+ for _, entity in ipairs(candidates) do
90
+ if opts.filter and not opts.filter(entity) then
91
+ _sk.filter = _sk.filter + 1
92
+ else
93
+ local root = getRoot(entity)
94
+ if not root then
95
+ _sk.root = _sk.root + 1
96
+ elseif opts.fovRadius and not A.inFOV(root.Position, opts.fovRadius) then
97
+ _sk.fov = _sk.fov + 1
98
+ else
99
+ local d = (root.Position - origin).Magnitude
100
+ if d < bestDist then
101
+ bestDist = d
102
+ nearest = entity
103
+ else
104
+ _sk.dist = _sk.dist + 1
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ if nearest == nil and #candidates > 0 then
111
+ Pulse.Log.trace("Aim", "findNearest: no result", {
112
+ n = #candidates,
113
+ filter = _sk.filter,
114
+ noRoot = _sk.root,
115
+ fov = _sk.fov,
116
+ dist = _sk.dist,
117
+ })
118
+ end
119
+
120
+ return nearest
121
+ end
122
+
123
+ -- Smooth-lerp the camera toward a world position (no VIM sync, no triple-set).
124
+ -- alpha = lerp factor per frame; 0.95 feels like a snappy but smooth lock.
125
+ function A.smoothTo(pos, alpha)
126
+ local cam = workspace.CurrentCamera
127
+ if not cam then return end
128
+ alpha = alpha or 0.95
129
+ local origin = cam.CFrame.Position
130
+ local currentLook = cam.CFrame.LookVector
131
+ local desiredLook = (pos - origin).Unit
132
+ local smoothed = currentLook:Lerp(desiredLook, alpha)
133
+ cam.CFrame = CFrame.new(origin, origin + smoothed)
134
+ end
135
+
136
+ -- Create a stateful locker for a single locked target.
137
+ -- opts:
138
+ -- getPos(entity) → Vector3 — where to aim at on the entity (required for aim())
139
+ -- validate(entity) → bool — is this entity still a valid target?
140
+ function A.locker(opts)
141
+ opts = opts or {}
142
+ local _target = nil
143
+ local L = {}
144
+
145
+ function L:lock(entity)
146
+ _target = entity
147
+ end
148
+
149
+ function L:release()
150
+ _target = nil
151
+ end
152
+
153
+ function L:getTarget()
154
+ return _target
155
+ end
156
+
157
+ -- Validate the current lock. Returns false (and clears) if invalid.
158
+ function L:isValid()
159
+ if not _target then return false end
160
+ if opts.validate then
161
+ if not opts.validate(_target) then _target = nil; return false end
162
+ return true
163
+ end
164
+ if not _target.Parent then _target = nil; return false end
165
+ return true
166
+ end
167
+
168
+ -- Move camera toward locked target.
169
+ -- mode = "smooth" (default) | "snap"
170
+ -- Returns false if the target was invalid (lock cleared).
171
+ function L:aim(mode, alpha)
172
+ if not self:isValid() then return false end
173
+ local pos
174
+ if opts.getPos then
175
+ pos = opts.getPos(_target)
176
+ else
177
+ local root = _target:FindFirstChild("HumanoidRootPart")
178
+ pos = root and root.Position
179
+ end
180
+ if not pos then _target = nil; return false end
181
+ if mode == "snap" then
182
+ A.lookAt(pos)
183
+ else
184
+ A.smoothTo(pos, alpha or 0.95)
185
+ end
186
+ return true
187
+ end
188
+
189
+ return L
190
+ end
191
+
192
+ return A
193
+ end)()