pulse-rb 1.4.2 → 1.4.4

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.
@@ -25,6 +25,14 @@ local _BundleComponents = Components or {}
25
25
  local _BundlePages = _Pages or {}
26
26
  local _BundlePulse = Pulse or {}
27
27
 
28
+ -- Auto-enable dev logging when layout.dev = true.
29
+ -- Runs synchronously (before any task.spawn) so all UI setup logs are captured.
30
+ if _PULSE_LAYOUT.dev and _BundlePulse and _BundlePulse.Log then
31
+ _BundlePulse.Log.enable()
32
+ _BundlePulse.Log.configure({ level = "debug", console = true })
33
+ _BundlePulse.Log.info("dev", "dev build active")
34
+ end
35
+
28
36
  local _WINDUI_URL = "https://github.com/Footagesus/WindUI/releases/download/1.6.64-fix/main.lua"
29
37
  local _PULSE_LOGO = "https://pulse-rb.vercel.app/img/logo.svg"
30
38
 
@@ -51,6 +59,7 @@ local _toggleKeyName = "RightControl"
51
59
  local _windowVisible = true
52
60
 
53
61
  local _isMobile = _UIS.TouchEnabled and not _UIS.KeyboardEnabled
62
+ local _PULSE_PREMIUM = false -- set true during key validation if key grants premium
54
63
 
55
64
  -- ── Adapter ───────────────────────────────────────────────────────────────────
56
65
 
@@ -132,7 +141,7 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
132
141
  local openBtnMobileOnly = opts.open_btn_mobile_only ~= false
133
142
  local openBtnIcon = (opts.open_btn_icon and opts.open_btn_icon ~= "") and opts.open_btn_icon or nil
134
143
 
