rbxts-transform-boost 0.1.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.
@@ -0,0 +1,26 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ registry-url: https://registry.npmjs.org
19
+
20
+ - run: npm ci
21
+
22
+ - run: npm run build
23
+
24
+ - run: npm publish --access public
25
+ env:
26
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # rbxts-transform-boost
2
+
3
+ > **Successor to [`rbxts-transformer-luau-annotate`](https://github.com/Loner1536/rbxts-transformer-luau-annotate).** That package was accidentally removed from npm. Migrate to this one.
4
+ >
5
+ > **What carried over:** Luau type annotation injection on function parameters for native codegen (primitives, Roblox value types, arrays).
6
+ >
7
+ > **What's new:** `--!optimize 2` on every file, `game:GetService()` hoisting to module-level locals, repeated property chain hoisting, loop bounds hoisting — all without needing `//!native` in every file.
8
+ >
9
+ > **What's different:** The old package also annotated return types, local variable declarations, class methods, and user-defined interfaces/type aliases. Those are not yet in this package — they're planned but the implementation is non-trivial. The old package also had reliability issues that this rewrite addresses.
10
+
11
+ A roblox-ts transformer that automatically applies Luau performance directives at compile time — no runtime cost, no code changes required.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install --save-dev rbxts-transform-boost
17
+ ```
18
+
19
+ `tsconfig.json`:
20
+ ```json
21
+ {
22
+ "compilerOptions": {
23
+ "plugins": [
24
+ {
25
+ "transform": "rbxts-transform-boost",
26
+ "optimize": true,
27
+ "hoist": true
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Options
35
+
36
+ | Option | Type | Default | Description |
37
+ |--------|------|---------|-------------|
38
+ | `optimize` | `boolean` | `true` | Prepend `--!optimize 2` to every file that doesn't already have it |
39
+ | `hoist` | `boolean` | `true` | Hoist `GetService` calls and repeated property reads to locals |
40
+
41
+ `--!native` is never auto-inserted. Add `//!native` at the top of your TypeScript file for hot paths you've profiled — rotor preserves it.
42
+
43
+ ```typescript
44
+ //!native
45
+ export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number) {
46
+ ...
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## What it does
53
+
54
+ ### `--!optimize 2` — always on top
55
+
56
+ Every file gets `--!optimize 2` prepended if it doesn't already have it. Roblox already runs all scripts at optimization level 2, but the directive makes Studio behaviour match production and signals intent.
57
+
58
+ ```typescript
59
+ // TypeScript source
60
+ export function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number {
61
+ const fixed = math.floor(value * scale);
62
+ const clamped = math.clamp(fixed, -32768, 32767);
63
+ buffer.writei16(buf, offset, clamped);
64
+ return offset + 2;
65
+ }
66
+ ```
67
+
68
+ ```lua
69
+ -- Without transformer
70
+ -- Compiled with rotor v2.2.0
71
+ local function encodeFixed(buf, offset, value, scale)
72
+ local fixed = math.floor(value * scale)
73
+ local clamped = math.clamp(fixed, -32768, 32767)
74
+ buffer.writei16(buf, offset, clamped)
75
+ return offset + 2
76
+ end
77
+ ```
78
+
79
+ ```lua
80
+ -- With transformer
81
+ --!optimize 2
82
+ -- Compiled with rotor v2.2.0
83
+ local function encodeFixed(buf: buffer, offset: number, value: number, scale: number)
84
+ local fixed = math.floor(value * scale)
85
+ local clamped = math.clamp(fixed, -32768, 32767)
86
+ buffer.writei16(buf, offset, clamped)
87
+ return offset + 2
88
+ end
89
+ ```
90
+
91
+ ---
92
+
93
+ ### GetService hoisting
94
+
95
+ Every `game:GetService("X")` call in a file is hoisted to a module-level local on first load. Functions that call `GetService` on every invocation — the most common pattern in rotor output — pay the registry lookup cost zero times at runtime.
96
+
97
+ ```typescript
98
+ // TypeScript source
99
+ export function serviceWork(): string {
100
+ const count = game.GetService("Players").GetPlayers().size();
101
+ const running = game.GetService("RunService").IsRunning();
102
+ return `${count}-${running}`;
103
+ }
104
+ ```
105
+
106
+ ```lua
107
+ -- Without transformer
108
+ local function serviceWork()
109
+ local count = #game:GetService("Players"):GetPlayers()
110
+ local running = game:GetService("RunService"):IsRunning()
111
+ return `{count}-{running}`
112
+ end
113
+ ```
114
+
115
+ ```lua
116
+ -- With transformer
117
+ --!optimize 2
118
+ local _Players = game:GetService("Players") -- hoisted once at module load
119
+ local _RunService = game:GetService("RunService")
120
+
121
+ local function serviceWork()
122
+ local count = #_Players:GetPlayers()
123
+ local running = _RunService:IsRunning()
124
+ return `{count}-{running}`
125
+ end
126
+ ```
127
+
128
+ **2.4× faster** — `GetService` calls eliminated from the hot path entirely.
129
+
130
+ ---
131
+
132
+ ### Property chain hoisting
133
+
134
+ Any property access chain that appears **2 or more times** inside the same function is hoisted to a local. Instance property reads go through Roblox's C++ property system — doing the same read twice is wasted work.
135
+
136
+ ```typescript
137
+ // TypeScript source
138
+ export function cameraWork(camera: Camera): number {
139
+ const pos = camera.CFrame.Position;
140
+ const look = camera.CFrame.LookVector; // camera.CFrame read twice
141
+ const fov = camera.FieldOfView;
142
+ return pos.Magnitude + look.X + fov;
143
+ }
144
+ ```
145
+
146
+ ```lua
147
+ -- Without transformer
148
+ local function cameraWork(camera)
149
+ local pos = camera.CFrame.Position -- engine call #1
150
+ local look = camera.CFrame.LookVector -- engine call #2
151
+ local fov = camera.FieldOfView
152
+ return pos.Magnitude + look.X + fov
153
+ end
154
+ ```
155
+
156
+ ```lua
157
+ -- With transformer
158
+ local function cameraWork(camera: Camera)
159
+ local _cache0 = camera.CFrame -- one engine call
160
+ local pos = _cache0.Position -- local read
161
+ local look = _cache0.LookVector -- local read
162
+ local fov = camera.FieldOfView
163
+ return pos.Magnitude + look.X + fov
164
+ end
165
+ ```
166
+
167
+ **1.5× faster.** Also hoists value type field reads (`.X`, `.Y`, `.Z`) when they appear multiple times — the `cross` function reads each component twice, so all six are hoisted:
168
+
169
+ ```typescript
170
+ // TypeScript source
171
+ export function cross(a: Vector3, b: Vector3): Vector3 {
172
+ return new Vector3(
173
+ a.Y * b.Z - a.Z * b.Y,
174
+ a.Z * b.X - a.X * b.Z,
175
+ a.X * b.Y - a.Y * b.X,
176
+ );
177
+ }
178
+ ```
179
+
180
+ ```lua
181
+ -- Without transformer
182
+ local function cross(a, b)
183
+ return Vector3.new(
184
+ a.Y * b.Z - a.Z * b.Y,
185
+ a.Z * b.X - a.X * b.Z,
186
+ a.X * b.Y - a.Y * b.X
187
+ )
188
+ end
189
+ ```
190
+
191
+ ```lua
192
+ -- With transformer
193
+ local function cross(a: Vector3, b: Vector3)
194
+ local _cache0 = a.Y -- each component read twice → all hoisted
195
+ local _cache1 = b.Z
196
+ local _cache2 = a.Z
197
+ local _cache3 = b.Y
198
+ local _cache4 = b.X
199
+ local _cache5 = a.X
200
+ return Vector3.new(
201
+ _cache0 * _cache1 - _cache2 * _cache3,
202
+ _cache2 * _cache4 - _cache5 * _cache1,
203
+ _cache5 * _cache3 - _cache0 * _cache4
204
+ )
205
+ end
206
+ ```
207
+
208
+ **2.7× faster.**
209
+
210
+ ---
211
+
212
+ ### Loop bounds hoisting
213
+
214
+ `arr.size()` in a `for` loop condition is re-evaluated on every iteration in rotor's output. The loops pass hoists it to a `const` before the loop.
215
+
216
+ ```typescript
217
+ // TypeScript source
218
+ export function sumWeighted(values: Array<number>, weights: Array<number>): number {
219
+ let total = 0;
220
+ for (let i = 0; i < values.size(); i++) {
221
+ total += values[i] * weights[i];
222
+ }
223
+ return total;
224
+ }
225
+ ```
226
+
227
+ ```lua
228
+ -- Without transformer
229
+ local function sumWeighted(values, weights)
230
+ local total = 0
231
+ for i = 0, #values - 1 do -- #values evaluated once per iteration
232
+ total += values[i + 1] * weights[i + 1]
233
+ end
234
+ return total
235
+ end
236
+ ```
237
+
238
+ ```lua
239
+ -- With transformer
240
+ local function sumWeighted(values: {number}, weights: {number})
241
+ local total = 0
242
+ local _len_values = #values -- hoisted: evaluated once total
243
+ for i = 0, _len_values - 1 do
244
+ total += values[i + 1] * weights[i + 1]
245
+ end
246
+ return total
247
+ end
248
+ ```
249
+
250
+ **2.3× faster** (combined with `--!native` and type annotations on `values`/`weights`).
251
+
252
+ ---
253
+
254
+ ### Luau type annotation injection
255
+
256
+ After rotor writes `.luau` files, the transformer injects Luau type annotations on function parameters. This lets the native compiler generate specialized code for numeric and Roblox value types.
257
+
258
+ ```typescript
259
+ // TypeScript source
260
+ export function dot(a: Vector3, b: Vector3): number {
261
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
262
+ }
263
+ ```
264
+
265
+ ```lua
266
+ -- Without transformer
267
+ local function dot(a, b)
268
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z
269
+ end
270
+ ```
271
+
272
+ ```lua
273
+ -- With transformer
274
+ local function dot(a: Vector3, b: Vector3)
275
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z
276
+ end
277
+ ```
278
+
279
+ Supported types: `number`, `string`, `boolean`, `Vector3`, `Vector2`, `CFrame`, `UDim2`, `Color3`, `buffer`, `Instance`, `BasePart`, `Player`, `Camera`, `RunService`, `Players`, `Workspace`, and array forms (`{number}`, `{Vector3}`, etc.).
280
+
281
+ ---
282
+
283
+ ## Benchmarks
284
+
285
+ Measured in Roblox Studio server context. 100,000 iterations per benchmark (10,000 for `cfLookAt`). Same TypeScript source compiled two ways — with and without the transformer.
286
+
287
+ | Benchmark | With transformer | Without | Speedup | Driver |
288
+ |-----------|-----------------|---------|---------|--------|
289
+ | integrate (Verlet) | 0.042 µs | 0.055 µs | **1.3×** | `--!native` + type annotations |
290
+ | dot (V3 manual) | 0.016 µs | 0.032 µs | **2.0×** | `--!native` |
291
+ | cross (V3 manual) | 0.018 µs | 0.049 µs | **2.7×** | `--!native` + 6× field hoisting |
292
+ | lerpVec3 (V3 manual) | 0.015 µs | 0.047 µs | **3.1×** | `--!native` + 3× field hoisting |
293
+ | encodeFixed (buf+math) | 0.015 µs | 0.032 µs | **2.1×** | `--!native` |
294
+ | encodePacket (3× fixed) | 0.018 µs | 0.067 µs | **3.7×** | `--!native` stacked across 3 calls |
295
+ | sumWeighted (loop) | 0.046 µs | 0.107 µs | **2.3×** | `--!native` + loop bounds hoist |
296
+ | dotProduct (loop) | 0.067 µs | 0.109 µs | **1.6×** | `--!native` + loop bounds hoist |
297
+ | norm (loop+sqrt) | 0.048 µs | 0.111 µs | **2.3×** | `--!native` + loop bounds hoist |
298
+ | mathHeavy (trig+sqrt) | 0.038 µs | 0.052 µs | **1.4×** | `--!native` |
299
+ | fib(20) (iter) | 0.048 µs | 0.155 µs | **3.2×** | `--!native` on integer loop |
300
+ | cfLookAt (ctor) | 0.079 µs | 0.079 µs | 1.0× | C++ floor — no Luau work |
301
+ | cfChain (mul+angles) | 0.076 µs | 0.077 µs | 1.0× | C++ floor — no Luau work |
302
+ | serviceWork (GetService ×2) | 0.185 µs | 0.440 µs | **2.4×** | GetService hoisting |
303
+ | multiSvc (GetService ×3) | 0.142 µs | 0.480 µs | **3.4×** | GetService hoisting |
304
+ | cameraWork (prop chain) | 0.148 µs | 0.215 µs | **1.5×** | `camera.CFrame` hoisted (2 reads → 1) |
305
+ | formatStats (template) | 0.170 µs | 0.175 µs | ~1× | String — no arithmetic |
306
+ | buildKey (template) | 0.068 µs | 0.086 µs | **1.3×** | `--!native` |
307
+
308
+ ### What the transformer cannot help with
309
+
310
+ - **Pure engine API calls** — `CFrame.lookAt`, `CFrame.Angles`, `CFrame` multiplication all execute immediately in C++. `--!native` cannot speed up code that is already running natively. `cfLookAt` and `cfChain` show 1.0× for this reason.
311
+ - **Single-access properties** — the cache pass only hoists when a property is read 2+ times in the same function. One read has nothing to eliminate.
312
+ - **String-heavy functions** — Luau string operations are not meaningfully accelerated by the native compiler.
313
+
314
+ ---
315
+
316
+ ## Development
317
+
318
+ ```bash
319
+ npm run build # build the transformer
320
+ npm run bench:rbxlx # build both versions + produce bench/benchmark.rbxlx
321
+ ```
322
+
323
+ Open `bench/benchmark.rbxlx` in Roblox Studio and run the server. The optimized suite prints first, then the baseline suite, sequentially in the output window.
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "perf-benchmark",
3
+ "emitLegacyScripts": false,
4
+ "tree": {
5
+ "$className": "DataModel",
6
+ "ServerScriptService": {
7
+ "$className": "ServerScriptService",
8
+ "$path": "out/src/server"
9
+ },
10
+ "ReplicatedStorage": {
11
+ "$className": "ReplicatedStorage",
12
+ "shared": {
13
+ "$path": "out/src/shared"
14
+ },
15
+ "include": {
16
+ "$path": "out/include"
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "rbxts-transform-perf-test",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "name": "rbxts-transform-perf-test",
8
+ "devDependencies": {
9
+ "@rbxts/compiler-types": "^3.0.0-types.0",
10
+ "@rbxts/types": "^1.0.908",
11
+ "rbxts-transform-boost": "file:../",
12
+ "typescript": "=5.5.3"
13
+ }
14
+ },
15
+ "..": {
16
+ "name": "rbxts-transform-boost",
17
+ "version": "0.1.0",
18
+ "dev": true,
19
+ "devDependencies": {
20
+ "@types/node": "^26.0.0"
21
+ },
22
+ "peerDependencies": {
23
+ "typescript": ">=5.0.0"
24
+ }
25
+ },
26
+ "node_modules/@rbxts/compiler-types": {
27
+ "version": "3.0.0-types.0",
28
+ "resolved": "https://registry.npmjs.org/@rbxts/compiler-types/-/compiler-types-3.0.0-types.0.tgz",
29
+ "integrity": "sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==",
30
+ "dev": true,
31
+ "license": "MIT"
32
+ },
33
+ "node_modules/@rbxts/types": {
34
+ "version": "1.0.928",
35
+ "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.928.tgz",
36
+ "integrity": "sha512-GzmzBNn8fyn0ed9kVQkXUbBXpUg9Gfyf9AZJKT51VGqyrf4w6J0X72eSSGxt6YN2BMHNbqhzHCBn/fM8bdqMTg==",
37
+ "dev": true,
38
+ "license": "MIT"
39
+ },
40
+ "node_modules/rbxts-transform-boost": {
41
+ "resolved": "..",
42
+ "link": true
43
+ },
44
+ "node_modules/typescript": {
45
+ "version": "5.5.3",
46
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
47
+ "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
48
+ "dev": true,
49
+ "license": "Apache-2.0",
50
+ "bin": {
51
+ "tsc": "bin/tsc",
52
+ "tsserver": "bin/tsserver"
53
+ },
54
+ "engines": {
55
+ "node": ">=14.17"
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "rbxts-transform-perf-test",
3
+ "private": true,
4
+ "scripts": {
5
+ "compile": "lumen src -o out/src -i out/include -p default.project.json",
6
+ "watch": "lumen src -o out/src -i out/include -p default.project.json --watch"
7
+ },
8
+ "devDependencies": {
9
+ "@rbxts/compiler-types": "^3.0.0-types.0",
10
+ "@rbxts/types": "^1.0.908",
11
+ "typescript": "=5.5.3",
12
+ "rbxts-transform-boost": "file:../"
13
+ }
14
+ }
@@ -0,0 +1,57 @@
1
+ import * as opt from "../shared/fns";
2
+ import * as base from "../shared/fns-bare";
3
+
4
+ function bench(label: string, n: number, fn: () => void): void {
5
+ task.wait(0.05);
6
+ const t0 = os.clock();
7
+ for (let i = 0; i < n; i++) fn();
8
+ const elapsed = os.clock() - t0;
9
+ print(` ${label}: ${string.format("%.3f", (elapsed / n) * 1e6)} us/iter`);
10
+ }
11
+
12
+ const N = 100000;
13
+ const NS = 10000;
14
+
15
+ const pos = new Vector3(1, 2, 3);
16
+ const vel = new Vector3(0, 1, 0);
17
+ const acc = new Vector3(0, -9.8, 0);
18
+ const vecA = new Vector3(1, 0, 0);
19
+ const vecB = new Vector3(0, 1, 0);
20
+ const eye = new Vector3(0, 5, 10);
21
+ const tgt = new Vector3(0, 0, 0);
22
+ const cf = CFrame.lookAt(eye, tgt);
23
+ const buf = buffer.create(256);
24
+ const vals = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
25
+ const wts = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
26
+ const cam = game.GetService("Workspace").CurrentCamera!;
27
+
28
+ function runSuite(fns: typeof opt): void {
29
+ bench("integrate (verlet)", N, () => fns.integrate(pos, vel, acc, 1 / 60));
30
+ bench("dot (V3 manual)", N, () => fns.dot(vecA, vecB));
31
+ bench("cross (V3 manual)", N, () => fns.cross(vecA, vecB));
32
+ bench("lerpVec3 (V3 manual)", N, () => fns.lerpVec3(pos, eye, 0.5));
33
+ bench("encodeFixed (buf+math)", N, () => fns.encodeFixed(buf, 0, 3.14, 100));
34
+ bench("encodePacket(3x fixed)", N, () => fns.encodePacket(buf, 1.1, 2.2, 3.3, 100));
35
+ bench("sumWeighted (loop)", N, () => fns.sumWeighted(vals, wts));
36
+ bench("dotProduct (loop)", N, () => fns.dotProduct(vals, vals));
37
+ bench("norm (loop+sqrt)", N, () => fns.norm(vals));
38
+ bench("mathHeavy (trig+sqrt)", N, () => fns.mathHeavy(1.23, 4.56));
39
+ bench("fib(20) (iter)", N, () => fns.fib(20));
40
+ bench("cfLookAt (ctor)", NS, () => fns.cfLookAt(eye, tgt));
41
+ bench("cfChain (mul+angles)", N, () => fns.cfChain(cf, 0.016));
42
+ bench("serviceWork (GetService x2)", N, () => fns.serviceWork());
43
+ bench("multiSvc (GetService x3)", N, () => fns.multiService());
44
+ bench("cameraWork (prop chain)", N, () => fns.cameraWork(cam));
45
+ bench("formatStats (template)", N, () => fns.formatStats("speed", 9.81, "m/s"));
46
+ bench("buildKey (template)", N, () => fns.buildKey("player", 42, "data"));
47
+ }
48
+
49
+ print("\n=== optimized (--!native + transformer) ===");
50
+ runSuite(opt);
51
+ print("===========================================\n");
52
+
53
+ task.wait(1);
54
+
55
+ print("\n=== baseline (plain rotor, no transformer) ===");
56
+ runSuite(base);
57
+ print("==============================================\n");
@@ -0,0 +1,127 @@
1
+ // ── Vector3 / physics ────────────────────────────────────────────────────────
2
+
3
+ export function integrate(pos: Vector3, vel: Vector3, acc: Vector3, dt: number): [Vector3, Vector3] {
4
+ const newVel = vel.add(acc.mul(dt));
5
+ const newPos = pos.add(newVel.mul(dt));
6
+ return [newPos, newVel];
7
+ }
8
+
9
+ export function dot(a: Vector3, b: Vector3): number {
10
+ return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
11
+ }
12
+
13
+ export function cross(a: Vector3, b: Vector3): Vector3 {
14
+ return new Vector3(
15
+ a.Y * b.Z - a.Z * b.Y,
16
+ a.Z * b.X - a.X * b.Z,
17
+ a.X * b.Y - a.Y * b.X,
18
+ );
19
+ }
20
+
21
+ export function lerpVec3(a: Vector3, b: Vector3, t: number): Vector3 {
22
+ return new Vector3(
23
+ a.X + (b.X - a.X) * t,
24
+ a.Y + (b.Y - a.Y) * t,
25
+ a.Z + (b.Z - a.Z) * t,
26
+ );
27
+ }
28
+
29
+ // ── Buffer / encoding ────────────────────────────────────────────────────────
30
+
31
+ export function encodeFixed(buf: buffer, offset: number, value: number, scale: number): number {
32
+ const fixed = math.floor(value * scale);
33
+ const clamped = math.clamp(fixed, -32768, 32767);
34
+ buffer.writei16(buf, offset, clamped);
35
+ return offset + 2;
36
+ }
37
+
38
+ export function encodePacket(buf: buffer, x: number, y: number, z: number, scale: number): void {
39
+ let off = 0;
40
+ off = encodeFixed(buf, off, x, scale);
41
+ off = encodeFixed(buf, off, y, scale);
42
+ encodeFixed(buf, off, z, scale);
43
+ }
44
+
45
+ // ── Math / arithmetic ────────────────────────────────────────────────────────
46
+
47
+ export function sumWeighted(values: Array<number>, weights: Array<number>): number {
48
+ let total = 0;
49
+ for (let i = 0; i < values.size(); i++) {
50
+ total += values[i] * weights[i];
51
+ }
52
+ return total;
53
+ }
54
+
55
+ export function dotProduct(a: Array<number>, b: Array<number>): number {
56
+ let sum = 0;
57
+ for (let i = 0; i < a.size(); i++) {
58
+ sum += a[i] * b[i];
59
+ }
60
+ return sum;
61
+ }
62
+
63
+ export function norm(values: Array<number>): number {
64
+ let sq = 0;
65
+ for (let i = 0; i < values.size(); i++) {
66
+ sq += values[i] * values[i];
67
+ }
68
+ return math.sqrt(sq);
69
+ }
70
+
71
+ export function mathHeavy(x: number, y: number): number {
72
+ return math.sin(x) * math.cos(y) + math.sqrt(x * x + y * y) + math.atan2(y, x);
73
+ }
74
+
75
+ export function fib(n: number): number {
76
+ if (n <= 1) return n;
77
+ let a = 0;
78
+ let b = 1;
79
+ for (let i = 2; i <= n; i++) {
80
+ const tmp = a + b;
81
+ a = b;
82
+ b = tmp;
83
+ }
84
+ return b;
85
+ }
86
+
87
+ // ── CFrame ───────────────────────────────────────────────────────────────────
88
+
89
+ export function cfLookAt(eye: Vector3, target: Vector3): CFrame {
90
+ return CFrame.lookAt(eye, target);
91
+ }
92
+
93
+ export function cfChain(cf: CFrame, dt: number): CFrame {
94
+ return cf.mul(CFrame.Angles(0, dt, 0));
95
+ }
96
+
97
+ // ── Services / instance ──────────────────────────────────────────────────────
98
+
99
+ export function serviceWork(): string {
100
+ const count = game.GetService("Players").GetPlayers().size();
101
+ const running = game.GetService("RunService").IsRunning();
102
+ return `${count}-${running}`;
103
+ }
104
+
105
+ export function multiService(): boolean {
106
+ const players = game.GetService("Players");
107
+ const rs = game.GetService("RunService");
108
+ const ws = game.GetService("Workspace");
109
+ return rs.IsRunning() && players.MaxPlayers > 0 && ws.Gravity > 0;
110
+ }
111
+
112
+ export function cameraWork(camera: Camera): number {
113
+ const pos = camera.CFrame.Position;
114
+ const look = camera.CFrame.LookVector;
115
+ const fov = camera.FieldOfView;
116
+ return pos.Magnitude + look.X + fov;
117
+ }
118
+
119
+ // ── String ───────────────────────────────────────────────────────────────────
120
+
121
+ export function formatStats(label: string, value: number, unit: string): string {
122
+ return `${label}: ${string.format("%.4f", value)} ${unit}`;
123
+ }
124
+
125
+ export function buildKey(prefix: string, id: number, suffix: string): string {
126
+ return `${prefix}_${id}_${suffix}`;
127
+ }