pulse-rb 1.3.2 → 1.4.0

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.
@@ -5,13 +5,20 @@
5
5
 
6
6
  -- Executor loadstring() environments are sandboxed — each chunk has its OWN environment
7
7
  -- table, separate from _G. Global reads in this chunk go through getfenv(1), not _G.
8
- -- The compiler passes the bundle table (_P) as an argument; we write every key into
9
- -- this chunk's own environment so names like "Pulse" resolve correctly below.
10
- local _P = ...
8
+ -- The compiler passes (_P, _PULSE_LAYOUT) as arguments.
9
+ local _P, _PULSE_LAYOUT = ...
11
10
  if type(_P) == "table" then
12
11
  local _env = (getfenv and getfenv(1)) or _ENV or _G
13
12
  for _k, _v in pairs(_P) do _env[_k] = _v end
14
13
  end
14
+ _PULSE_LAYOUT = _PULSE_LAYOUT or {}
15
+
16
+ -- Capture bundle table references as upvalues immediately so the startup block
17
+ -- can access them without global lookups (which are environment-table-dependent
18
+ -- in executor sandboxes and may fail in task.spawn callbacks).
19
+ local _BundleComponents = type(_P) == "table" and _P.Components or {}
20
+ local _BundlePages = type(_P) == "table" and _P._Pages or {}
21
+ local _BundlePulse = type(_P) == "table" and _P.Pulse or Pulse
15
22
 
16
23
  local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
17
24
  local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
@@ -210,7 +217,12 @@ function _UIAdapter:addToggle(gb, id, opts)
210
217
  Desc = opts.tip or nil,
211
218
  Value = opts.signal(),
212
219
  Flag = id,
213
- Callback = function(v) opts.signal(v) end,
220
+ Callback = function(v)
221
+ if _BundlePulse and _BundlePulse.Log then
222
+ _BundlePulse.Log.debug("ui", "toggle "..tostring(id).." → "..tostring(v))
223
+ end
224
+ opts.signal(v)
225
+ end,
214
226
  })
215
227
  _widgets[id] = elem
216
228
  _G.Toggles = _G.Toggles or {}
@@ -240,7 +252,12 @@ function _UIAdapter:addSlider(gb, id, opts)
240
252
  Step = step,
241
253
  Value = { Min = min, Max = max, Default = def },
242
254
  Flag = id,
243
- Callback = function(v) opts.signal(v) end,
255
+ Callback = function(v)
256
+ if _BundlePulse and _BundlePulse.Log then
257
+ _BundlePulse.Log.debug("ui", "slider "..tostring(id).." → "..tostring(v))
258
+ end
259
+ opts.signal(v)
260
+ end,
244
261
  })
245
262
  _widgets[id] = elem
246
263
  _G.Options = _G.Options or {}
@@ -357,7 +374,7 @@ function _UIAdapter:mount(component, gb)
357
374
  if not ui then return end
358
375
  for _, w in ipairs(ui) do
359
376
  local t = w.type
360
- local sig = component[w.signal]
377
+ local sig = w.signal -- PulseSignal stored directly by .bind(sig) in TS widget builders
361
378
  if t == "toggle" then
362
379
  self:addToggle(gb, w.id, { label = w.label, signal = sig, tip = w.tip })
363
380
  elseif t == "slider" then
@@ -509,3 +526,564 @@ function Pulse.UI.section(container, title, icon, opened)
509
526
  function proxy:section(...) return Pulse.UI.section(self, ...) end
510
527
  return proxy
511
528
  end