135
- _windWindow = WindUI:CreateWindow({
144
+ local _winCfg = {
136
145
  Title = title,
137
146
  Icon = icon,
138
147
  Author = author,
@@ -143,7 +152,6 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
143
152
  Transparent = acrylic or (transparency > 0),
144
153
  BackgroundImageTransparency = acrylic and 0 or transparency,
145
154
  Acrylic = acrylic,
146
-
147
155
  OpenButton = {
148
156
  Enabled = true,
149
157
  Draggable = true,
@@ -153,7 +161,62 @@ function _UIAdapter:CreateWindow(title, w, h, opts)
153
161
  CornerRadius = UDim.new(1, 0),
154
162
  StrokeThickness = 3,
155
163
  },
156
- })
164
+ }
165
+
166
+ -- Key system — configured via layout.keySystem
167
+ local _ks = _PULSE_LAYOUT.keySystem
168
+ if _ks then
169
+ local function _checkPremium(key)
170
+ local _pr = _PULSE_LAYOUT.premium
171
+ if not _pr then return end
172
+ if _pr.keys then
173
+ for _, k in ipairs(_pr.keys) do
174
+ if k == key then _PULSE_PREMIUM = true; return end
175
+ end
176
+ end
177
+ if _pr.validatorUrl and _pr.validatorUrl ~= "" then
178
+ local ok, resp = pcall(function() return game:HttpGet(_pr.validatorUrl .. key) end)
179
+ if ok and resp and resp:lower():match("^true") then _PULSE_PREMIUM = true end
180
+ end
181
+ end
182
+ local _ksCfg = {
183
+ Title = _ks.title or "Key Required",
184
+ Note = (_ks.note and _ks.note ~= "") and _ks.note or nil,
185
+ SaveKey = _ks.saveKey == true,
186
+ URL = (_ks.getKeyUrl and _ks.getKeyUrl ~= "") and _ks.getKeyUrl or nil,
187
+ KeyValidator = function(key)
188
+ local passed = false
189
+ -- Check against static keys list
190
+ if _ks.keys then
191
+ for _, k in ipairs(_ks.keys) do
192
+ if k == key then passed = true; break end
193
+ end
194
+ end
195
+ -- HTTP validation (server-side) when no static list or key not found
196
+ if not passed and _ks.validatorUrl and _ks.validatorUrl ~= "" then
197
+ local ok, resp = pcall(function() return game:HttpGet(_ks.validatorUrl .. key) end)
198
+ if ok and resp and resp:lower():match("^true") then passed = true end
199
+ end
200
+ -- No validation configured → any non-empty key passes (dev mode)
201
+ if not passed and (not _ks.keys or #_ks.keys == 0)
202
+ and (not _ks.validatorUrl or _ks.validatorUrl == "") then
203
+ passed = key ~= nil and key ~= ""
204
+ end
205
+ _checkPremium(key)
206
+ return passed or _PULSE_PREMIUM -- premium key also grants basic access
207
+ end,
208
+ }
209
+ if _ks.thumbnail then
210
+ _ksCfg.Thumbnail = {
211
+ Image = _ks.thumbnail.image or "",
212
+ Title = _ks.thumbnail.title or "",
213
+ Width = _ks.thumbnail.width or 100,
214
+ }
215
+ end
216
+ _winCfg.KeySystem = _ksCfg
217
+ end
218
+
219
+ _windWindow = WindUI:CreateWindow(_winCfg)
157
220
 
158
221
  pcall(function()
159
222
  _windConfig = _windWindow.ConfigManager:Config(_folder)
@@ -544,6 +607,28 @@ task.spawn(function()
544
607
 
545
608
  local L = _PULSE_LAYOUT
546
609
 
610
+ -- Premium dynamic unlock system
611
+ -- Each entry is a function that destroys the locked UI and mounts the real component.
612
+ local _premiumPendingUnlocks = {}
613
+ local _premiumStatusPara = nil
614
+
615
+ local function _activatePremium()
616
+ _PULSE_PREMIUM = true
617
+ -- Update settings status paragraph if it exists
618
+ pcall(function()
619
+ if _premiumStatusPara then
620
+ _premiumStatusPara:SetTitle("Premium Active")
621
+ _premiumStatusPara:SetDesc("All premium features are now unlocked.")
622
+ end
623
+ end)
624
+ -- Swap locked UI → real components for every registered premium groupbox
625
+ for _, fn in ipairs(_premiumPendingUnlocks) do
626
+ task.spawn(function() pcall(fn) end)
627
+ end
628
+ _premiumPendingUnlocks = {}
629
+ pcall(function() _PulseNotify("Premium unlocked!", 3) end)
630
+ end
631
+
547
632
  -- Resolve toggle key
548
633
  local _tkName = (L.toggleKey and L.toggleKey ~= "") and L.toggleKey or "RightControl"
549
634
  local _tk = pcall(function() return Enum.KeyCode[_tkName] end) and Enum.KeyCode[_tkName]
@@ -594,13 +679,12 @@ task.spawn(function()
594
679
  local _execName = "Unknown"
595
680
  local _execVer = ""
596
681
  pcall(function()
597
- if type(rawget(_G, "identifyexecutor")) == "function" then
598
- local ok, n, v = pcall(identifyexecutor)
599
- if ok then _execName = tostring(n or "Unknown"); _execVer = tostring(v or "") end
600
- end
601
- if _execName == "Unknown" and type(rawget(_G, "getexecutorname")) == "function" then
602
- local ok, n = pcall(getexecutorname)
603
- if ok and n then _execName = tostring(n) end
682
+ -- pcall each call directly — avoids rawget missing __index-proxied executor globals
683
+ local ok, n, v = pcall(function() return identifyexecutor() end)
684
+ if ok and n then _execName = tostring(n); _execVer = tostring(v or "") end
685
+ if _execName == "Unknown" then
686
+ local ok2, n2 = pcall(function() return getexecutorname() end)
687
+ if ok2 and n2 then _execName = tostring(n2) end
604
688
  end
605
689
  if _execName == "Unknown" then
606
690
  if rawget(_G, "syn") or rawget(_G, "Synapse") then _execName = "Synapse X"
@@ -771,7 +855,107 @@ task.spawn(function()
771
855
  end
772
856
  if gb.mount then
773
857
  local comp = _BundleComponents[gb.mount]
774
- if comp then
858
+ -- Premium gate: show inline key-entry UI; dynamically replaced on unlock
859
+ if gb.premium and not _PULSE_PREMIUM then
860
+ local _mountComp = comp
861
+ local _mountCont = container
862
+ local _destroyFns = {}
863
+
864
+ pcall(function()
865
+ local _keyValue = ""
866
+ local _statusPara
867
+
868
+ local function _setStatus(msg)
869
+ pcall(function() if _statusPara then _statusPara:SetDesc(msg) end end)
870
+ end
871
+
872
+ -- Header
873
+ local _header = _mountCont._container:Paragraph({
874
+ Title = "Premium Feature",
875
+ Desc = "Enter your key below to unlock this feature.",
876
+ })
877
+ table.insert(_destroyFns, function() pcall(function() _header:Destroy() end) end)
878
+
879
+ -- Key input (wrapped in pcall — Input may not exist on all Section types)
880
+ local _inputElem
881
+ pcall(function()
882
+ _inputElem = _mountCont._container:Input({
883
+ Title = "Premium Key",
884
+ Placeholder = "Paste your key here…",
885
+ Flag = (gb.mount or "prem") .. "_premKey",
886
+ Callback = function(v) _keyValue = v end,
887
+ })
888
+ table.insert(_destroyFns, function() pcall(function() _inputElem:Destroy() end) end)
889
+ end)
890
+
891
+ -- Validate + unlock
892
+ local function _doCheck()
893
+ local key = (_keyValue or ""):match("^%s*(.-)%s*$")
894
+ if key == "" then _setStatus("Enter a key first."); return end
895
+ local valid = false
896
+ local _pr = _PULSE_LAYOUT.premium
897
+ if _pr then
898
+ if _pr.keys then
899
+ for _, k in ipairs(_pr.keys) do
900
+ if k == key then valid = true; break end
901
+ end
902
+ end
903
+ if not valid and _pr.validatorUrl and _pr.validatorUrl ~= "" then
904
+ _setStatus("Checking…")
905
+ local ok, resp = pcall(function()
906
+ return game:HttpGet(_pr.validatorUrl .. key)
907
+ end)
908
+ if ok and resp and resp:lower():match("^true") then valid = true end
909
+ end
910
+ end
911
+ if valid then
912
+ _setStatus("Valid! Unlocking…")
913
+ task.delay(0.4, _activatePremium)
914
+ else
915
+ _setStatus("Invalid key.")
916
+ end
917
+ end
918
+
919
+ -- "Copy Link" button
920
+ local _pKeyUrl = (_PULSE_LAYOUT.premium and _PULSE_LAYOUT.premium.getKeyUrl)
921
+ or (_PULSE_LAYOUT.keySystem and _PULSE_LAYOUT.keySystem.getKeyUrl) or ""
922
+ local _copyBtn = _mountCont._container:Button({
923
+ Title = "Copy Link",
924
+ Desc = "Get your premium key",
925
+ Callback = function()
926
+ if _pKeyUrl == "" then _setStatus("No link configured."); return end
927
+ local ok = false
928
+ pcall(function() setclipboard(_pKeyUrl); ok = true end)
929
+ pcall(function() if not ok then toclipboard(_pKeyUrl); ok = true end end)
930
+ _setStatus(ok and "Link copied!" or "No clipboard API.")
931
+ end,
932
+ })
933
+ table.insert(_destroyFns, function() pcall(function() _copyBtn:Destroy() end) end)
934
+
935
+ -- "Check Key" button
936
+ local _checkBtn = _mountCont._container:Button({
937
+ Title = "Check Key",
938
+ Callback = _doCheck,
939
+ })
940
+ table.insert(_destroyFns, function() pcall(function() _checkBtn:Destroy() end) end)
941
+
942
+ -- Status line
943
+ _statusPara = _mountCont._container:Paragraph({
944
+ Title = "Status",
945
+ Desc = "—",
946
+ })
947
+ table.insert(_destroyFns, function() pcall(function() _statusPara:Destroy() end) end)
948
+ end)
949
+
950
+ -- Register: when premium activates, destroy locked UI and mount real component
951
+ table.insert(_premiumPendingUnlocks, function()
952
+ for _, fn in ipairs(_destroyFns) do fn() end
953
+ task.wait(0.05)
954
+ if _mountComp then
955
+ pcall(function() _UIAdapter:mount(_mountComp, _mountCont) end)
956
+ end
957
+ end)
958
+ elseif comp then
775
959
  local ok, err = pcall(function() _UIAdapter:mount(comp, container) end)
776
960
  if not ok then
777
961
  warn("[Pulse] mount '" .. gb.mount .. "' error: " .. tostring(err))
@@ -845,6 +1029,43 @@ task.spawn(function()
845
1029
  end,
846
1030
  })
847
1031
 
1032
+ -- Premium status groupbox — shown only when premium tier is configured
1033
+ if _PULSE_LAYOUT.premium then
1034
+ local gb_prem = _settingsTab:AddRightGroupbox("Premium")
1035
+ if _PULSE_PREMIUM then
1036
+ pcall(function()
1037
+ gb_prem._container:Paragraph({
1038
+ Title = "Premium Active",
1039
+ Desc = "All premium features are unlocked.",
1040
+ })
1041
+ end)
1042
+ else
1043
+ pcall(function()
1044
+ -- Store reference so _activatePremium can update it live
1045
+ _premiumStatusPara = gb_prem._container:Paragraph({
1046
+ Title = "Premium Locked",
1047
+ Desc = "Enter a key in any locked feature to unlock.",
1048
+ })
1049
+ end)
1050
+ local _pKeyUrl = (_PULSE_LAYOUT.premium and _PULSE_LAYOUT.premium.getKeyUrl)
1051
+ or (_PULSE_LAYOUT.keySystem and _PULSE_LAYOUT.keySystem.getKeyUrl)
1052
+ if _pKeyUrl and _pKeyUrl ~= "" then
1053
+ pcall(function()
1054
+ gb_prem._container:Button({
1055
+ Title = "Get Premium Key",
1056
+ Desc = "Copy the link to your key page",
1057
+ Callback = function()
1058
+ local ok = false
1059
+ pcall(function() setclipboard(_pKeyUrl); ok = true end)
1060
+ pcall(function() if not ok then toclipboard(_pKeyUrl); ok = true end end)
1061
+ _PulseNotify(ok and "Link copied!" or _pKeyUrl, 4)
1062
+ end,
1063
+ })
1064
+ end)
1065
+ end
1066
+ end
1067
+ end
1068
+
848
1069
  -- Menu settings groupbox
849
1070
  local gb_menu = _settingsTab:AddRightGroupbox("Menu Settings")
850
1071
  _UIAdapter:addButton(gb_menu, {
@@ -1056,12 +1277,6 @@ task.spawn(function()
1056
1277
  })
1057
1278
  end)
1058
1279
 
1059
- -- Auto-scan on inject
1060
- task.spawn(function()
1061
- if not game:IsLoaded() then game.Loaded:Wait() end
1062
- task.wait(2)
1063
- _doScan()
1064
- end)
1065
1280
  end
1066
1281
 
1067
1282
  -- Info section — framework version + signal counts for debugging
@@ -1091,47 +1306,6 @@ task.spawn(function()
1091
1306
  end
1092
1307
  end
1093
1308
 
1094
- -- Heartbeat Monitor section — tick counts per component (left)
1095
- -- Uses Pulse.Monitor which is independent of the log system.
1096
- local _monSect
1097
- pcall(function()
1098
- _monSect = _devLeft:Section({ Title="Heartbeat Monitor", Icon="activity", Box=true, BoxBorder=true, Opened=true })
1099
- end)
1100
- if _monSect then
1101
- local _monPara
1102
- pcall(function()
1103
- _monPara = _monSect:Paragraph({
1104
- Title = "raw / cond (updates 2s)",
1105
- Desc = "raw = every HB fire · cond = when() passed",
1106
- })
1107
- end)
1108
- -- Reads _rawHb/_condHb plain-Lua counters set in runtime._hbBind.
1109
- -- No dependency on Pulse.Monitor — works regardless of log system state.
1110
- task.spawn(function()
1111
- while _windWindow do
1112
- task.wait(2)
1113
- pcall(function()
1114
- local lines = {}
1115
- for cname, comp in pairs(_BundleComponents) do
1116
- if type(comp) == "table" and comp._rawHb ~= nil then
1117
- lines[#lines+1] = cname
1118
- .. " raw=" .. tostring(comp._rawHb)
1119
- .. " cond=" .. tostring(comp._condHb)
1120
- end
1121
- end
1122
- if _monPara then
1123
- if #lines == 0 then
1124
- _monPara:SetDesc("No heartbeat components registered")
1125
- else
1126
- table.sort(lines)
1127
- _monPara:SetDesc(table.concat(lines, "\n"))
1128
- end
1129
- end
1130
- end)
1131
- end
1132
- end)
1133
- end
1134
-
1135
1309
  -- Console section — controls for the Roblox developer console (F9).
1136
1310
  -- "Clear" wipes the output so only your next prints are visible.
1137
1311
  -- "Print Logs" re-dumps all buffered Pulse.Log entries so you can read
@@ -1177,6 +1351,17 @@ task.spawn(function()
1177
1351
  print("─────────────────────────────────────────")
1178
1352
  end
1179
1353
 
1354
+ pcall(function()
1355
+ _consoleSect:Button({
1356
+ Title = "Open Console",
1357
+ Desc = "Show the Roblox developer console (F9)",
1358
+ Callback = function()
1359
+ pcall(function()
1360
+ game:GetService("StarterGui"):SetCore("DevConsoleVisible", true)
1361
+ end)
1362
+ end,
1363
+ })
1364
+ end)
1180
1365
  pcall(function()
1181
1366
  _consoleSect:Button({
1182
1367
  Title = "Clear Console",
@@ -1212,6 +1397,30 @@ task.spawn(function()
1212
1397
  end
1213
1398
  end
1214
1399
 
1400
+ -- Apply defaults and notify ready — runs after all widgets are created so
1401
+ -- Toggles/Options are populated and SetValue calls land correctly.
1402
+ task.spawn(function()
1403
+ task.wait(0.5)
1404
+ for _, d in ipairs(_PULSE_DEFAULTS) do
1405
+ if d.set then pcall(d.set, d.value) end
1406
+ if d.type == "toggle" then
1407
+ pcall(function()
1408
+ local t = Toggles and Toggles[d.id]
1409
+ if t then t:SetValue(d.value) end
1410
+ end)
1411
+ else
1412
+ pcall(function()
1413
+ local t = Options and Options[d.id]
1414
+ if t then t:SetValue(d.value) end
1415
+ end)
1416
+ end
1417
+ task.wait(0.05)
1418
+ end
1419
+ if #_PULSE_DEFAULTS > 0 and _PulseNotify then
1420
+ _PulseNotify((L.title or "Hub") .. " loaded!", 5)
1421
+ end
1422
+ end)
1423
+
1215
1424
  -- Re-select first tab (Home) — Settings is last so Wind UI leaves it active
1216
1425
  task.defer(function()
1217
1426
  if _UIAdapter._firstTab then
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.4.2";
59
+ RB_VERSION = "1.4.4";
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");
@@ -1110,7 +1110,7 @@ function rglob(dir, exts) {
1110
1110
  walk(dir);
1111
1111
  return results;
1112
1112
  }
1113
- var PULSE_DIR, PULSE_DEV_DIR, PULSE_UI_DIR, REEXEC_GUARD, DESTROY_REGISTRATION, DEFAULTS_RUNNER, Compiler;
1113
+ var PULSE_DIR, PULSE_DEV_DIR, PULSE_UI_DIR, REEXEC_GUARD, DESTROY_REGISTRATION, Compiler;
1114
1114
  var init_compiler = __esm({
1115
1115
  "src/compiler.ts"() {
1116
1116
  init_cjs_shims();
@@ -1130,43 +1130,6 @@ if _G.__AOT_R_DESTROY then pcall(_G.__AOT_R_DESTROY) end
1130
1130
  `;
1131
1131
  DESTROY_REGISTRATION = `-- Register this instance so the next re-execution can clean it up.
1132
1132
  _G.__AOT_R_DESTROY = _PulseDestroy
1133
- `;
1134
- DEFAULTS_RUNNER = `task.spawn(function()
1135
- task.wait(0.5)
1136
- for _, d in ipairs(_PULSE_DEFAULTS) do
1137
- local effectiveValue = d.value
1138
- if d.type == "toggle" and Pulse.TestMode and Pulse.TestMode.isActive() then
1139
- if not Pulse.TestMode.isTarget(d.id) then
1140
- effectiveValue = false
1141
- end
1142
- end
1143
- if d.set then pcall(d.set, effectiveValue) end
1144
- if d.type == "toggle" then
1145
- pcall(function()
1146
- local t = Toggles and Toggles[d.id]
1147
- if t then t:SetValue(effectiveValue) end
1148
- end)
1149
- else
1150
- pcall(function()
1151
- local t = Options and Options[d.id]
1152
- if t then t:SetValue(d.value) end
1153
- end)
1154
- end
1155
- task.wait(0.05)
1156
- end
1157
- if #_PULSE_DEFAULTS > 0 then
1158
- local _notifyMsg = "AOT:Resistance Hub ready!"
1159
- if Pulse.TestMode and Pulse.TestMode.isActive() then
1160
- local _tt = Pulse.TestMode.getTargets()
1161
- if #_tt > 0 then
1162
- _notifyMsg = "Test mode: " .. table.concat(_tt, ", ")
1163
- end
1164
- end
1165
- if _PulseNotify then
1166
- _PulseNotify(_notifyMsg, 5)
1167
- end
1168
- end
1169
- end)
1170
1133
  `;
1171
1134
  Compiler = class {
1172
1135
  root;
@@ -1193,6 +1156,52 @@ end)
1193
1156
  }
1194
1157
  return "windui";
1195
1158
  }
1159
+ // Extract content between matching braces for a named TS field.
1160
+ // Returns null if field is absent or commented out.
1161
+ extractLayoutBlock(src, field) {
1162
+ const rx = new RegExp(`\\b${field}\\s*:\\s*\\{`);
1163
+ const m = src.match(rx);
1164
+ if (!m || m.index === void 0) return null;
1165
+ const before = src.slice(0, m.index);
1166
+ const lineStart = before.lastIndexOf("\n") + 1;
1167
+ if (before.slice(lineStart).trimStart().startsWith("//")) return null;
1168
+ let depth = 0, i = m.index + m[0].length - 1;
1169
+ const start = i + 1;
1170
+ for (; i < src.length; i++) {
1171
+ if (src[i] === "{") depth++;
1172
+ else if (src[i] === "}") {
1173
+ depth--;
1174
+ if (depth === 0) break;
1175
+ }
1176
+ }
1177
+ return src.slice(start, i);
1178
+ }
1179
+ // Convert an extracted TS block body to a Lua table literal string.
1180
+ blockToLuaTable(block) {
1181
+ const str = (k) => block.match(new RegExp(`\\b${k}\\s*:\\s*['"]([^'"]+)['"]`))?.at(1) ?? null;
1182
+ const bool = (k) => {
1183
+ const m = block.match(new RegExp(`\\b${k}\\s*:\\s*(true|false)\\b`));
1184
+ return m ? m[1] === "true" : null;
1185
+ };
1186
+ const arr = (k) => {
1187
+ const m = block.match(new RegExp(`\\b${k}\\s*:\\s*\\[([^\\]]*)\\]`));
1188
+ return m ? [...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]) : null;
1189
+ };
1190
+ const parts = [];
1191
+ const title = str("title");
1192
+ if (title !== null) parts.push(`title=${JSON.stringify(title)}`);
1193
+ const note = str("note");
1194
+ if (note !== null) parts.push(`note=${JSON.stringify(note)}`);
1195
+ const save = bool("saveKey");
1196
+ if (save !== null) parts.push(`saveKey=${save}`);
1197
+ const gkUrl = str("getKeyUrl");
1198
+ if (gkUrl !== null) parts.push(`getKeyUrl=${JSON.stringify(gkUrl)}`);
1199
+ const valUrl = str("validatorUrl");
1200
+ if (valUrl !== null) parts.push(`validatorUrl=${JSON.stringify(valUrl)}`);
1201
+ const keys = arr("keys");
1202
+ if (keys && keys.length) parts.push(`keys={${keys.map((k) => JSON.stringify(k)).join(",")}}`);
1203
+ return parts.length ? `{${parts.join(",")}}` : null;
1204
+ }
1196
1205
  readLayoutConfig() {
1197
1206
  const layoutTs = path.join(this.srcDir, "layout.ts");
1198
1207
  const cfg = {
@@ -1361,16 +1370,21 @@ end)
1361
1370
  if (lc["discord"]) parts.push(`,discord=${JSON.stringify(lc["discord"])}`);
1362
1371
  if (lc["version"]) parts.push(`,version=${JSON.stringify(lc["version"])}`);
1363
1372
  if (opts.dev) parts.push(`,dev=true`);
1373
+ const layoutSrc = fs.existsSync(path.join(this.srcDir, "layout.ts")) ? fs.readFileSync(path.join(this.srcDir, "layout.ts"), "utf8") : "";
1374
+ const ksBlock = this.extractLayoutBlock(layoutSrc, "keySystem");
1375
+ const prBlock = this.extractLayoutBlock(layoutSrc, "premium");
1376
+ if (ksBlock) {
1377
+ const lua = this.blockToLuaTable(ksBlock);
1378
+ if (lua) parts.push(`,keySystem=${lua}`);
1379
+ }
1380
+ if (prBlock) {
1381
+ const lua = this.blockToLuaTable(prBlock);
1382
+ if (lua) parts.push(`,premium=${lua}`);
1383
+ }
1364
1384
  parts.push(`}
1365
1385
  `);
1366
1386
  parts.push(`local _P=loadstring(game:HttpGet("${base}/pulse.lua"))(_PULSE_LAYOUT)
1367
1387
  `);
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
- }
1374
1388
  parts.push(`for _k,_v in pairs(_P) do _G[_k]=_v end
1375
1389
  `);
1376
1390
  parts.push(
@@ -1463,16 +1477,6 @@ end
1463
1477
  }
1464
1478
  parts.push("\n");
1465
1479
  }
1466
- if (order.some((f) => f.endsWith(".rblua") || f.endsWith(".ts"))) {
1467
- parts.push("-- [generated: defaults runner]\n");
1468
- parts.push(DEFAULTS_RUNNER);
1469
- parts.push("\n");
1470
- }
1471
- if (opts.dev) {
1472
- parts.push(
1473
- '-- [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'
1474
- );
1475
- }
1476
1480
  parts.push("-- [generated: destroy registration]\n");
1477
1481
  parts.push(DESTROY_REGISTRATION);
1478
1482
  parts.push("\n");
@@ -1612,7 +1616,12 @@ async function obfuscateSource(source, ib2Dir) {
1612
1616
  if (stdout.startsWith("ERR:")) throw new Error(stdout.slice(4).trim());
1613
1617
  if (result.status !== 0) throw new Error(stderr || stdout || `exit ${result.status}`);
1614
1618
  if (!fs.existsSync(outPath)) throw new Error("IronBrew2 CLI produced no output file");
1615
- return fs.readFileSync(outPath, "utf8");
1619
+ let out = fs.readFileSync(outPath, "utf8");
1620
+ if (out.startsWith("--[[")) {
1621
+ const closeIdx = out.indexOf("]]");
1622
+ if (closeIdx !== -1) out = out.slice(closeIdx + 2).replace(/^\r?\n/, "");
1623
+ }
1624
+ return out;
1616
1625
  } finally {
1617
1626
  try {
1618
1627
  __require("fs").unlinkSync(tmpIn);
@@ -1883,7 +1892,7 @@ function makeClaudeMd(name) {
1883
1892
  function makeAgentsMd(name) {
1884
1893
  return read("AGENTS.md").replace(/{NAME}/g, name);
1885
1894
  }
1886
- 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;
1895
+ var DIR, TEMPLATE_GLOBALS, TEMPLATE_REMOTES, MODULE_BOILERPLATE, REMOTE_BOILERPLATE, TEMPLATE_GITIGNORE, TEMPLATE_DEPLOY_EXAMPLE, TEMPLATE_LAYOUT_TS, TEMPLATE_PAGE_COMBAT_TS, TEMPLATE_COMPONENT_TS, TEMPLATE_EX_SPEED_TS, TEMPLATE_EX_FOV_TS, TEMPLATE_EX_ESP_TS, TEMPLATE_EX_AIMBOT_TS;
1887
1896
  var init_templates = __esm({
1888
1897
  "src/templates.ts"() {
1889
1898
  init_cjs_shims();
@@ -1895,11 +1904,13 @@ var init_templates = __esm({
1895
1904
  TEMPLATE_GITIGNORE = read("gitignore");
1896
1905
  TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
1897
1906
  TEMPLATE_LAYOUT_TS = read("layout.ts");
1898
- TEMPLATE_PAGE_HOME_TS = read("page_home.ts");
1907
+ read("page_home.ts");
1908
+ TEMPLATE_PAGE_COMBAT_TS = read("page_combat.ts");
1899
1909
  TEMPLATE_COMPONENT_TS = read("component.ts");
1900
1910
  TEMPLATE_EX_SPEED_TS = read("component_speed.ts");
1901
1911
  TEMPLATE_EX_FOV_TS = read("component_fov.ts");
1902
1912
  TEMPLATE_EX_ESP_TS = read("component_esp.ts");
1913
+ TEMPLATE_EX_AIMBOT_TS = read("component_aimbot.ts");
1903
1914
  }
1904
1915
  });
1905
1916
 
@@ -1988,9 +1999,10 @@ async function cmdInit(args) {
1988
1999
  ["src/misc/globals.lua", TEMPLATE_GLOBALS.replace(/{NAME}/g, name)],
1989
2000
  ["src/misc/remotes.lua", TEMPLATE_REMOTES],
1990
2001
  ["src/layout.ts", TEMPLATE_LAYOUT_TS.replace(/{NAME}/g, name)],
1991
- ["src/pages/1_Home.ts", TEMPLATE_PAGE_HOME_TS],
2002
+ ["src/pages/1_Combat.ts", TEMPLATE_PAGE_COMBAT_TS],
1992
2003
  ["src/combat/SpeedHack.ts", TEMPLATE_EX_SPEED_TS],
1993
2004
  ["src/combat/FOVChanger.ts", TEMPLATE_EX_FOV_TS],
2005
+ ["src/combat/Aimbot.ts", TEMPLATE_EX_AIMBOT_TS],
1994
2006
  ["src/visuals/PlayerESP.ts", TEMPLATE_EX_ESP_TS],
1995
2007
  ["tsconfig.json", TSCONFIG],
1996
2008
  [".rb-deploy.example", TEMPLATE_DEPLOY_EXAMPLE],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-rb",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "rb CLI — Pulse framework build tool for Roblox script projects",
5
5
  "bin": {
6
6
  "rb": "./bin/rb.js"
package/pulse/runtime.lua CHANGED
@@ -343,10 +343,11 @@ local function _signal(default)
343
343
  _sigCounter = _sigCounter + 1
344
344
  local s = Signal(default)
345
345
  s._id = ((_currentComponent and _currentComponent._name) or "g") .. "_s" .. _sigCounter
346
- s.set = function(_, v) s:set(v) end
347
- s.toggle = function(_) s:set(not s:get()) end
346
+ local _rawSet = s.set -- capture original before any override to avoid infinite recursion
347
+ s.set = function(_, v) _rawSet(s, v) end
348
+ s.toggle = function(_) _rawSet(s, not s:get()) end
348
349
  s.watch = function(_, fn) return s:onChange(fn) end
349
- s.update = function(_, fn) s:set(fn(s:get())) end
350
+ s.update = function(_, fn) _rawSet(s, fn(s:get())) end
350
351
  return s
351
352
  end
352
353
 
@@ -543,6 +544,7 @@ local function _groupbox(side, title, opts)
543
544
  icon=(opts and opts.icon) or "",
544
545
  mount=(opts and opts.mount) or nil,
545
546
  plain=(opts and opts.plain) or false,
547
+ premium=(opts and opts.premium) or false,
546
548
  widgets=(opts and opts.widgets) or {} }
547
549
  end
548
550
 
@@ -0,0 +1,18 @@
1
+ // Example premium component — mark the groupbox as { premium: true } in your page.
2
+ // Users see an inline key-entry form until they unlock with a valid premium key.
3
+ defineComponent('Aimbot', (): WidgetDef[] => {
4
+ const enabled = signal<boolean>(false)
5
+ const fovRadius = signal<number>(90)
6
+ const smoothing = signal<number>(0.15)
7
+
8
+ on.heartbeat({ when: enabled, every: 0.016 }, (): void => {
9
+ // Aimbot logic — find nearest enemy and aim toward them.
10
+ // Use Pulse.World helpers and _PulseGetHRP() for the local root part.
11
+ })
12
+
13
+ return [
14
+ toggle('Aimbot').bind(enabled),
15
+ slider('FOV Radius', { min: 10, max: 360, suffix: '°' }).bind(fovRadius),
16
+ slider('Smoothing', { min: 0, max: 1 }).bind(smoothing),
17
+ ]
18
+ })
@@ -1,9 +1,12 @@
1
- // Pulse layout — configures the script window.
2
- // Based on Next.js root layout convention.
1
+ // Pulse layout — configures the script window and optional key/premium systems.
2
+ // This file is never compiled into the output; the rb CLI reads it at build time.
3
3
 
4
4
  export default {
5
5
  title: '{NAME}',
6
6
  author: '',
7
+ version: '1.0.0',
8
+ description: 'A powerful script for {NAME}. Enable features from the tabs above.',
9
+ discord: '', // shown as "Join Discord" button in Home tab
7
10
  toggleKey: 'RightControl',
8
11
  size: [850, 560] as [number, number],
9
12
  uiLibrary: 'windui' as const,
@@ -16,4 +19,34 @@ export default {
16
19
  openButtonIcon: 'code-2',
17
20
  themes: [] as LayoutConfig['themes'],
18
21
  compatExclude: [] as string[],
22
+
23
+ // ── Key system (optional) ────────────────────────────────────────────────────
24
+ // Blocks the UI until the user enters a valid key.
25
+ // Remove this block entirely to let anyone use the script without a key.
26
+ //
27
+ // keySystem: {
28
+ // title: 'Key Required',
29
+ // note: 'Get your free key from our Discord',
30
+ // saveKey: true, // persist key to executor filesystem
31
+ // getKeyUrl: 'https://discord.gg/example',
32
+ //
33
+ // // Option A — static list (client-side):
34
+ // keys: ['FREE_KEY_1', 'FREE_KEY_2'],
35
+ //
36
+ // // Option B — server-side validator (more secure, any string "true" = valid):
37
+ // // validatorUrl: 'https://yoursite.com/validate?key=',
38
+ // },
39
+
40
+ // ── Premium tier (optional) ──────────────────────────────────────────────────
41
+ // A second key layer on top of the free tier.
42
+ // Mark groupboxes with { premium: true } in your pages to lock them.
43
+ // Users see an inline key-entry UI in each locked groupbox; entering a valid
44
+ // premium key unlocks all of them live — no re-injection needed.
45
+ // Premium keys also grant basic (keySystem) access automatically.
46
+ //
47
+ // premium: {
48
+ // keys: ['PREMIUM_KEY_1'],
49
+ // // OR: validatorUrl: 'https://yoursite.com/premium?key=',
50
+ // getKeyUrl: 'https://yoursite.com/premium', // shown in "Copy Link" / "Get Premium Key" buttons
51
+ // },
19
52
  } satisfies LayoutConfig
@@ -0,0 +1,9 @@
1
+ // Page names must not clash with the built-in "Home" and "Settings" tabs.
2
+ // Rename this page and add more pages as needed.
3
+ definePage('Combat', { icon: 'swords' }, () => [
4
+ groupbox('left', 'Movement', { icon: 'person', mount: 'SpeedHack' }),
5
+ groupbox('left', 'Camera', { icon: 'eye', mount: 'FOVChanger' }),
6
+ groupbox('right', 'Visuals', { icon: 'scan-eye', mount: 'PlayerESP' }),
7
+ // premium: true — shows inline key-entry form until the user unlocks with a premium key.
8
+ groupbox('right', 'Aimbot', { icon: 'crosshair', mount: 'Aimbot', premium: true }),
9
+ ])