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,128 @@
1
+ -- ── Pulse.Remote ─────────────────────────────────────────────────────────────
2
+ -- Lazy-resolved, path-cached RemoteEvent / RemoteFunction wrapper.
3
+ -- Paths are slash-separated from ReplicatedStorage: "Remotes/Building".
4
+ --
5
+ -- Usage:
6
+ -- -- Direct fire / invoke
7
+ -- Pulse.Remote.fire("Remotes/Building", "Place", cf)
8
+ -- local result = Pulse.Remote.invoke("Remotes/GetStats")
9
+ --
10
+ -- -- Pre-bound function (preferred in a game's remotes.lua)
11
+ -- func.Remote_Build = Pulse.Remote.bind("Remotes/Building")
12
+ -- func.Remote_Build("Place", cf)
13
+ --
14
+ -- -- Client-event listener
15
+ -- Pulse.Remote.connect("Remotes/Notify", function(msg) ... end)
16
+ --
17
+ -- -- Warm the cache before first use (avoids WaitForChild latency in hot loops)
18
+ -- Pulse.Remote.prefetch("Remotes/Building", "Remotes/Storage")
19
+
20
+ Pulse.Remote = (function()
21
+ local R = {}
22
+ local _RS = game:GetService("ReplicatedStorage")
23
+ local _PENDING = {} -- sentinel: resolution in progress
24
+ local _cache = {} -- path → Instance | false | _PENDING
25
+
26
+ local function _split(path)
27
+ local parts = {}
28
+ for part in path:gmatch("[^/]+") do parts[#parts + 1] = part end
29
+ return parts
30
+ end
31
+
32
+ -- Walk a slash-separated path under ReplicatedStorage via WaitForChild.
33
+ -- Yields on the first call; O(1) cache hit on every subsequent call.
34
+ -- Returns the resolved Instance, or nil if any segment is missing.
35
+ local function _resolve(path)
36
+ -- Another coroutine may be resolving the same path; wait for it.
37
+ while _cache[path] == _PENDING do task.wait(0.05) end
38
+
39
+ local cached = _cache[path]
40
+ if cached ~= nil then return cached ~= false and cached or nil end
41
+
42
+ _cache[path] = _PENDING
43
+ local node = _RS
44
+ for _, part in ipairs(_split(path)) do
45
+ local ok, child = pcall(function()
46
+ return node:WaitForChild(part, 10)
47
+ end)
48
+ if not ok or not child then
49
+ Pulse.Log.warn("Remote", "path not found",
50
+ { path = path, segment = part })
51
+ _cache[path] = false
52
+ return nil
53
+ end
54
+ node = child
55
+ end
56
+ _cache[path] = node
57
+ return node
58
+ end
59
+
60
+ -- FireServer a RemoteEvent at `path`.
61
+ function R.fire(path, ...)
62
+ local remote = _resolve(path)
63
+ if not remote then return end
64
+ local args = { ... }
65
+ local ok, err = pcall(function()
66
+ remote:FireServer(table.unpack(args))
67
+ end)
68
+ if not ok then
69
+ Pulse.Log.warn("Remote", "fire failed",
70
+ { path = path, err = tostring(err) })
71
+ end
72
+ end
73
+
74
+ -- InvokeServer a RemoteFunction at `path`. Returns the result or nil.
75
+ function R.invoke(path, ...)
76
+ local remote = _resolve(path)
77
+ if not remote then return nil end
78
+ local args = { ... }
79
+ local ok, result = pcall(function()
80
+ return remote:InvokeServer(table.unpack(args))
81
+ end)
82
+ if not ok then
83
+ Pulse.Log.warn("Remote", "invoke failed",
84
+ { path = path, err = tostring(result) })
85
+ return nil
86
+ end
87
+ return result
88
+ end
89
+
90
+ -- Connect `fn` to a RemoteEvent's OnClientEvent. Returns the connection.
91
+ function R.connect(path, fn)
92
+ local remote = _resolve(path)
93
+ if not remote then return nil end
94
+ return remote.OnClientEvent:Connect(fn)
95
+ end
96
+
97
+ -- Returns a pre-bound fire function: fn(...) → fires path with those args.
98
+ -- Use this in a game's remotes.lua to define named wrappers without boilerplate.
99
+ function R.bind(path)
100
+ return function(...) R.fire(path, ...) end
101
+ end
102
+
103
+ -- Returns a pre-bound invoke function: fn(...) → returns invoke result.
104
+ function R.bindi(path)
105
+ return function(...) return R.invoke(path, ...) end
106
+ end
107
+
108
+ -- Returns a wrapper object with :fire, :invoke, :connect, :get.
109
+ -- Useful when you need multiple operations on the same remote in one place.
110
+ function R.wrap(path)
111
+ local self = {}
112
+ function self:fire(...) R.fire(path, ...) end
113
+ function self:invoke(...) return R.invoke(path, ...) end
114
+ function self:connect(fn) return R.connect(path, fn) end
115
+ function self:get() return _resolve(path) end
116
+ return self
117
+ end
118
+
119
+ -- Resolve paths eagerly in background threads so first-use has zero latency.
120
+ -- Call Pulse.Remote.prefetch(...) inside a task.spawn in init {}.
121
+ function R.prefetch(...)
122
+ for _, path in ipairs({...}) do
123
+ task.spawn(function() _resolve(path) end)
124
+ end
125
+ end
126
+
127
+ return R
128
+ end)()
@@ -0,0 +1,59 @@
1
+ -- ── Pulse.Restore ────────────────────────────────────────────────────────────
2
+ -- Save and restore Instance property values.
3
+ -- Replaces _originalSizes tables, OriginalProtectionValues, etc.
4
+ -- Usage:
5
+ -- local r = Pulse.Restore.new()
6
+ -- r:save(part, "Size") -- remember current part.Size
7
+ -- r:save(boolValue, "Value") -- remember current .Value
8
+ -- r:all() -- restore everything to saved state
9
+ -- r:one(part, "Size") -- restore just one entry
10
+ -- r:has(part, "Size") → bool
11
+
12
+ Pulse.Restore = (function()
13
+ local R = {}
14
+
15
+ function R.new()
16
+ local self = { _data = {} }
17
+
18
+ local function key(obj, prop)
19
+ return tostring(obj) .. "\0" .. prop
20
+ end
21
+
22
+ -- Save obj[prop] if not already saved (first-save wins).
23
+ function self:save(obj, prop)
24
+ local k = key(obj, prop)
25
+ if self._data[k] then return end
26
+ local ok, val = pcall(function() return obj[prop] end)
27
+ if ok then self._data[k] = { obj = obj, prop = prop, val = val } end
28
+ end
29
+
30
+ -- Restore one saved entry.
31
+ function self:one(obj, prop)
32
+ local k = key(obj, prop)
33
+ local rec = self._data[k]
34
+ if rec then
35
+ pcall(function() rec.obj[rec.prop] = rec.val end)
36
+ self._data[k] = nil
37
+ end
38
+ end
39
+
40
+ -- Restore all saved entries and clear the table.
41
+ function self:all()
42
+ for _, rec in pairs(self._data) do
43
+ pcall(function() rec.obj[rec.prop] = rec.val end)
44
+ end
45
+ self._data = {}
46
+ end
47
+
48
+ -- Forget saved values without restoring.
49
+ function self:clear() self._data = {} end
50
+
51
+ function self:has(obj, prop)
52
+ return self._data[key(obj, prop)] ~= nil
53
+ end
54
+
55
+ return self
56
+ end
57
+
58
+ return R
59
+ end)()
@@ -0,0 +1,39 @@
1
+ -- ── Pulse.Store ───────────────────────────────────────────────────────────────
2
+ -- Key-value reactive store — cross-component shared state via named signals.
3
+ -- Usage:
4
+ -- Pulse.Store.define("key", defaultValue)
5
+ -- Pulse.Store.get("key") → current value
6
+ -- Pulse.Store.set("key", value) → writes + fires watchers
7
+ -- Pulse.Store.watch("key", fn) → fn(newValue) on change
8
+ -- Pulse.Store.signal("key") → raw Signal object
9
+
10
+ Pulse.Store = (function()
11
+ local S = {}
12
+ local _entries = {}
13
+
14
+ function S.define(key, default)
15
+ if _entries[key] then return end
16
+ _entries[key] = Signal(default)
17
+ end
18
+
19
+ function S.get(key)
20
+ local e = _entries[key]
21
+ return e and e() or nil
22
+ end
23
+
24
+ function S.set(key, value)
25
+ local e = _entries[key]
26
+ if e then e(value) end
27
+ end
28
+
29
+ function S.watch(key, fn)
30
+ local e = _entries[key]
31
+ if e then e:onChange(fn) end
32
+ end
33
+
34
+ function S.signal(key)
35
+ return _entries[key]
36
+ end
37
+
38
+ return S
39
+ end)()
@@ -0,0 +1,83 @@
1
+ -- ── Pulse.Team ────────────────────────────────────────────────────────────────
2
+ -- Configurable faction/enemy resolver factory.
3
+ -- Decouples "who is an enemy" logic from features that act on enemies.
4
+ -- Each resolver is an independent object — create one per use-case.
5
+ --
6
+ -- Usage:
7
+ -- local r = Pulse.Team.resolver({
8
+ -- isSelf = function(e) return e == _LocalPlayer end,
9
+ -- isValid = function(e) return e.Character ~= nil end,
10
+ -- isHostile = function(e) return func.IsOnWarriorsTeam() ~= func.IsWarrior(e) end,
11
+ -- exclude = { func.IsParamountShifterFriendly },
12
+ -- })
13
+ --
14
+ -- r:isEnemy(player) → bool
15
+ -- r:isAlly(player) → bool (valid, not self, not enemy)
16
+ -- r:filter(playerList) → list of enemies only
17
+ -- r:partition(playerList) → allies, enemies (two return values)
18
+ --
19
+ -- opts fields:
20
+ -- isSelf(entity) → bool — entity is the local player / our own unit; always skipped
21
+ -- isValid(entity) → bool — entity is in a valid state (alive, has character, etc.)
22
+ -- Returning false means "skip" but NOT enemy — used for
23
+ -- pre-filtering dead/respawning players.
24
+ -- isHostile(entity) → bool — REQUIRED core check: is this entity on the opposing side?
25
+ -- exclude = { fn, fn, ... }
26
+ -- Additional veto functions. Any fn(entity) returning true
27
+ -- means "treat as not-enemy" (friendly exceptions, etc.).
28
+
29
+ Pulse.Team = {}
30
+
31
+ function Pulse.Team.resolver(opts)
32
+ opts = opts or {}
33
+ local R = {}
34
+
35
+ -- Internal: run all checks in order. Returns "enemy", "skip", or "ally".
36
+ local function _classify(entity)
37
+ if not entity then return "skip" end
38
+ if opts.isSelf and opts.isSelf(entity) then return "skip" end
39
+ if opts.isValid and not opts.isValid(entity) then return "skip" end
40
+ if opts.exclude then
41
+ for _, fn in ipairs(opts.exclude) do
42
+ if fn(entity) then return "ally" end
43
+ end
44
+ end
45
+ if not opts.isHostile then return "ally" end
46
+ return opts.isHostile(entity) and "enemy" or "ally"
47
+ end
48
+
49
+ -- True if entity should be targeted as an enemy.
50
+ function R:isEnemy(entity)
51
+ return _classify(entity) == "enemy"
52
+ end
53
+
54
+ -- True if entity is valid, not self, and not an enemy.
55
+ function R:isAlly(entity)
56
+ return _classify(entity) == "ally"
57
+ end
58
+
59
+ -- Returns a new list containing only enemies from `list`.
60
+ function R:filter(list)
61
+ local out = {}
62
+ for _, e in ipairs(list) do
63
+ if _classify(e) == "enemy" then out[#out + 1] = e end
64
+ end
65
+ return out
66
+ end
67
+
68
+ -- Splits `list` into two lists: allies, enemies.
69
+ function R:partition(list)
70
+ local allies, enemies = {}, {}
71
+ for _, e in ipairs(list) do
72
+ local c = _classify(e)
73
+ if c == "enemy" then
74
+ enemies[#enemies + 1] = e
75
+ elseif c == "ally" then
76
+ allies[#allies + 1] = e
77
+ end
78
+ end
79
+ return allies, enemies
80
+ end
81
+
82
+ return R
83
+ end
@@ -0,0 +1,80 @@
1
+ -- Pulse.TestMode — dev-build test harness.
2
+ -- Active only when _PULSE_DEV = true. In prod every call is a no-op.
3
+ --
4
+ -- Usage (in rb/pulse/dev/devconfig.lua):
5
+ -- Pulse.TestMode.configure({ target = "ShifterPartsCutter" })
6
+ -- Pulse.TestMode.configure({ target = { "Aimbot", "ShifterCutter" } })
7
+ -- Pulse.TestMode.configure({ all = true }) -- all features on (normal dev mode)
8
+ --
9
+ -- The defaults runner in compiler.py reads isActive()/isTarget(id) to force
10
+ -- non-target toggles to false before the UI is shown.
11
+
12
+ do
13
+ local _active = (_PULSE_DEV == true)
14
+ local _enabled = false
15
+ local _targets = {} -- set: lowercased target names
16
+
17
+ Pulse.TestMode = {}
18
+
19
+ -- Configure test mode. Call from devlog.lua before the defaults runner fires.
20
+ -- opts.target = "Name" | { "Name1", "Name2" } — only these features enabled
21
+ -- opts.all = true — all features on (disable test filter)
22
+ function Pulse.TestMode.configure(opts)
23
+ if not _active then return end
24
+ _targets = {}
25
+ _enabled = false
26
+
27
+ if opts.all then
28
+ -- explicit "all on" — not in filtered test mode
29
+ Pulse.Monitor.set("test", "all-on")
30
+ return
31
+ end
32
+
33
+ local raw = opts.target
34
+ if raw == nil then return end
35
+
36
+ local list = type(raw) == "table" and raw or { raw }
37
+ for _, name in ipairs(list) do
38
+ if type(name) == "string" and name ~= "" then
39
+ _targets[name:lower()] = true
40
+ end
41
+ end
42
+
43
+ if next(_targets) then
44
+ _enabled = true
45
+ local names = {}
46
+ for n in pairs(_targets) do names[#names + 1] = n end
47
+ table.sort(names)
48
+ Pulse.Monitor.set("test", "only:" .. table.concat(names, ","))
49
+ Pulse.Log.info("testmode", "test mode active", { targets = names })
50
+ else
51
+ Pulse.Monitor.set("test", "all-on")
52
+ end
53
+ end
54
+
55
+ -- Returns true when test mode is filtering features (some are forced off).
56
+ function Pulse.TestMode.isActive()
57
+ return _active and _enabled
58
+ end
59
+
60
+ -- Returns true if widgetId belongs to a test target.
61
+ -- Uses case-insensitive substring match:
62
+ -- target "ShifterCutter" matches "ShifterCutterEnable", "ShifterCutterKey", etc.
63
+ function Pulse.TestMode.isTarget(widgetId)
64
+ if not _active or not _enabled then return true end
65
+ if next(_targets) == nil then return true end
66
+ local lower = widgetId:lower()
67
+ for name in pairs(_targets) do
68
+ if lower:find(name, 1, true) then return true end
69
+ end
70
+ return false
71
+ end
72
+
73
+ -- Returns a sorted list of target names (used by the defaults runner notify).
74
+ function Pulse.TestMode.getTargets()
75
+ local t = {}
76
+ for name in pairs(_targets) do t[#t + 1] = name end
77
+ table.sort(t)
78
+ return t
79
+ end
80
+ end
@@ -0,0 +1,111 @@
1
+ -- Pulse.Trace — inter-function call tracing for dev builds.
2
+ -- Active only when _PULSE_DEV = true. In prod every call is a no-op.
3
+ --
4
+ -- Usage (in rb/pulse/dev/devconfig.lua):
5
+ -- Pulse.Trace.instrument(func, "globals") -- wrap all func.* functions
6
+ -- Pulse.Trace.wrap(someFunc, "tag", "name") -- wrap a single function
7
+ -- Pulse.Trace.disable() -- stop tracing without unwrapping
8
+ -- Pulse.Trace.enable() -- re-enable
9
+ --
10
+ -- Output example:
11
+ -- → IsWarrior Player1
12
+ -- ← IsWarrior true
13
+ -- → GetCachedTitans (none)
14
+ -- ← GetCachedTitans {n=3}
15
+
16
+ do
17
+ local _active = (_PULSE_DEV == true)
18
+ local _on = true
19
+
20
+ -- ── Value serializer ─────────────────────────────────────────────────────
21
+ -- Converts a single Lua/Roblox value to a short readable string.
22
+ local function _val(v)
23
+ local t = type(v)
24
+ if t == "nil" then return "nil" end
25
+ if t == "boolean" then return tostring(v) end
26
+ if t == "number" then
27
+ -- trim unnecessary decimals
28
+ local s = string.format("%.4g", v)
29
+ return s
30
+ end
31
+ if t == "string" then
32
+ local s = v:sub(1, 28)
33
+ return '"' .. s .. (v:len() > 28 and '…"' or '"')
34
+ end
35
+ if t == "table" then
36
+ local n = 0; for _ in pairs(v) do n = n + 1 end
37
+ return "{n=" .. n .. "}"
38
+ end
39
+ -- Roblox Instance, Vector3, CFrame, etc.
40
+ local ok, s = pcall(tostring, v)
41
+ if ok then return s:sub(1, 36) end
42
+ return "<?>"
43
+ end
44
+
45
+ -- Serializes variadic args to a single display string.
46
+ local function _args(...)
47
+ local n = select("#", ...)
48
+ if n == 0 then return "(none)" end
49
+ local parts = {}
50
+ for i = 1, math.min(n, 4) do
51
+ parts[i] = _val(select(i, ...))
52
+ end
53
+ if n > 4 then parts[#parts + 1] = "+" .. (n - 4) .. " more" end
54
+ return table.concat(parts, " ")
55
+ end
56
+
57
+ -- Serializes return values (same logic).
58
+ local function _rets(pack)
59
+ local n = pack.n
60
+ if n == 0 then return "(none)" end
61
+ local parts = {}
62
+ for i = 1, math.min(n, 4) do
63
+ parts[i] = _val(pack[i])
64
+ end
65
+ if n > 4 then parts[#parts + 1] = "+" .. (n - 4) .. " more" end
66
+ return table.concat(parts, " ")
67
+ end
68
+
69
+ -- ── Public API ────────────────────────────────────────────────────────────
70
+
71
+ Pulse.Trace = {}
72
+
73
+ function Pulse.Trace.enable() _on = true end
74
+ function Pulse.Trace.disable() _on = false end
75
+
76
+ -- Wrap a single function. The returned function is safe — it never swallows
77
+ -- errors; it re-throws after logging so callers behave identically.
78
+ function Pulse.Trace.wrap(fn, tag, name)
79
+ if not _active then return fn end
80
+ local _name = tostring(name or "?")
81
+ return function(...)
82
+ if not _on then return fn(...) end
83
+ Pulse.Log.trace(tag, "→ " .. _name, { in_ = _args(...) })
84
+ local results = table.pack(pcall(fn, ...))
85
+ local ok = table.remove(results, 1)
86
+ results.n = results.n - 1
87
+ if ok then
88
+ Pulse.Log.trace(tag, "← " .. _name, { out = _rets(results) })
89
+ return table.unpack(results, 1, results.n)
90
+ else
91
+ local err = tostring(results[1])
92
+ Pulse.Log.error(tag, "✕ " .. _name .. " threw", { err = err })
93
+ error(results[1], 2)
94
+ end
95
+ end
96
+ end
97
+
98
+ -- Wrap every function-valued field in tbl in-place.
99
+ -- Useful for tracing all func.* helpers at once from devlog.lua.
100
+ function Pulse.Trace.instrument(tbl, tag)
101
+ if not _active then return end
102
+ local count = 0
103
+ for k, v in pairs(tbl) do
104
+ if type(v) == "function" then
105
+ tbl[k] = Pulse.Trace.wrap(v, tag, k)
106
+ count = count + 1
107
+ end
108
+ end
109
+ Pulse.Log.debug(tag, "instrumented", { functions = count })
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ -- ── Pulse.Track ──────────────────────────────────────────────────────────────
2
+ -- Entity lifecycle tracking — apply effects and auto-cleanup when they leave.
3
+ -- Usage:
4
+ -- local tracker = Pulse.Track.new("Aimbot") -- name is optional but helps logs
5
+ --
6
+ -- tracker:apply(titan, function(t)
7
+ -- Pulse.Draw.removeHighlight(t:FindFirstChild("Hitboxes"), "ESP")
8
+ -- end)
9
+ --
10
+ -- on Heartbeat every 1.0 when titanEnabled {
11
+ -- tracker:cleanup(func.IsTitanDead)
12
+ -- for _, t in ipairs(func.GetCachedTitans()) do
13
+ -- if not tracker:has(t) then applyESP(t) end
14
+ -- end
15
+ -- }
16
+ --
17
+ -- on titanEnabled { if not v then tracker:clear() end }
18
+
19
+ Pulse.Track = (function()
20
+ local T = {}
21
+
22
+ function T.new(name)
23
+ local _tag = "track" .. (name and (":" .. name) or "")
24
+ local self = { _e = {}, _c = {} }
25
+
26
+ -- Mark entity as tracked; cleanupFn(entity) is called when removed.
27
+ function self:apply(entity, cleanupFn)
28
+ self._e[entity] = true
29
+ if cleanupFn then self._c[entity] = cleanupFn end
30
+ Pulse.Log.trace(_tag, "apply", { name = pcall(function() return entity.Name end) and entity.Name or "?" })
31
+ end
32
+
33
+ -- Remove one entity and call its cleanup function.
34
+ function self:remove(entity)
35
+ if not self._e[entity] then return end
36
+ local fn = self._c[entity]
37
+ if fn then pcall(fn, entity) end
38
+ local eName = "?"
39
+ pcall(function() eName = entity.Name end)
40
+ Pulse.Log.trace(_tag, "remove", { name = eName })
41
+ self._e[entity] = nil; self._c[entity] = nil
42
+ end
43
+
44
+ -- Remove all entities where predicate(entity) returns true.
45
+ function self:cleanup(predicate)
46
+ local toRemove = {}
47
+ for entity in pairs(self._e) do
48
+ local ok, result = pcall(predicate, entity)
49
+ if ok and result then toRemove[#toRemove + 1] = entity end
50
+ end
51
+ if #toRemove > 0 then
52
+ Pulse.Log.debug(_tag, "cleanup", { removed = #toRemove, remaining = self:count() - #toRemove })
53
+ end
54
+ for _, entity in ipairs(toRemove) do self:remove(entity) end
55
+ end
56
+
57
+ -- Remove every tracked entity and call all cleanup functions.
58
+ function self:clear()
59
+ local n = self:count()
60
+ if n > 0 then
61
+ Pulse.Log.debug(_tag, "clear", { count = n })
62
+ end
63
+ for entity in pairs(self._e) do
64
+ local fn = self._c[entity]
65
+ if fn then pcall(fn, entity) end
66
+ end
67
+ self._e = {}; self._c = {}
68
+ end
69
+
70
+ function self:has(entity) return self._e[entity] == true end
71
+
72
+ function self:count()
73
+ local n = 0; for _ in pairs(self._e) do n = n + 1 end; return n
74
+ end
75
+
76
+ -- Call fn(entity) for every tracked entity (errors are swallowed).
77
+ function self:each(fn)
78
+ for entity in pairs(self._e) do pcall(fn, entity) end
79
+ end
80
+
81
+ return self
82
+ end
83
+
84
+ return T
85
+ end)()
@@ -0,0 +1,52 @@
1
+ -- ── Pulse.Vec ────────────────────────────────────────────────────────────────
2
+ -- NaN-safe Vector3 utilities.
3
+ -- Replaces `tostring(dir.X) == "nan"` guards and zero-distance crashes.
4
+ -- Usage:
5
+ -- local dir = Pulse.Vec.dir(fromPos, toPos) -- safe unit direction
6
+ -- local dir = Pulse.Vec.flatDir(fromPos, toPos) -- XZ-plane only
7
+ -- local ok = Pulse.Vec.isValid(someVec) -- no NaN/inf
8
+
9
+ Pulse.Vec = (function()
10
+ local V = {}
11
+ local _n = math.huge -- used for inf check
12
+
13
+ local function isNaN(n) return n ~= n end
14
+
15
+ -- Unit direction from `from` to `to`. Returns fallback if positions overlap.
16
+ function V.dir(from, to, fallback)
17
+ local d = to - from
18
+ if d.Magnitude < 1e-6 then
19
+ return fallback or Vector3.new(0, 0, 1)
20
+ end
21
+ return d.Unit
22
+ end
23
+
24
+ -- XZ-plane unit direction (Y ignored). Falls back to camera look if coincident.
25
+ function V.flatDir(from, to, fallback)
26
+ local dx = to.X - from.X
27
+ local dz = to.Z - from.Z
28
+ local mag = math.sqrt(dx * dx + dz * dz)
29
+ if mag < 1e-6 then
30
+ if fallback then return fallback end
31
+ local cam = workspace.CurrentCamera
32
+ if cam then
33
+ local lv = cam.CFrame.LookVector
34
+ if math.sqrt(lv.X * lv.X + lv.Z * lv.Z) > 1e-6 then
35
+ return Vector3.new(lv.X, 0, lv.Z).Unit
36
+ end
37
+ end
38
+ return Vector3.new(0, 0, 1)
39
+ end
40
+ return Vector3.new(dx / mag, 0, dz / mag)
41
+ end
42
+
43
+ -- True if vec has no NaN or infinite components.
44
+ function V.isValid(vec)
45
+ if typeof(vec) ~= "Vector3" then return false end
46
+ return not (isNaN(vec.X) or isNaN(vec.Y) or isNaN(vec.Z)
47
+ or vec.X == _n or vec.Y == _n or vec.Z == _n
48
+ or vec.X == -_n or vec.Y == -_n or vec.Z == -_n)
49
+ end
50
+
51
+ return V
52
+ end)()