├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── default.project.json ├── package.json ├── pesde.lock ├── pesde.toml ├── pnpm-lock.yaml ├── rokit.toml ├── selene.toml ├── src ├── exports.ts ├── index.d.ts ├── init.luau ├── source │ ├── physics-object.ts │ ├── physics.ts │ └── util │ │ ├── helper │ │ └── test-part.ts │ │ └── math │ │ ├── rotations-near.ts │ │ └── vectors-near.ts └── tests │ └── engine.spec.ts ├── testez-companion.toml ├── tsconfig.json ├── wally.lock └── wally.toml /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "jsx": true, 5 | "useJSXTextNode": true, 6 | "ecmaVersion": 2018, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "ignorePatterns": [ 11 | "/out" 12 | ], 13 | "plugins": [ 14 | "@typescript-eslint", 15 | "roblox-ts", 16 | "prettier" 17 | ], 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:roblox-ts/recommended", 22 | "plugin:prettier/recommended" 23 | ], 24 | "rules": { 25 | "prettier/prettier": "warn" 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /out 3 | /include 4 | *.tsbuildinfo 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "trailingComma": "all", 5 | "useTabs": true 6 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "roblox-ts.vscode-roblox-ts", 4 | "dbaeumer.vscode-eslint" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.eol": "\n", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 6 | "editor.formatOnSave": true 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | "editor.formatOnSave": true 11 | }, 12 | "eslint.run": "onType", 13 | "eslint.format.enable": true, 14 | "workbench.colorCustomizations": { 15 | "minimap.background": "#00000000", 16 | "scrollbar.shadow": "#00000000" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delta 2 | 3 | A deterministic physics simulation library for Roblox written in TypeScript. 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Features](#features) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [TypeScript](#typescript) 12 | - [Luau](#luau) 13 | - [API Documentation](#api-documentation) 14 | - [PhysicsObject](#physicsobject) 15 | - [PhysicsEngine](#physicsengine) 16 | - [Examples](#examples) 17 | - [Notes](#notes) 18 | - [License](#license) 19 | 20 | ## Overview 21 | 22 | The library provides a modular, deterministic physics simulation system for Roblox. The library operates entirely in world units (meters, m/s, m/s²) and features: 23 | 24 | - **Translation and Rotation:** Euler integration for position, velocity, and rotation. 25 | - **Collision Detection:** Supports both simple AABB (for point and box collisions) and oriented bounding box (OBB) collision detection for static, rotated objects. 26 | - **Collision Resolution:** Impulse-based resolution with restitution and friction impulses. 27 | - **Friction & Damping:** Applies friction and damping on dynamic objects when in contact with static objects. 28 | - **Customizable Time Control:** Fixed time step integration with a SetTime method for fast-forwarding the simulation. 29 | 30 | ## Features 31 | 32 | - **World-Unit Integration:** All positions, velocities, and forces are expressed in world units. 33 | - **Oriented Static Collision:** Supports collisions with rotated static platforms via OBB calculations (no physical part is required). 34 | - **Dynamic Collision Resolution:** Uses impulse-based resolution with friction between dynamic objects. 35 | - **Rotational Dynamics:** Updates rotation, angular velocity, and angular acceleration. 36 | - **Time Control:** A fixed time-step integrator and a SetTime method that advances simulation until the target time is reached. 37 | 38 | ## Installation 39 | 40 | ### NPM 41 | 42 | 1. In the terminal run: `npm i @rbxts/delta` 43 | 44 | ### Pesde 45 | 46 | 1. For luau you must use the [pesde](https://pesde.dev/) package manager 47 | 2. To add the package run: `pesde add prismatyx/delta` 48 | 3. to install the package run `pesde install` 49 | 50 | ### Roblox 51 | 52 | 1. For Roblox, navigate to the [github releases page](https://github.com/prisma-dev/delta/releases) 53 | 2. Download the rbxm file of the latest version 54 | 55 | ## Usage 56 | 57 | ### TypeScript 58 | 59 | ```typescript 60 | import { PhysicsEngine, PhysicsObject } from "@rbxts/delta"; 61 | 62 | // Create engine and start it. 63 | const engine = new PhysicsEngine(); 64 | engine.Start(); 65 | 66 | // Create a ball. 67 | const ball = new PhysicsObject( 68 | "ball", 69 | { 70 | mass: 10, 71 | friction: 0.5, 72 | elasticity: 0.8, 73 | collisionMethod: "box", 74 | size: new Vector3(2, 2, 2), 75 | ignoreGlobalForces: false, 76 | }, 77 | { 78 | position: new Vector3(0, 10, 0), 79 | velocity: new Vector3(0, 0, 0), 80 | acceleration: new Vector3(0, 0, 0), 81 | forces: [], 82 | }, 83 | ); 84 | 85 | // Create a static platform. 86 | const platform = new PhysicsObject( 87 | "platform", 88 | { 89 | mass: 1e9, 90 | friction: 0.4, 91 | elasticity: 0.4, 92 | collisionMethod: "box", 93 | size: new Vector3(20, 2, 20), 94 | ignoreGlobalForces: true, 95 | }, 96 | { 97 | position: new Vector3(0, 0, 0), 98 | velocity: new Vector3(0, 0, 0), 99 | acceleration: new Vector3(0, 0, 0), 100 | forces: [], 101 | }, 102 | ); 103 | 104 | // Add objects to the engine. 105 | engine.AddObject("ball", ball); 106 | engine.AddObject("platform", platform); 107 | 108 | // Add gravity. 109 | engine.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 110 | 111 | // Simulate. 112 | game.GetService("RunService").Stepped.Connect((_, dt) => { 113 | engine.Step(dt); 114 | }); 115 | ``` 116 | 117 | ### Luau 118 | 119 | ```lua 120 | local delta = require(game.ReplicatedStorage.delta) 121 | local PhysicsEngine = delta.PhysicsEngine 122 | local PhysicsObject = delta.PhysicsObject 123 | 124 | local engine = PhysicsEngine.new() 125 | engine:Start() 126 | 127 | local ball = PhysicsObject.new("ball", { 128 | mass = 10, 129 | friction = 0.5, 130 | elasticity = 0.8, 131 | collisionMethod = "box", 132 | size = Vector3.new(2, 2, 2), 133 | ignoreGlobalForces = false, 134 | }, { 135 | position = Vector3.new(0, 10, 0), 136 | velocity = Vector3.new(0, 0, 0), 137 | acceleration = Vector3.new(0, 0, 0), 138 | forces = {} 139 | }) 140 | 141 | local platform = PhysicsObject.new("platform", { 142 | mass = 1e9, 143 | friction = 0.4, 144 | elasticity = 0.4, 145 | collisionMethod = "box", 146 | size = Vector3.new(20, 2, 20), 147 | ignoreGlobalForces = true, 148 | }, { 149 | position = Vector3.new(0, 0, 0), 150 | velocity = Vector3.new(0, 0, 0), 151 | acceleration = Vector3.new(0, 0, 0), 152 | forces = {} 153 | }) 154 | 155 | engine:AddObject("ball", ball) 156 | engine:AddObject("platform", platform) 157 | engine:AddGlobalForce("gravity", Vector3.new(0, -9.81, 0)) 158 | 159 | game:GetService("RunService").Stepped:Connect(function(_, dt) 160 | engine:Step(dt) 161 | end) 162 | ``` 163 | 164 | ## API Documentation 165 | 166 | ### PhysicsObject 167 | 168 | - **Constructor:** 169 | `new(id: string, properties: PhysicsObjectProperties, state?: ObjectState, part?: BasePart)` 170 | Creates a new physics object. 171 | _Properties:_ 172 | 173 | - `mass`: Mass of the object. 174 | - `friction`: Friction coefficient (default 0.3). 175 | - `damping`: Additional damping for contact with static objects (default 0.5). 176 | - `elasticity`: Restitution coefficient (default 0.4). 177 | - `momentOfInertia`: Rotational inertia (default 1). 178 | - `collisionMethod`: "box", "point", or "none". 179 | - `size`: For box collisions, the dimensions of the hitbox. 180 | - `ignoreGlobalForces`: If true, the object will not receive global forces (e.g. static objects). 181 | 182 | - **ApplyForce(force: Vector3, offset?: Vector3): void** 183 | Applies a force (and optional offset for torque) to the object. 184 | 185 | - **Update(dt: number): void** 186 | Updates the object state (position, velocity, rotation) over a time step. 187 | 188 | - **GetWorldPosition(): Vector3** 189 | Returns the current world position. 190 | 191 | - **GetState(): PhysicsObjectState** 192 | Returns the current state (position, velocity, acceleration). 193 | 194 | - **Destroy(): void** 195 | Cleans up the object. 196 | 197 | ### PhysicsEngine 198 | 199 | - **Constructor:** 200 | `new()` 201 | Creates a new physics engine instance. 202 | 203 | - **Start(): void** 204 | Starts the simulation. 205 | 206 | - **Stop(): void** 207 | Stops the simulation. 208 | 209 | - **Step(realDeltaTime: number): void** 210 | Advances the simulation by the given delta time (adjusted by timeScale). 211 | 212 | - **SetTime(targetTime: number): void** 213 | Advances the simulation until the current time reaches the target time. 214 | 215 | - **AddGlobalForce(name: string, accel: Vector3): void** 216 | Adds a global force (provided as acceleration) to all dynamic objects. 217 | 218 | - **AddObject(id: string, object: PhysicsObject): void** 219 | Adds a physics object to the simulation. 220 | 221 | - **DestroyObject(id: string): void** 222 | Removes and destroys the object with the given id. 223 | 224 | - **GetObject(id: string): PhysicsObject | undefined** 225 | Retrieves an object by its id. 226 | 227 | - **GetObjects(): PhysicsObject[]** 228 | Returns all physics objects. 229 | 230 | ## Notes 231 | 232 | - **Rotational Collisions:** 233 | The library currently updates rotation via applied torques but doesn't automatically compute collision based torques. This will be added sometime in the future 234 | 235 | - **Performance:** 236 | Point collision detection is simpler and is more performant than box collision. However, the performance difference is generally negligible unless you have a very large number of objects 237 | 238 | - **Customization:** 239 | Adjust parameters (mass, friction, damping, elasticity, fixedDeltaTime) to tune the simulation behavior to your needs 240 | 241 | ## License 242 | 243 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 244 | -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delta", 3 | "globIgnorePaths": ["**/package.json", "**/tsconfig.json"], 4 | "tree": { 5 | "$path": "out", 6 | "include": { 7 | "$path": "include", 8 | "node_modules": { 9 | "$className": "Folder", 10 | "@rbxts": { 11 | "$path": "node_modules/@rbxts" 12 | } 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/delta", 3 | "version": "1.0.0", 4 | "description": "A lightweight deterministic physics library for Roblox.", 5 | "main": "out/init.lua", 6 | "scripts": { 7 | "build-npm": "rbxtsc --type package", 8 | "build": "rbxtsc", 9 | "watch": "concurrently \"rbxtsc -w\" \"rojo serve\"", 10 | "prepublishOnly": "pnpm run build" 11 | }, 12 | "keywords": [], 13 | "author": "prismatyx", 14 | "license": "MIT", 15 | "types": "out/index.d.ts", 16 | "files": [ 17 | "out", 18 | "!**/*.tsbuildinfo" 19 | ], 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "devDependencies": { 24 | "@rbxts/compiler-types": "3.0.0-types.0", 25 | "@rbxts/testez": "0.4.2-ts.0", 26 | "@rbxts/types": "^1.0.839", 27 | "@types/jest": "^29.5.14", 28 | "@types/mocha": "^10.0.10", 29 | "@typescript-eslint/eslint-plugin": "^8.25.0", 30 | "@typescript-eslint/parser": "^8.25.0", 31 | "eslint": "^9.21.0", 32 | "eslint-config-prettier": "^10.0.2", 33 | "eslint-plugin-prettier": "^5.2.3", 34 | "eslint-plugin-roblox-ts": "^0.0.36", 35 | "prettier": "^3.5.2", 36 | "roblox-ts": "^3.0.0", 37 | "typescript": "^5.8.2" 38 | }, 39 | "dependencies": { 40 | "@rbxts/janitor": "1.17.0-ts.1", 41 | "@rbxts/object-utils": "^1.0.4", 42 | "@rbxts/services": "^1.5.5" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/prisma-dev/delta.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/prisma-dev/delta/issues" 50 | }, 51 | "homepage": "https://github.com/prisma-dev/delta#readme" 52 | } 53 | -------------------------------------------------------------------------------- /pesde.lock: -------------------------------------------------------------------------------- 1 | name = "prismatyx/delta" 2 | version = "1.0.4" 3 | target = "roblox" 4 | 5 | [graph."0x5eal/unzip@0.1.3 luau"] 6 | resolved_ty = "standard" 7 | 8 | [graph."0x5eal/unzip@0.1.3 luau".pkg_ref] 9 | ref_ty = "pesde" 10 | index_url = "https://github.com/pesde-pkg/index" 11 | 12 | [graph."0x5eal/unzip@0.1.3 luau".pkg_ref.dependencies] 13 | asciitable = [{ name = "kimpure/asciitable", version = "^0.2.1", index = "https://github.com/pesde-pkg/index" }, "dev"] 14 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 15 | luau-lsp = [{ name = "pesde/luau_lsp", version = "=1.39.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 16 | stylua = [{ name = "pesde/stylua", version = "=2.0.2", index = "https://github.com/pesde-pkg/index", target = "lune" }, "dev"] 17 | 18 | [graph."corecii/greentea@0.4.11 lune"] 19 | resolved_ty = "standard" 20 | 21 | [graph."corecii/greentea@0.4.11 lune".pkg_ref] 22 | ref_ty = "pesde" 23 | index_url = "https://github.com/daimond113/pesde-index" 24 | 25 | [graph."jiwonz/dirs@0.3.0 lune"] 26 | resolved_ty = "standard" 27 | 28 | [graph."jiwonz/dirs@0.3.0 lune".dependencies] 29 | "corecii/greentea@0.4.11 lune" = "greentea" 30 | "jiwonz/pathfs@0.3.2 lune" = "pathfs" 31 | 32 | [graph."jiwonz/dirs@0.3.0 lune".pkg_ref] 33 | ref_ty = "pesde" 34 | index_url = "https://github.com/pesde-pkg/index" 35 | 36 | [graph."jiwonz/dirs@0.3.0 lune".pkg_ref.dependencies] 37 | greentea = [{ name = "corecii/greentea", version = "^0.4.10", index = "https://github.com/daimond113/pesde-index" }, "standard"] 38 | luau_lsp = [{ name = "pesde/luau_lsp", version = "^1.38.1", index = "https://github.com/daimond113/pesde-index" }, "dev"] 39 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.3.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] 40 | selene = [{ name = "pesde/selene", version = "^0.28.0", index = "https://github.com/daimond113/pesde-index" }, "dev"] 41 | stylua = [{ name = "pesde/stylua", version = "^2.0.2", index = "https://github.com/daimond113/pesde-index" }, "dev"] 42 | 43 | [graph."jiwonz/luau_disk@0.1.4 luau"] 44 | resolved_ty = "standard" 45 | 46 | [graph."jiwonz/luau_disk@0.1.4 luau".pkg_ref] 47 | ref_ty = "pesde" 48 | index_url = "https://github.com/pesde-pkg/index" 49 | 50 | [graph."jiwonz/luau_path@0.1.1 luau"] 51 | resolved_ty = "standard" 52 | 53 | [graph."jiwonz/luau_path@0.1.1 luau".dependencies] 54 | "jiwonz/luau_disk@0.1.4 luau" = "luau_disk" 55 | 56 | [graph."jiwonz/luau_path@0.1.1 luau".pkg_ref] 57 | ref_ty = "pesde" 58 | index_url = "https://github.com/daimond113/pesde-index" 59 | 60 | [graph."jiwonz/luau_path@0.1.1 luau".pkg_ref.dependencies] 61 | luau_disk = [{ name = "jiwonz/luau_disk", version = "^0.1.2", index = "https://github.com/pesde-pkg/index" }, "standard"] 62 | 63 | [graph."jiwonz/pathfs@0.3.2 lune"] 64 | resolved_ty = "standard" 65 | 66 | [graph."jiwonz/pathfs@0.3.2 lune".dependencies] 67 | "corecii/greentea@0.4.11 lune" = "greentea" 68 | "jiwonz/luau_path@0.1.1 luau" = "luau_path" 69 | 70 | [graph."jiwonz/pathfs@0.3.2 lune".pkg_ref] 71 | ref_ty = "pesde" 72 | index_url = "https://github.com/pesde-pkg/index" 73 | 74 | [graph."jiwonz/pathfs@0.3.2 lune".pkg_ref.dependencies] 75 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://github.com/daimond113/pesde-index" }, "dev"] 76 | greentea = [{ name = "corecii/greentea", version = "^0.4.11", index = "https://github.com/daimond113/pesde-index" }, "standard"] 77 | luau_check = [{ name = "jiwonz/luau_check", version = "^0.3.8", index = "https://github.com/daimond113/pesde-index" }, "dev"] 78 | luau_path = [{ name = "jiwonz/luau_path", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index", target = "luau" }, "standard"] 79 | 80 | [graph."lukadev_0/option@1.2.0 lune"] 81 | resolved_ty = "standard" 82 | 83 | [graph."lukadev_0/option@1.2.0 lune".pkg_ref] 84 | ref_ty = "pesde" 85 | index_url = "https://github.com/daimond113/pesde-index" 86 | 87 | [graph."lukadev_0/result@1.2.0 lune"] 88 | resolved_ty = "standard" 89 | 90 | [graph."lukadev_0/result@1.2.0 lune".pkg_ref] 91 | ref_ty = "pesde" 92 | index_url = "https://github.com/daimond113/pesde-index" 93 | 94 | [graph."pesde/rojo@7.4.4 lune"] 95 | direct = ["rojo", { name = "pesde/rojo", version = "^7.4.4", index = "default", target = "lune" }, "dev"] 96 | resolved_ty = "dev" 97 | 98 | [graph."pesde/rojo@7.4.4 lune".dependencies] 99 | "lukadev_0/option@1.2.0 lune" = "option" 100 | "lukadev_0/result@1.2.0 lune" = "result" 101 | "pesde/toolchainlib@0.1.12 lune" = "core" 102 | 103 | [graph."pesde/rojo@7.4.4 lune".pkg_ref] 104 | ref_ty = "pesde" 105 | index_url = "https://github.com/pesde-pkg/index" 106 | 107 | [graph."pesde/rojo@7.4.4 lune".pkg_ref.dependencies] 108 | core = [{ name = "pesde/toolchainlib", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "standard"] 109 | option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] 110 | result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "standard"] 111 | 112 | [graph."pesde/scripts_core@0.0.1 lune"] 113 | resolved_ty = "standard" 114 | 115 | [graph."pesde/scripts_core@0.0.1 lune".pkg_ref] 116 | ref_ty = "pesde" 117 | index_url = "https://github.com/pesde-pkg/index" 118 | 119 | [graph."pesde/scripts_core@0.0.1 lune".pkg_ref.dependencies] 120 | frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://github.com/pesde-pkg/index" }, "dev"] 121 | luau-lsp = [{ name = "pesde/luau_lsp", version = "^1.36.0", index = "https://github.com/pesde-pkg/index" }, "dev"] 122 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/pesde-pkg/index" }, "dev"] 123 | stylua = [{ name = "pesde/stylua", version = "^2.0.1", index = "https://github.com/pesde-pkg/index" }, "dev"] 124 | 125 | [graph."pesde/scripts_rojo@0.1.0 lune"] 126 | direct = ["scripts", { name = "pesde/scripts_rojo", version = "^0.1.0", index = "default", target = "lune" }, "dev"] 127 | resolved_ty = "dev" 128 | 129 | [graph."pesde/scripts_rojo@0.1.0 lune".dependencies] 130 | "pesde/rojo@7.4.4 lune" = "rojo" 131 | "pesde/scripts_core@0.0.1 lune" = "core" 132 | 133 | [graph."pesde/scripts_rojo@0.1.0 lune".pkg_ref] 134 | ref_ty = "pesde" 135 | index_url = "https://github.com/pesde-pkg/index" 136 | 137 | [graph."pesde/scripts_rojo@0.1.0 lune".pkg_ref.dependencies] 138 | core = [{ name = "pesde/scripts_core", version = "^0.0.1", index = "https://github.com/pesde-pkg/index", target = "lune" }, "standard"] 139 | rojo = [{ name = "pesde/rojo", version = "^7.4.4", index = "https://github.com/pesde-pkg/index" }, "peer"] 140 | 141 | [graph."pesde/toolchainlib@0.1.12 lune"] 142 | resolved_ty = "standard" 143 | 144 | [graph."pesde/toolchainlib@0.1.12 lune".dependencies] 145 | "0x5eal/unzip@0.1.3 luau" = "unzip" 146 | "jiwonz/dirs@0.3.0 lune" = "dirs" 147 | "jiwonz/pathfs@0.3.2 lune" = "pathfs" 148 | "lukadev_0/option@1.2.0 lune" = "option" 149 | "lukadev_0/result@1.2.0 lune" = "result" 150 | 151 | [graph."pesde/toolchainlib@0.1.12 lune".pkg_ref] 152 | ref_ty = "pesde" 153 | index_url = "https://github.com/daimond113/pesde-index" 154 | 155 | [graph."pesde/toolchainlib@0.1.12 lune".pkg_ref.dependencies] 156 | dirs = [{ name = "jiwonz/dirs", version = "^0.3.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 157 | option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/pesde-pkg/index" }, "peer"] 158 | pathfs = [{ name = "jiwonz/pathfs", version = "^0.3.0", index = "https://github.com/pesde-pkg/index" }, "standard"] 159 | result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/pesde-pkg/index" }, "peer"] 160 | unzip = [{ name = "0x5eal/unzip", version = "^0.1.0", index = "https://github.com/pesde-pkg/index", target = "luau" }, "standard"] 161 | -------------------------------------------------------------------------------- /pesde.toml: -------------------------------------------------------------------------------- 1 | name = "prismatyx/delta" 2 | version = "1.0.4" 3 | description = "Deterministic physics library for Roblox" 4 | authors = ["prismatyx"] 5 | repository = "https://github.com/prisma-dev/delta" 6 | license = "MIT" 7 | includes = ["out/**/*.luau", "out/**/*.lua","README.md","pesde.toml"] 8 | 9 | [target] 10 | environment = "roblox" 11 | lib = "out/init.luau" 12 | build_files = ["out"] 13 | 14 | [indices] 15 | default = "https://github.com/pesde-pkg/index" 16 | 17 | [scripts] 18 | roblox_sync_config_generator = ".pesde/scripts/roblox_sync_config_generator.luau" 19 | sourcemap_generator = ".pesde/scripts/sourcemap_generator.luau" 20 | 21 | [dev_dependencies] 22 | scripts = { name = "pesde/scripts_rojo", version = "^0.1.0", target = "lune" } 23 | rojo = { name = "pesde/rojo", version = "^7.4.4", target = "lune" } 24 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@rbxts/janitor': 12 | specifier: 1.17.0-ts.1 13 | version: 1.17.0-ts.1 14 | '@rbxts/object-utils': 15 | specifier: ^1.0.4 16 | version: 1.0.4 17 | '@rbxts/services': 18 | specifier: ^1.5.5 19 | version: 1.5.5 20 | devDependencies: 21 | '@rbxts/compiler-types': 22 | specifier: 3.0.0-types.0 23 | version: 3.0.0-types.0 24 | '@rbxts/testez': 25 | specifier: 0.4.2-ts.0 26 | version: 0.4.2-ts.0 27 | '@rbxts/types': 28 | specifier: ^1.0.839 29 | version: 1.0.839 30 | '@types/jest': 31 | specifier: ^29.5.14 32 | version: 29.5.14 33 | '@types/mocha': 34 | specifier: ^10.0.10 35 | version: 10.0.10 36 | '@typescript-eslint/eslint-plugin': 37 | specifier: ^8.25.0 38 | version: 8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) 39 | '@typescript-eslint/parser': 40 | specifier: ^8.25.0 41 | version: 8.25.0(eslint@9.21.0)(typescript@5.8.2) 42 | eslint: 43 | specifier: ^9.21.0 44 | version: 9.21.0 45 | eslint-config-prettier: 46 | specifier: ^10.0.2 47 | version: 10.0.2(eslint@9.21.0) 48 | eslint-plugin-prettier: 49 | specifier: ^5.2.3 50 | version: 5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.2) 51 | eslint-plugin-roblox-ts: 52 | specifier: ^0.0.36 53 | version: 0.0.36(eslint@9.21.0) 54 | prettier: 55 | specifier: ^3.5.2 56 | version: 3.5.2 57 | roblox-ts: 58 | specifier: ^3.0.0 59 | version: 3.0.0 60 | typescript: 61 | specifier: ^5.8.2 62 | version: 5.8.2 63 | 64 | packages: 65 | 66 | '@babel/code-frame@7.26.2': 67 | resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} 68 | engines: {node: '>=6.9.0'} 69 | 70 | '@babel/helper-validator-identifier@7.25.9': 71 | resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} 72 | engines: {node: '>=6.9.0'} 73 | 74 | '@eslint-community/eslint-utils@4.4.1': 75 | resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} 76 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 77 | peerDependencies: 78 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 79 | 80 | '@eslint-community/regexpp@4.12.1': 81 | resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 82 | engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 83 | 84 | '@eslint/config-array@0.19.2': 85 | resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} 86 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 87 | 88 | '@eslint/core@0.12.0': 89 | resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} 90 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 91 | 92 | '@eslint/eslintrc@3.3.0': 93 | resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} 94 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 95 | 96 | '@eslint/js@9.21.0': 97 | resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} 98 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 99 | 100 | '@eslint/object-schema@2.1.6': 101 | resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} 102 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 103 | 104 | '@eslint/plugin-kit@0.2.7': 105 | resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} 106 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 107 | 108 | '@humanfs/core@0.19.1': 109 | resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} 110 | engines: {node: '>=18.18.0'} 111 | 112 | '@humanfs/node@0.16.6': 113 | resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} 114 | engines: {node: '>=18.18.0'} 115 | 116 | '@humanwhocodes/module-importer@1.0.1': 117 | resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 118 | engines: {node: '>=12.22'} 119 | 120 | '@humanwhocodes/retry@0.3.1': 121 | resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} 122 | engines: {node: '>=18.18'} 123 | 124 | '@humanwhocodes/retry@0.4.2': 125 | resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} 126 | engines: {node: '>=18.18'} 127 | 128 | '@jest/expect-utils@29.7.0': 129 | resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} 130 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 131 | 132 | '@jest/schemas@29.6.3': 133 | resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} 134 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 135 | 136 | '@jest/types@29.6.3': 137 | resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} 138 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 139 | 140 | '@nodelib/fs.scandir@2.1.5': 141 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 142 | engines: {node: '>= 8'} 143 | 144 | '@nodelib/fs.stat@2.0.5': 145 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 146 | engines: {node: '>= 8'} 147 | 148 | '@nodelib/fs.walk@1.2.8': 149 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 150 | engines: {node: '>= 8'} 151 | 152 | '@pkgr/core@0.1.1': 153 | resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} 154 | engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 155 | 156 | '@rbxts/compiler-types@3.0.0-types.0': 157 | resolution: {integrity: sha512-VGOHJPoL7+56NTatMGqQj3K7xWuzEV+aP4QD5vZiHu+bcff3kiTmtoadaF6NkJrmwfFAvbsd4Dg764ZjWNceag==} 158 | 159 | '@rbxts/janitor@1.17.0-ts.1': 160 | resolution: {integrity: sha512-LFtmpiwx1lixjVH1HV0EUUPaDF8BCmw41vjgsXcRq+d4yl2dVmT+oVHrMkdYWxltSezjDpK+9GbL18twlAGSwg==} 161 | 162 | '@rbxts/object-utils@1.0.4': 163 | resolution: {integrity: sha512-dLLhf022ipV+9i910sOE7kl9losKHoon0WgeerHqVMQA5EYsLUsVT2AxhJuhk8MiDn5oJ2GiFofE/LadY9TpJQ==} 164 | 165 | '@rbxts/services@1.5.5': 166 | resolution: {integrity: sha512-Hu7QH2ecefS60zpakKG3T2vgABydoOJqpSG7nXr59wFAbPk/cA+OcsntYPHUWyHAat0VFGZ5jNZLTLdiqfssvg==} 167 | 168 | '@rbxts/testez@0.4.2-ts.0': 169 | resolution: {integrity: sha512-8Q+OG9ddTD2P3aARCuRKpPqUBvuifgSnHvQMZ6jBMqUzxhIysnb0l4c3vnnaQnvdyZ1OW9tKxcdEHMGTb67uOw==} 170 | 171 | '@rbxts/types@1.0.839': 172 | resolution: {integrity: sha512-/xU81ua7ULRo6msHp8kFhcEd1RV/jhsKbmrUrQ1MIU9picaWNnrBRm/QVTI5XsaeSHGmhaKYbfVIi+2xvb6Osg==} 173 | 174 | '@roblox-ts/luau-ast@2.0.0': 175 | resolution: {integrity: sha512-cmMi093IdwBOLVxwuordhM8AmtbyTIyRpsTbB0D/JauidW4SXsQRQowSwWjHo4QP0DRJBXvOIlxtqEQi50uNzQ==} 176 | 177 | '@roblox-ts/path-translator@1.1.0': 178 | resolution: {integrity: sha512-D0akTmnNYqBw+ZIek5JxocT3BjmbgGOuOy0x1nIIxHBPNLGCpzseToY8jyYs/0mlvnN2xnSP/k8Tv+jvGOQSwQ==} 179 | 180 | '@roblox-ts/rojo-resolver@1.1.0': 181 | resolution: {integrity: sha512-QmvVryu1EeME+3QUoG5j/gHGJoJUaffCgZ92mhlG7cJSd1uyhgpY4CNWriZAwZJYkTlzd5Htkpn+18yDFbOFXA==} 182 | 183 | '@sinclair/typebox@0.27.8': 184 | resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} 185 | 186 | '@types/estree@1.0.6': 187 | resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} 188 | 189 | '@types/istanbul-lib-coverage@2.0.6': 190 | resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} 191 | 192 | '@types/istanbul-lib-report@3.0.3': 193 | resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} 194 | 195 | '@types/istanbul-reports@3.0.4': 196 | resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} 197 | 198 | '@types/jest@29.5.14': 199 | resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} 200 | 201 | '@types/json-schema@7.0.15': 202 | resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 203 | 204 | '@types/mocha@10.0.10': 205 | resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} 206 | 207 | '@types/node@20.17.19': 208 | resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==} 209 | 210 | '@types/semver@7.5.8': 211 | resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} 212 | 213 | '@types/stack-utils@2.0.3': 214 | resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} 215 | 216 | '@types/yargs-parser@21.0.3': 217 | resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} 218 | 219 | '@types/yargs@17.0.33': 220 | resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} 221 | 222 | '@typescript-eslint/eslint-plugin@8.25.0': 223 | resolution: {integrity: sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==} 224 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 225 | peerDependencies: 226 | '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 227 | eslint: ^8.57.0 || ^9.0.0 228 | typescript: '>=4.8.4 <5.8.0' 229 | 230 | '@typescript-eslint/experimental-utils@5.62.0': 231 | resolution: {integrity: sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==} 232 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 233 | peerDependencies: 234 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 235 | 236 | '@typescript-eslint/parser@8.25.0': 237 | resolution: {integrity: sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==} 238 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 239 | peerDependencies: 240 | eslint: ^8.57.0 || ^9.0.0 241 | typescript: '>=4.8.4 <5.8.0' 242 | 243 | '@typescript-eslint/scope-manager@5.62.0': 244 | resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} 245 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 246 | 247 | '@typescript-eslint/scope-manager@8.25.0': 248 | resolution: {integrity: sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==} 249 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 250 | 251 | '@typescript-eslint/type-utils@8.25.0': 252 | resolution: {integrity: sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==} 253 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 254 | peerDependencies: 255 | eslint: ^8.57.0 || ^9.0.0 256 | typescript: '>=4.8.4 <5.8.0' 257 | 258 | '@typescript-eslint/types@5.62.0': 259 | resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} 260 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 261 | 262 | '@typescript-eslint/types@8.25.0': 263 | resolution: {integrity: sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==} 264 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 265 | 266 | '@typescript-eslint/typescript-estree@5.62.0': 267 | resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} 268 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 269 | peerDependencies: 270 | typescript: '*' 271 | peerDependenciesMeta: 272 | typescript: 273 | optional: true 274 | 275 | '@typescript-eslint/typescript-estree@8.25.0': 276 | resolution: {integrity: sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==} 277 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 278 | peerDependencies: 279 | typescript: '>=4.8.4 <5.8.0' 280 | 281 | '@typescript-eslint/utils@5.62.0': 282 | resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} 283 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 284 | peerDependencies: 285 | eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 286 | 287 | '@typescript-eslint/utils@8.25.0': 288 | resolution: {integrity: sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==} 289 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 290 | peerDependencies: 291 | eslint: ^8.57.0 || ^9.0.0 292 | typescript: '>=4.8.4 <5.8.0' 293 | 294 | '@typescript-eslint/visitor-keys@5.62.0': 295 | resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} 296 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 297 | 298 | '@typescript-eslint/visitor-keys@8.25.0': 299 | resolution: {integrity: sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==} 300 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 301 | 302 | acorn-jsx@5.3.2: 303 | resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 304 | peerDependencies: 305 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 306 | 307 | acorn@8.14.0: 308 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 309 | engines: {node: '>=0.4.0'} 310 | hasBin: true 311 | 312 | ajv@6.12.6: 313 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 314 | 315 | ajv@8.17.1: 316 | resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} 317 | 318 | ansi-regex@5.0.1: 319 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 320 | engines: {node: '>=8'} 321 | 322 | ansi-styles@4.3.0: 323 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 324 | engines: {node: '>=8'} 325 | 326 | ansi-styles@5.2.0: 327 | resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} 328 | engines: {node: '>=10'} 329 | 330 | anymatch@3.1.3: 331 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 332 | engines: {node: '>= 8'} 333 | 334 | argparse@2.0.1: 335 | resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 336 | 337 | array-union@2.1.0: 338 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 339 | engines: {node: '>=8'} 340 | 341 | balanced-match@1.0.2: 342 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 343 | 344 | binary-extensions@2.3.0: 345 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 346 | engines: {node: '>=8'} 347 | 348 | brace-expansion@1.1.11: 349 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 350 | 351 | brace-expansion@2.0.1: 352 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 353 | 354 | braces@3.0.3: 355 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 356 | engines: {node: '>=8'} 357 | 358 | callsites@3.1.0: 359 | resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 360 | engines: {node: '>=6'} 361 | 362 | chalk@4.1.2: 363 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 364 | engines: {node: '>=10'} 365 | 366 | chokidar@3.6.0: 367 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 368 | engines: {node: '>= 8.10.0'} 369 | 370 | ci-info@3.9.0: 371 | resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} 372 | engines: {node: '>=8'} 373 | 374 | cliui@8.0.1: 375 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 376 | engines: {node: '>=12'} 377 | 378 | color-convert@2.0.1: 379 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 380 | engines: {node: '>=7.0.0'} 381 | 382 | color-name@1.1.4: 383 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 384 | 385 | concat-map@0.0.1: 386 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 387 | 388 | cross-spawn@7.0.6: 389 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 390 | engines: {node: '>= 8'} 391 | 392 | debug@4.4.0: 393 | resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} 394 | engines: {node: '>=6.0'} 395 | peerDependencies: 396 | supports-color: '*' 397 | peerDependenciesMeta: 398 | supports-color: 399 | optional: true 400 | 401 | deep-is@0.1.4: 402 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 403 | 404 | diff-sequences@29.6.3: 405 | resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} 406 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 407 | 408 | dir-glob@3.0.1: 409 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 410 | engines: {node: '>=8'} 411 | 412 | emoji-regex@8.0.0: 413 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 414 | 415 | escalade@3.2.0: 416 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 417 | engines: {node: '>=6'} 418 | 419 | escape-string-regexp@2.0.0: 420 | resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} 421 | engines: {node: '>=8'} 422 | 423 | escape-string-regexp@4.0.0: 424 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 425 | engines: {node: '>=10'} 426 | 427 | eslint-config-prettier@10.0.2: 428 | resolution: {integrity: sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==} 429 | hasBin: true 430 | peerDependencies: 431 | eslint: '>=7.0.0' 432 | 433 | eslint-plugin-prettier@5.2.3: 434 | resolution: {integrity: sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==} 435 | engines: {node: ^14.18.0 || >=16.0.0} 436 | peerDependencies: 437 | '@types/eslint': '>=8.0.0' 438 | eslint: '>=8.0.0' 439 | eslint-config-prettier: '*' 440 | prettier: '>=3.0.0' 441 | peerDependenciesMeta: 442 | '@types/eslint': 443 | optional: true 444 | eslint-config-prettier: 445 | optional: true 446 | 447 | eslint-plugin-roblox-ts@0.0.36: 448 | resolution: {integrity: sha512-vlbgfHY1heWSyDunVX3gIOamCy28KqrJlDkmuDOyYVBo8uLqdvbFkbWkdGxn59Vky1m6pFcvZGU6TObfmwm5Pg==} 449 | engines: {node: '>=0.10.0'} 450 | 451 | eslint-scope@5.1.1: 452 | resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 453 | engines: {node: '>=8.0.0'} 454 | 455 | eslint-scope@8.2.0: 456 | resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} 457 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 458 | 459 | eslint-visitor-keys@3.4.3: 460 | resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 461 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 462 | 463 | eslint-visitor-keys@4.2.0: 464 | resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} 465 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 466 | 467 | eslint@9.21.0: 468 | resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} 469 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 470 | hasBin: true 471 | peerDependencies: 472 | jiti: '*' 473 | peerDependenciesMeta: 474 | jiti: 475 | optional: true 476 | 477 | espree@10.3.0: 478 | resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} 479 | engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 480 | 481 | esquery@1.6.0: 482 | resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 483 | engines: {node: '>=0.10'} 484 | 485 | esrecurse@4.3.0: 486 | resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 487 | engines: {node: '>=4.0'} 488 | 489 | estraverse@4.3.0: 490 | resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} 491 | engines: {node: '>=4.0'} 492 | 493 | estraverse@5.3.0: 494 | resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 495 | engines: {node: '>=4.0'} 496 | 497 | esutils@2.0.3: 498 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 499 | engines: {node: '>=0.10.0'} 500 | 501 | expect@29.7.0: 502 | resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} 503 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 504 | 505 | fast-deep-equal@3.1.3: 506 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 507 | 508 | fast-diff@1.3.0: 509 | resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} 510 | 511 | fast-glob@3.3.3: 512 | resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 513 | engines: {node: '>=8.6.0'} 514 | 515 | fast-json-stable-stringify@2.1.0: 516 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 517 | 518 | fast-levenshtein@2.0.6: 519 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 520 | 521 | fast-uri@3.0.6: 522 | resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} 523 | 524 | fastq@1.19.1: 525 | resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 526 | 527 | file-entry-cache@8.0.0: 528 | resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} 529 | engines: {node: '>=16.0.0'} 530 | 531 | fill-range@7.1.1: 532 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 533 | engines: {node: '>=8'} 534 | 535 | find-up@5.0.0: 536 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 537 | engines: {node: '>=10'} 538 | 539 | flat-cache@4.0.1: 540 | resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} 541 | engines: {node: '>=16'} 542 | 543 | flatted@3.3.3: 544 | resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 545 | 546 | fs-extra@11.3.0: 547 | resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} 548 | engines: {node: '>=14.14'} 549 | 550 | fsevents@2.3.3: 551 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 552 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 553 | os: [darwin] 554 | 555 | function-bind@1.1.2: 556 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 557 | 558 | get-caller-file@2.0.5: 559 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 560 | engines: {node: 6.* || 8.* || >= 10.*} 561 | 562 | glob-parent@5.1.2: 563 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 564 | engines: {node: '>= 6'} 565 | 566 | glob-parent@6.0.2: 567 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 568 | engines: {node: '>=10.13.0'} 569 | 570 | globals@14.0.0: 571 | resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} 572 | engines: {node: '>=18'} 573 | 574 | globby@11.1.0: 575 | resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 576 | engines: {node: '>=10'} 577 | 578 | graceful-fs@4.2.11: 579 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 580 | 581 | graphemer@1.4.0: 582 | resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 583 | 584 | has-flag@4.0.0: 585 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 586 | engines: {node: '>=8'} 587 | 588 | hasown@2.0.2: 589 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 590 | engines: {node: '>= 0.4'} 591 | 592 | ignore@5.3.2: 593 | resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 594 | engines: {node: '>= 4'} 595 | 596 | import-fresh@3.3.1: 597 | resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 598 | engines: {node: '>=6'} 599 | 600 | imurmurhash@0.1.4: 601 | resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 602 | engines: {node: '>=0.8.19'} 603 | 604 | is-binary-path@2.1.0: 605 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 606 | engines: {node: '>=8'} 607 | 608 | is-core-module@2.16.1: 609 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 610 | engines: {node: '>= 0.4'} 611 | 612 | is-extglob@2.1.1: 613 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 614 | engines: {node: '>=0.10.0'} 615 | 616 | is-fullwidth-code-point@3.0.0: 617 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 618 | engines: {node: '>=8'} 619 | 620 | is-glob@4.0.3: 621 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 622 | engines: {node: '>=0.10.0'} 623 | 624 | is-number@7.0.0: 625 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 626 | engines: {node: '>=0.12.0'} 627 | 628 | isexe@2.0.0: 629 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 630 | 631 | jest-diff@29.7.0: 632 | resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} 633 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 634 | 635 | jest-get-type@29.6.3: 636 | resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} 637 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 638 | 639 | jest-matcher-utils@29.7.0: 640 | resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} 641 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 642 | 643 | jest-message-util@29.7.0: 644 | resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} 645 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 646 | 647 | jest-util@29.7.0: 648 | resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} 649 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 650 | 651 | js-tokens@4.0.0: 652 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 653 | 654 | js-yaml@4.1.0: 655 | resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 656 | hasBin: true 657 | 658 | json-buffer@3.0.1: 659 | resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 660 | 661 | json-schema-traverse@0.4.1: 662 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 663 | 664 | json-schema-traverse@1.0.0: 665 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} 666 | 667 | json-stable-stringify-without-jsonify@1.0.1: 668 | resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 669 | 670 | jsonfile@6.1.0: 671 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 672 | 673 | keyv@4.5.4: 674 | resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 675 | 676 | kleur@4.1.5: 677 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 678 | engines: {node: '>=6'} 679 | 680 | levn@0.4.1: 681 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 682 | engines: {node: '>= 0.8.0'} 683 | 684 | locate-path@6.0.0: 685 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 686 | engines: {node: '>=10'} 687 | 688 | lodash.merge@4.6.2: 689 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 690 | 691 | merge2@1.4.1: 692 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 693 | engines: {node: '>= 8'} 694 | 695 | micromatch@4.0.8: 696 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 697 | engines: {node: '>=8.6'} 698 | 699 | minimatch@3.1.2: 700 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 701 | 702 | minimatch@9.0.5: 703 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 704 | engines: {node: '>=16 || 14 >=14.17'} 705 | 706 | ms@2.1.3: 707 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 708 | 709 | natural-compare@1.4.0: 710 | resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 711 | 712 | normalize-path@3.0.0: 713 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 714 | engines: {node: '>=0.10.0'} 715 | 716 | optionator@0.9.4: 717 | resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 718 | engines: {node: '>= 0.8.0'} 719 | 720 | p-limit@3.1.0: 721 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 722 | engines: {node: '>=10'} 723 | 724 | p-locate@5.0.0: 725 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 726 | engines: {node: '>=10'} 727 | 728 | parent-module@1.0.1: 729 | resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 730 | engines: {node: '>=6'} 731 | 732 | path-exists@4.0.0: 733 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 734 | engines: {node: '>=8'} 735 | 736 | path-key@3.1.1: 737 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 738 | engines: {node: '>=8'} 739 | 740 | path-parse@1.0.7: 741 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 742 | 743 | path-type@4.0.0: 744 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 745 | engines: {node: '>=8'} 746 | 747 | picocolors@1.1.1: 748 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 749 | 750 | picomatch@2.3.1: 751 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 752 | engines: {node: '>=8.6'} 753 | 754 | prelude-ls@1.2.1: 755 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 756 | engines: {node: '>= 0.8.0'} 757 | 758 | prettier-linter-helpers@1.0.0: 759 | resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} 760 | engines: {node: '>=6.0.0'} 761 | 762 | prettier@3.5.2: 763 | resolution: {integrity: sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==} 764 | engines: {node: '>=14'} 765 | hasBin: true 766 | 767 | pretty-format@29.7.0: 768 | resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} 769 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 770 | 771 | punycode@2.3.1: 772 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 773 | engines: {node: '>=6'} 774 | 775 | queue-microtask@1.2.3: 776 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 777 | 778 | react-is@18.3.1: 779 | resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} 780 | 781 | readdirp@3.6.0: 782 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 783 | engines: {node: '>=8.10.0'} 784 | 785 | require-directory@2.1.1: 786 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 787 | engines: {node: '>=0.10.0'} 788 | 789 | require-from-string@2.0.2: 790 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 791 | engines: {node: '>=0.10.0'} 792 | 793 | resolve-from@4.0.0: 794 | resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 795 | engines: {node: '>=4'} 796 | 797 | resolve@1.22.10: 798 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 799 | engines: {node: '>= 0.4'} 800 | hasBin: true 801 | 802 | reusify@1.1.0: 803 | resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 804 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 805 | 806 | roblox-ts@3.0.0: 807 | resolution: {integrity: sha512-hwAC2frIFlLJOtHd6F+5opMEhBgfAMK9z5l1mP+bykLBbMO5cn1q5lIwhhXbeh9Pq07rlhF8uGHlmeRLPd/3AA==} 808 | hasBin: true 809 | 810 | run-parallel@1.2.0: 811 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 812 | 813 | semver@7.7.1: 814 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 815 | engines: {node: '>=10'} 816 | hasBin: true 817 | 818 | shebang-command@2.0.0: 819 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 820 | engines: {node: '>=8'} 821 | 822 | shebang-regex@3.0.0: 823 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 824 | engines: {node: '>=8'} 825 | 826 | slash@3.0.0: 827 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 828 | engines: {node: '>=8'} 829 | 830 | stack-utils@2.0.6: 831 | resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} 832 | engines: {node: '>=10'} 833 | 834 | string-width@4.2.3: 835 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 836 | engines: {node: '>=8'} 837 | 838 | strip-ansi@6.0.1: 839 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 840 | engines: {node: '>=8'} 841 | 842 | strip-json-comments@3.1.1: 843 | resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 844 | engines: {node: '>=8'} 845 | 846 | supports-color@7.2.0: 847 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 848 | engines: {node: '>=8'} 849 | 850 | supports-preserve-symlinks-flag@1.0.0: 851 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 852 | engines: {node: '>= 0.4'} 853 | 854 | synckit@0.9.2: 855 | resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} 856 | engines: {node: ^14.18.0 || >=16.0.0} 857 | 858 | to-regex-range@5.0.1: 859 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 860 | engines: {node: '>=8.0'} 861 | 862 | ts-api-utils@2.0.1: 863 | resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} 864 | engines: {node: '>=18.12'} 865 | peerDependencies: 866 | typescript: '>=4.8.4' 867 | 868 | tslib@1.14.1: 869 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 870 | 871 | tslib@2.8.1: 872 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 873 | 874 | tsutils@3.21.0: 875 | resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} 876 | engines: {node: '>= 6'} 877 | peerDependencies: 878 | typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' 879 | 880 | type-check@0.4.0: 881 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 882 | engines: {node: '>= 0.8.0'} 883 | 884 | typescript@5.5.3: 885 | resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} 886 | engines: {node: '>=14.17'} 887 | hasBin: true 888 | 889 | typescript@5.8.2: 890 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} 891 | engines: {node: '>=14.17'} 892 | hasBin: true 893 | 894 | undici-types@6.19.8: 895 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 896 | 897 | universalify@2.0.1: 898 | resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} 899 | engines: {node: '>= 10.0.0'} 900 | 901 | uri-js@4.4.1: 902 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 903 | 904 | which@2.0.2: 905 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 906 | engines: {node: '>= 8'} 907 | hasBin: true 908 | 909 | word-wrap@1.2.5: 910 | resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 911 | engines: {node: '>=0.10.0'} 912 | 913 | wrap-ansi@7.0.0: 914 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 915 | engines: {node: '>=10'} 916 | 917 | y18n@5.0.8: 918 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 919 | engines: {node: '>=10'} 920 | 921 | yargs-parser@21.1.1: 922 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 923 | engines: {node: '>=12'} 924 | 925 | yargs@17.7.2: 926 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 927 | engines: {node: '>=12'} 928 | 929 | yocto-queue@0.1.0: 930 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 931 | engines: {node: '>=10'} 932 | 933 | snapshots: 934 | 935 | '@babel/code-frame@7.26.2': 936 | dependencies: 937 | '@babel/helper-validator-identifier': 7.25.9 938 | js-tokens: 4.0.0 939 | picocolors: 1.1.1 940 | 941 | '@babel/helper-validator-identifier@7.25.9': {} 942 | 943 | '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0)': 944 | dependencies: 945 | eslint: 9.21.0 946 | eslint-visitor-keys: 3.4.3 947 | 948 | '@eslint-community/regexpp@4.12.1': {} 949 | 950 | '@eslint/config-array@0.19.2': 951 | dependencies: 952 | '@eslint/object-schema': 2.1.6 953 | debug: 4.4.0 954 | minimatch: 3.1.2 955 | transitivePeerDependencies: 956 | - supports-color 957 | 958 | '@eslint/core@0.12.0': 959 | dependencies: 960 | '@types/json-schema': 7.0.15 961 | 962 | '@eslint/eslintrc@3.3.0': 963 | dependencies: 964 | ajv: 6.12.6 965 | debug: 4.4.0 966 | espree: 10.3.0 967 | globals: 14.0.0 968 | ignore: 5.3.2 969 | import-fresh: 3.3.1 970 | js-yaml: 4.1.0 971 | minimatch: 3.1.2 972 | strip-json-comments: 3.1.1 973 | transitivePeerDependencies: 974 | - supports-color 975 | 976 | '@eslint/js@9.21.0': {} 977 | 978 | '@eslint/object-schema@2.1.6': {} 979 | 980 | '@eslint/plugin-kit@0.2.7': 981 | dependencies: 982 | '@eslint/core': 0.12.0 983 | levn: 0.4.1 984 | 985 | '@humanfs/core@0.19.1': {} 986 | 987 | '@humanfs/node@0.16.6': 988 | dependencies: 989 | '@humanfs/core': 0.19.1 990 | '@humanwhocodes/retry': 0.3.1 991 | 992 | '@humanwhocodes/module-importer@1.0.1': {} 993 | 994 | '@humanwhocodes/retry@0.3.1': {} 995 | 996 | '@humanwhocodes/retry@0.4.2': {} 997 | 998 | '@jest/expect-utils@29.7.0': 999 | dependencies: 1000 | jest-get-type: 29.6.3 1001 | 1002 | '@jest/schemas@29.6.3': 1003 | dependencies: 1004 | '@sinclair/typebox': 0.27.8 1005 | 1006 | '@jest/types@29.6.3': 1007 | dependencies: 1008 | '@jest/schemas': 29.6.3 1009 | '@types/istanbul-lib-coverage': 2.0.6 1010 | '@types/istanbul-reports': 3.0.4 1011 | '@types/node': 20.17.19 1012 | '@types/yargs': 17.0.33 1013 | chalk: 4.1.2 1014 | 1015 | '@nodelib/fs.scandir@2.1.5': 1016 | dependencies: 1017 | '@nodelib/fs.stat': 2.0.5 1018 | run-parallel: 1.2.0 1019 | 1020 | '@nodelib/fs.stat@2.0.5': {} 1021 | 1022 | '@nodelib/fs.walk@1.2.8': 1023 | dependencies: 1024 | '@nodelib/fs.scandir': 2.1.5 1025 | fastq: 1.19.1 1026 | 1027 | '@pkgr/core@0.1.1': {} 1028 | 1029 | '@rbxts/compiler-types@3.0.0-types.0': {} 1030 | 1031 | '@rbxts/janitor@1.17.0-ts.1': {} 1032 | 1033 | '@rbxts/object-utils@1.0.4': {} 1034 | 1035 | '@rbxts/services@1.5.5': {} 1036 | 1037 | '@rbxts/testez@0.4.2-ts.0': {} 1038 | 1039 | '@rbxts/types@1.0.839': {} 1040 | 1041 | '@roblox-ts/luau-ast@2.0.0': {} 1042 | 1043 | '@roblox-ts/path-translator@1.1.0': 1044 | dependencies: 1045 | ajv: 8.17.1 1046 | fs-extra: 11.3.0 1047 | 1048 | '@roblox-ts/rojo-resolver@1.1.0': 1049 | dependencies: 1050 | ajv: 8.17.1 1051 | fs-extra: 11.3.0 1052 | 1053 | '@sinclair/typebox@0.27.8': {} 1054 | 1055 | '@types/estree@1.0.6': {} 1056 | 1057 | '@types/istanbul-lib-coverage@2.0.6': {} 1058 | 1059 | '@types/istanbul-lib-report@3.0.3': 1060 | dependencies: 1061 | '@types/istanbul-lib-coverage': 2.0.6 1062 | 1063 | '@types/istanbul-reports@3.0.4': 1064 | dependencies: 1065 | '@types/istanbul-lib-report': 3.0.3 1066 | 1067 | '@types/jest@29.5.14': 1068 | dependencies: 1069 | expect: 29.7.0 1070 | pretty-format: 29.7.0 1071 | 1072 | '@types/json-schema@7.0.15': {} 1073 | 1074 | '@types/mocha@10.0.10': {} 1075 | 1076 | '@types/node@20.17.19': 1077 | dependencies: 1078 | undici-types: 6.19.8 1079 | 1080 | '@types/semver@7.5.8': {} 1081 | 1082 | '@types/stack-utils@2.0.3': {} 1083 | 1084 | '@types/yargs-parser@21.0.3': {} 1085 | 1086 | '@types/yargs@17.0.33': 1087 | dependencies: 1088 | '@types/yargs-parser': 21.0.3 1089 | 1090 | '@typescript-eslint/eslint-plugin@8.25.0(@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': 1091 | dependencies: 1092 | '@eslint-community/regexpp': 4.12.1 1093 | '@typescript-eslint/parser': 8.25.0(eslint@9.21.0)(typescript@5.8.2) 1094 | '@typescript-eslint/scope-manager': 8.25.0 1095 | '@typescript-eslint/type-utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) 1096 | '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) 1097 | '@typescript-eslint/visitor-keys': 8.25.0 1098 | eslint: 9.21.0 1099 | graphemer: 1.4.0 1100 | ignore: 5.3.2 1101 | natural-compare: 1.4.0 1102 | ts-api-utils: 2.0.1(typescript@5.8.2) 1103 | typescript: 5.8.2 1104 | transitivePeerDependencies: 1105 | - supports-color 1106 | 1107 | '@typescript-eslint/experimental-utils@5.62.0(eslint@9.21.0)(typescript@5.8.2)': 1108 | dependencies: 1109 | '@typescript-eslint/utils': 5.62.0(eslint@9.21.0)(typescript@5.8.2) 1110 | eslint: 9.21.0 1111 | transitivePeerDependencies: 1112 | - supports-color 1113 | - typescript 1114 | 1115 | '@typescript-eslint/parser@8.25.0(eslint@9.21.0)(typescript@5.8.2)': 1116 | dependencies: 1117 | '@typescript-eslint/scope-manager': 8.25.0 1118 | '@typescript-eslint/types': 8.25.0 1119 | '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 1120 | '@typescript-eslint/visitor-keys': 8.25.0 1121 | debug: 4.4.0 1122 | eslint: 9.21.0 1123 | typescript: 5.8.2 1124 | transitivePeerDependencies: 1125 | - supports-color 1126 | 1127 | '@typescript-eslint/scope-manager@5.62.0': 1128 | dependencies: 1129 | '@typescript-eslint/types': 5.62.0 1130 | '@typescript-eslint/visitor-keys': 5.62.0 1131 | 1132 | '@typescript-eslint/scope-manager@8.25.0': 1133 | dependencies: 1134 | '@typescript-eslint/types': 8.25.0 1135 | '@typescript-eslint/visitor-keys': 8.25.0 1136 | 1137 | '@typescript-eslint/type-utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)': 1138 | dependencies: 1139 | '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 1140 | '@typescript-eslint/utils': 8.25.0(eslint@9.21.0)(typescript@5.8.2) 1141 | debug: 4.4.0 1142 | eslint: 9.21.0 1143 | ts-api-utils: 2.0.1(typescript@5.8.2) 1144 | typescript: 5.8.2 1145 | transitivePeerDependencies: 1146 | - supports-color 1147 | 1148 | '@typescript-eslint/types@5.62.0': {} 1149 | 1150 | '@typescript-eslint/types@8.25.0': {} 1151 | 1152 | '@typescript-eslint/typescript-estree@5.62.0(typescript@5.8.2)': 1153 | dependencies: 1154 | '@typescript-eslint/types': 5.62.0 1155 | '@typescript-eslint/visitor-keys': 5.62.0 1156 | debug: 4.4.0 1157 | globby: 11.1.0 1158 | is-glob: 4.0.3 1159 | semver: 7.7.1 1160 | tsutils: 3.21.0(typescript@5.8.2) 1161 | optionalDependencies: 1162 | typescript: 5.8.2 1163 | transitivePeerDependencies: 1164 | - supports-color 1165 | 1166 | '@typescript-eslint/typescript-estree@8.25.0(typescript@5.8.2)': 1167 | dependencies: 1168 | '@typescript-eslint/types': 8.25.0 1169 | '@typescript-eslint/visitor-keys': 8.25.0 1170 | debug: 4.4.0 1171 | fast-glob: 3.3.3 1172 | is-glob: 4.0.3 1173 | minimatch: 9.0.5 1174 | semver: 7.7.1 1175 | ts-api-utils: 2.0.1(typescript@5.8.2) 1176 | typescript: 5.8.2 1177 | transitivePeerDependencies: 1178 | - supports-color 1179 | 1180 | '@typescript-eslint/utils@5.62.0(eslint@9.21.0)(typescript@5.8.2)': 1181 | dependencies: 1182 | '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) 1183 | '@types/json-schema': 7.0.15 1184 | '@types/semver': 7.5.8 1185 | '@typescript-eslint/scope-manager': 5.62.0 1186 | '@typescript-eslint/types': 5.62.0 1187 | '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) 1188 | eslint: 9.21.0 1189 | eslint-scope: 5.1.1 1190 | semver: 7.7.1 1191 | transitivePeerDependencies: 1192 | - supports-color 1193 | - typescript 1194 | 1195 | '@typescript-eslint/utils@8.25.0(eslint@9.21.0)(typescript@5.8.2)': 1196 | dependencies: 1197 | '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) 1198 | '@typescript-eslint/scope-manager': 8.25.0 1199 | '@typescript-eslint/types': 8.25.0 1200 | '@typescript-eslint/typescript-estree': 8.25.0(typescript@5.8.2) 1201 | eslint: 9.21.0 1202 | typescript: 5.8.2 1203 | transitivePeerDependencies: 1204 | - supports-color 1205 | 1206 | '@typescript-eslint/visitor-keys@5.62.0': 1207 | dependencies: 1208 | '@typescript-eslint/types': 5.62.0 1209 | eslint-visitor-keys: 3.4.3 1210 | 1211 | '@typescript-eslint/visitor-keys@8.25.0': 1212 | dependencies: 1213 | '@typescript-eslint/types': 8.25.0 1214 | eslint-visitor-keys: 4.2.0 1215 | 1216 | acorn-jsx@5.3.2(acorn@8.14.0): 1217 | dependencies: 1218 | acorn: 8.14.0 1219 | 1220 | acorn@8.14.0: {} 1221 | 1222 | ajv@6.12.6: 1223 | dependencies: 1224 | fast-deep-equal: 3.1.3 1225 | fast-json-stable-stringify: 2.1.0 1226 | json-schema-traverse: 0.4.1 1227 | uri-js: 4.4.1 1228 | 1229 | ajv@8.17.1: 1230 | dependencies: 1231 | fast-deep-equal: 3.1.3 1232 | fast-uri: 3.0.6 1233 | json-schema-traverse: 1.0.0 1234 | require-from-string: 2.0.2 1235 | 1236 | ansi-regex@5.0.1: {} 1237 | 1238 | ansi-styles@4.3.0: 1239 | dependencies: 1240 | color-convert: 2.0.1 1241 | 1242 | ansi-styles@5.2.0: {} 1243 | 1244 | anymatch@3.1.3: 1245 | dependencies: 1246 | normalize-path: 3.0.0 1247 | picomatch: 2.3.1 1248 | 1249 | argparse@2.0.1: {} 1250 | 1251 | array-union@2.1.0: {} 1252 | 1253 | balanced-match@1.0.2: {} 1254 | 1255 | binary-extensions@2.3.0: {} 1256 | 1257 | brace-expansion@1.1.11: 1258 | dependencies: 1259 | balanced-match: 1.0.2 1260 | concat-map: 0.0.1 1261 | 1262 | brace-expansion@2.0.1: 1263 | dependencies: 1264 | balanced-match: 1.0.2 1265 | 1266 | braces@3.0.3: 1267 | dependencies: 1268 | fill-range: 7.1.1 1269 | 1270 | callsites@3.1.0: {} 1271 | 1272 | chalk@4.1.2: 1273 | dependencies: 1274 | ansi-styles: 4.3.0 1275 | supports-color: 7.2.0 1276 | 1277 | chokidar@3.6.0: 1278 | dependencies: 1279 | anymatch: 3.1.3 1280 | braces: 3.0.3 1281 | glob-parent: 5.1.2 1282 | is-binary-path: 2.1.0 1283 | is-glob: 4.0.3 1284 | normalize-path: 3.0.0 1285 | readdirp: 3.6.0 1286 | optionalDependencies: 1287 | fsevents: 2.3.3 1288 | 1289 | ci-info@3.9.0: {} 1290 | 1291 | cliui@8.0.1: 1292 | dependencies: 1293 | string-width: 4.2.3 1294 | strip-ansi: 6.0.1 1295 | wrap-ansi: 7.0.0 1296 | 1297 | color-convert@2.0.1: 1298 | dependencies: 1299 | color-name: 1.1.4 1300 | 1301 | color-name@1.1.4: {} 1302 | 1303 | concat-map@0.0.1: {} 1304 | 1305 | cross-spawn@7.0.6: 1306 | dependencies: 1307 | path-key: 3.1.1 1308 | shebang-command: 2.0.0 1309 | which: 2.0.2 1310 | 1311 | debug@4.4.0: 1312 | dependencies: 1313 | ms: 2.1.3 1314 | 1315 | deep-is@0.1.4: {} 1316 | 1317 | diff-sequences@29.6.3: {} 1318 | 1319 | dir-glob@3.0.1: 1320 | dependencies: 1321 | path-type: 4.0.0 1322 | 1323 | emoji-regex@8.0.0: {} 1324 | 1325 | escalade@3.2.0: {} 1326 | 1327 | escape-string-regexp@2.0.0: {} 1328 | 1329 | escape-string-regexp@4.0.0: {} 1330 | 1331 | eslint-config-prettier@10.0.2(eslint@9.21.0): 1332 | dependencies: 1333 | eslint: 9.21.0 1334 | 1335 | eslint-plugin-prettier@5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.2): 1336 | dependencies: 1337 | eslint: 9.21.0 1338 | prettier: 3.5.2 1339 | prettier-linter-helpers: 1.0.0 1340 | synckit: 0.9.2 1341 | optionalDependencies: 1342 | eslint-config-prettier: 10.0.2(eslint@9.21.0) 1343 | 1344 | eslint-plugin-roblox-ts@0.0.36(eslint@9.21.0): 1345 | dependencies: 1346 | '@types/node': 20.17.19 1347 | '@typescript-eslint/experimental-utils': 5.62.0(eslint@9.21.0)(typescript@5.8.2) 1348 | typescript: 5.8.2 1349 | transitivePeerDependencies: 1350 | - eslint 1351 | - supports-color 1352 | 1353 | eslint-scope@5.1.1: 1354 | dependencies: 1355 | esrecurse: 4.3.0 1356 | estraverse: 4.3.0 1357 | 1358 | eslint-scope@8.2.0: 1359 | dependencies: 1360 | esrecurse: 4.3.0 1361 | estraverse: 5.3.0 1362 | 1363 | eslint-visitor-keys@3.4.3: {} 1364 | 1365 | eslint-visitor-keys@4.2.0: {} 1366 | 1367 | eslint@9.21.0: 1368 | dependencies: 1369 | '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) 1370 | '@eslint-community/regexpp': 4.12.1 1371 | '@eslint/config-array': 0.19.2 1372 | '@eslint/core': 0.12.0 1373 | '@eslint/eslintrc': 3.3.0 1374 | '@eslint/js': 9.21.0 1375 | '@eslint/plugin-kit': 0.2.7 1376 | '@humanfs/node': 0.16.6 1377 | '@humanwhocodes/module-importer': 1.0.1 1378 | '@humanwhocodes/retry': 0.4.2 1379 | '@types/estree': 1.0.6 1380 | '@types/json-schema': 7.0.15 1381 | ajv: 6.12.6 1382 | chalk: 4.1.2 1383 | cross-spawn: 7.0.6 1384 | debug: 4.4.0 1385 | escape-string-regexp: 4.0.0 1386 | eslint-scope: 8.2.0 1387 | eslint-visitor-keys: 4.2.0 1388 | espree: 10.3.0 1389 | esquery: 1.6.0 1390 | esutils: 2.0.3 1391 | fast-deep-equal: 3.1.3 1392 | file-entry-cache: 8.0.0 1393 | find-up: 5.0.0 1394 | glob-parent: 6.0.2 1395 | ignore: 5.3.2 1396 | imurmurhash: 0.1.4 1397 | is-glob: 4.0.3 1398 | json-stable-stringify-without-jsonify: 1.0.1 1399 | lodash.merge: 4.6.2 1400 | minimatch: 3.1.2 1401 | natural-compare: 1.4.0 1402 | optionator: 0.9.4 1403 | transitivePeerDependencies: 1404 | - supports-color 1405 | 1406 | espree@10.3.0: 1407 | dependencies: 1408 | acorn: 8.14.0 1409 | acorn-jsx: 5.3.2(acorn@8.14.0) 1410 | eslint-visitor-keys: 4.2.0 1411 | 1412 | esquery@1.6.0: 1413 | dependencies: 1414 | estraverse: 5.3.0 1415 | 1416 | esrecurse@4.3.0: 1417 | dependencies: 1418 | estraverse: 5.3.0 1419 | 1420 | estraverse@4.3.0: {} 1421 | 1422 | estraverse@5.3.0: {} 1423 | 1424 | esutils@2.0.3: {} 1425 | 1426 | expect@29.7.0: 1427 | dependencies: 1428 | '@jest/expect-utils': 29.7.0 1429 | jest-get-type: 29.6.3 1430 | jest-matcher-utils: 29.7.0 1431 | jest-message-util: 29.7.0 1432 | jest-util: 29.7.0 1433 | 1434 | fast-deep-equal@3.1.3: {} 1435 | 1436 | fast-diff@1.3.0: {} 1437 | 1438 | fast-glob@3.3.3: 1439 | dependencies: 1440 | '@nodelib/fs.stat': 2.0.5 1441 | '@nodelib/fs.walk': 1.2.8 1442 | glob-parent: 5.1.2 1443 | merge2: 1.4.1 1444 | micromatch: 4.0.8 1445 | 1446 | fast-json-stable-stringify@2.1.0: {} 1447 | 1448 | fast-levenshtein@2.0.6: {} 1449 | 1450 | fast-uri@3.0.6: {} 1451 | 1452 | fastq@1.19.1: 1453 | dependencies: 1454 | reusify: 1.1.0 1455 | 1456 | file-entry-cache@8.0.0: 1457 | dependencies: 1458 | flat-cache: 4.0.1 1459 | 1460 | fill-range@7.1.1: 1461 | dependencies: 1462 | to-regex-range: 5.0.1 1463 | 1464 | find-up@5.0.0: 1465 | dependencies: 1466 | locate-path: 6.0.0 1467 | path-exists: 4.0.0 1468 | 1469 | flat-cache@4.0.1: 1470 | dependencies: 1471 | flatted: 3.3.3 1472 | keyv: 4.5.4 1473 | 1474 | flatted@3.3.3: {} 1475 | 1476 | fs-extra@11.3.0: 1477 | dependencies: 1478 | graceful-fs: 4.2.11 1479 | jsonfile: 6.1.0 1480 | universalify: 2.0.1 1481 | 1482 | fsevents@2.3.3: 1483 | optional: true 1484 | 1485 | function-bind@1.1.2: {} 1486 | 1487 | get-caller-file@2.0.5: {} 1488 | 1489 | glob-parent@5.1.2: 1490 | dependencies: 1491 | is-glob: 4.0.3 1492 | 1493 | glob-parent@6.0.2: 1494 | dependencies: 1495 | is-glob: 4.0.3 1496 | 1497 | globals@14.0.0: {} 1498 | 1499 | globby@11.1.0: 1500 | dependencies: 1501 | array-union: 2.1.0 1502 | dir-glob: 3.0.1 1503 | fast-glob: 3.3.3 1504 | ignore: 5.3.2 1505 | merge2: 1.4.1 1506 | slash: 3.0.0 1507 | 1508 | graceful-fs@4.2.11: {} 1509 | 1510 | graphemer@1.4.0: {} 1511 | 1512 | has-flag@4.0.0: {} 1513 | 1514 | hasown@2.0.2: 1515 | dependencies: 1516 | function-bind: 1.1.2 1517 | 1518 | ignore@5.3.2: {} 1519 | 1520 | import-fresh@3.3.1: 1521 | dependencies: 1522 | parent-module: 1.0.1 1523 | resolve-from: 4.0.0 1524 | 1525 | imurmurhash@0.1.4: {} 1526 | 1527 | is-binary-path@2.1.0: 1528 | dependencies: 1529 | binary-extensions: 2.3.0 1530 | 1531 | is-core-module@2.16.1: 1532 | dependencies: 1533 | hasown: 2.0.2 1534 | 1535 | is-extglob@2.1.1: {} 1536 | 1537 | is-fullwidth-code-point@3.0.0: {} 1538 | 1539 | is-glob@4.0.3: 1540 | dependencies: 1541 | is-extglob: 2.1.1 1542 | 1543 | is-number@7.0.0: {} 1544 | 1545 | isexe@2.0.0: {} 1546 | 1547 | jest-diff@29.7.0: 1548 | dependencies: 1549 | chalk: 4.1.2 1550 | diff-sequences: 29.6.3 1551 | jest-get-type: 29.6.3 1552 | pretty-format: 29.7.0 1553 | 1554 | jest-get-type@29.6.3: {} 1555 | 1556 | jest-matcher-utils@29.7.0: 1557 | dependencies: 1558 | chalk: 4.1.2 1559 | jest-diff: 29.7.0 1560 | jest-get-type: 29.6.3 1561 | pretty-format: 29.7.0 1562 | 1563 | jest-message-util@29.7.0: 1564 | dependencies: 1565 | '@babel/code-frame': 7.26.2 1566 | '@jest/types': 29.6.3 1567 | '@types/stack-utils': 2.0.3 1568 | chalk: 4.1.2 1569 | graceful-fs: 4.2.11 1570 | micromatch: 4.0.8 1571 | pretty-format: 29.7.0 1572 | slash: 3.0.0 1573 | stack-utils: 2.0.6 1574 | 1575 | jest-util@29.7.0: 1576 | dependencies: 1577 | '@jest/types': 29.6.3 1578 | '@types/node': 20.17.19 1579 | chalk: 4.1.2 1580 | ci-info: 3.9.0 1581 | graceful-fs: 4.2.11 1582 | picomatch: 2.3.1 1583 | 1584 | js-tokens@4.0.0: {} 1585 | 1586 | js-yaml@4.1.0: 1587 | dependencies: 1588 | argparse: 2.0.1 1589 | 1590 | json-buffer@3.0.1: {} 1591 | 1592 | json-schema-traverse@0.4.1: {} 1593 | 1594 | json-schema-traverse@1.0.0: {} 1595 | 1596 | json-stable-stringify-without-jsonify@1.0.1: {} 1597 | 1598 | jsonfile@6.1.0: 1599 | dependencies: 1600 | universalify: 2.0.1 1601 | optionalDependencies: 1602 | graceful-fs: 4.2.11 1603 | 1604 | keyv@4.5.4: 1605 | dependencies: 1606 | json-buffer: 3.0.1 1607 | 1608 | kleur@4.1.5: {} 1609 | 1610 | levn@0.4.1: 1611 | dependencies: 1612 | prelude-ls: 1.2.1 1613 | type-check: 0.4.0 1614 | 1615 | locate-path@6.0.0: 1616 | dependencies: 1617 | p-locate: 5.0.0 1618 | 1619 | lodash.merge@4.6.2: {} 1620 | 1621 | merge2@1.4.1: {} 1622 | 1623 | micromatch@4.0.8: 1624 | dependencies: 1625 | braces: 3.0.3 1626 | picomatch: 2.3.1 1627 | 1628 | minimatch@3.1.2: 1629 | dependencies: 1630 | brace-expansion: 1.1.11 1631 | 1632 | minimatch@9.0.5: 1633 | dependencies: 1634 | brace-expansion: 2.0.1 1635 | 1636 | ms@2.1.3: {} 1637 | 1638 | natural-compare@1.4.0: {} 1639 | 1640 | normalize-path@3.0.0: {} 1641 | 1642 | optionator@0.9.4: 1643 | dependencies: 1644 | deep-is: 0.1.4 1645 | fast-levenshtein: 2.0.6 1646 | levn: 0.4.1 1647 | prelude-ls: 1.2.1 1648 | type-check: 0.4.0 1649 | word-wrap: 1.2.5 1650 | 1651 | p-limit@3.1.0: 1652 | dependencies: 1653 | yocto-queue: 0.1.0 1654 | 1655 | p-locate@5.0.0: 1656 | dependencies: 1657 | p-limit: 3.1.0 1658 | 1659 | parent-module@1.0.1: 1660 | dependencies: 1661 | callsites: 3.1.0 1662 | 1663 | path-exists@4.0.0: {} 1664 | 1665 | path-key@3.1.1: {} 1666 | 1667 | path-parse@1.0.7: {} 1668 | 1669 | path-type@4.0.0: {} 1670 | 1671 | picocolors@1.1.1: {} 1672 | 1673 | picomatch@2.3.1: {} 1674 | 1675 | prelude-ls@1.2.1: {} 1676 | 1677 | prettier-linter-helpers@1.0.0: 1678 | dependencies: 1679 | fast-diff: 1.3.0 1680 | 1681 | prettier@3.5.2: {} 1682 | 1683 | pretty-format@29.7.0: 1684 | dependencies: 1685 | '@jest/schemas': 29.6.3 1686 | ansi-styles: 5.2.0 1687 | react-is: 18.3.1 1688 | 1689 | punycode@2.3.1: {} 1690 | 1691 | queue-microtask@1.2.3: {} 1692 | 1693 | react-is@18.3.1: {} 1694 | 1695 | readdirp@3.6.0: 1696 | dependencies: 1697 | picomatch: 2.3.1 1698 | 1699 | require-directory@2.1.1: {} 1700 | 1701 | require-from-string@2.0.2: {} 1702 | 1703 | resolve-from@4.0.0: {} 1704 | 1705 | resolve@1.22.10: 1706 | dependencies: 1707 | is-core-module: 2.16.1 1708 | path-parse: 1.0.7 1709 | supports-preserve-symlinks-flag: 1.0.0 1710 | 1711 | reusify@1.1.0: {} 1712 | 1713 | roblox-ts@3.0.0: 1714 | dependencies: 1715 | '@roblox-ts/luau-ast': 2.0.0 1716 | '@roblox-ts/path-translator': 1.1.0 1717 | '@roblox-ts/rojo-resolver': 1.1.0 1718 | chokidar: 3.6.0 1719 | fs-extra: 11.3.0 1720 | kleur: 4.1.5 1721 | resolve: 1.22.10 1722 | typescript: 5.5.3 1723 | yargs: 17.7.2 1724 | 1725 | run-parallel@1.2.0: 1726 | dependencies: 1727 | queue-microtask: 1.2.3 1728 | 1729 | semver@7.7.1: {} 1730 | 1731 | shebang-command@2.0.0: 1732 | dependencies: 1733 | shebang-regex: 3.0.0 1734 | 1735 | shebang-regex@3.0.0: {} 1736 | 1737 | slash@3.0.0: {} 1738 | 1739 | stack-utils@2.0.6: 1740 | dependencies: 1741 | escape-string-regexp: 2.0.0 1742 | 1743 | string-width@4.2.3: 1744 | dependencies: 1745 | emoji-regex: 8.0.0 1746 | is-fullwidth-code-point: 3.0.0 1747 | strip-ansi: 6.0.1 1748 | 1749 | strip-ansi@6.0.1: 1750 | dependencies: 1751 | ansi-regex: 5.0.1 1752 | 1753 | strip-json-comments@3.1.1: {} 1754 | 1755 | supports-color@7.2.0: 1756 | dependencies: 1757 | has-flag: 4.0.0 1758 | 1759 | supports-preserve-symlinks-flag@1.0.0: {} 1760 | 1761 | synckit@0.9.2: 1762 | dependencies: 1763 | '@pkgr/core': 0.1.1 1764 | tslib: 2.8.1 1765 | 1766 | to-regex-range@5.0.1: 1767 | dependencies: 1768 | is-number: 7.0.0 1769 | 1770 | ts-api-utils@2.0.1(typescript@5.8.2): 1771 | dependencies: 1772 | typescript: 5.8.2 1773 | 1774 | tslib@1.14.1: {} 1775 | 1776 | tslib@2.8.1: {} 1777 | 1778 | tsutils@3.21.0(typescript@5.8.2): 1779 | dependencies: 1780 | tslib: 1.14.1 1781 | typescript: 5.8.2 1782 | 1783 | type-check@0.4.0: 1784 | dependencies: 1785 | prelude-ls: 1.2.1 1786 | 1787 | typescript@5.5.3: {} 1788 | 1789 | typescript@5.8.2: {} 1790 | 1791 | undici-types@6.19.8: {} 1792 | 1793 | universalify@2.0.1: {} 1794 | 1795 | uri-js@4.4.1: 1796 | dependencies: 1797 | punycode: 2.3.1 1798 | 1799 | which@2.0.2: 1800 | dependencies: 1801 | isexe: 2.0.0 1802 | 1803 | word-wrap@1.2.5: {} 1804 | 1805 | wrap-ansi@7.0.0: 1806 | dependencies: 1807 | ansi-styles: 4.3.0 1808 | string-width: 4.2.3 1809 | strip-ansi: 6.0.1 1810 | 1811 | y18n@5.0.8: {} 1812 | 1813 | yargs-parser@21.1.1: {} 1814 | 1815 | yargs@17.7.2: 1816 | dependencies: 1817 | cliui: 8.0.1 1818 | escalade: 3.2.0 1819 | get-caller-file: 2.0.5 1820 | require-directory: 2.1.1 1821 | string-width: 4.2.3 1822 | y18n: 5.0.8 1823 | yargs-parser: 21.1.1 1824 | 1825 | yocto-queue@0.1.0: {} 1826 | -------------------------------------------------------------------------------- /rokit.toml: -------------------------------------------------------------------------------- 1 | # This file lists tools managed by Rokit, a toolchain manager for Roblox projects. 2 | # For more information, see https://github.com/rojo-rbx/rokit 3 | 4 | # New tools can be added by running `rokit add ` in a terminal. 5 | 6 | [tools] 7 | rojo = "rojo-rbx/rojo@7.4.4" 8 | selene = "Kampfkarren/selene@0.28.0" 9 | wally = "UpliftGames/wally@0.3.2" 10 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | 3 | [lints] 4 | global_usage = "allow" -------------------------------------------------------------------------------- /src/exports.ts: -------------------------------------------------------------------------------- 1 | export * from "./source/physics" 2 | export * from "./source/physics-object" 3 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./exports" -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | export type PhysicsEngine = { 2 | Start: (self: PhysicsEngine) -> (), 3 | Stop: (self: PhysicsEngine) -> (), 4 | Step: (self: PhysicsEngine, realDeltaTime: number) -> (), 5 | SetTime: (self: PhysicsEngine, targetTime: number) -> (), 6 | AddGlobalForce: (self: PhysicsEngine, name: string, force: Vector3) -> (), 7 | AddObject: (self: PhysicsEngine, id: string, object: PhysicsObject) -> (), 8 | DestroyObject: (self: PhysicsEngine, id: string) -> (), 9 | ClearObjects: (self: PhysicsEngine) -> (), 10 | GetObject: (self: PhysicsEngine, id: string) -> PhysicsObject?, 11 | GetObjects: (self: PhysicsEngine) -> { [number]: PhysicsObject }, 12 | GetSnapshot: (self: PhysicsEngine) -> { 13 | timestamp: number, 14 | objects: { [string]: { position: Vector3, velocity: Vector3, acceleration: Vector3 } } 15 | }, 16 | ApplySnapshot: (self: PhysicsEngine, snapshot: { 17 | timestamp: number, 18 | objects: { [string]: { position: Vector3, velocity: Vector3, acceleration: Vector3 } } 19 | }) -> (), 20 | Destroy: (self: PhysicsEngine) -> (), 21 | 22 | currentTime: number, 23 | } 24 | 25 | export type PhysicsEngineConstructor = { 26 | new: () -> PhysicsEngine, 27 | } 28 | 29 | export type PhysicsObject = { 30 | ApplyState: (self: PhysicsObject, state: { position: Vector3, velocity: Vector3, acceleration: Vector3, forces: { Vector3 } }) -> (), 31 | RestoreState: (self: PhysicsObject, state: { position: Vector3, velocity: Vector3, acceleration: Vector3 }) -> (), 32 | ApplyForce: (self: PhysicsObject, force: Vector3, offset: Vector3?) -> (), 33 | ClearForces: (self: PhysicsObject) -> (), 34 | GetWorldPosition: (self: PhysicsObject) -> Vector3, 35 | Update: (self: PhysicsObject, deltaTime: number) -> (), 36 | GetState: (self: PhysicsObject) -> { position: Vector3, velocity: Vector3, acceleration: Vector3 }, 37 | Destroy: (self: PhysicsObject) -> (), 38 | 39 | id: string, 40 | mass: number, 41 | friction: number, 42 | elasticity: number, 43 | momentOfInertia: number, 44 | position: Vector3, 45 | velocity: Vector3, 46 | acceleration: Vector3, 47 | rotation: CFrame, 48 | angularVelocity: Vector3, 49 | angularAcceleration: Vector3, 50 | collisionMethod: "box" | "point" | "none", 51 | size: Vector3?, 52 | ignoreGlobalForces: boolean, 53 | part: Instance?, 54 | } 55 | 56 | export type PhysicsObjectConstructor = { 57 | new: ( 58 | id: string, 59 | properties: { 60 | mass: number, 61 | friction: number?, 62 | elasticity: number?, 63 | momentOfInertia: number?, 64 | collisionMethod: "box" | "point" | "none", 65 | size: Vector3?, 66 | ignoreGlobalForces: boolean?, 67 | }, 68 | state: { position: Vector3, velocity: Vector3, acceleration: Vector3, forces: { Vector3 } }?, 69 | part: Instance? 70 | ) -> PhysicsObject, 71 | } 72 | 73 | export type delta = { 74 | PhysicsEngine: PhysicsEngineConstructor, 75 | PhysicsObject: PhysicsObjectConstructor, 76 | } 77 | 78 | local TS = script:FindFirstChild("include") 79 | and require(script:WaitForChild("include"):WaitForChild("RuntimeLib")) 80 | or _G[script] 81 | 82 | local exports = {} 83 | 84 | for _k, _v in TS.import(script, script, "exports") or {} do 85 | exports[_k] = _v 86 | end 87 | 88 | table.freeze(exports) 89 | 90 | return exports :: delta 91 | -------------------------------------------------------------------------------- /src/source/physics-object.ts: -------------------------------------------------------------------------------- 1 | import { PhysicsObjectState } from "./physics"; 2 | 3 | export interface ObjectState extends PhysicsObjectState { 4 | forces: Vector3[]; 5 | } 6 | 7 | export interface PhysicsObjectProperties { 8 | mass: number; 9 | friction?: number; 10 | elasticity?: number; 11 | momentOfInertia?: number; 12 | collisionMethod?: "box" | "point" | "none"; 13 | size?: Vector3; 14 | ignoreGlobalForces?: boolean; 15 | } 16 | 17 | export class PhysicsObject { 18 | public id: string; 19 | public mass: number; 20 | public friction: number; 21 | public elasticity: number; 22 | public momentOfInertia: number; 23 | public position: Vector3 = Vector3.zero; 24 | public prevPosition: Vector3 = Vector3.zero; 25 | public velocity: Vector3 = Vector3.zero; 26 | public acceleration: Vector3 = Vector3.zero; 27 | public rotation: CFrame = new CFrame(); 28 | public angularVelocity: Vector3 = Vector3.zero; 29 | public angularAcceleration: Vector3 = Vector3.zero; 30 | public collisionMethod: "box" | "point" | "none"; 31 | public size?: Vector3; 32 | public ignoreGlobalForces: boolean; 33 | private forces: Vector3[] = []; 34 | private torques: Vector3[] = []; 35 | private part?: BasePart; 36 | private hitbox?: Part; 37 | 38 | constructor(id: string, properties: PhysicsObjectProperties, state?: ObjectState, part?: BasePart) { 39 | this.id = id; 40 | this.mass = properties.mass; 41 | this.friction = properties.friction ?? 0.3; 42 | this.elasticity = properties.elasticity ?? 0.4; 43 | this.momentOfInertia = properties.momentOfInertia ?? 1; 44 | this.ignoreGlobalForces = properties.ignoreGlobalForces ?? false; 45 | this.collisionMethod = properties.collisionMethod ?? "point"; 46 | 47 | if (this.collisionMethod === "box") { 48 | if (!properties.size) error("Box collision requires a size."); 49 | this.size = properties.size; 50 | } 51 | 52 | this.part = part; 53 | 54 | if (state !== undefined) { 55 | this.ApplyState(state); 56 | } else { 57 | this.position = new Vector3(0, 0, 0); 58 | this.prevPosition = this.position; 59 | this.velocity = new Vector3(0, 0, 0); 60 | this.acceleration = new Vector3(0, 0, 0); 61 | } 62 | } 63 | 64 | public ApplyState(state: ObjectState): void { 65 | this.position = state.position; 66 | this.prevPosition = this.position; 67 | this.velocity = state.velocity; 68 | this.acceleration = state.acceleration; 69 | this.forces = state.forces; 70 | } 71 | 72 | public RestoreState(state: PhysicsObjectState): void { 73 | this.position = state.position; 74 | this.prevPosition = this.position; 75 | this.velocity = state.velocity; 76 | this.acceleration = state.acceleration; 77 | } 78 | 79 | public ApplyForce(force: Vector3, offset?: Vector3): void { 80 | this.forces.push(force) 81 | if (offset) { 82 | const torque = offset.Cross(force); 83 | this.torques.push(torque) 84 | } 85 | } 86 | 87 | public ClearForces(): void { 88 | this.forces = []; 89 | } 90 | 91 | public ClearTorques(): void { 92 | this.torques = []; 93 | } 94 | 95 | public GetWorldPosition(): Vector3 { 96 | return this.position; 97 | } 98 | 99 | public GetSmoothedWorldPosition(alpha: number): Vector3 { 100 | return this.prevPosition.Lerp(this.position, alpha); 101 | } 102 | 103 | public Update(dt: number): void { 104 | this.prevPosition = this.position; 105 | let totalAccel = new Vector3(0, 0, 0); 106 | 107 | for (const f of this.forces) { 108 | totalAccel = totalAccel.add(f); 109 | } 110 | 111 | this.ClearForces(); 112 | this.acceleration = totalAccel; 113 | this.velocity = this.velocity.add(this.acceleration.mul(dt)); 114 | 115 | this.position = this.position.add(this.velocity.mul(dt)); 116 | let totalTorque = new Vector3(0, 0, 0); 117 | 118 | for (const t of this.torques) { 119 | totalTorque = totalTorque.add(t); 120 | } 121 | 122 | this.ClearTorques(); 123 | this.angularAcceleration = totalTorque.div(this.momentOfInertia); 124 | this.angularVelocity = this.angularVelocity.add(this.angularAcceleration.mul(dt)); 125 | const rotInc = CFrame.fromEulerAnglesXYZ(this.angularVelocity.X * dt, this.angularVelocity.Y * dt, this.angularVelocity.Z * dt); 126 | this.rotation = this.rotation.mul(rotInc); 127 | if (this.part) { 128 | this.part.CFrame = this.rotation.add(this.GetWorldPosition()); 129 | } 130 | } 131 | 132 | public GetState(): PhysicsObjectState { 133 | return { 134 | position: this.position, 135 | velocity: this.velocity, 136 | acceleration: this.acceleration, 137 | }; 138 | } 139 | 140 | public Destroy(): void { 141 | if (this.part) { 142 | this.part.Destroy(); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/source/physics.ts: -------------------------------------------------------------------------------- 1 | import Object from "@rbxts/object-utils"; 2 | import { PhysicsObject } from "./physics-object"; 3 | 4 | export interface PhysicsObjectState { 5 | position: Vector3; 6 | velocity: Vector3; 7 | acceleration: Vector3; 8 | } 9 | 10 | export interface PhysicsSnapshot { 11 | timestamp: number; 12 | objects: Record; 13 | } 14 | 15 | export const SCALE = 1000; 16 | 17 | export class PhysicsEngine { 18 | private objects = new Map(); 19 | private globalForces = new Map(); 20 | public currentTime = 0; 21 | private accumulatedTime = 0; 22 | private running = false; 23 | public timeScale = 1; 24 | private readonly fixedDeltaTime = 1 / 60; 25 | 26 | constructor() {} 27 | 28 | public Start(): void { 29 | this.running = true; 30 | } 31 | 32 | public Stop(): void { 33 | this.running = false; 34 | } 35 | 36 | public Step(realDeltaTime: number): void { 37 | if (!this.running) { 38 | return 39 | }; 40 | 41 | const dt = realDeltaTime * this.timeScale; 42 | this.accumulatedTime += dt; 43 | 44 | while (this.accumulatedTime >= this.fixedDeltaTime) { 45 | this.currentTime += this.fixedDeltaTime; 46 | this.ApplyGlobalForces(); 47 | this.UpdateObjects(this.fixedDeltaTime); 48 | this.DetectAndResolveCollisions(this.fixedDeltaTime); 49 | this.accumulatedTime -= this.fixedDeltaTime; 50 | } 51 | } 52 | 53 | public SetTime(targetTime: number): void { 54 | while (this.currentTime < targetTime) { 55 | this.Step(this.fixedDeltaTime); 56 | } 57 | } 58 | 59 | private UpdateObjects(dt: number): void { 60 | for (const [, obj] of this.objects) { 61 | obj.Update(dt); 62 | } 63 | } 64 | 65 | public AddGlobalForce(name: string, accel: Vector3): void { 66 | this.globalForces.set(name, accel); 67 | } 68 | 69 | private ApplyGlobalForces(): void { 70 | for (const [, obj] of this.objects) { 71 | if (obj.ignoreGlobalForces) continue; 72 | for (const [, accel] of this.globalForces) { 73 | obj.ApplyForce(accel); 74 | } 75 | } 76 | } 77 | 78 | public AddObject(id: string, object: PhysicsObject): void { 79 | this.objects.set(id, object); 80 | } 81 | 82 | public DestroyObject(id: string): void { 83 | this.objects.get(id)?.Destroy(); 84 | this.objects.delete(id); 85 | } 86 | 87 | public GetObject(id: string): PhysicsObject | undefined { 88 | return this.objects.get(id); 89 | } 90 | 91 | public GetObjects(): PhysicsObject[] { 92 | return Object.values(this.objects); 93 | } 94 | 95 | private DetectAndResolveCollisions(dt: number): void { 96 | const objs = Object.values(this.objects); 97 | for (let i = 0; i < objs.size(); i++) { 98 | for (let j = i + 1; j < objs.size(); j++) { 99 | const objA = objs[i]; 100 | const objB = objs[j]; 101 | if (objA.collisionMethod === "none" || objB.collisionMethod === "none") continue; 102 | if (this.CheckCollision(objA, objB)) { 103 | this.ResolveCollision(objA, objB, dt); 104 | } 105 | } 106 | } 107 | } 108 | 109 | private CheckCollision(objA: PhysicsObject, objB: PhysicsObject): boolean { 110 | const posA = objA.GetWorldPosition(); 111 | const posB = objB.GetWorldPosition(); 112 | 113 | if (objA.collisionMethod === "point" && objB.collisionMethod === "point") { 114 | const threshold = 0.5; 115 | return posA.sub(posB).Magnitude < threshold; 116 | } 117 | 118 | if (objA.collisionMethod === "box" && objB.collisionMethod === "box") { 119 | if (!objA.size || !objB.size) return false; 120 | const diff = posA.sub(posB); 121 | return math.abs(diff.X) < (objA.size.X + objB.size.X) / 2 && 122 | math.abs(diff.Y) < (objA.size.Y + objB.size.Y) / 2 && 123 | math.abs(diff.Z) < (objA.size.Z + objB.size.Z) / 2; 124 | } 125 | 126 | if (objA.collisionMethod === "point" && objB.collisionMethod === "box") { 127 | if (!objB.size) return false; 128 | const diff = posA.sub(posB); 129 | return math.abs(diff.X) < (objB.size.X / 2) && 130 | math.abs(diff.Y) < (objB.size.Y / 2) && 131 | math.abs(diff.Z) < (objB.size.Z / 2); 132 | } 133 | 134 | if (objA.collisionMethod === "box" && objB.collisionMethod === "point") { 135 | if (!objA.size) return false; 136 | const diff = posB.sub(posA); 137 | return math.abs(diff.X) < (objA.size.X / 2) && 138 | math.abs(diff.Y) < (objA.size.Y / 2) && 139 | math.abs(diff.Z) < (objA.size.Z / 2); 140 | } 141 | 142 | return false; 143 | } 144 | 145 | private ResolveStaticCollisionCCD(dynamicObj: PhysicsObject, staticObj: PhysicsObject, dt: number): void { 146 | const staticUp = staticObj.rotation.VectorToWorldSpace(new Vector3(0, 1, 0)); 147 | const halfHeight = staticObj.size ? staticObj.size.Y / 2 : 0.5; 148 | const groundTop = staticObj.GetWorldPosition().Dot(staticUp) + halfHeight; 149 | const p = dynamicObj.GetWorldPosition(); 150 | const pProj = p.Dot(staticUp); 151 | const v = dynamicObj.velocity; 152 | const vNorm = v.Dot(staticUp); 153 | const groundFriction = 0.5; 154 | 155 | if (vNorm < 0) { 156 | const p_next = p.add(v.mul(dt)); 157 | const pProjNext = p_next.Dot(staticUp); 158 | if (pProj >= groundTop && pProjNext < groundTop) { 159 | const t_collide = (pProj - groundTop) / (pProj - pProjNext); 160 | const collisionPos = p.add(v.mul(dt * t_collide)); 161 | const restitution = dynamicObj.elasticity; 162 | const new_vNorm = -vNorm * restitution; 163 | const dt_remain = dt * (1 - t_collide); 164 | const vTan = v.sub(staticUp.mul(vNorm)); 165 | const finalPos = collisionPos.add(vTan.mul(dt_remain)).add(staticUp.mul(new_vNorm * dt_remain)); 166 | 167 | dynamicObj.position = finalPos; 168 | dynamicObj.velocity = vTan.add(staticUp.mul(new_vNorm)); 169 | 170 | const new_vTan = vTan.mul(math.clamp(1 - groundFriction * dt, 0, 1)); 171 | dynamicObj.velocity = new_vTan.add(staticUp.mul(new_vNorm)); 172 | 173 | return; 174 | } 175 | } 176 | if (pProj < groundTop) { 177 | const penetration = groundTop - pProj; 178 | dynamicObj.position = dynamicObj.position.add(staticUp.mul(penetration)); 179 | 180 | if (vNorm < 0) { 181 | const restitution = dynamicObj.elasticity; 182 | const new_vNorm = -vNorm * restitution; 183 | const vTan = v.sub(staticUp.mul(vNorm)); 184 | dynamicObj.velocity = vTan.add(staticUp.mul(new_vNorm)); 185 | 186 | const new_vTan = vTan.mul(math.clamp(1 - groundFriction * dt, 0, 1)); 187 | dynamicObj.velocity = new_vTan.add(staticUp.mul(new_vNorm)); 188 | } 189 | } 190 | } 191 | 192 | private ResolveDynamicCollision(objA: PhysicsObject, objB: PhysicsObject): void { 193 | const posA = objA.GetWorldPosition(); 194 | const posB = objB.GetWorldPosition(); 195 | 196 | const normal = posB.sub(posA).Unit; 197 | const relativeVelocity = objB.velocity.sub(objA.velocity); 198 | const velAlongNormal = relativeVelocity.Dot(normal); 199 | 200 | if (velAlongNormal > 0) { 201 | return 202 | }; 203 | 204 | const restitution = (objA.elasticity + objB.elasticity) / 2; 205 | const impulseScalar = -(1 + restitution) * velAlongNormal / (1 / objA.mass + 1 / objB.mass); 206 | const impulse = normal.mul(impulseScalar); 207 | 208 | objA.velocity = objA.velocity.sub(impulse.div(objA.mass)); 209 | objB.velocity = objB.velocity.add(impulse.div(objB.mass)); 210 | 211 | const tangent = relativeVelocity.sub(normal.mul(velAlongNormal)); 212 | 213 | if (tangent.Magnitude > 0) { 214 | const tangentUnit = tangent.Unit; 215 | const frictionCoefficient = (objA.friction + objB.friction) / 2; 216 | const jt = -relativeVelocity.Dot(tangentUnit) / (1 / objA.mass + 1 / objB.mass); 217 | const frictionImpulseScalar = math.abs(jt) < impulseScalar * frictionCoefficient ? jt : -impulseScalar * frictionCoefficient; 218 | const frictionImpulse = tangentUnit.mul(frictionImpulseScalar); 219 | 220 | objA.velocity = objA.velocity.sub(frictionImpulse.div(objA.mass)); 221 | objB.velocity = objB.velocity.add(frictionImpulse.div(objB.mass)); 222 | } 223 | 224 | const newRel = objB.velocity.sub(objA.velocity); 225 | const newTangent = newRel.sub(normal.mul(newRel.Dot(normal))); 226 | 227 | if (newTangent.Magnitude < 0.01) { 228 | const normA = objA.velocity.Dot(normal); 229 | const normB = objB.velocity.Dot(normal); 230 | objA.velocity = normal.mul(normA); 231 | objB.velocity = normal.mul(normB); 232 | } 233 | } 234 | 235 | private ResolveCollision(objA: PhysicsObject, objB: PhysicsObject, dt: number): void { 236 | if (objA.ignoreGlobalForces !== objB.ignoreGlobalForces) { 237 | if (objA.ignoreGlobalForces) { 238 | this.ResolveStaticCollisionCCD(objB, objA, dt); 239 | } else { 240 | this.ResolveStaticCollisionCCD(objA, objB, dt); 241 | } 242 | return; 243 | } 244 | if (this.CheckCollision(objA, objB)) { 245 | this.ResolveDynamicCollision(objA, objB); 246 | } 247 | } 248 | 249 | public GetSnapshot(): PhysicsSnapshot { 250 | const snapshot: PhysicsSnapshot = { 251 | timestamp: this.currentTime, 252 | objects: {}, 253 | }; 254 | this.objects.forEach((obj, id) => { 255 | snapshot.objects[id] = obj.GetState(); 256 | }); 257 | return snapshot; 258 | } 259 | 260 | public ApplySnapshot(snapshot: PhysicsSnapshot): void { 261 | this.currentTime = snapshot.timestamp; 262 | for (const [id, state] of Object.entries(snapshot.objects)) { 263 | const obj = this.objects.get(id); 264 | if (obj) { 265 | obj.RestoreState(state); 266 | } 267 | } 268 | } 269 | 270 | public Destroy(): void { 271 | this.Stop(); 272 | this.ClearObjects(); 273 | } 274 | 275 | public ClearObjects(): void { 276 | this.objects.forEach((obj) => obj.Destroy()); 277 | this.objects.clear(); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/source/util/helper/test-part.ts: -------------------------------------------------------------------------------- 1 | import { Janitor } from "@rbxts/janitor" 2 | import { Workspace } from "@rbxts/services" 3 | 4 | export default function createTestPart(janitor?: Janitor): Part { 5 | const testPart = new Instance("Part", Workspace) 6 | testPart.Shape = Enum.PartType.Ball 7 | testPart.Size = new Vector3(2, 2, 2) 8 | testPart.Anchored = true 9 | testPart.CanCollide = false 10 | testPart.Color = new Color3(math.random(), math.random(), math.random()) 11 | 12 | if (janitor !== undefined){ 13 | janitor.Add(testPart, "Destroy") 14 | } 15 | 16 | return testPart 17 | } -------------------------------------------------------------------------------- /src/source/util/math/rotations-near.ts: -------------------------------------------------------------------------------- 1 | export default function rotationsNear(cf1: CFrame, cf2: CFrame, tolerance: number): boolean { 2 | const [ax, ay, az] = cf1.ToEulerAnglesXYZ(); 3 | const [bx, by, bz] = cf2.ToEulerAnglesXYZ(); 4 | return math.abs(ax - bx) < tolerance && 5 | math.abs(ay - by) < tolerance && 6 | math.abs(az - bz) < tolerance; 7 | } -------------------------------------------------------------------------------- /src/source/util/math/vectors-near.ts: -------------------------------------------------------------------------------- 1 | export default function vectorsNear(v1: Vector3, v2: Vector3, tolerance: number): boolean { 2 | return math.abs(v1.X - v2.X) < tolerance && 3 | math.abs(v1.Y - v2.Y) < tolerance && 4 | math.abs(v1.Z - v2.Z) < tolerance 5 | } -------------------------------------------------------------------------------- /src/tests/engine.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { Workspace } from "@rbxts/services" 4 | import { Janitor } from "@rbxts/janitor" 5 | import createTestPart from "../source/util/helper/test-part" 6 | import { PhysicsEngine, PhysicsObject } from "../exports" 7 | import vectorsNear from "../source/util/math/vectors-near" 8 | import rotationsNear from "../source/util/math/rotations-near" 9 | 10 | export = () => { 11 | const janitor = new Janitor() 12 | let testEngine: PhysicsEngine 13 | const objectName = "test" 14 | 15 | beforeEach(() => { 16 | testEngine = new PhysicsEngine() 17 | }) 18 | 19 | describe("Engine Setup", () => { 20 | it("should start the engine without errors", () => { 21 | testEngine.Start() 22 | expect(true).to.equal(true) 23 | }) 24 | 25 | it("should stop the engine without errors", () => { 26 | testEngine.Stop() 27 | expect(true).to.equal(true) 28 | }) 29 | }) 30 | 31 | describe("Object Manipulation", () => { 32 | let testObject: PhysicsObject 33 | let testPart: BasePart 34 | 35 | beforeEach(() => { 36 | testPart = createTestPart(janitor) 37 | testObject = new PhysicsObject(objectName, { mass: 10 }, undefined, testPart) 38 | }) 39 | 40 | it("should add an object to the engine", () => { 41 | testEngine.AddObject(objectName, testObject) 42 | expect(testEngine.GetObject(objectName)).to.equal(testObject) 43 | }) 44 | 45 | it("should get the correct object", () => { 46 | testEngine.AddObject(objectName, testObject) 47 | const retrieved = testEngine.GetObject(objectName) 48 | expect(retrieved).to.equal(testObject) 49 | }) 50 | 51 | it("should clear objects from the engine", () => { 52 | testEngine.AddObject(objectName, testObject) 53 | testEngine.ClearObjects() 54 | expect(testEngine.GetObject(objectName)).to.equal(undefined) 55 | }) 56 | }) 57 | 58 | describe("Simulation Step", () => { 59 | let testObject: PhysicsObject 60 | let testPart: BasePart 61 | 62 | beforeEach(() => { 63 | testPart = createTestPart(janitor) 64 | testObject = new PhysicsObject(objectName, { mass: 10 }, undefined, testPart) 65 | testEngine.AddObject(objectName, testObject) 66 | 67 | testObject.ApplyState({ 68 | position: new Vector3(0, 10, 0), 69 | velocity: new Vector3(0, 0, 0), 70 | acceleration: new Vector3(0, 0, 0), 71 | forces: [] 72 | }) 73 | }) 74 | 75 | it("should update object position when stepped", () => { 76 | testObject.ApplyForce(new Vector3(0, -9.81, 0)) 77 | testEngine.Start() 78 | testEngine.Step(1/60) 79 | 80 | const newPos = testObject.GetWorldPosition() 81 | expect(newPos.Y < 10).to.equal(true) 82 | }) 83 | 84 | it("should produce deterministic results over multiple steps", () => { 85 | testObject.ApplyForce(new Vector3(0, -9.81, 0)) 86 | testEngine.Start() 87 | for (let i = 0; i < 10; i++) { 88 | testEngine.Step(1/60) 89 | } 90 | const pos1 = testObject.GetWorldPosition() 91 | 92 | testEngine.ClearObjects() 93 | const testPart2 = createTestPart(janitor) 94 | testObject = new PhysicsObject(objectName, { mass: 10 }, undefined, testPart2) 95 | testEngine.AddObject(objectName, testObject) 96 | testObject.ApplyState({ 97 | position: new Vector3(0, 10, 0), 98 | velocity: new Vector3(0, 0, 0), 99 | acceleration: new Vector3(0, 0, 0), 100 | forces: [] 101 | }) 102 | testObject.ApplyForce(new Vector3(0, -9.81, 0)) 103 | for (let i = 0; i < 10; i++) { 104 | testEngine.Step(1/60) 105 | } 106 | const pos2 = testObject.GetWorldPosition() 107 | 108 | expect(vectorsNear(pos1, pos2, 0.001)).to.equal(true) 109 | }) 110 | }) 111 | 112 | describe("Rotational Forces", () => { 113 | let testObject: PhysicsObject; 114 | let testPart: BasePart; 115 | 116 | beforeEach(() => { 117 | testPart = createTestPart(janitor); 118 | testObject = new PhysicsObject(objectName, { mass: 10, momentOfInertia: 2 }, undefined, testPart); 119 | testEngine.AddObject(objectName, testObject); 120 | 121 | testObject.ApplyState({ 122 | position: new Vector3(0, 10, 0), 123 | velocity: new Vector3(0, 0, 0), 124 | acceleration: new Vector3(0, 0, 0), 125 | forces: [] 126 | }); 127 | }); 128 | 129 | it("should update angular velocity and rotation when a force with an offset is applied", () => { 130 | const initialRotation = testObject.rotation; 131 | const initialAngularVelocity = testObject.angularVelocity; 132 | 133 | testObject.ApplyForce(new Vector3(0, -9.81, 0), new Vector3(1, 0, 0)); 134 | testEngine.Start(); 135 | testEngine.Step(1/60); 136 | 137 | const newAngularVelocity = testObject.angularVelocity; 138 | const newRotation = testObject.rotation; 139 | 140 | expect(newAngularVelocity.Magnitude > 0).to.equal(true); 141 | 142 | const [ix, iy, iz] = initialRotation.ToEulerAnglesXYZ(); 143 | const [nx, ny, nz] = newRotation.ToEulerAnglesXYZ(); 144 | expect((math.abs(nx - ix) > 0) || (math.abs(ny - iy) > 0) || (math.abs(nz - iz) > 0)).to.equal(true); 145 | }); 146 | }); 147 | 148 | describe("Collision Simulation: Bouncing Balls on Ground", () => { 149 | let ground: PhysicsObject; 150 | let ball1: PhysicsObject; 151 | let ball2: PhysicsObject; 152 | let groundPart: BasePart; 153 | let ball1Part: BasePart; 154 | let ball2Part: BasePart; 155 | 156 | beforeEach(() => { 157 | groundPart = createTestPart(janitor); 158 | groundPart.Position = new Vector3(0, 0, 0); 159 | ground = new PhysicsObject("ground", { 160 | mass: 1e9, 161 | collisionMethod: "box", 162 | size: new Vector3(100, 1, 100), 163 | ignoreGlobalForces: true, 164 | }, { 165 | position: new Vector3(0, 0.5, 0), 166 | velocity: new Vector3(0, 0, 0), 167 | acceleration: new Vector3(0, 0, 0), 168 | forces: [] 169 | }, groundPart); 170 | 171 | ball1Part = createTestPart(janitor); 172 | ball2Part = createTestPart(janitor); 173 | ball1 = new PhysicsObject("ball1", { 174 | mass: 10, 175 | collisionMethod: "box", 176 | size: new Vector3(1, 1, 1), 177 | ignoreGlobalForces: false, 178 | elasticity: 0.4, 179 | }, { 180 | position: new Vector3(0, 19, 0), 181 | velocity: new Vector3(0, 0, 0), 182 | acceleration: new Vector3(0, 0, 0), 183 | forces: [] 184 | }, ball1Part); 185 | ball2 = new PhysicsObject("ball2", { 186 | mass: 10, 187 | collisionMethod: "point", 188 | ignoreGlobalForces: false, 189 | elasticity: 0.4, 190 | }, { 191 | position: new Vector3(5, 19, 0), 192 | velocity: new Vector3(0, 0, 0), 193 | acceleration: new Vector3(0, 0, 0), 194 | forces: [] 195 | }, ball2Part); 196 | 197 | testEngine.AddObject("ground", ground); 198 | testEngine.AddObject("ball1", ball1); 199 | testEngine.AddObject("ball2", ball2); 200 | 201 | testEngine.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 202 | 203 | testEngine.Start(); 204 | }); 205 | 206 | it("should simulate box collision", () => { 207 | let ball1Bounced = false; 208 | 209 | for (let i = 0; i < 300; i++) { 210 | testEngine.Step(1/60); 211 | if (i > 60 && ball1.velocity.Y > 0) { 212 | ball1Bounced = true; 213 | } 214 | } 215 | 216 | expect(ball1Bounced).to.equal(true); 217 | expect(ball1.GetWorldPosition().Y >= 1).to.equal(true); 218 | 219 | expect(vectorsNear(ground.GetWorldPosition(), new Vector3(0, 0.5, 0), 0.001)).to.equal(true); 220 | }); 221 | 222 | it("should simulate point collision", () => { 223 | let ball2Bounced = false; 224 | 225 | for (let i = 0; i < 300; i++) { 226 | testEngine.Step(1/60); 227 | 228 | if (i > 60 && ball2.velocity.Y > 0) { 229 | ball2Bounced = true; 230 | } 231 | } 232 | 233 | expect(ball2Bounced).to.equal(true); 234 | 235 | expect(ball2.GetWorldPosition().Y >= 1).to.equal(true); 236 | 237 | expect(vectorsNear(ground.GetWorldPosition(), new Vector3(0, 0.5, 0), 0.001)).to.equal(true); 238 | }); 239 | }); 240 | 241 | describe("Non-Collidable Objects and Ignoring Global Forces", () => { 242 | let nonCollidable: PhysicsObject; 243 | let testPart: BasePart; 244 | 245 | beforeEach(() => { 246 | testPart = createTestPart(janitor); 247 | nonCollidable = new PhysicsObject("nonCollidable", { 248 | mass: 5, 249 | collisionMethod: "none", 250 | ignoreGlobalForces: true, 251 | }, { 252 | position: new Vector3(10, 10, 0), 253 | velocity: new Vector3(0, 0, 0), 254 | acceleration: new Vector3(0, 0, 0), 255 | forces: [] 256 | }, testPart); 257 | 258 | testEngine.AddObject("nonCollidable", nonCollidable); 259 | testEngine.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 260 | testEngine.Start(); 261 | }); 262 | 263 | it("should not apply collisions to non-collidable objects", () => { 264 | for (let i = 0; i < 60; i++) { 265 | testEngine.Step(1/60); 266 | } 267 | expect(nonCollidable.velocity.Magnitude).to.equal(0); 268 | }); 269 | 270 | it("should ignore global forces for objects marked to ignore them", () => { 271 | for (let i = 0; i < 60; i++) { 272 | testEngine.Step(1/60); 273 | } 274 | expect(vectorsNear(nonCollidable.GetWorldPosition(), new Vector3(10, 10, 0), 0.001)).to.equal(true); 275 | }); 276 | }); 277 | 278 | describe("Part Rotation & Positioning", () => { 279 | let testObject: PhysicsObject; 280 | let testPart: BasePart; 281 | 282 | beforeEach(() => { 283 | testPart = createTestPart(janitor); 284 | testObject = new PhysicsObject(objectName, { mass: 10, momentOfInertia: 2 }, undefined, testPart); 285 | testEngine.AddObject(objectName, testObject); 286 | 287 | testObject.ApplyState({ 288 | position: new Vector3(0, 10, 0), 289 | velocity: new Vector3(0, 0, 0), 290 | acceleration: new Vector3(0, 0, 0), 291 | forces: [] 292 | }); 293 | }); 294 | 295 | it("should update the attached part's CFrame with rotation and position", () => { 296 | testObject.ApplyForce(new Vector3(0, -9.81, 0), new Vector3(1, 0, 0)); 297 | testEngine.Start(); 298 | testEngine.Step(1/60); 299 | 300 | const expectedCFrame = testObject.rotation.add(testObject.GetWorldPosition()); 301 | const partCFrame = testPart.CFrame; 302 | 303 | const posDiff = (partCFrame.Position.sub(expectedCFrame.Position)).Magnitude; 304 | expect(posDiff < 0.001).to.equal(true); 305 | 306 | expect(rotationsNear(expectedCFrame, partCFrame, 0.001)).to.equal(true); 307 | }); 308 | }); 309 | 310 | describe("Deterministic Simulation", () => { 311 | it("should produce identical final position given same initial state and force", () => { 312 | const engine1 = new PhysicsEngine(); 313 | const testObject1 = new PhysicsObject("detTest", { 314 | mass: 10, 315 | collisionMethod: "none" 316 | }, { 317 | position: new Vector3(0, 10, 0), 318 | velocity: new Vector3(0, 0, 0), 319 | acceleration: new Vector3(0, 0, 0), 320 | forces: [] 321 | }); 322 | engine1.AddObject("detTest", testObject1); 323 | engine1.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 324 | 325 | const appliedForce = new Vector3(5, 20, -3); 326 | testObject1.ApplyForce(appliedForce); 327 | engine1.Start(); 328 | 329 | for (let i = 0; i < 300; i++) { 330 | engine1.Step(1/60); 331 | } 332 | const finalPos1 = testObject1.GetWorldPosition(); 333 | 334 | const engine2 = new PhysicsEngine(); 335 | const testObject2 = new PhysicsObject("detTest", { 336 | mass: 10, 337 | collisionMethod: "none" 338 | }, { 339 | position: new Vector3(0, 10, 0), 340 | velocity: new Vector3(0, 0, 0), 341 | acceleration: new Vector3(0, 0, 0), 342 | forces: [] 343 | }); 344 | engine2.AddObject("detTest", testObject2); 345 | engine2.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 346 | testObject2.ApplyForce(appliedForce); 347 | engine2.Start(); 348 | for (let i = 0; i < 300; i++) { 349 | engine2.Step(1/60); 350 | } 351 | const finalPos2 = testObject2.GetWorldPosition(); 352 | 353 | expect(vectorsNear(finalPos1, finalPos2, 0.001)).to.equal(true); 354 | }); 355 | }); 356 | 357 | describe("SetTime Method", () => { 358 | let testEngine: PhysicsEngine; 359 | let testObject: PhysicsObject; 360 | 361 | beforeEach(() => { 362 | testEngine = new PhysicsEngine(); 363 | testObject = new PhysicsObject("test", { mass: 10, collisionMethod: "none" }, { 364 | position: new Vector3(0, 10, 0), 365 | velocity: new Vector3(0, 0, 0), 366 | acceleration: new Vector3(0, 0, 0), 367 | forces: [] 368 | }); 369 | testEngine.AddObject("test", testObject); 370 | testEngine.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 371 | }); 372 | 373 | it("should update simulation state to the target time", () => { 374 | testEngine.Start(); 375 | testEngine.Step(1); 376 | const posBefore = testObject.GetWorldPosition(); 377 | 378 | testEngine.SetTime(10); 379 | const posAfter = testObject.GetWorldPosition(); 380 | const timeAfter = testEngine.currentTime; 381 | 382 | expect(math.round(timeAfter)).to.equal(10); 383 | expect(posAfter.Y < posBefore.Y).to.equal(true); 384 | }); 385 | }); 386 | 387 | describe("Global Forces", () => { 388 | let testObject: PhysicsObject; 389 | let testPart: BasePart; 390 | 391 | beforeEach(() => { 392 | testPart = createTestPart(janitor); 393 | testObject = new PhysicsObject(objectName, { mass: 10 }, undefined, testPart); 394 | testEngine.AddObject(objectName, testObject); 395 | 396 | testObject.ApplyState({ 397 | position: new Vector3(0, 10, 0), 398 | velocity: new Vector3(0, 0, 0), 399 | acceleration: new Vector3(0, 0, 0), 400 | forces: [] 401 | }); 402 | }); 403 | 404 | it("should apply global forces to all objects", () => { 405 | testEngine.AddGlobalForce("gravity", new Vector3(0, -9.81, 0)); 406 | testEngine.Start(); 407 | testEngine.Step(1/60); 408 | 409 | const state = testObject.GetState(); 410 | expect(state.velocity.Y < 0).to.equal(true); 411 | 412 | const worldPos = testObject.GetWorldPosition(); 413 | expect(worldPos.Y < 10).to.equal(true); 414 | }); 415 | }); 416 | 417 | describe("Snapshot and Restore", () => { 418 | let testObject: PhysicsObject 419 | let testPart: BasePart 420 | 421 | beforeEach(() => { 422 | testPart = createTestPart(janitor) 423 | testObject = new PhysicsObject(objectName, { mass: 10 }, undefined, testPart) 424 | testEngine.AddObject(objectName, testObject) 425 | 426 | testObject.ApplyState({ 427 | position: new Vector3(0, 10, 0), 428 | velocity: new Vector3(0, 0, 0), 429 | acceleration: new Vector3(0, 0, 0), 430 | forces: [] 431 | }) 432 | testEngine.Start() 433 | testEngine.Step(1/60) 434 | }) 435 | 436 | it("should create a snapshot and restore it", () => { 437 | const snapshot = testEngine.GetSnapshot() 438 | 439 | testObject.ApplyState({ 440 | position: new Vector3(100, 100, 100), 441 | velocity: new Vector3(0, 0, 0), 442 | acceleration: new Vector3(0, 0, 0), 443 | forces: [] 444 | }) 445 | 446 | testEngine.ApplySnapshot(snapshot) 447 | const restoredState = testObject.GetState() 448 | const snapState = snapshot.objects[objectName] 449 | 450 | expect(vectorsNear(restoredState.position, snapState.position, 0.001)).to.equal(true) 451 | expect(vectorsNear(restoredState.velocity, snapState.velocity, 0.001)).to.equal(true) 452 | expect(vectorsNear(restoredState.acceleration, snapState.acceleration, 0.001)).to.equal(true) 453 | }) 454 | }) 455 | 456 | afterAll(() => janitor.Destroy()) 457 | } 458 | -------------------------------------------------------------------------------- /testez-companion.toml: -------------------------------------------------------------------------------- 1 | roots = ["ReplicatedStorage/delta/tests"] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // required 4 | "allowSyntheticDefaultImports": true, 5 | "downlevelIteration": true, 6 | "jsx": "react", 7 | "jsxFactory": "Roact.createElement", 8 | "jsxFragmentFactory": "Roact.createFragment", 9 | "module": "commonjs", 10 | "moduleResolution": "Node", 11 | "noLib": true, 12 | "resolveJsonModule": true, 13 | "experimentalDecorators": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "moduleDetection": "force", 16 | "strict": true, 17 | "target": "ESNext", 18 | "typeRoots": ["node_modules/@rbxts"], 19 | 20 | // configurable 21 | "rootDir": "src", 22 | "outDir": "out", 23 | "incremental": true, 24 | "tsBuildInfoFile": "out/tsconfig.tsbuildinfo", 25 | "declaration": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "prisma-dev/delta" 7 | version = "1.1.1" 8 | dependencies = [] 9 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prisma-dev/delta" 3 | description = "Deterministic physics library for Roblox" 4 | version = "1.1.4" 5 | license = "MIT" 6 | registry = "https://github.com/UpliftGames/wally-index" 7 | realm = "shared" 8 | exclude = ["**"] 9 | include = ["delta","delta/include/**","delta/source/**","delta/tests/**","delta/source/util/**","delta/source/util/helper/**","delta/source/util/math/**","delta/include/node_modules/**","delta/include/node_modules/***","default.project.json", "wally.toml"] --------------------------------------------------------------------------------