roblox-opencode 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/README.md +122 -0
  2. package/commands/setup-game.md +108 -0
  3. package/commands/sync-check.md +53 -0
  4. package/core/roblox-core.md +93 -0
  5. package/dist/server.js +167 -0
  6. package/package.json +35 -0
  7. package/skills/roblox-analytics/SKILL.md +277 -0
  8. package/skills/roblox-analytics/references/event-batcher.luau +75 -0
  9. package/skills/roblox-animation-vfx/SKILL.md +1325 -0
  10. package/skills/roblox-architecture/SKILL.md +863 -0
  11. package/skills/roblox-architecture/references/combat-systems.md +1381 -0
  12. package/skills/roblox-code-review/SKILL.md +687 -0
  13. package/skills/roblox-data/SKILL.md +889 -0
  14. package/skills/roblox-data/references/inventory-systems.md +1729 -0
  15. package/skills/roblox-debug/SKILL.md +99 -0
  16. package/skills/roblox-gui/SKILL.md +1103 -0
  17. package/skills/roblox-gui-fusion/SKILL.md +150 -0
  18. package/skills/roblox-gui-fusion/references/inventory.luau +427 -0
  19. package/skills/roblox-gui-fusion/references/settings-menu.luau +579 -0
  20. package/skills/roblox-gui-fusion/references/shop.luau +411 -0
  21. package/skills/roblox-luau-mastery/SKILL.md +1519 -0
  22. package/skills/roblox-monetization/SKILL.md +1084 -0
  23. package/skills/roblox-monetization/references/process-receipt.luau +131 -0
  24. package/skills/roblox-networking/SKILL.md +669 -0
  25. package/skills/roblox-networking/references/remote-validator.luau +193 -0
  26. package/skills/roblox-publish-checklist/SKILL.md +128 -0
  27. package/skills/roblox-runtime/SKILL.md +753 -0
  28. package/skills/roblox-sharp-edges/SKILL.md +295 -0
  29. package/skills/roblox-sync/SKILL.md +126 -0
  30. package/skills/roblox-testing/SKILL.md +943 -0
  31. package/skills/roblox-tooling/SKILL.md +150 -0
  32. package/vendor/LICENSES/ProfileStore-LICENSE +201 -0
  33. package/vendor/LICENSES/RbxUtil-LICENSE +7 -0
  34. package/vendor/LICENSES/promise-LICENSE +21 -0
  35. package/vendor/LICENSES/t-LICENSE +21 -0
  36. package/vendor/LICENSES/testez-LICENSE +201 -0
  37. package/vendor/README.md +84 -0
  38. package/vendor/fusion/Animation/ExternalTime.luau +84 -0
  39. package/vendor/fusion/Animation/Spring.luau +322 -0
  40. package/vendor/fusion/Animation/Stopwatch.luau +128 -0
  41. package/vendor/fusion/Animation/Tween.luau +187 -0
  42. package/vendor/fusion/Animation/getTweenDuration.luau +27 -0
  43. package/vendor/fusion/Animation/getTweenRatio.luau +47 -0
  44. package/vendor/fusion/Animation/lerpType.luau +164 -0
  45. package/vendor/fusion/Animation/packType.luau +100 -0
  46. package/vendor/fusion/Animation/springCoefficients.luau +80 -0
  47. package/vendor/fusion/Animation/unpackType.luau +103 -0
  48. package/vendor/fusion/Colour/Oklab.luau +70 -0
  49. package/vendor/fusion/Colour/sRGB.luau +55 -0
  50. package/vendor/fusion/External.luau +168 -0
  51. package/vendor/fusion/ExternalDebug.luau +70 -0
  52. package/vendor/fusion/Graph/Observer.luau +114 -0
  53. package/vendor/fusion/Graph/castToGraph.luau +29 -0
  54. package/vendor/fusion/Graph/change.luau +81 -0
  55. package/vendor/fusion/Graph/depend.luau +33 -0
  56. package/vendor/fusion/Graph/evaluate.luau +56 -0
  57. package/vendor/fusion/Instances/Attribute.luau +58 -0
  58. package/vendor/fusion/Instances/AttributeChange.luau +47 -0
  59. package/vendor/fusion/Instances/AttributeOut.luau +63 -0
  60. package/vendor/fusion/Instances/Child.luau +21 -0
  61. package/vendor/fusion/Instances/Children.luau +148 -0
  62. package/vendor/fusion/Instances/Hydrate.luau +33 -0
  63. package/vendor/fusion/Instances/New.luau +53 -0
  64. package/vendor/fusion/Instances/OnChange.luau +50 -0
  65. package/vendor/fusion/Instances/OnEvent.luau +54 -0
  66. package/vendor/fusion/Instances/Out.luau +69 -0
  67. package/vendor/fusion/Instances/applyInstanceProps.luau +149 -0
  68. package/vendor/fusion/Instances/defaultProps.luau +194 -0
  69. package/vendor/fusion/LICENSE +21 -0
  70. package/vendor/fusion/Logging/formatError.luau +49 -0
  71. package/vendor/fusion/Logging/messages.luau +52 -0
  72. package/vendor/fusion/Logging/parseError.luau +25 -0
  73. package/vendor/fusion/Memory/checkLifetime.luau +134 -0
  74. package/vendor/fusion/Memory/deriveScope.luau +24 -0
  75. package/vendor/fusion/Memory/deriveScopeImpl.luau +45 -0
  76. package/vendor/fusion/Memory/doCleanup.luau +79 -0
  77. package/vendor/fusion/Memory/innerScope.luau +34 -0
  78. package/vendor/fusion/Memory/legacyCleanup.luau +18 -0
  79. package/vendor/fusion/Memory/needsDestruction.luau +17 -0
  80. package/vendor/fusion/Memory/poisonScope.luau +34 -0
  81. package/vendor/fusion/Memory/scopePool.luau +55 -0
  82. package/vendor/fusion/Memory/scoped.luau +27 -0
  83. package/vendor/fusion/Memory/whichLivesLonger.luau +75 -0
  84. package/vendor/fusion/RobloxExternal.luau +98 -0
  85. package/vendor/fusion/State/Computed.luau +139 -0
  86. package/vendor/fusion/State/For/Disassembly.luau +211 -0
  87. package/vendor/fusion/State/For/ForTypes.luau +30 -0
  88. package/vendor/fusion/State/For/init.luau +110 -0
  89. package/vendor/fusion/State/ForKeys.luau +94 -0
  90. package/vendor/fusion/State/ForPairs.luau +97 -0
  91. package/vendor/fusion/State/ForValues.luau +94 -0
  92. package/vendor/fusion/State/Value.luau +88 -0
  93. package/vendor/fusion/State/castToState.luau +26 -0
  94. package/vendor/fusion/State/peek.luau +31 -0
  95. package/vendor/fusion/State/updateAll.luau +1 -0
  96. package/vendor/fusion/Types.luau +314 -0
  97. package/vendor/fusion/Utility/Contextual.luau +91 -0
  98. package/vendor/fusion/Utility/Safe.luau +23 -0
  99. package/vendor/fusion/Utility/isSimilar.luau +29 -0
  100. package/vendor/fusion/Utility/merge.luau +35 -0
  101. package/vendor/fusion/Utility/nameOf.luau +35 -0
  102. package/vendor/fusion/Utility/never.luau +14 -0
  103. package/vendor/fusion/Utility/nicknames.luau +11 -0
  104. package/vendor/fusion/Utility/xtypeof.luau +27 -0
  105. package/vendor/fusion/init.luau +82 -0
  106. package/vendor/profilestore/init.luau +2243 -0
  107. package/vendor/promise/init.luau +1982 -0
  108. package/vendor/rbxutil/buffer-util/Buffer.test.luau +25 -0
  109. package/vendor/rbxutil/buffer-util/BufferReader.luau +228 -0
  110. package/vendor/rbxutil/buffer-util/BufferWriter.luau +269 -0
  111. package/vendor/rbxutil/buffer-util/DataTypeBuffer.luau +223 -0
  112. package/vendor/rbxutil/buffer-util/Types.luau +60 -0
  113. package/vendor/rbxutil/buffer-util/index.d.ts +153 -0
  114. package/vendor/rbxutil/buffer-util/init.luau +41 -0
  115. package/vendor/rbxutil/buffer-util/package.json +16 -0
  116. package/vendor/rbxutil/buffer-util/wally.toml +9 -0
  117. package/vendor/rbxutil/comm/Client/ClientComm.luau +232 -0
  118. package/vendor/rbxutil/comm/Client/ClientRemoteProperty.luau +156 -0
  119. package/vendor/rbxutil/comm/Client/ClientRemoteSignal.luau +109 -0
  120. package/vendor/rbxutil/comm/Client/init.luau +135 -0
  121. package/vendor/rbxutil/comm/Server/RemoteProperty.luau +295 -0
  122. package/vendor/rbxutil/comm/Server/RemoteSignal.luau +211 -0
  123. package/vendor/rbxutil/comm/Server/ServerComm.luau +211 -0
  124. package/vendor/rbxutil/comm/Server/init.luau +140 -0
  125. package/vendor/rbxutil/comm/Types.luau +18 -0
  126. package/vendor/rbxutil/comm/Util.luau +27 -0
  127. package/vendor/rbxutil/comm/init.luau +35 -0
  128. package/vendor/rbxutil/comm/wally.toml +13 -0
  129. package/vendor/rbxutil/component/init.luau +759 -0
  130. package/vendor/rbxutil/component/init.test.luau +311 -0
  131. package/vendor/rbxutil/component/wally.toml +14 -0
  132. package/vendor/rbxutil/concur/init.luau +542 -0
  133. package/vendor/rbxutil/concur/init.test.luau +364 -0
  134. package/vendor/rbxutil/concur/wally.toml +8 -0
  135. package/vendor/rbxutil/enum-list/init.luau +101 -0
  136. package/vendor/rbxutil/enum-list/init.test.luau +91 -0
  137. package/vendor/rbxutil/enum-list/wally.toml +8 -0
  138. package/vendor/rbxutil/find/index.d.ts +20 -0
  139. package/vendor/rbxutil/find/init.luau +44 -0
  140. package/vendor/rbxutil/find/package.json +17 -0
  141. package/vendor/rbxutil/find/wally.toml +8 -0
  142. package/vendor/rbxutil/input/Gamepad.luau +559 -0
  143. package/vendor/rbxutil/input/Keyboard.luau +124 -0
  144. package/vendor/rbxutil/input/Mouse.luau +278 -0
  145. package/vendor/rbxutil/input/PreferredInput.luau +91 -0
  146. package/vendor/rbxutil/input/Touch.luau +120 -0
  147. package/vendor/rbxutil/input/init.luau +33 -0
  148. package/vendor/rbxutil/input/wally.toml +12 -0
  149. package/vendor/rbxutil/loader/index.d.ts +15 -0
  150. package/vendor/rbxutil/loader/init.luau +137 -0
  151. package/vendor/rbxutil/loader/wally.toml +8 -0
  152. package/vendor/rbxutil/log/index.d.ts +38 -0
  153. package/vendor/rbxutil/log/init.luau +746 -0
  154. package/vendor/rbxutil/log/wally.toml +8 -0
  155. package/vendor/rbxutil/net/init.luau +190 -0
  156. package/vendor/rbxutil/net/wally.toml +8 -0
  157. package/vendor/rbxutil/option/index.d.ts +44 -0
  158. package/vendor/rbxutil/option/init.luau +489 -0
  159. package/vendor/rbxutil/option/init.test.luau +342 -0
  160. package/vendor/rbxutil/option/wally.toml +8 -0
  161. package/vendor/rbxutil/pid/index.d.ts +53 -0
  162. package/vendor/rbxutil/pid/init.luau +195 -0
  163. package/vendor/rbxutil/pid/package.json +16 -0
  164. package/vendor/rbxutil/pid/wally.toml +9 -0
  165. package/vendor/rbxutil/quaternion/index.d.ts +117 -0
  166. package/vendor/rbxutil/quaternion/init.luau +570 -0
  167. package/vendor/rbxutil/quaternion/package.json +16 -0
  168. package/vendor/rbxutil/quaternion/wally.toml +9 -0
  169. package/vendor/rbxutil/query/index.d.ts +43 -0
  170. package/vendor/rbxutil/query/init.luau +117 -0
  171. package/vendor/rbxutil/query/package.json +18 -0
  172. package/vendor/rbxutil/query/wally.toml +9 -0
  173. package/vendor/rbxutil/sequent/index.d.ts +28 -0
  174. package/vendor/rbxutil/sequent/init.luau +340 -0
  175. package/vendor/rbxutil/sequent/package.json +16 -0
  176. package/vendor/rbxutil/sequent/wally.toml +9 -0
  177. package/vendor/rbxutil/ser/init.luau +175 -0
  178. package/vendor/rbxutil/ser/init.test.luau +50 -0
  179. package/vendor/rbxutil/ser/wally.toml +11 -0
  180. package/vendor/rbxutil/shake/index.d.ts +36 -0
  181. package/vendor/rbxutil/shake/init.luau +532 -0
  182. package/vendor/rbxutil/shake/init.test.luau +267 -0
  183. package/vendor/rbxutil/shake/package.json +16 -0
  184. package/vendor/rbxutil/shake/wally.toml +9 -0
  185. package/vendor/rbxutil/signal/index.d.ts +100 -0
  186. package/vendor/rbxutil/signal/init.luau +432 -0
  187. package/vendor/rbxutil/signal/init.test.luau +190 -0
  188. package/vendor/rbxutil/signal/package.json +17 -0
  189. package/vendor/rbxutil/signal/wally.toml +9 -0
  190. package/vendor/rbxutil/silo/TableWatcher.luau +65 -0
  191. package/vendor/rbxutil/silo/Util.luau +55 -0
  192. package/vendor/rbxutil/silo/init.luau +338 -0
  193. package/vendor/rbxutil/silo/init.test.luau +215 -0
  194. package/vendor/rbxutil/silo/wally.toml +8 -0
  195. package/vendor/rbxutil/spring/index.d.ts +40 -0
  196. package/vendor/rbxutil/spring/init.luau +97 -0
  197. package/vendor/rbxutil/spring/package.json +17 -0
  198. package/vendor/rbxutil/spring/wally.toml +8 -0
  199. package/vendor/rbxutil/stream/index.d.ts +88 -0
  200. package/vendor/rbxutil/stream/init.luau +597 -0
  201. package/vendor/rbxutil/stream/package.json +18 -0
  202. package/vendor/rbxutil/stream/wally.toml +9 -0
  203. package/vendor/rbxutil/streamable/Streamable.luau +202 -0
  204. package/vendor/rbxutil/streamable/StreamableUtil.luau +80 -0
  205. package/vendor/rbxutil/streamable/init.luau +8 -0
  206. package/vendor/rbxutil/streamable/wally.toml +12 -0
  207. package/vendor/rbxutil/symbol/init.luau +56 -0
  208. package/vendor/rbxutil/symbol/init.test.luau +37 -0
  209. package/vendor/rbxutil/symbol/wally.toml +8 -0
  210. package/vendor/rbxutil/table-util/init.luau +938 -0
  211. package/vendor/rbxutil/table-util/init.test.luau +439 -0
  212. package/vendor/rbxutil/table-util/wally.toml +8 -0
  213. package/vendor/rbxutil/task-queue/index.d.ts +27 -0
  214. package/vendor/rbxutil/task-queue/init.luau +97 -0
  215. package/vendor/rbxutil/task-queue/wally.toml +8 -0
  216. package/vendor/rbxutil/timer/index.d.ts +81 -0
  217. package/vendor/rbxutil/timer/init.luau +249 -0
  218. package/vendor/rbxutil/timer/init.test.luau +73 -0
  219. package/vendor/rbxutil/timer/wally.toml +11 -0
  220. package/vendor/rbxutil/tree/index.d.ts +15 -0
  221. package/vendor/rbxutil/tree/init.luau +137 -0
  222. package/vendor/rbxutil/tree/wally.toml +8 -0
  223. package/vendor/rbxutil/trove/index.d.ts +46 -0
  224. package/vendor/rbxutil/trove/init.luau +787 -0
  225. package/vendor/rbxutil/trove/init.test.luau +203 -0
  226. package/vendor/rbxutil/trove/wally.toml +8 -0
  227. package/vendor/rbxutil/typed-remote/init.luau +196 -0
  228. package/vendor/rbxutil/typed-remote/wally.toml +8 -0
  229. package/vendor/rbxutil/wait-for/index.d.ts +17 -0
  230. package/vendor/rbxutil/wait-for/init.luau +257 -0
  231. package/vendor/rbxutil/wait-for/init.test.luau +182 -0
  232. package/vendor/rbxutil/wait-for/wally.toml +11 -0
  233. package/vendor/t/t.lua +1350 -0
  234. package/vendor/testez/Context.lua +26 -0
  235. package/vendor/testez/Expectation.lua +311 -0
  236. package/vendor/testez/ExpectationContext.lua +38 -0
  237. package/vendor/testez/LifecycleHooks.lua +89 -0
  238. package/vendor/testez/Reporters/TeamCityReporter.lua +102 -0
  239. package/vendor/testez/Reporters/TextReporter.lua +106 -0
  240. package/vendor/testez/Reporters/TextReporterQuiet.lua +97 -0
  241. package/vendor/testez/TestBootstrap.lua +147 -0
  242. package/vendor/testez/TestEnum.lua +28 -0
  243. package/vendor/testez/TestPlan.lua +304 -0
  244. package/vendor/testez/TestPlanner.lua +40 -0
  245. package/vendor/testez/TestResults.lua +112 -0
  246. package/vendor/testez/TestRunner.lua +188 -0
  247. package/vendor/testez/TestSession.lua +243 -0
  248. package/vendor/testez/init.lua +40 -0
