pulse-rb 1.3.2 → 1.4.1

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.
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.1";
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,54 @@ 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(
1377
+ `do
1378
+ local _rs=game:GetService("RunService")
1379
+ local _lp_=game:GetService("Players").LocalPlayer
1380
+ local _uis_=game:GetService("UserInputService")
1381
+ _rs.Heartbeat:Connect(function(a,b) if _PulseRunHeartbeat then _PulseRunHeartbeat(a,b) end end)
1382
+ _rs.RenderStepped:Connect(function(a,b) if _PulseRunRenderStepped then _PulseRunRenderStepped(a,b) end end)
1383
+ _rs.Stepped:Connect(function(a,b) if _PulseRunStepped then _PulseRunStepped(a,b) end end)
1384
+ _lp_.CharacterAdded:Connect(function(c) if _PulseRunCharAdded then _PulseRunCharAdded(c) end end)
1385
+ _lp_.CharacterRemoving:Connect(function(c) if _PulseRunCharRemoving then _PulseRunCharRemoving(c) end end)
1386
+ _uis_.InputBegan:Connect(function(i,g) if _PulseRunInputBegan then _PulseRunInputBegan(i,g) end end)
1387
+ _uis_.InputEnded:Connect(function(i,g) if _PulseRunInputEnded then _PulseRunInputEnded(i,g) end end)
1388
+ end
1389
+ `
1390
+ );
1391
+ parts.push(`local _A=loadstring(game:HttpGet("${base}/adapters/${ui}.lua"))(_P,_PULSE_LAYOUT)
1298
1392
  `);
1299
1393
  parts.push(`local signal,computed,defineComponent,on=_P.signal,_P.computed,_P.defineComponent,_P.on
1300
1394
  `);