529
+
530
+ -- ── Startup — create window, render pages, build Settings tab ─────────────────
531
+ -- Runs after user code has registered all components and pages (task.spawn defers
532
+ -- execution to the next frame, by which point all top-level TS calls have run).
533
+
534
+ task.spawn(function()
535
+ task.wait(0) -- yield one frame so definePage/defineComponent calls finish
536
+
537
+ local L = _PULSE_LAYOUT
538
+
539
+ -- Resolve toggle key
540
+ local _tkName = (L.toggleKey and L.toggleKey ~= "") and L.toggleKey or "RightControl"
541
+ local _tk = pcall(function() return Enum.KeyCode[_tkName] end) and Enum.KeyCode[_tkName]
542
+ _PULSE_TOGGLE_KEY = _tk
543
+ _PULSE_DEFAULT_THEME = L.theme or "Dark"
544
+
545
+ -- Create the Wind UI window
546
+ local _win = _UIAdapter:CreateWindow(
547
+ L.title or "Hub",
548
+ L.sizeW or 950,
549
+ L.sizeH or 600,
550
+ {
551
+ icon = (L.icon and L.icon ~= "") and L.icon or nil,
552
+ author = L.author or nil,
553
+ theme = L.theme or "Dark",
554
+ folder = (L.folder and L.folder ~= "") and L.folder or "Hub",
555
+ acrylic = L.acrylic or false,
556
+ transparency = L.transparency or 0.8,
557
+ toggleKey = _tk,
558
+ openButtonMobileOnly = L.openButtonMobileOnly or false,
559
+ openButtonIcon = (L.openButtonIcon and L.openButtonIcon ~= "") and L.openButtonIcon or nil,
560
+ }
561
+ )
562
+ _UIAdapter:SetToggleKey(_tk)
563
+
564
+ -- ── Home tab ──────────────────────────────────────────────────────────────
565
+ -- Always the first tab: player welcome, script info, executor/game context.
566
+
567
+ -- Player info
568
+ local _PS2 = game:GetService("Players")
569
+ local _LP2 = _PS2.LocalPlayer
570
+ local _dspName = (_LP2 and _LP2.DisplayName) or "Player"
571
+ local _usrName = (_LP2 and _LP2.Name) or ""
572
+
573
+ -- Avatar thumbnail (GetUserThumbnailAsync yields — fine inside task.spawn)
574
+ local _avatarUrl = ""
575
+ pcall(function()
576
+ if _LP2 then
577
+ _avatarUrl = _PS2:GetUserThumbnailAsync(
578
+ _LP2.UserId,
579
+ Enum.ThumbnailType.HeadShot,
580
+ Enum.ThumbnailSize.Size100x100
581
+ )
582
+ end
583
+ end)
584
+
585
+ -- Executor detection
586
+ local _execName = "Unknown"
587
+ local _execVer = ""
588
+ pcall(function()
589
+ if type(rawget(_G, "identifyexecutor")) == "function" then
590
+ local n, v = identifyexecutor()
591
+ _execName = tostring(n or "Unknown")
592
+ _execVer = tostring(v or "")
593
+ elseif rawget(_G, "syn") then _execName = "Synapse X"
594
+ elseif rawget(_G, "KRNL_LOADED") then _execName = "Krnl"
595
+ elseif rawget(_G, "Delta") then _execName = "Delta"
596
+ elseif rawget(_G, "is_sirhurt_closure") then _execName = "Sirhurt"
597
+ elseif rawget(_G, "EXECUTOR_NAME") then _execName = tostring(rawget(_G, "EXECUTOR_NAME"))
598
+ end
599
+ end)
600
+
601
+ -- Game name (GetProductInfo yields)
602
+ local _gameName = "PlaceId " .. tostring(game.PlaceId)
603
+ pcall(function()
604
+ local info = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId)
605
+ if info and info.Name then _gameName = info.Name end
606
+ end)
607
+
608
+ local _homeTab = _win:AddTab("Home", "house")
609
+
610
+ -- Left column ──────────────────────────────────────────────────────────────
611
+
612
+ -- Welcome / player section
613
+ local _welcomeGb = _homeTab:AddLeftGroupbox("Welcome", "person")
614
+ if _avatarUrl ~= "" then
615
+ -- WindUI Image element — silently skipped if unsupported
616
+ pcall(function()
617
+ _welcomeGb._container:Image({
618
+ Source = _avatarUrl,
619
+ Size = UDim2.fromOffset(96, 96),
620
+ })
621
+ end)
622
+ end
623
+ _welcomeGb._container:Paragraph({
624
+ Title = _dspName,
625
+ Desc = "@" .. _usrName,
626
+ })
627
+
628
+ if L.description and L.description ~= "" then
629
+ _welcomeGb._container:Divider()
630
+ _welcomeGb._container:Paragraph({ Title = "", Desc = L.description })
631
+ end
632
+
633
+ if L.discord and L.discord ~= "" then
634
+ local _discGb = _homeTab:AddLeftGroupbox("Community", "message-circle")
635
+ _discGb._container:Button({
636
+ Title = "Join Discord",
637
+ Desc = "Tap to copy the invite link",
638
+ Callback = function()
639
+ local ok = false
640
+ if not ok then pcall(function() setclipboard(L.discord); ok=true end) end
641
+ if not ok then pcall(function() toclipboard(L.discord); ok=true end) end
642
+ if not ok then pcall(function() writeclipboard(L.discord); ok=true end) end
643
+ _PulseNotify(ok and "Discord link copied!" or "No clipboard API available", 3)
644
+ end,
645
+ })
646
+ end
647
+
648
+ -- Right column ─────────────────────────────────────────────────────────────
649
+
650
+ -- Script metadata
651
+ local _scriptGb = _homeTab:AddRightGroupbox("Script", "code-2")
652
+ local _verStr = (L.version and L.version ~= "") and ("v" .. L.version) or ""
653
+ local _authStr = (L.author and L.author ~= "") and ("by " .. L.author) or ""
654
+ local _metaParts = {}
655
+ if _verStr ~= "" then _metaParts[#_metaParts+1] = _verStr end
656
+ if _authStr ~= "" then _metaParts[#_metaParts+1] = _authStr end
657
+ _scriptGb._container:Paragraph({
658
+ Title = L.title or "Hub",
659
+ Desc = #_metaParts > 0 and table.concat(_metaParts, " · ") or "—",
660
+ })
661
+
662
+ -- System info
663
+ local _sysGb = _homeTab:AddRightGroupbox("System", "monitor")
664
+ _sysGb._container:Paragraph({
665
+ Title = "Executor",
666
+ Desc = _execName .. (_execVer ~= "" and (" " .. _execVer) or ""),
667
+ })
668
+ _sysGb._container:Paragraph({ Title = "Game", Desc = _gameName })
669
+
670
+ -- Unsupported executors (optional — set L.unsupported in layout config)
671
+ if L.unsupported and L.unsupported ~= "" then
672
+ local _unsupList = {}
673
+ if type(L.unsupported) == "table" then
674
+ for _, v in ipairs(L.unsupported) do _unsupList[#_unsupList+1] = tostring(v) end
675
+ elseif type(L.unsupported) == "string" then
676
+ for part in L.unsupported:gmatch("[^,]+") do
677
+ _unsupList[#_unsupList+1] = part:match("^%s*(.-)%s*$")
678
+ end
679
+ end
680
+ if #_unsupList > 0 then
681
+ local _unsupGb = _homeTab:AddRightGroupbox("Not Supported", "alert-triangle")
682
+ _unsupGb._container:Paragraph({
683
+ Title = "Not compatible with:",
684
+ Desc = table.concat(_unsupList, ", "),
685
+ })
686
+ end
687
+ end
688
+
689
+ -- Render user pages (upvalue _BundlePages — avoids global lookup in executor sandbox)
690
+ for _, page in ipairs(_BundlePages) do
691
+ local tab = _win:AddTab(page.title, page.icon)
692
+ for _, gb in ipairs(page.layout or {}) do
693
+ local container
694
+ if gb.side == "left" then
695
+ container = tab:AddLeftGroupbox(gb.title, gb.icon)
696
+ else
697
+ container = tab:AddRightGroupbox(gb.title, gb.icon)
698
+ end
699
+ if gb.mount then
700
+ local comp = _BundleComponents[gb.mount]
701
+ if comp then
702
+ local ok, err = pcall(function() _UIAdapter:mount(comp, container) end)
703
+ if not ok then
704
+ warn("[Pulse] mount '" .. gb.mount .. "' error: " .. tostring(err))
705
+ if _BundlePulse and _BundlePulse.Log then
706
+ _BundlePulse.Log.error("ui", "mount '"..gb.mount.."' failed: "..tostring(err))
707
+ end
708
+ else
709
+ if _BundlePulse and _BundlePulse.Log then
710
+ local nw = comp._ui and #comp._ui or 0
711
+ _BundlePulse.Log.debug("ui", "mounted '"..gb.mount.."' ("..nw.." widgets)")
712
+ end
713
+ end
714
+ else
715
+ warn("[Pulse] component '" .. gb.mount .. "' not found in registry")
716
+ if _BundlePulse and _BundlePulse.Log then
717
+ _BundlePulse.Log.error("ui", "component '"..gb.mount.."' not in registry")
718
+ end
719
+ end
720
+ else
721
+ for _, w in ipairs(gb.widgets or {}) do
722
+ pcall(function() _UIAdapter:mount({ _ui = {w}, _name = gb.title }, container) end)
723
+ end
724
+ end
725
+ end
726
+ end
727
+
728
+ -- ── Settings tab ──────────────────────────────────────────────────────────
729
+ local _settingsTab = _win:AddTab("Settings", "settings")
730
+
731
+ -- Appearance groupbox
732
+ local gb_appear = _settingsTab:AddLeftGroupbox("Appearance")
733
+ local _themeValues = _UIAdapter:GetThemeNames()
734
+ or { "Amber","CottonCandy","Crimson","Dark","Emerald","Indigo","Light",
735
+ "Mellowsi","Midnight","MonokaiPro","Plant","Rainbow","Red","Rose","Sky","Violet" }
736
+ gb_appear._container:Dropdown({
737
+ Title = "Theme",
738
+ Values = _themeValues,
739
+ Value = _PULSE_DEFAULT_THEME,
740
+ Flag = "Settings_theme",
741
+ Callback = function(v) _UIAdapter:SetTheme(v) end,
742
+ })
743
+
744
+ -- Config groupbox
745
+ local gb_cfg = _settingsTab:AddLeftGroupbox("Configuration")
746
+ _UIAdapter:addParagraph(gb_cfg,
747
+ "Config is stored in your executor's filesystem.",
748
+ "Save writes the current state. Load restores the last saved state."
749
+ )
750
+ gb_cfg._container:Button({
751
+ Title = "Save Config",
752
+ Icon = "save",
753
+ Callback = function()
754
+ if _UIAdapter.Config then
755
+ local ok, err = pcall(function() _UIAdapter.Config:Save() end)
756
+ _PulseNotify(ok and "Config saved." or ("Save failed: " .. tostring(err)), 3)
757
+ else
758
+ _PulseNotify("Config manager unavailable.", 3)
759
+ end
760
+ end,
761
+ })
762
+ gb_cfg._container:Button({
763
+ Title = "Load Config",
764
+ Icon = "folder-open",
765
+ Callback = function()
766
+ if _UIAdapter.Config then
767
+ local ok, err = pcall(function() _UIAdapter.Config:Load() end)
768
+ _PulseNotify(ok and "Config loaded." or ("Load failed: " .. tostring(err)), 3)
769
+ else
770
+ _PulseNotify("Config manager unavailable.", 3)
771
+ end
772
+ end,
773
+ })
774
+
775
+ -- Menu settings groupbox
776
+ local gb_menu = _settingsTab:AddRightGroupbox("Menu Settings")
777
+ _UIAdapter:addButton(gb_menu, {
778
+ label = "Destroy UI",
779
+ action = function() _PulseDestroy() end,
780
+ tip = "Unloads the script and removes all UI",
781
+ })
782
+ _UIAdapter:addKeybind(gb_menu, "Settings_MenuKeybind", {
783
+ label = "Menu Toggle",
784
+ key = _PULSE_TOGGLE_KEY,
785
+ action = function() _UIAdapter:ToggleWindow() end,
786
+ })
787
+
788
+ -- Auto-load saved config on startup
789
+ task.spawn(function()
790
+ task.wait(1.5)
791
+ if _UIAdapter.Config then
792
+ local ok = pcall(function() _UIAdapter.Config:Load() end)
793
+ if ok and _BundlePulse and _BundlePulse.Log then _BundlePulse.Log.info("settings", "config auto-loaded") end
794
+ end
795
+ end)
796
+
797
+ -- ── Dev tab (--dev builds only) ───────────────────────────────────────────
798
+ if L.dev then
799
+ local _devTab
800
+ pcall(function() _devTab = _windWindow:Tab({ Title = "Dev", Icon = "bug" }) end)
801
+ if _devTab then
802
+ local _devCol
803
+ pcall(function() _devCol = _devTab:VStack({}) end)
804
+ _devCol = _devCol or _devTab
805
+
806
+ -- Tools section
807
+ local _toolSect
808
+ pcall(function()
809
+ _toolSect = _devCol:Section({ Title="Tools", Icon="wrench", Box=true, BoxBorder=true, Opened=true })
810
+ end)
811
+
812
+ local _TOOLS = {
813
+ { name="Hydroxide", desc="Remote spy with web interface", url=nil,
814
+ load=function()
815
+ local function webImport(f)
816
+ loadstring(game:HttpGetAsync(("https://raw.githubusercontent.com/Upbolt/Hydroxide/revision/%s.lua"):format(f)), f..".lua")()
817
+ end
818
+ webImport("init"); webImport("ui/main")
819
+ end },
820
+ { name="Infinite Yield", desc="Admin / exploit command panel", url="https://raw.githubusercontent.com/DarkNetworks/Infinite-Yield/main/latest.lua" },
821
+ { name="Dex Explorer", desc="Browse and inspect the live game tree",url="https://raw.githubusercontent.com/infyiff/backup/main/dex.lua" },
822
+ }
823
+
824
+ if _toolSect then
825
+ for _, tool in ipairs(_TOOLS) do
826
+ local _launched = false
827
+ local _btn
828
+ pcall(function()
829
+ _btn = _toolSect:Button({
830
+ Title = tool.name,
831
+ Desc = tool.desc,
832
+ Callback = function()
833
+ if _launched then _PulseNotify("Already running: "..tool.name, 2); return end
834
+ pcall(function() _btn:SetTitle(tool.name.." · loading…") end)
835
+ task.spawn(function()
836
+ local ok, err = pcall(function()
837
+ if tool.load then
838
+ tool.load()
839
+ else
840
+ local src = game:HttpGet(tool.url)
841
+ local fn, ce = loadstring(src)
842
+ if not fn then error(ce) end
843
+ fn()
844
+ end
845
+ end)
846
+ if ok then
847
+ _launched = true
848
+ pcall(function() _btn:SetTitle("✓ "..tool.name) end)
849
+ _PulseNotify(tool.name.." launched", 3)
850
+ else
851
+ pcall(function() _btn:SetTitle(tool.name.." · failed") end)
852
+ _PulseNotify(tool.name.." failed: "..tostring(err):sub(1,60), 4)
853
+ task.delay(3, function()
854
+ if not _launched then pcall(function() _btn:SetTitle(tool.name) end) end
855
+ end)
856
+ end
857
+ end)
858
+ end,
859
+ })
860
+ end)
861
+ end
862
+ end
863
+
864
+ -- Scanner section
865
+ local _scanSect
866
+ pcall(function()
867
+ _scanSect = _devCol:Section({ Title="Scanner", Icon="scan", Box=true, BoxBorder=true, Opened=true })
868
+ end)
869
+
870
+ if _scanSect then
871
+ local _lastScan = ""
872
+ local _statusPara
873
+ pcall(function()
874
+ _statusPara = _scanSect:Paragraph({
875
+ Title = "Game Scanner",
876
+ Desc = "Auto-scan runs on inject. Results copied to clipboard.",
877
+ })
878
+ end)
879
+
880
+ local function _devCopy(text)
881
+ local ok = false
882
+ if not ok then pcall(function() setclipboard(text); ok=true end) end
883
+ if not ok then pcall(function() toclipboard(text); ok=true end) end
884
+ if not ok then pcall(function() writeclipboard(text); ok=true end) end
885
+ return ok
886
+ end
887
+
888
+ local _ALIASES = {
889
+ { "Players.LocalPlayer.PlayerGui.", "PG." },
890
+ { "Players.LocalPlayer.", "LP." },
891
+ { "ReplicatedStorage.", "RS." },
892
+ { "ReplicatedFirst.", "RF." },
893
+ { "workspace.", "WS." },
894
+ }
895
+ local function _compress(obj)
896
+ local parts = {}; local cur = obj
897
+ while cur and cur ~= game do table.insert(parts, 1, cur.Name); cur = cur.Parent end
898
+ local s = table.concat(parts, ".")
899
+ for _, pair in ipairs(_ALIASES) do
900
+ if s:sub(1, #pair[1]) == pair[1] then return pair[2]..s:sub(#pair[1]+1) end
901
+ end
902
+ return s
903
+ end
904
+
905
+ local function _doScan()
906
+ pcall(function() _statusPara:SetDesc("Scanning…") end)
907
+ task.spawn(function()
908
+ local out = {}
909
+ local RS = game:GetService("ReplicatedStorage")
910
+ local gameName = "?"
911
+ pcall(function()
912
+ gameName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
913
+ end)
914
+ table.insert(out, ("[%s] pid:%d"):format(gameName, game.PlaceId))
915
+
916
+ local remotes = {}
917
+ local function _scanRemotes(inst, depth)
918
+ if depth > 8 then return end
919
+ local ok, ch = pcall(function() return inst:GetChildren() end)
920
+ if not ok then return end
921
+ for _, c in ipairs(ch) do
922
+ local cls = c.ClassName
923
+ if cls == "RemoteEvent" or cls == "RemoteFunction" then
924
+ table.insert(remotes, { p=_compress(c), t=cls=="RemoteEvent" and "RE" or "RF" })
925
+ end
926
+ if c:IsA("Folder") or c:IsA("Configuration") or c:IsA("Model") then
927
+ _scanRemotes(c, depth+1)
928
+ end
929
+ end
930
+ end
931
+ pcall(_scanRemotes, RS, 0)
932
+ table.sort(remotes, function(a,b) return a.p < b.p end)
933
+ table.insert(out, ("REMOTES (%d)"):format(#remotes))
934
+ for i, r in ipairs(remotes) do
935
+ if i > 50 then table.insert(out, (" ...+%d more"):format(#remotes-50)); break end
936
+ table.insert(out, (" %-52s %s"):format(r.p, r.t))
937
+ end
938
+
939
+ local txt = table.concat(out, "\n")
940
+ _lastScan = txt
941
+ local copied = _devCopy(txt)
942
+ local summary = ("%d remotes%s"):format(#remotes, copied and " · copied" or "")
943
+ pcall(function()
944
+ _statusPara:SetTitle("Scan "..os.date("%H:%M:%S"))
945
+ _statusPara:SetDesc(summary)
946
+ end)
947
+ if copied then _PulseNotify("Scan complete · "..summary, 4) end
948
+ end)
949
+ end
950
+
951
+ pcall(function()
952
+ _scanSect:Button({
953
+ Title="Scan Now", Desc="Scan remotes and copy to clipboard",
954
+ Callback = _doScan,
955
+ })
956
+ end)
957
+ pcall(function()
958
+ _scanSect:Button({
959
+ Title="Copy Last Results", Desc="Re-copy most recent scan",
960
+ Callback = function()
961
+ if _lastScan == "" then _PulseNotify("No scan yet", 2); return end
962
+ local ok = _devCopy(_lastScan)
963
+ _PulseNotify(ok and "Copied" or "No clipboard function", 2)
964
+ end,
965
+ })
966
+ end)
967
+
968
+ -- Auto-scan on inject
969
+ task.spawn(function()
970
+ if not game:IsLoaded() then game.Loaded:Wait() end
971
+ task.wait(2)
972
+ _doScan()
973
+ end)
974
+ end
975
+
976
+ -- Info section — framework version + signal counts for debugging
977
+ local _infoSect
978
+ pcall(function()
979
+ _infoSect = _devCol:Section({ Title="Pulse Info", Icon="info", Box=true, BoxBorder=true, Opened=true })
980
+ end)
981
+ if _infoSect then
982
+ local nPages = #_BundlePages
983
+ local nComps = 0
984
+ for _ in pairs(_BundleComponents) do nComps = nComps + 1 end
985
+ pcall(function()
986
+ _infoSect:Paragraph({
987
+ Title = "Framework",
988
+ Desc = ("pages=%d components=%d theme=%s"):format(nPages, nComps, L.theme or "?"),
989
+ })
990
+ end)
991
+ for cname, comp in pairs(_BundleComponents) do
992
+ if type(comp) == "table" and comp._ui then
993
+ pcall(function()
994
+ _infoSect:Paragraph({
995
+ Title = cname,
996
+ Desc = ("%d widget(s)"):format(#comp._ui),
997
+ })
998
+ end)
999
+ end
1000
+ end
1001
+ end
1002
+
1003
+ -- Console section — controls for the Roblox developer console (F9).
1004
+ -- "Clear" wipes the output so only your next prints are visible.
1005
+ -- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
1006
+ -- them cleanly after a clear without any engine noise in the way.
1007
+ -- "Copy Logs" copies the full log buffer to clipboard.
1008
+ local _consoleSect
1009
+ pcall(function()
1010
+ _consoleSect = _devCol:Section({ Title="Console (F9)", Icon="terminal", Box=true, BoxBorder=true, Opened=true })
1011
+ end)
1012
+ if _consoleSect and _BundlePulse and _BundlePulse.Log then
1013
+ local _Log = _BundlePulse.Log
1014
+
1015
+ -- Clear console: try executor functions, fall back to blank lines separator.
1016
+ local function _clearConsole()
1017
+ local cleared = false
1018
+ if not cleared then pcall(function() consoleclear(); cleared=true end) end
1019
+ if not cleared then pcall(function() clearoutput(); cleared=true end) end
1020
+ if not cleared then pcall(function()
1021
+ game:GetService("LogService"):ClearOutput(); cleared=true
1022
+ end) end
1023
+ if not cleared then
1024
+ -- visual separator when no clear API is available
1025
+ print(("\n"):rep(40))
1026
+ print("──────────── console cleared ────────────")
1027
+ end
1028
+ end
1029
+
1030
+ local function _copyLogs()
1031
+ local txt = _Log.dump()
1032
+ local ok = false
1033
+ if not ok then pcall(function() setclipboard(txt); ok=true end) end
1034
+ if not ok then pcall(function() toclipboard(txt); ok=true end) end
1035
+ if not ok then pcall(function() writeclipboard(txt); ok=true end) end
1036
+ _PulseNotify(ok and "Logs copied to clipboard" or "No clipboard API available", 3)
1037
+ end
1038
+
1039
+ local function _printLogs()
1040
+ local entries = _Log.getNewEntries(0)
1041
+ print("──────────── Pulse Logs (" .. #entries .. ") ────────────")
1042
+ for _, e in ipairs(entries) do
1043
+ if e.n >= 3 then warn(e.line) else print(e.line) end
1044
+ end
1045
+ print("─────────────────────────────────────────")
1046
+ end
1047
+
1048
+ pcall(function()
1049
+ _consoleSect:Button({
1050
+ Title = "Clear Console",
1051
+ Desc = "Wipe F9 output — then use Print Logs to see only yours",
1052
+ Callback = _clearConsole,
1053
+ })
1054
+ end)
1055
+ pcall(function()
1056
+ _consoleSect:Button({
1057
+ Title = "Print Logs",
1058
+ Desc = "Re-print all Pulse.Log entries to F9 console",
1059
+ Callback = _printLogs,
1060
+ })
1061
+ end)
1062
+ pcall(function()
1063
+ _consoleSect:Button({
1064
+ Title = "Copy Logs",
1065
+ Desc = "Copy full log buffer to clipboard",
1066
+ Callback = _copyLogs,
1067
+ })
1068
+ end)
1069
+ pcall(function()
1070
+ _consoleSect:Button({
1071
+ Title = "Clear Log Buffer",
1072
+ Desc = "Reset the in-memory Pulse.Log buffer",
1073
+ Callback = function()
1074
+ _Log.clear()
1075
+ _PulseNotify("Log buffer cleared", 2)
1076
+ end,
1077
+ })
1078
+ end)
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ -- Re-select first tab (Home) — Settings is last so Wind UI leaves it active
1084
+ task.defer(function()
1085
+ if _UIAdapter._firstTab then
1086
+ pcall(function() _UIAdapter._firstTab:Select() end)
1087
+ end
1088
+ end)
1089
+ end)
package/bin/rb.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ var RB_VERSION, RB_HOME, IRONBREW2_DIR, IRONBREW2_REPO, ALWAYS_EXCLUDE, VALID_UI
56
56
  var init_constants = __esm({
57
57
  "src/constants.ts"() {
58
58
  init_cjs_shims();
59
- RB_VERSION = "1.3.2";
59
+ RB_VERSION = "1.4.0";
60
60
  RB_HOME = path.join(os.homedir(), ".rb");
61
61
  path.join(RB_HOME, "bin");
62
62
  IRONBREW2_DIR = path.join(RB_HOME, "ironbrew2");
@@ -1193,6 +1193,59 @@ end)
1193
1193
  }
1194
1194
  return "windui";
1195
1195
  }
1196
+ readLayoutConfig() {
1197
+ const layoutTs = path.join(this.srcDir, "layout.ts");
1198
+ const cfg = {
1199
+ title: "Script Hub",
1200
+ author: "",
1201
+ toggleKey: "RightControl",
1202
+ sizeW: 950,
1203
+ sizeH: 600,
1204
+ theme: "Dark",
1205
+ icon: "",
1206
+ folder: "Hub",
1207
+ acrylic: false,
1208
+ transparency: 0.8,
1209
+ openButtonMobileOnly: false,
1210
+ openButtonIcon: "",
1211
+ description: "",
1212
+ discord: "",
1213
+ version: ""
1214
+ };
1215
+ if (!fs.existsSync(layoutTs)) return cfg;
1216
+ const src = fs.readFileSync(layoutTs, "utf8");
1217
+ const str = (key) => {
1218
+ const m = src.match(new RegExp(key + `\\s*:\\s*['"]([^'"]+)['"]`));
1219
+ if (m) cfg[key] = m[1];
1220
+ };
1221
+ const bool = (key) => {
1222
+ const m = src.match(new RegExp(key + `\\s*:\\s*(true|false)`));
1223
+ if (m) cfg[key] = m[1] === "true";
1224
+ };
1225
+ const num = (key) => {
1226
+ const m = src.match(new RegExp(key + `\\s*:\\s*([\\d.]+)`));
1227
+ if (m) cfg[key] = parseFloat(m[1]);
1228
+ };
1229
+ str("title");
1230
+ str("author");
1231
+ str("toggleKey");
1232
+ str("theme");
1233
+ str("icon");
1234
+ str("folder");
1235
+ str("openButtonIcon");
1236
+ str("description");
1237
+ str("discord");
1238
+ str("version");
1239
+ bool("acrylic");
1240
+ bool("openButtonMobileOnly");
1241
+ num("transparency");
1242
+ const sz = src.match(/size\s*:\s*\[(\d+)\s*,\s*(\d+)\]/);
1243
+ if (sz) {
1244
+ cfg["sizeW"] = parseInt(sz[1]);
1245
+ cfg["sizeH"] = parseInt(sz[2]);
1246
+ }
1247
+ return cfg;
1248
+ }
1196
1249
  resolveUi(ui) {
1197
1250
  if (!ui) ui = this.getUiLibrary();
1198
1251
  if (!VALID_UI_LIBS.has(ui)) throw new Error(`Unknown UI library '${ui}'. Valid: ${[...VALID_UI_LIBS].sort().join(", ")}`);
@@ -1247,7 +1300,7 @@ end)
1247
1300
  if (path.relative(src, f).split(/[/\\]/).length === 1) continue;
1248
1301
  middle.push(f);
1249
1302
  }
1250
- if (opts.dev) {
1303
+ if (opts.dev && !opts.cdn) {
1251
1304
  const fwDevUi = path.join(PULSE_DEV_DIR, "ui");
1252
1305
  if (fs.existsSync(fwDevUi)) {
1253
1306
  for (const f of rglob(fwDevUi, [".lua", ".rblua", ".ts"]).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
@@ -1288,13 +1341,39 @@ end)
1288
1341
  parts.push("\n");
1289
1342
  if (hasTs) {
1290
1343
  const base = `${CDN_BASE_URL}/v${RB_VERSION}`;
1344
+ const lc = this.readLayoutConfig();
1345
+ const luaBool = (v) => v ? "true" : "false";
1291
1346
  parts.push(`-- Pulse v${RB_VERSION}
1347
+ `);
1348
+ parts.push(`local _PULSE_LAYOUT={`);
1349
+ parts.push(`title=${JSON.stringify(lc["title"])},`);
1350
+ parts.push(`author=${JSON.stringify(lc["author"])},`);
1351
+ parts.push(`toggleKey=${JSON.stringify(lc["toggleKey"])},`);
1352
+ parts.push(`sizeW=${lc["sizeW"]},sizeH=${lc["sizeH"]},`);
1353
+ parts.push(`theme=${JSON.stringify(lc["theme"])},`);
1354
+ parts.push(`icon=${JSON.stringify(lc["icon"])},`);
1355
+ parts.push(`folder=${JSON.stringify(lc["folder"])},`);
1356
+ parts.push(`acrylic=${luaBool(lc["acrylic"])},`);
1357
+ parts.push(`transparency=${lc["transparency"]},`);
1358
+ parts.push(`openButtonMobileOnly=${luaBool(lc["openButtonMobileOnly"])},`);
1359
+ parts.push(`openButtonIcon=${JSON.stringify(lc["openButtonIcon"])}`);
1360
+ if (lc["description"]) parts.push(`,description=${JSON.stringify(lc["description"])}`);
1361
+ if (lc["discord"]) parts.push(`,discord=${JSON.stringify(lc["discord"])}`);
1362
+ if (lc["version"]) parts.push(`,version=${JSON.stringify(lc["version"])}`);
1363
+ if (opts.dev) parts.push(`,dev=true`);
1364
+ parts.push(`}
1292
1365
  `);
1293
1366
  parts.push(`local _P=loadstring(game:HttpGet("${base}/bundle.lua"))()
1294
1367
  `);
1368
+ if (opts.dev) {
1369
+ parts.push(`_PULSE_DEV = true
1370
+ `);
1371
+ parts.push(`if _P.Pulse and _P.Pulse.Log then _P.Pulse.Log.enable() end
1372
+ `);
1373
+ }
1295
1374
  parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
1296
1375
  `);
1297
- parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P)
1376
+ parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P,_PULSE_LAYOUT)
1298
1377
  `);
1299
1378
  parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
1300
1379
  `);
@@ -1304,17 +1383,6 @@ end)
1304
1383
  `);
1305
1384
  parts.push("\n");
1306
1385
  }
1307
- if (opts.dev && fs.existsSync(PULSE_DEV_DIR)) {
1308
- const rbRoot = path.join(__dirname, "..", "..");
1309
- for (const f of fs.readdirSync(PULSE_DEV_DIR).filter((n) => n.endsWith(".lua")).sort()) {
1310
- const full = path.join(PULSE_DEV_DIR, f);
1311
- const rel = path.relative(rbRoot, full).replace(/\\/g, "/");
1312
- parts.push(`-- [${rel}]
1313
- `);
1314
- parts.push(fs.readFileSync(full, "utf8"));
1315
- parts.push("\n");
1316
- }
1317
- }
1318
1386
  const label = (p) => {
1319
1387
  try {
1320
1388
  return path.relative(this.root, p).replace(/\\/g, "/");
@@ -1387,6 +1455,11 @@ end)
1387
1455
  parts.push(DEFAULTS_RUNNER);
1388
1456
  parts.push("\n");
1389
1457
  }
1458
+ if (opts.dev) {
1459
+ parts.push(
1460
+ '-- [dev: log config]\nif Pulse and Pulse.Log then\n Pulse.Log.configure({ level = "debug", console = true })\n Pulse.Log.info("dev", "dev build active")\nend\n\n'
1461
+ );
1462
+ }
1390
1463
  parts.push("-- [generated: destroy registration]\n");
1391
1464
  parts.push(DESTROY_REGISTRATION);
1392
1465
  parts.push("\n");
@@ -1797,25 +1870,20 @@ function makeClaudeMd(name) {
1797
1870
  function makeAgentsMd(name) {
1798
1871
  return read("AGENTS.md").replace(/{NAME}/g, name);
1799
1872
  }
1800
- var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, TEMPLATE_GITIGNORE, TEMPLATE_DEPLOY_EXAMPLE, MODULE_BOILERPLATE, MODULE_RBLUA_BOILERPLATE, REMOTE_BOILERPLATE, TEMPLATE_LAYOUT_TS, TEMPLATE_PAGE_HOME_TS, TEMPLATE_EX_SPEED_TS, TEMPLATE_EX_FOV_TS, TEMPLATE_EX_ESP_TS;
1873
+ var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, MODULE_BOILERPLATE, REMOTE_BOILERPLATE, TEMPLATE_GITIGNORE, TEMPLATE_DEPLOY_EXAMPLE, TEMPLATE_LAYOUT_TS, TEMPLATE_PAGE_HOME_TS, TEMPLATE_COMPONENT_TS, TEMPLATE_EX_SPEED_TS, TEMPLATE_EX_FOV_TS, TEMPLATE_EX_ESP_TS;
1801
1874
  var init_templates = __esm({
1802
1875
  "src/templates.ts"() {
1803
1876
  init_cjs_shims();
1804
1877
  DIR = path.join(__dirname, "..", "templates");
1805
1878
  TEMPLATE_GLOBALS = read("globals.lua");
1806
1879
  TEMPLATE_REMOTES = read("remotes.lua");
1807
- TEMPLATE_GITIGNORE = read("gitignore");
1808
- read("layout.rblua");
1809
- read("page_home.rblua");
1810
- read("example_speed.rblua");
1811
- read("example_fov.rblua");
1812
- read("example_esp.rblua");
1813
- TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
1814
1880
  MODULE_BOILERPLATE = read("module.lua");
1815
- MODULE_RBLUA_BOILERPLATE = read("module.rblua");
1816
1881
  REMOTE_BOILERPLATE = read("remote.lua");
1882
+ TEMPLATE_GITIGNORE = read("gitignore");
1883
+ TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
1817
1884
  TEMPLATE_LAYOUT_TS = read("layout.ts");
1818
1885
  TEMPLATE_PAGE_HOME_TS = read("page_home.ts");
1886
+ TEMPLATE_COMPONENT_TS = read("component.ts");
1819
1887
  TEMPLATE_EX_SPEED_TS = read("component_speed.ts");
1820
1888
  TEMPLATE_EX_FOV_TS = read("component_fov.ts");
1821
1889
  TEMPLATE_EX_ESP_TS = read("component_esp.ts");
@@ -2015,9 +2083,8 @@ function toCamel(name) {
2015
2083
  function cmdNew(args) {
2016
2084
  const root = requireProjectRoot();
2017
2085
  if (args.type === "module") {
2018
- const plain = args.plain ?? false;
2019
2086
  let relPath = args.path;
2020
- if (plain) {
2087
+ if (args.plain) {
2021
2088
  if (!relPath.endsWith(".lua")) relPath += ".lua";
2022
2089
  const dest = pathe.join(root, "src", relPath);
2023
2090
  const camel = toCamel(relPath);
@@ -2033,23 +2100,23 @@ function cmdNew(args) {
2033
2100
  pOk(`src/${relPath}`);
2034
2101
  console.log();
2035
2102
  pKv("File", `src/${relPath}`);
2036
- pKv("Tip", "add to func for shared helpers; keep private state local");
2103
+ pKv("Tip", "add helpers onto func; keep state local");
2037
2104
  } else {
2038
- if (!relPath.endsWith(".rblua")) relPath += ".rblua";
2105
+ if (!relPath.endsWith(".ts")) relPath += ".ts";
2039
2106
  const dest = pathe.join(root, "src", relPath);
2040
2107
  const camel = toCamel(relPath);
2108
+ const content = TEMPLATE_COMPONENT_TS.replace(/{Camel}/g, camel);
2041
2109
  if (fs.existsSync(dest)) {
2042
2110
  pFail(`src/${relPath} already exists`);
2043
2111
  process.exit(1);
2044
2112
  }
2045
2113
  fs.mkdirSync(pathe.join(dest, ".."), { recursive: true });
2046
- fs.writeFileSync(dest, MODULE_RBLUA_BOILERPLATE, "utf8");
2114
+ fs.writeFileSync(dest, content, "utf8");
2047
2115
  pOk(`src/${relPath}`);
2048
2116
  console.log();
2049
2117
  pKv("Component", camel);
2050
- pKv("Enabled", `Components.${camel}.enabled(true)`);
2051
- pKv("Toggle", `Components.${camel}.Toggle()`);
2052
- pKv("Listen", `Components.${camel}.onEnable:connect(...)`);
2118
+ pKv("Mount", `groupbox('left', '${camel}', { mount: '${camel}' })`);
2119
+ pKv("Tip", "add to a page groupbox in src/pages/*.ts to render it");
2053
2120
  }
2054
2121
  console.log();
2055
2122
  } else if (args.type === "remote") {
@@ -2067,14 +2134,14 @@ function cmdNew(args) {
2067
2134
  pOk(`func.Remote_${remoteName}(...) ${gray("\u2192 remotes.lua")}`);
2068
2135
  console.log();
2069
2136
  } else {
2070
- pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new remote");
2137
+ pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new module --plain | rb new remote");
2071
2138
  process.exit(1);
2072
2139
  }
2073
2140
  }
2074
2141
  async function cmdRemove(args) {
2075
2142
  const root = requireProjectRoot();
2076
2143
  let relPath = args.path;
2077
- if (!relPath.endsWith(".lua") && !relPath.endsWith(".rblua")) relPath += ".lua";
2144
+ if (!relPath.endsWith(".lua") && !relPath.endsWith(".ts")) relPath += ".ts";
2078
2145
  const target = pathe.join(root, "src", relPath);
2079
2146
  if (!fs.existsSync(target)) {
2080
2147
  pFail(`src/${relPath} not found`);
@@ -2931,19 +2998,12 @@ async function cmdPublish(_args) {
2931
2998
  const uploads = [
2932
2999
  { remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
2933
3000
  ];
2934
- const pulseUiDir = pathe.join(PULSE_DIR2, "ui");
2935
3001
  for (const adapter of ["windui"]) {
2936
3002
  const p = pathe.join(ADAPTERS, `${adapter}.lua`);
2937
3003
  if (!fs.existsSync(p)) continue;
2938
- const parts = [fs.readFileSync(p, "utf8").trimEnd()];
2939
- const settings = pathe.join(pulseUiDir, `${adapter}_settings.lua`);
2940
- if (fs.existsSync(settings)) {
2941
- parts.push(`-- ${adapter}_settings`);
2942
- parts.push(fs.readFileSync(settings, "utf8").trimEnd());
2943
- }
2944
3004
  uploads.push({
2945
3005
  remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
2946
- content: Buffer.from(parts.join("\n") + "\n", "utf8")
3006
+ content: Buffer.from(fs.readFileSync(p, "utf8"), "utf8")
2947
3007
  });
2948
3008
  }
2949
3009
  pSection(`Uploading to R2 ${gray("(v" + RB_VERSION + " \xB7 " + uploads.length + " files)")}`);
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "pulse-rb",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "rb CLI — Pulse framework build tool for Roblox script projects",
5
5
  "bin": {
6
6
  "rb": "./bin/rb.js"
7
7
  },
8
8
  "main": "./dist/index.js",
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsx src/index.ts",
12
+ "test": "vitest",
13
+ "prepublishOnly": "pnpm run build"
14
+ },
9
15
  "files": [
10
16
  "dist",
11
17
  "bin",
@@ -33,6 +39,7 @@
33
39
  "zod": "^4.4.3"
34
40
  },
35
41
  "devDependencies": {
42
+ "@rb-pulse/core": "workspace:*",
36
43
  "@rbxts/types": "^1.0.924",
37
44
  "@types/fs-extra": "^11.0.4",
38
45
  "@types/luaparse": "^0.2.9",
@@ -40,8 +47,7 @@
40
47
  "tsup": "^8.3.0",
41
48
  "tsx": "^4.19.0",
42
49
  "typescript": "^5.8.3",
43
- "vitest": "^2.1.0",
44
- "@rb-pulse/core": "1.3.2"
50
+ "vitest": "^2.1.0"
45
51
  },
46
52
  "engines": {
47
53
  "node": ">=18.0.0"
@@ -53,10 +59,5 @@
53
59
  "pulse",
54
60
  "script"
55
61
  ],
56
- "license": "MIT",
57
- "scripts": {
58
- "build": "tsup",
59
- "dev": "tsx src/index.ts",
60
- "test": "vitest"
61
- }
62
- }
62
+ "license": "MIT"
63
+ }
package/pulse/runtime.lua CHANGED
@@ -364,13 +364,16 @@ end
364
364
  local function _computed(fn) return Computed(fn) end
365
365
 
366
366
  local function _widgetBase(t)
367
- function t:bind(sig)
368
- self.signal = sig
369
- self.id = sig and (sig._id or "?") or nil
370
- return self
367
+ -- TSTL compiles TypeScript `.bind(sig)` as Lua dot-call: `t.bind(sig)`.
368
+ -- Colon-style methods receive the wrong args (sig becomes self, actual sig is nil).
369
+ -- Close over t so the function works correctly regardless of call style.
370
+ t.bind = function(sig)
371
+ t.signal = sig
372
+ t.id = sig and (sig._id or "?") or nil
373
+ return t
371
374
  end
372
- function t:withTip(text) self.tip = text; return self end
373
- function t:withDefault(v) self.default = v; return self end
375
+ t.withTip = function(text) t.tip = text; return t end
376
+ t.withDefault = function(v) t.default = v; return t end
374
377
  return t
375
378
  end
376
379
 
@@ -429,8 +432,17 @@ local function _hbBind(svc, optsOrFn, fn)
429
432
  local t=type(every)=="table" and every() or every
430
433
  local now=tick(); if (now-last)<t then return end; last=now
431
434
  end
435
+ -- Throttled trace: confirms heartbeat is running when condition passes.
436
+ if Pulse and Pulse.Log then
437
+ Pulse.Log.throttle(comp._name, 5, "debug", "tick (condition passed)")
438
+ end
432
439
  local ok,err=pcall(cb,a,b)
433
- if not ok then warn("[Pulse] "..comp._name.." event error: "..tostring(err)) end
440
+ if not ok then
441
+ warn("[Pulse] "..comp._name.." event error: "..tostring(err))
442
+ if Pulse and Pulse.Log then
443
+ Pulse.Log.error(comp._name, "tick error: "..tostring(err))
444
+ end
445
+ end
434
446
  end))
435
447
  end
436
448
 
@@ -443,7 +455,15 @@ _on.inputEnded = function(fn) local c=_needComp("inputEnded"); c
443
455
  _on.characterAdded = function(fn) local c=_needComp("characterAdded"); c:bind("ca_".._uid(), _LocalPlayer.CharacterAdded:Connect(fn)) end
444
456
  _on.characterRemoving = function(fn) local c=_needComp("characterRemoving"); c:bind("cr_".._uid(), _LocalPlayer.CharacterRemoving:Connect(fn)) end
445
457
  _on.respawn = function(fn) _needComp("respawn"):onRespawn(fn) end
446
- _on.signal = function(sig,fn) _needComp("signal"):watch(sig,fn) end
458
+ _on.signal = function(sig,fn)
459
+ local comp = _needComp("signal")
460
+ comp:watch(sig, function(v)
461
+ if Pulse and Pulse.Log then
462
+ Pulse.Log.debug(comp._name, "signal "..tostring(sig._id or "?").." → "..tostring(v))
463
+ end
464
+ fn(v)
465
+ end)
466
+ end
447
467
  _on.after = function(s,fn) _needComp("after"):task(s,fn) end
448
468
 
449
469
  local function _defineComponent(name, setup)
@@ -0,0 +1,11 @@
1
+ defineComponent('{Camel}', (): WidgetDef[] => {
2
+ const enabled = signal<boolean>(false)
3
+
4
+ on.heartbeat({ when: enabled }, (): void => {
5
+ // add your per-frame logic here
6
+ })
7
+
8
+ return [
9
+ toggle('{Camel}').bind(enabled),
10
+ ]
11
+ })
@@ -1,34 +1,26 @@
1
1
  -- ── Script Config ─────────────────────────────────────────────────────────────
2
- -- Edit these values. SCRIPT_NAME is used as the UI window title and notify_title.
2
+ -- Edit these values. SCRIPT_NAME is used as the UI window title.
3
3
 
4
4
  local SCRIPT_NAME = "{NAME}"
5
5
  local SCRIPT_VERSION = "1.0.0"
6
6
  local SCRIPT_AUTHOR = "you"
7
- local SCRIPT_DISCORD = "" -- optional: discord invite link (shown in UI if set)
7
+ local SCRIPT_DISCORD = "" -- optional: discord invite link
8
8
 
9
9
  -- ── Shared helpers ─────────────────────────────────────────────────────────────
10
- -- Add your game-specific helpers here. Generic helpers (Pulse.*) are injected
11
- -- automatically by the compiler no need to define them here.
10
+ -- Add game-specific shared helpers here. Pulse runtime helpers (_PulseGet*)
11
+ -- and all TypeScript component globals are injected automatically.
12
12
 
13
13
  local func = {}
14
14
 
15
15
  -- ── Players ────────────────────────────────────────────────────────────────────
16
- -- Event-driven cache: updates instantly when players join or leave.
16
+ -- Event-driven player cache stays up to date as players join/leave.
17
17
  local _playerSet = {}
18
18
  local _PS = game:GetService("Players")
19
19
  local _LP = _PS.LocalPlayer
20
20
 
21
21
  for _, p in ipairs(_PS:GetPlayers()) do _playerSet[p] = true end
22
- _PS.PlayerAdded:Connect(function(p)
23
- _playerSet[p] = true
24
- local n = 0; for _ in pairs(_playerSet) do n = n + 1 end
25
- Pulse.Monitor.set("players", n)
26
- end)
27
- _PS.PlayerRemoving:Connect(function(p)
28
- _playerSet[p] = nil
29
- local n = 0; for _ in pairs(_playerSet) do n = n + 1 end
30
- Pulse.Monitor.set("players", n)
31
- end)
22
+ _PS.PlayerAdded:Connect(function(p) _playerSet[p] = true end)
23
+ _PS.PlayerRemoving:Connect(function(p) _playerSet[p] = nil end)
32
24
 
33
25
  func.GetCachedPlayers = function()
34
26
  local t = {}
@@ -36,24 +28,16 @@ func.GetCachedPlayers = function()
36
28
  return t
37
29
  end
38
30
 
39
- -- ── Team monitor ──────────────────────────────────────────────────────────────
40
- -- Tracks the local player's team in the dev overlay.
41
- Pulse.Monitor.set("team", _LP.Team and _LP.Team.Name or "none")
42
- _LP:GetPropertyChangedSignal("Team"):Connect(function()
43
- Pulse.Monitor.set("team", _LP.Team and _LP.Team.Name or "none")
44
- end)
45
-
46
31
  -- ── Entity folder watcher ──────────────────────────────────────────────────────
47
- -- Use _watchFolder to keep a set live as entities spawn/despawn.
48
- -- Add your game-specific entity caches below.
32
+ -- Use _watchFolder to keep a set live as instances spawn/despawn.
33
+ -- Uncomment and adapt to your game's folder structure.
49
34
  local function _watchFolder(folder, set)
50
35
  for _, child in ipairs(folder:GetChildren()) do set[child] = true end
51
36
  folder.ChildAdded:Connect(function(c) set[c] = true end)
52
37
  folder.ChildRemoved:Connect(function(c) set[c] = nil end)
53
38
  end
54
39
 
55
- -- Example — uncomment and adapt to your game's entity folder:
56
- --
40
+ -- Example:
57
41
  -- local _enemySet = {}
58
42
  -- task.spawn(function()
59
43
  -- local folder = workspace:WaitForChild("Enemies", 30)
@@ -1,8 +1,5 @@
1
- // Home page tab — Next.js-style file-based routing.
2
- // Filename prefix (1_) determines tab order.
3
-
4
1
  definePage('Home', { icon: 'house' }, () => [
5
- groupbox('left', 'Player', { icon: 'person', mount: 'SpeedHack' }),
6
- groupbox('left', 'Player', { icon: 'person', mount: 'FOVChanger' }),
7
- groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
2
+ groupbox('left', 'Player', { icon: 'person', mount: 'SpeedHack' }),
3
+ groupbox('left', 'Player', { icon: 'person', mount: 'FOVChanger' }),
4
+ groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
8
5
  ])