@@ -0,0 +1,84 @@
1
+ # Vendor Libraries
2
+
3
+ AI routing table for vendored libraries. The harness ships these so studio-native users
4
+ don't need Wally. Two tiers:
5
+
6
+ ## Core (auto-placed with mention)
7
+
8
+ These are placed into the project automatically when relevant. The agent mentions the choice;
9
+ user can veto. Context-mode queries check for existing equivalents before auto-placing.
10
+
11
+ | Library | Path | Source | License | Use Instead Of |
12
+ |---------|------|--------|---------|----------------|
13
+ | **ProfileStore** | `profilestore/init.luau` | loleris/MadStudioRoblox | Apache 2.0 | Raw DataStoreService |
14
+ | **Trove** | `rbxutil/trove/` | Sleitnick/RbxUtil | MIT | Manual connection tracking |
15
+ | **Signal** | `rbxutil/signal/` | Sleitnick/RbxUtil | MIT | BindableEvent for module-to-module |
16
+ | **Promise** | `promise/init.luau` | evaera/roblox-lua-promise | MIT | Raw coroutines for async |
17
+ | **Comm** | `rbxutil/comm/` | Sleitnick/RbxUtil | MIT | Raw RemoteEvent/RemoteFunction |
18
+ | **Component** | `rbxutil/component/` | Sleitnick/RbxUtil | MIT | Manual CollectionService tag listeners |
19
+
20
+ ## Recommended (not auto-placed, suggest when relevant)
21
+
22
+ These require user buy-in. Recommend when the task calls for them.
23
+
24
+ | Library | Path | Source | License | Use For |
25
+ |---------|------|--------|---------|---------|
26
+ | **t** | `t/t.lua` | osyrisrblx/t v3.1.1 | MIT | Runtime type checking, RemoteEvent validation, function args |
27
+ | **TestEZ** | `testez/` | Roblox/testez v0.4.2 | Apache 2.0 | BDD testing (.spec files) |
28
+
29
+ ## Available (use when specifically needed)
30
+
31
+ Additional RbxUtil packages. Don't recommend proactively - use when the task specifically
32
+ calls for their functionality.
33
+
34
+ | Library | Path | Purpose |
35
+ |---------|------|---------|
36
+ | **Streamable** | `rbxutil/streamable/` | Safe instance access with StreamingEnabled |
37
+ | **Net** | `rbxutil/net/` | Typed networking wrapper |
38
+ | **Timer** | `rbxutil/timer/` | Repeating/countdown timers with pause/resume |
39
+ | **Shake** | `rbxutil/shake/` | Camera/UI shake effects |
40
+ | **Spring** | `rbxutil/spring/` | Physics-based spring animations |
41
+ | **Input** | `rbxutil/input/` | Gamepad/keyboard/mouse/touch abstraction |
42
+ | **TableUtil** | `rbxutil/table-util/` | Table manipulation utilities (deep copy, merge, etc.) |
43
+ | **Option** | `rbxutil/option/` | Rust-style Option type for nil safety |
44
+ | **Concur** | `rbxutil/concur/` | Structured concurrency primitives |
45
+ | **Silo** | `rbxutil/silo/` | State management (Redux-like) |
46
+ | **Log** | `rbxutil/log/` | Structured logging with levels |
47
+ | **Loader** | `rbxutil/loader/` | Module loader with Init/Start lifecycle |
48
+ | **Query** | `rbxutil/query/` | Instance query builder |
49
+ | **Find** | `rbxutil/find/` | Safe instance finding with type narrowing |
50
+ | **WaitFor** | `rbxutil/wait-for/` | Promise-based WaitForChild |
51
+ | **BufferUtil** | `rbxutil/buffer-util/` | Binary buffer read/write |
52
+ | **Quaternion** | `rbxutil/quaternion/` | Quaternion math for rotations |
53
+ | **PID** | `rbxutil/pid/` | PID controller for smooth following |
54
+ | **Stream** | `rbxutil/stream/` | Reactive data streams |
55
+ | **Sequent** | `rbxutil/sequent/` | Sequential task execution |
56
+ | **TaskQueue** | `rbxutil/task-queue/` | Deferred task batching |
57
+ | **EnumList** | `rbxutil/enum-list/` | Custom enum definitions |
58
+ | **Symbol** | `rbxutil/symbol/` | Unique symbol identifiers |
59
+ | **Ser** | `rbxutil/ser/` | Instance serialization |
60
+ | **Tree** | `rbxutil/tree/` | Instance tree traversal utilities |
61
+ | **TypedRemote** | `rbxutil/typed-remote/` | Type-safe remote events |
62
+
63
+ ## Licenses
64
+
65
+ All license files are in `LICENSES/`. Every vendored library is MIT or Apache 2.0.
66
+
67
+ ## Require Paths
68
+
69
+ When placing vendored libraries into a project, use ReplicatedStorage for shared modules:
70
+
71
+ ```luau
72
+ -- Core libraries (auto-placed into ReplicatedStorage.Packages/)
73
+ local Trove = require(game.ReplicatedStorage.Packages.Trove)
74
+ local Signal = require(game.ReplicatedStorage.Packages.Signal)
75
+ local Promise = require(game.ReplicatedStorage.Packages.Promise)
76
+ local Comm = require(game.ReplicatedStorage.Packages.Comm)
77
+ local Component = require(game.ReplicatedStorage.Packages.Component)
78
+
79
+ -- ProfileStore (server-only, placed in ServerScriptService)
80
+ local ProfileStore = require(game.ServerScriptService.Packages.ProfileStore) -- profilestore/init.luau
81
+ ```
82
+
83
+ Note: Vendor source lives in `.opencode/vendor/` on disk. The require paths above
84
+ reference where Script Sync maps them in the DataModel - not the filesystem path.
@@ -0,0 +1,84 @@
1
+ --!strict
2
+ --!nolint LocalUnused
3
+ --!nolint LocalShadow
4
+ local task = nil -- Disable usage of Roblox's task scheduler
5
+
6
+ --[[
7
+ Outputs the current external time as a state object.
8
+ ]]
9
+
10
+ local Package = script.Parent.Parent
11
+ local Types = require(Package.Types)
12
+ local External = require(Package.External)
13
+ -- Graph
14
+ local change = require(Package.Graph.change)
15
+ -- Utility
16
+ local nicknames = require(Package.Utility.nicknames)
17
+
18
+ type ExternalTime = Types.StateObject<number>
19
+
20
+ type Self = ExternalTime
21
+
22
+ local class = {}
23
+ class.type = "State"
24
+ class.kind = "ExternalTime"
25
+ class.timeliness = "lazy"
26
+ class.dependencySet = table.freeze {}
27
+ class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
28
+
29
+ local METATABLE = table.freeze {__index = class}
30
+
31
+ local allTimers: {Self} = {}
32
+
33
+ local function ExternalTime(
34
+ scope: Types.Scope<unknown>
35
+ ): ExternalTime
36
+ local createdAt = os.clock()
37
+ local self: Self = setmetatable(
38
+ {
39
+ createdAt = createdAt,
40
+ dependentSet = {},
41
+ lastChange = nil,
42
+ scope = scope,
43
+ validity = "invalid"
44
+ },
45
+ METATABLE
46
+ ) :: any
47
+ local destroy = function()
48
+ self.scope = nil
49
+ local index = table.find(allTimers, self)
50
+ if index ~= nil then
51
+ table.remove(allTimers, index)
52
+ end
53
+ end
54
+ self.oldestTask = destroy
55
+ nicknames[self.oldestTask] = "ExternalTime"
56
+ table.insert(scope, destroy)
57
+ table.insert(allTimers, self)
58
+ return self
59
+ end
60
+
61
+ function class._evaluate(
62
+ self: Self
63
+ ): boolean
64
+ -- While someone else could call `change()` on this object, it wouldn't be
65
+ -- idiomatic. So, since the only idiomatic time this function runs is when
66
+ -- the external update step runs, it's safe enough to assume that the result
67
+ -- has always meaningfully changed. The worst that can happen is unexpected
68
+ -- refreshing for people doing unorthodox shenanigans, which is an OK trade.
69
+ return true
70
+ end
71
+
72
+ External.bindToUpdateStep(function(
73
+ externalNow: number
74
+ ): ()
75
+ class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep()
76
+ for _, timer in allTimers do
77
+ change(timer)
78
+ end
79
+ end)
80
+
81
+ -- Do *not* freeze the class table, because it stores the shared value of all
82
+ -- external time objects, and is updated every frame because of that.
83
+ -- table.freeze(class)
84
+ return ExternalTime
@@ -0,0 +1,322 @@
1
+ --!strict
2
+ --!nolint LocalUnused
3
+ --!nolint LocalShadow
4
+ local task = nil -- Disable usage of Roblox's task scheduler
5
+
6
+ --[[
7
+ A specialised state object for following a goal state smoothly over time,
8
+ using physics to shape the motion.
9
+
10
+ https://elttob.uk/Fusion/0.3/api-reference/animation/types/spring/
11
+ ]]
12
+
13
+ local Package = script.Parent.Parent
14
+ local Types = require(Package.Types)
15
+ local External = require(Package.External)
16
+ -- Memory
17
+ local checkLifetime = require(Package.Memory.checkLifetime)
18
+ -- Graph
19
+ local depend = require(Package.Graph.depend)
20
+ local change = require(Package.Graph.change)
21
+ local evaluate = require(Package.Graph.evaluate)
22
+ -- State
23
+ local castToState = require(Package.State.castToState)
24
+ local peek = require(Package.State.peek)
25
+ -- Animation
26
+ local ExternalTime = require(Package.Animation.ExternalTime)
27
+ local Stopwatch = require(Package.Animation.Stopwatch)
28
+ local packType = require(Package.Animation.packType)
29
+ local unpackType = require(Package.Animation.unpackType)
30
+ local springCoefficients = require(Package.Animation.springCoefficients)
31
+ -- Utility
32
+ local nicknames = require(Package.Utility.nicknames)
33
+
34
+ local EPSILON = 0.00001
35
+
36
+ type Self<T> = Types.Spring<T> & {
37
+ _activeDamping: number,
38
+ _activeGoal: T,
39
+ _activeLatestP: {number},
40
+ _activeLatestV: {number},
41
+ _activeNumSprings: number,
42
+ _activeSpeed: number,
43
+ _activeStartP: {number},
44
+ _activeStartV: {number},
45
+ _activeTargetP: {number},
46
+ _activeType: string,
47
+ _speed: Types.UsedAs<number>,
48
+ _damping: Types.UsedAs<number>,
49
+ _goal: Types.UsedAs<T>,
50
+ _stopwatch: Stopwatch.Stopwatch
51
+ }
52
+
53
+ local class = {}
54
+ class.type = "State"
55
+ class.kind = "Spring"
56
+ class.timeliness = "eager"
57
+
58
+ local METATABLE = table.freeze {__index = class}
59
+
60
+ local function Spring<T>(
61
+ scope: Types.Scope<unknown>,
62
+ goal: Types.UsedAs<T>,
63
+ speed: Types.UsedAs<number>?,
64
+ damping: Types.UsedAs<number>?
65
+ ): Types.Spring<T>
66
+ local createdAt = os.clock()
67
+ if typeof(scope) ~= "table" or castToState(scope) ~= nil then
68
+ External.logError("scopeMissing", nil, "Springs", "myScope:Spring(goalState, speed, damping)")
69
+ end
70
+
71
+ local goalState = castToState(goal)
72
+ local stopwatch = nil
73
+ if goalState ~= nil then
74
+ stopwatch = Stopwatch(scope, ExternalTime(scope))
75
+ stopwatch:unpause()
76
+ end
77
+
78
+ local speed = speed or 10
79
+ local damping = damping or 1
80
+
81
+ local self: Self<T> = setmetatable(
82
+ {
83
+ createdAt = createdAt,
84
+ dependencySet = {},
85
+ dependentSet = {},
86
+ lastChange = nil,
87
+ scope = scope,
88
+ validity = "invalid",
89
+ _activeDamping = -1,
90
+ _activeGoal = nil,
91
+ _activeLatestP = {},
92
+ _activeLatestV = {},
93
+ _activeNumSprings = 0,
94
+ _activeSpeed = -1,
95
+ _activeStartP = {},
96
+ _activeStartV = {},
97
+ _activeTargetP = {},
98
+ _activeType = "",
99
+ _damping = damping,
100
+ _EXTREMELY_DANGEROUS_usedAsValue = peek(goal),
101
+ _goal = goal,
102
+ _speed = speed,
103
+ _stopwatch = stopwatch
104
+ },
105
+ METATABLE
106
+ ) :: any
107
+ local destroy = function()
108
+ self.scope = nil
109
+ for dependency in pairs(self.dependencySet) do
110
+ dependency.dependentSet[self] = nil
111
+ end
112
+ end
113
+ self.oldestTask = destroy
114
+ nicknames[self.oldestTask] = "Spring"
115
+ table.insert(scope, destroy)
116
+
117
+ if goalState ~= nil then
118
+ checkLifetime.bOutlivesA(
119
+ scope, self.oldestTask,
120
+ goalState.scope, goalState.oldestTask,
121
+ checkLifetime.formatters.animationGoal
122
+ )
123
+ end
124
+ local speedState = castToState(speed)
125
+ if speedState ~= nil then
126
+ checkLifetime.bOutlivesA(
127
+ scope, self.oldestTask,
128
+ speedState.scope, speedState.oldestTask,
129
+ checkLifetime.formatters.parameter, "speed"
130
+ )
131
+ end
132
+ local dampingState = castToState(damping)
133
+ if dampingState ~= nil then
134
+ checkLifetime.bOutlivesA(
135
+ scope, self.oldestTask,
136
+ dampingState.scope, dampingState.oldestTask,
137
+ checkLifetime.formatters.parameter, "damping"
138
+ )
139
+ end
140
+
141
+ -- Eagerly evaluated objects need to evaluate themselves so that they're
142
+ -- valid at all times.
143
+ evaluate(self, true)
144
+
145
+ return self
146
+ end
147
+
148
+ function class.addVelocity<T>(
149
+ self: Self<T>,
150
+ deltaValue: T
151
+ ): ()
152
+ evaluate(self, false) -- ensure the _active params are up to date
153
+ local deltaType = typeof(deltaValue)
154
+ if deltaType ~= self._activeType then
155
+ External.logError("springTypeMismatch", nil, deltaType, self._activeType)
156
+ end
157
+ local newStartV = unpackType(deltaValue, deltaType)
158
+ for index, velocity in self._activeLatestV do
159
+ newStartV[index] += velocity
160
+ end
161
+ self._activeStartP = table.clone(self._activeLatestP)
162
+ self._activeStartV = newStartV
163
+ self._stopwatch:zero()
164
+ self._stopwatch:unpause()
165
+ change(self)
166
+ end
167
+
168
+ function class.get<T>(
169
+ self: Self<T>
170
+ ): never
171
+ return External.logError("stateGetWasRemoved")
172
+ end
173
+
174
+ function class.setPosition<T>(
175
+ self: Self<T>,
176
+ newValue: T
177
+ ): ()
178
+ evaluate(self, false) -- ensure the _active params are up to date
179
+ local newType = typeof(newValue)
180
+ if newType ~= self._activeType then
181
+ External.logError("springTypeMismatch", nil, newType, self._activeType)
182
+ end
183
+ self._activeStartP = unpackType(newValue, newType)
184
+ self._activeStartV = table.clone(self._activeLatestV)
185
+ self._stopwatch:zero()
186
+ self._stopwatch:unpause()
187
+ change(self)
188
+ end
189
+
190
+ function class.setVelocity<T>(
191
+ self: Self<T>,
192
+ newValue: T
193
+ ): ()
194
+ evaluate(self, false) -- ensure the _active params are up to date
195
+ local newType = typeof(newValue)
196
+ if newType ~= self._activeType then
197
+ External.logError("springTypeMismatch", nil, newType, self._activeType)
198
+ end
199
+ self._activeStartP = table.clone(self._activeLatestP)
200
+ self._activeStartV = unpackType(newValue, newType)
201
+ self._stopwatch:zero()
202
+ self._stopwatch:unpause()
203
+ change(self)
204
+ end
205
+
206
+ function class._evaluate<T>(
207
+ self: Self<T>
208
+ ): boolean
209
+ local goal = castToState(self._goal)
210
+ -- Allow non-state goals to pass through transparently.
211
+ if goal == nil then
212
+ self._EXTREMELY_DANGEROUS_usedAsValue = self._goal :: T
213
+ return false
214
+ end
215
+ -- depend(self, goal)
216
+ local nextFrameGoal = peek(goal)
217
+ -- Protect against NaN goals.
218
+ if nextFrameGoal ~= nextFrameGoal then
219
+ External.logWarn("springNanGoal")
220
+ return false
221
+ end
222
+ local nextFrameGoalType = typeof(nextFrameGoal)
223
+ local discontinuous = nextFrameGoalType ~= self._activeType
224
+
225
+ local stopwatch = self._stopwatch :: Stopwatch.Stopwatch
226
+ local elapsed = peek(stopwatch)
227
+ depend(self, stopwatch)
228
+
229
+ local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
230
+ local newValue: T
231
+
232
+ if discontinuous then
233
+ -- Propagate changes in type instantly throughout the whole reactive
234
+ -- graph, even if simulation is logically one frame behind, because it
235
+ -- makes the whole graph behave more consistently.
236
+ newValue = nextFrameGoal
237
+ elseif elapsed <= 0 then
238
+ newValue = oldValue
239
+ else
240
+ -- Calculate spring motion.
241
+ -- IMPORTANT: use the parameters from last frame, not this frame. We're
242
+ -- integrating the motion that happened over the last frame, after all.
243
+ -- The stopwatch will have captured the length of time needed correctly.
244
+ local posPos, posVel, velPos, velVel = springCoefficients(
245
+ elapsed,
246
+ self._activeDamping,
247
+ self._activeSpeed
248
+ )
249
+ local isMoving = false
250
+ for index = 1, self._activeNumSprings do
251
+ local startP = self._activeStartP[index]
252
+ local targetP = self._activeTargetP[index]
253
+ local startV = self._activeStartV[index]
254
+ local startD = startP - targetP
255
+ local latestD = startD * posPos + startV * posVel
256
+ local latestV = startD * velPos + startV * velVel
257
+ if latestD ~= latestD or latestV ~= latestV then
258
+ External.logWarn("springNanMotion")
259
+ latestD, latestV = 0, 0
260
+ end
261
+ if math.abs(latestD) > EPSILON or math.abs(latestV) > EPSILON then
262
+ isMoving = true
263
+ end
264
+ local latestP = latestD + targetP
265
+ self._activeLatestP[index] = latestP
266
+ self._activeLatestV[index] = latestV
267
+ end
268
+ -- Sleep and snap to goal if the motion has decayed to a negligible amount.
269
+ if not isMoving then
270
+ for index = 1, self._activeNumSprings do
271
+ self._activeLatestP[index] = self._activeTargetP[index]
272
+ end
273
+ -- TODO: figure out how to do sleeping correctly for single frame
274
+ -- changes
275
+ -- stopwatch:pause()
276
+ -- stopwatch:zero()
277
+ end
278
+ -- Pack springs into final value.
279
+ newValue = packType(self._activeLatestP, self._activeType) :: any
280
+ end
281
+
282
+ -- Reconfigure spring when any of its parameters are changed.
283
+ -- This should happen after integrating the last frame's motion.
284
+ -- NOTE: don't need to add a dependency on these objects! they do not cause
285
+ -- a spring to wake from sleep, so the stopwatch dependency is sufficient.
286
+ local nextFrameSpeed = peek(self._speed) :: number
287
+ local nextFrameDamping = peek(self._damping) :: number
288
+ if
289
+ discontinuous or
290
+ nextFrameGoal ~= self._activeGoal or
291
+ nextFrameSpeed ~= self._activeSpeed or
292
+ nextFrameDamping ~= self._activeDamping
293
+ then
294
+ self._activeTargetP = unpackType(nextFrameGoal, nextFrameGoalType)
295
+ self._activeNumSprings = #self._activeTargetP
296
+ if discontinuous then
297
+ self._activeStartP = table.clone(self._activeTargetP)
298
+ self._activeLatestP = table.clone(self._activeTargetP)
299
+ self._activeStartV = table.create(self._activeNumSprings, 0)
300
+ self._activeLatestV = table.create(self._activeNumSprings, 0)
301
+ else
302
+ self._activeStartP = table.clone(self._activeLatestP)
303
+ self._activeStartV = table.clone(self._activeLatestV)
304
+ end
305
+ self._activeType = nextFrameGoalType
306
+ self._activeGoal = nextFrameGoal
307
+ self._activeDamping = nextFrameDamping
308
+ self._activeSpeed = nextFrameSpeed
309
+ stopwatch:zero()
310
+ stopwatch:unpause()
311
+ end
312
+
313
+ -- Push update and check for similarity.
314
+ -- Don't need to use the similarity test here because this code doesn't
315
+ -- deal with tables, and NaN is already guarded against, so the similarity
316
+ -- test doesn't actually add any new safety here.
317
+ self._EXTREMELY_DANGEROUS_usedAsValue = newValue
318
+ return oldValue ~= newValue
319
+ end
320
+
321
+ table.freeze(class)
322
+ return Spring :: Types.SpringConstructor
@@ -0,0 +1,128 @@
1
+ --!strict
2
+ --!nolint LocalUnused
3
+ --!nolint LocalShadow
4
+ local task = nil -- Disable usage of Roblox's task scheduler
5
+
6
+ --[[
7
+ State object for measuring time since an event using a reference timer.
8
+
9
+ TODO: this should not be exposed to users until it has a proper reactive API
10
+ surface
11
+ ]]
12
+
13
+ local Package = script.Parent.Parent
14
+ local Types = require(Package.Types)
15
+ -- Memory
16
+ local checkLifetime = require(Package.Memory.checkLifetime)
17
+ -- Graph
18
+ local depend = require(Package.Graph.depend)
19
+ local change = require(Package.Graph.change)
20
+ -- State
21
+ local peek = require(Package.State.peek)
22
+ -- Utility
23
+ local nicknames = require(Package.Utility.nicknames)
24
+
25
+ export type Stopwatch = Types.StateObject<number> & {
26
+ zero: (Stopwatch) -> (),
27
+ pause: (Stopwatch) -> (),
28
+ unpause: (Stopwatch) -> ()
29
+ }
30
+
31
+ type Self = Stopwatch & {
32
+ _measureTimeSince: number,
33
+ _playing: boolean,
34
+ _timer: Types.StateObject<number>
35
+ }
36
+
37
+ local class = {}
38
+ class.type = "State"
39
+ class.kind = "Stopwatch"
40
+ class.timeliness = "lazy"
41
+
42
+ local METATABLE = table.freeze {__index = class}
43
+
44
+ local function Stopwatch(
45
+ scope: Types.Scope<unknown>,
46
+ timer: Types.StateObject<number>
47
+ ): Stopwatch
48
+ local createdAt = os.clock()
49
+ local self: Self = setmetatable(
50
+ {
51
+ awake = true,
52
+ createdAt = createdAt,
53
+ dependencySet = {},
54
+ dependentSet = {},
55
+ lastChange = nil,
56
+ scope = scope,
57
+ validity = "invalid",
58
+ _EXTREMELY_DANGEROUS_usedAsValue = 0,
59
+ _measureTimeSince = 0, -- this should be set on unpause
60
+ _playing = false,
61
+ _timer = timer
62
+ },
63
+ METATABLE
64
+ ) :: any
65
+ local destroy = function()
66
+ self.scope = nil
67
+ end
68
+ self.oldestTask = destroy
69
+ nicknames[self.oldestTask] = "Stopwatch"
70
+ table.insert(scope, destroy)
71
+
72
+ checkLifetime.bOutlivesA(
73
+ scope, self.oldestTask,
74
+ timer.scope, timer.oldestTask,
75
+ checkLifetime.formatters.parameter, "timer"
76
+ )
77
+ depend(self, timer)
78
+ return self
79
+ end
80
+
81
+ function class.zero(
82
+ self: Self
83
+ ): ()
84
+ local newTimepoint = peek(self._timer)
85
+ if newTimepoint ~= self._measureTimeSince then
86
+ self._measureTimeSince = newTimepoint
87
+ self._EXTREMELY_DANGEROUS_usedAsValue = 0
88
+ change(self)
89
+ end
90
+ end
91
+
92
+ function class.pause(
93
+ self: Self
94
+ ): ()
95
+ if self._playing == true then
96
+ self._playing = false
97
+ change(self)
98
+ end
99
+ end
100
+
101
+ function class.unpause(
102
+ self: Self
103
+ ): ()
104
+ if self._playing == false then
105
+ self._playing = true
106
+ self._measureTimeSince = peek(self._timer) - self._EXTREMELY_DANGEROUS_usedAsValue
107
+ change(self)
108
+ end
109
+ end
110
+
111
+ function class._evaluate(
112
+ self: Self
113
+ ): boolean
114
+ if self._playing then
115
+ depend(self, self._timer)
116
+ local currentTime = peek(self._timer)
117
+ local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue
118
+ local newValue = currentTime - self._measureTimeSince
119
+ self._EXTREMELY_DANGEROUS_usedAsValue = newValue
120
+ return oldValue ~= newValue
121
+ else
122
+ return false
123
+ end
124
+
125
+ end
126
+
127
+ table.freeze(class)
128
+ return Stopwatch