@@ -1304,17 +1398,6 @@ end)
1304
1398
  `);
1305
1399
  parts.push("\n");
1306
1400
  }
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
1401
  const label = (p) => {
1319
1402
  try {
1320
1403
  return path.relative(this.root, p).replace(/\\/g, "/");
@@ -1387,6 +1470,11 @@ end)
1387
1470
  parts.push(DEFAULTS_RUNNER);
1388
1471
  parts.push("\n");
1389
1472
  }
1473
+ if (opts.dev) {
1474
+ parts.push(
1475
+ '-- [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'
1476
+ );
1477
+ }
1390
1478
  parts.push("-- [generated: destroy registration]\n");
1391
1479
  parts.push(DESTROY_REGISTRATION);
1392
1480
  parts.push("\n");
@@ -1797,25 +1885,20 @@ function makeClaudeMd(name) {
1797
1885
  function makeAgentsMd(name) {
1798
1886
  return read("AGENTS.md").replace(/{NAME}/g, name);
1799
1887
  }
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;
1888
+ 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
1889
  var init_templates = __esm({
1802
1890
  "src/templates.ts"() {
1803
1891
  init_cjs_shims();
1804
1892
  DIR = path.join(__dirname, "..", "templates");
1805
1893
  TEMPLATE_GLOBALS = read("globals.lua");
1806
1894
  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
1895
  MODULE_BOILERPLATE = read("module.lua");
1815
- MODULE_RBLUA_BOILERPLATE = read("module.rblua");
1816
1896
  REMOTE_BOILERPLATE = read("remote.lua");
1897
+ TEMPLATE_GITIGNORE = read("gitignore");
1898
+ TEMPLATE_DEPLOY_EXAMPLE = read("deploy_config.example");
1817
1899
  TEMPLATE_LAYOUT_TS = read("layout.ts");
1818
1900
  TEMPLATE_PAGE_HOME_TS = read("page_home.ts");
1901
+ TEMPLATE_COMPONENT_TS = read("component.ts");
1819
1902
  TEMPLATE_EX_SPEED_TS = read("component_speed.ts");
1820
1903
  TEMPLATE_EX_FOV_TS = read("component_fov.ts");
1821
1904
  TEMPLATE_EX_ESP_TS = read("component_esp.ts");
@@ -2015,9 +2098,8 @@ function toCamel(name) {
2015
2098
  function cmdNew(args) {
2016
2099
  const root = requireProjectRoot();
2017
2100
  if (args.type === "module") {
2018
- const plain = args.plain ?? false;
2019
2101
  let relPath = args.path;
2020
- if (plain) {
2102
+ if (args.plain) {
2021
2103
  if (!relPath.endsWith(".lua")) relPath += ".lua";
2022
2104
  const dest = pathe.join(root, "src", relPath);
2023
2105
  const camel = toCamel(relPath);
@@ -2033,23 +2115,23 @@ function cmdNew(args) {
2033
2115
  pOk(`src/${relPath}`);
2034
2116
  console.log();
2035
2117
  pKv("File", `src/${relPath}`);
2036
- pKv("Tip", "add to func for shared helpers; keep private state local");
2118
+ pKv("Tip", "add helpers onto func; keep state local");
2037
2119
  } else {
2038
- if (!relPath.endsWith(".rblua")) relPath += ".rblua";
2120
+ if (!relPath.endsWith(".ts")) relPath += ".ts";
2039
2121
  const dest = pathe.join(root, "src", relPath);
2040
2122
  const camel = toCamel(relPath);
2123
+ const content = TEMPLATE_COMPONENT_TS.replace(/{Camel}/g, camel);
2041
2124
  if (fs.existsSync(dest)) {
2042
2125
  pFail(`src/${relPath} already exists`);
2043
2126
  process.exit(1);
2044
2127
  }
2045
2128
  fs.mkdirSync(pathe.join(dest, ".."), { recursive: true });
2046
- fs.writeFileSync(dest, MODULE_RBLUA_BOILERPLATE, "utf8");
2129
+ fs.writeFileSync(dest, content, "utf8");
2047
2130
  pOk(`src/${relPath}`);
2048
2131
  console.log();
2049
2132
  pKv("Component", camel);
2050
- pKv("Enabled", `Components.${camel}.enabled(true)`);
2051
- pKv("Toggle", `Components.${camel}.Toggle()`);
2052
- pKv("Listen", `Components.${camel}.onEnable:connect(...)`);
2133
+ pKv("Mount", `groupbox('left', '${camel}', { mount: '${camel}' })`);
2134
+ pKv("Tip", "add to a page groupbox in src/pages/*.ts to render it");
2053
2135
  }
2054
2136
  console.log();
2055
2137
  } else if (args.type === "remote") {
@@ -2067,14 +2149,14 @@ function cmdNew(args) {
2067
2149
  pOk(`func.Remote_${remoteName}(...) ${gray("\u2192 remotes.lua")}`);
2068
2150
  console.log();
2069
2151
  } else {
2070
- pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new remote");
2152
+ pFail(`Unknown type '${args.type}'`, "use: rb new module | rb new module --plain | rb new remote");
2071
2153
  process.exit(1);
2072
2154
  }
2073
2155
  }
2074
2156
  async function cmdRemove(args) {
2075
2157
  const root = requireProjectRoot();
2076
2158
  let relPath = args.path;
2077
- if (!relPath.endsWith(".lua") && !relPath.endsWith(".rblua")) relPath += ".lua";
2159
+ if (!relPath.endsWith(".lua") && !relPath.endsWith(".ts")) relPath += ".ts";
2078
2160
  const target = pathe.join(root, "src", relPath);
2079
2161
  if (!fs.existsSync(target)) {
2080
2162
  pFail(`src/${relPath} not found`);
@@ -2925,25 +3007,18 @@ async function cmdPublish(_args) {
2925
3007
  }
2926
3008
  }
2927
3009
  bundleParts.push(
2928
- "return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages}"
3010
+ "return {signal=signal,computed=computed,defineComponent=defineComponent,on=on,toggle=toggle,slider=slider,dropdown=dropdown,multidropdown=multidropdown,button=button,keybind=keybind,label=label,separator=separator,groupbox=groupbox,definePage=definePage,Pulse=Pulse,Signal=Signal,Computed=Computed,Component=Component,Components=Components,Store=Store,PulseEvent=PulseEvent,_PulseGetChar=_PulseGetChar,_PulseGetHRP=_PulseGetHRP,_PulseGetHumanoid=_PulseGetHumanoid,_PulseGetAlive=_PulseGetAlive,_PulseRS=_PulseRS,_PulseUIS=_PulseUIS,_PulseDestroy=_PulseDestroy,_PULSE_DEFAULTS=_PULSE_DEFAULTS,_Pages=_Pages,_PulseRunHeartbeat=_PulseRunHeartbeat,_PulseRunRenderStepped=_PulseRunRenderStepped,_PulseRunStepped=_PulseRunStepped,_PulseRunCharAdded=_PulseRunCharAdded,_PulseRunCharRemoving=_PulseRunCharRemoving,_PulseRunInputBegan=_PulseRunInputBegan,_PulseRunInputEnded=_PulseRunInputEnded}"
2929
3011
  );
2930
3012
  const bundleContent = Buffer.from(bundleParts.join("\n") + "\n", "utf8");
2931
3013
  const uploads = [
2932
3014
  { remote: `${VERSION_PATH}/bundle.lua`, content: bundleContent }
2933
3015
  ];
2934
- const pulseUiDir = pathe.join(PULSE_DIR2, "ui");
2935
3016
  for (const adapter of ["windui"]) {
2936
3017
  const p = pathe.join(ADAPTERS, `${adapter}.lua`);
2937
3018
  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
3019
  uploads.push({
2945
3020
  remote: `${VERSION_PATH}/adapters/${adapter}.lua`,
2946
- content: Buffer.from(parts.join("\n") + "\n", "utf8")
3021
+ content: Buffer.from(fs.readFileSync(p, "utf8"), "utf8")
2947
3022
  });
2948
3023
  }
2949
3024
  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.1",
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
+ }
@@ -73,7 +73,12 @@ do
73
73
  if #_buf > _bufMax then table.remove(_buf, 1) end
74
74
 
75
75
  if _console then
76
- if n >= 3 then warn(line) else print(line) end
76
+ -- Defer print outside the caller's thread (e.g. Heartbeat) so the
77
+ -- output always reaches the developer console regardless of context.
78
+ local _ln, _nw = line, n >= 3
79
+ task.spawn(function()
80
+ if _nw then warn(_ln) else print(_ln) end
81
+ end)
77
82
  end
78
83
  for _, fn in ipairs(_subs) do pcall(fn, entry) end
79
84
  if _file then
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
 
@@ -415,36 +418,116 @@ local function _needComp(name)
415
418
  return _currentComponent
416
419
  end
417
420
 
418
- local function _uid() return tostring(math.random(100000,999999)) end
421
+ -- ── Sandboxing-safe event runner ─────────────────────────────────────────────
422
+ -- Executor loadstring() environments sandbox Roblox event :Connect() — calls
423
+ -- from inside the bundle never fire. Fix: store callbacks in plain Lua tables
424
+ -- here; the compiler emits user-scope Roblox connections that call _PulseRun*,
425
+ -- routing events into these runners from the non-sandboxed user script.
426
+
427
+ local _cbSeq = 0
428
+ local function _nextId() _cbSeq = _cbSeq + 1; return _cbSeq end
429
+
430
+ local _HbCbs = {} -- Heartbeat
431
+ local _RsCbs = {} -- RenderStepped
432
+ local _StCbs = {} -- Stepped
433
+ local _CaCbs = {} -- CharacterAdded / respawn
434
+ local _CrCbs = {} -- CharacterRemoving
435
+ local _IbCbs = {} -- InputBegan
436
+ local _IeCbs = {} -- InputEnded
437
+
438
+ -- Mock RBXScriptConnection: removing from the list on :Disconnect().
439
+ local function _mockConn(id, list)
440
+ return { Disconnect = function() list[id] = nil end }
441
+ end
419
442
 
420
- local function _hbBind(svc, optsOrFn, fn)
443
+ local function _runFrameCbs(list, a, b)
444
+ for _, e in pairs(list) do
445
+ if e and e.comp then
446
+ e.comp._rawHb = (e.comp._rawHb or 0) + 1
447
+ local pass = not e.when or e.when()
448
+ if pass and e.every then
449
+ local t = type(e.every) == "table" and e.every() or e.every
450
+ local now = tick()
451
+ if (now - e.last) < t then pass = false else e.last = now end
452
+ end
453
+ if pass then
454
+ e.comp._condHb = (e.comp._condHb or 0) + 1
455
+ if Pulse and Pulse.Log then
456
+ Pulse.Log.throttle(e.comp._name, 5, "debug", "tick (condition passed)")
457
+ end
458
+ local ok, err = pcall(e.cb, a, b)
459
+ if not ok then
460
+ warn("[Pulse] "..tostring(e.comp._name).." error: "..tostring(err))
461
+ if Pulse and Pulse.Log then
462
+ Pulse.Log.error(e.comp._name, "event error: "..tostring(err))
463
+ end
464
+ end
465
+ end
466
+ end
467
+ end
468
+ end
469
+
470
+ local function _runEventCbs(list, a, b)
471
+ for _, e in pairs(list) do
472
+ if e then pcall(e.cb, a, b) end
473
+ end
474
+ end
475
+
476
+ -- Public runners — called by compiler-generated user-scope connections.
477
+ _PulseRunHeartbeat = function(a,b) _runFrameCbs(_HbCbs, a,b) end
478
+ _PulseRunRenderStepped = function(a,b) _runFrameCbs(_RsCbs, a,b) end
479
+ _PulseRunStepped = function(a,b) _runFrameCbs(_StCbs, a,b) end
480
+ _PulseRunCharAdded = function(c) _runEventCbs(_CaCbs, c) end
481
+ _PulseRunCharRemoving = function(c) _runEventCbs(_CrCbs, c) end
482
+ _PulseRunInputBegan = function(i,g) _runEventCbs(_IbCbs, i,g) end
483
+ _PulseRunInputEnded = function(i,g) _runEventCbs(_IeCbs, i,g) end
484
+
485
+ local function _hbBind(list, optsOrFn, fn)
421
486
  local comp = _needComp("event")
422
487
  local opts, cb
423
488
  if type(optsOrFn) == "function" then cb=optsOrFn; opts={}
424
489
  else opts=optsOrFn; cb=fn end
425
- local when=opts.when; local every=opts.every; local last=0
426
- comp:bind("ev_".._uid(), svc:Connect(function(a,b)
427
- if when and not when() then return end
428
- if every then
429
- local t=type(every)=="table" and every() or every
430
- local now=tick(); if (now-last)<t then return end; last=now
431
- end
432
- local ok,err=pcall(cb,a,b)
433
- if not ok then warn("[Pulse] "..comp._name.." event error: "..tostring(err)) end
434
- end))
490
+ local id = _nextId()
491
+ comp._rawHb = comp._rawHb or 0
492
+ comp._condHb = comp._condHb or 0
493
+ list[id] = { comp=comp, when=opts.when, every=opts.every, last=0, cb=cb }
494
+ comp:bind("ev_"..id, _mockConn(id, list))
435
495
  end
436
496
 
437
497
  local _on = {}
438
- _on.heartbeat = function(a,b) _hbBind(_PulseRS.Heartbeat, a,b) end
439
- _on.renderStepped = function(a,b) _hbBind(_PulseRS.RenderStepped, a,b) end
440
- _on.stepped = function(a,b) _hbBind(_PulseRS.Stepped, a,b) end
441
- _on.inputBegan = function(fn) local c=_needComp("inputBegan"); c:bind("ib_".._uid(), _PulseUIS.InputBegan:Connect(fn)) end
442
- _on.inputEnded = function(fn) local c=_needComp("inputEnded"); c:bind("ie_".._uid(), _PulseUIS.InputEnded:Connect(fn)) end
443
- _on.characterAdded = function(fn) local c=_needComp("characterAdded"); c:bind("ca_".._uid(), _LocalPlayer.CharacterAdded:Connect(fn)) end
444
- _on.characterRemoving = function(fn) local c=_needComp("characterRemoving"); c:bind("cr_".._uid(), _LocalPlayer.CharacterRemoving:Connect(fn)) end
445
- _on.respawn = function(fn) _needComp("respawn"):onRespawn(fn) end
446
- _on.signal = function(sig,fn) _needComp("signal"):watch(sig,fn) end
447
- _on.after = function(s,fn) _needComp("after"):task(s,fn) end
498
+ _on.heartbeat = function(a,b) _hbBind(_HbCbs, a,b) end
499
+ _on.renderStepped = function(a,b) _hbBind(_RsCbs, a,b) end
500
+ _on.stepped = function(a,b) _hbBind(_StCbs, a,b) end
501
+ _on.inputBegan = function(fn)
502
+ local c=_needComp("inputBegan"); local id=_nextId()
503
+ _IbCbs[id]={cb=fn}; c:bind("ib_"..id, _mockConn(id,_IbCbs))
504
+ end
505
+ _on.inputEnded = function(fn)
506
+ local c=_needComp("inputEnded"); local id=_nextId()
507
+ _IeCbs[id]={cb=fn}; c:bind("ie_"..id, _mockConn(id,_IeCbs))
508
+ end
509
+ _on.characterAdded = function(fn)
510
+ local c=_needComp("characterAdded"); local id=_nextId()
511
+ _CaCbs[id]={cb=fn}; c:bind("ca_"..id, _mockConn(id,_CaCbs))
512
+ end
513
+ _on.characterRemoving = function(fn)
514
+ local c=_needComp("characterRemoving"); local id=_nextId()
515
+ _CrCbs[id]={cb=fn}; c:bind("cr_"..id, _mockConn(id,_CrCbs))
516
+ end
517
+ _on.respawn = function(fn)
518
+ local c=_needComp("respawn"); local id=_nextId()
519
+ _CaCbs[id]={cb=fn}; c:bind("__respawn_"..c._name, _mockConn(id,_CaCbs))
520
+ end
521
+ _on.signal = function(sig,fn)
522
+ local comp = _needComp("signal")
523
+ comp:watch(sig, function(v)
524
+ if Pulse and Pulse.Log then
525
+ Pulse.Log.debug(comp._name, "signal "..tostring(sig._id or "?").." → "..tostring(v))
526
+ end
527
+ fn(v)
528
+ end)
529
+ end
530
+ _on.after = function(s,fn) _needComp("after"):task(s,fn) end
448
531
 
449
532
  local function _defineComponent(name, setup)
450
533
  local comp = Component(name)
@@ -470,6 +553,7 @@ local function _groupbox(side, title, opts)
470
553
  return { type="groupbox", side=side, title=title,
471
554
  icon=(opts and opts.icon) or "",
472
555
  mount=(opts and opts.mount) or nil,
556
+ plain=(opts and opts.plain) or false,
473
557
  widgets=(opts and opts.widgets) or {} }
474
558
  end
475
559
 
@@ -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
  ])