@rbxts/gravity-controller 1.0.5 → 1.0.7

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.
@@ -3,13 +3,16 @@
3
3
  <Properties>
4
4
  <string name="Name">GravityController</string>
5
5
  <BinaryString name="AttributesSerialize"></BinaryString>
6
- <bool name="DefinesCapabilities">false</bool>
6
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
7
7
  <bool name="Disabled">false</bool>
8
- <Content name="LinkedSource">
8
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
9
+ <ContentId name="LinkedSource">
9
10
  <null>
10
11
  </null>
11
- </Content>
12
+ </ContentId>
12
13
  <token name="RunContext">0</token>
14
+ <bool name="DefinesCapabilities">false</bool>
15
+ <string name="ScriptGuid">{eb65b9d8-9e71-469b-96da-3fd9967e0eea}</string>
13
16
  <string name="Source">local ReplicatedStorage = game:GetService("ReplicatedStorage")
14
17
  local StarterPlayerScripts = game:GetService("StarterPlayer"):WaitForChild("StarterPlayerScripts")
15
18
  local StarterCharacterScripts = game:GetService("StarterPlayer"):WaitForChild("StarterCharacterScripts")
@@ -29,39 +32,50 @@ replace(Client:WaitForChild("Animate"), StarterCharacterScripts)
29
32
  script:WaitForChild("GravityController").Parent = ReplicatedStorage</string>
30
33
  <int64 name="SourceAssetId">-1</int64>
31
34
  <BinaryString name="Tags"></BinaryString>
35
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005d6</UniqueId>
32
36
  </Properties>
33
37
  <Item class="Folder" referent="1">
34
38
  <Properties>
35
39
  <string name="Name">Client</string>
36
40
  <BinaryString name="AttributesSerialize"></BinaryString>
41
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
42
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
37
43
  <bool name="DefinesCapabilities">false</bool>
38
44
  <int64 name="SourceAssetId">-1</int64>
39
45
  <BinaryString name="Tags"></BinaryString>
46
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005d7</UniqueId>
40
47
  </Properties>
41
48
  <Item class="LocalScript" referent="2">
42
49
  <Properties>
43
50
  <string name="Name">Animate</string>
44
51
  <BinaryString name="AttributesSerialize"></BinaryString>
45
- <bool name="DefinesCapabilities">false</bool>
52
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
46
53
  <bool name="Disabled">false</bool>
47
- <Content name="LinkedSource">
54
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
55
+ <ContentId name="LinkedSource">
48
56
  <null>
49
57
  </null>
50
- </Content>
58
+ </ContentId>
51
59
  <token name="RunContext">0</token>
60
+ <bool name="DefinesCapabilities">false</bool>
61
+ <string name="ScriptGuid">{b0f949ba-48fa-4730-a891-99736e020579}</string>
52
62
  <string name="Source">require(script:WaitForChild("Controller"))</string>
53
63
  <int64 name="SourceAssetId">-1</int64>
54
64
  <BinaryString name="Tags"></BinaryString>
65
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005d8</UniqueId>
55
66
  </Properties>
56
67
  <Item class="ModuleScript" referent="3">
57
68
  <Properties>
58
69
  <string name="Name">Controller</string>
59
70
  <BinaryString name="AttributesSerialize"></BinaryString>
60
- <bool name="DefinesCapabilities">false</bool>
61
- <Content name="LinkedSource">
71
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
72
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
73
+ <ContentId name="LinkedSource">
62
74
  <null>
63
75
  </null>
64
- </Content>
76
+ </ContentId>
77
+ <bool name="DefinesCapabilities">false</bool>
78
+ <string name="ScriptGuid">{3d1c6cbf-6f7d-4e94-842b-2588e60c794e}</string>
65
79
  <string name="Source">local animate = script.Parent
66
80
  local humanoid = animate.Parent:WaitForChild("Humanoid")
67
81
  local loaded = animate:WaitForChild("Loaded")
@@ -80,15 +94,19 @@ loaded.Value = true
80
94
  return output</string>
81
95
  <int64 name="SourceAssetId">-1</int64>
82
96
  <BinaryString name="Tags"></BinaryString>
97
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005d9</UniqueId>
83
98
  </Properties>
84
99
  </Item>
85
100
  <Item class="BoolValue" referent="4">
86
101
  <Properties>
87
102
  <string name="Name">Loaded</string>
88
103
  <BinaryString name="AttributesSerialize"></BinaryString>
104
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
105
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
89
106
  <bool name="DefinesCapabilities">false</bool>
90
107
  <int64 name="SourceAssetId">-1</int64>
91
108
  <BinaryString name="Tags"></BinaryString>
109
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005da</UniqueId>
92
110
  <bool name="Value">false</bool>
93
111
  </Properties>
94
112
  </Item>
@@ -96,20 +114,26 @@ return output</string>
96
114
  <Properties>
97
115
  <string name="Name">PlayEmote</string>
98
116
  <BinaryString name="AttributesSerialize"></BinaryString>
117
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
118
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
99
119
  <bool name="DefinesCapabilities">false</bool>
100
120
  <int64 name="SourceAssetId">-1</int64>
101
121
  <BinaryString name="Tags"></BinaryString>
122
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005db</UniqueId>
102
123
  </Properties>
103
124
  </Item>
104
125
  <Item class="ModuleScript" referent="6">
105
126
  <Properties>
106
127
  <string name="Name">R15</string>
107
128
  <BinaryString name="AttributesSerialize"></BinaryString>
108
- <bool name="DefinesCapabilities">false</bool>
109
- <Content name="LinkedSource">
129
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
130
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
131
+ <ContentId name="LinkedSource">
110
132
  <null>
111
133
  </null>
112
- </Content>
134
+ </ContentId>
135
+ <bool name="DefinesCapabilities">false</bool>
136
+ <string name="ScriptGuid">{6754736d-045f-4c49-b9fb-cc82c0bfb1b5}</string>
113
137
  <string name="Source"><![CDATA[local script = script.Parent
114
138
 
115
139
  local Character = script.Parent
@@ -844,6 +868,7 @@ end
844
868
 
845
869
  local function onHook()
846
870
  onUnhook()
871
+ if not eventHum then return end
847
872
 
848
873
  pose = Humanoid.Sit and "Seated" or "Standing"
849
874
 
@@ -867,6 +892,9 @@ RepHumanoid.Changed:Connect(function()
867
892
  end)
868
893
 
869
894
  RepHumanoid.Value = Humanoid
895
+ if #events == 0 then
896
+ onHook()
897
+ end
870
898
 
871
899
  -- setup emote chat hook
872
900
  game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
@@ -935,17 +963,21 @@ return {
935
963
  ]]></string>
936
964
  <int64 name="SourceAssetId">-1</int64>
937
965
  <BinaryString name="Tags"></BinaryString>
966
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005dc</UniqueId>
938
967
  </Properties>
939
968
  </Item>
940
969
  <Item class="ModuleScript" referent="7">
941
970
  <Properties>
942
971
  <string name="Name">R6</string>
943
972
  <BinaryString name="AttributesSerialize"></BinaryString>
944
- <bool name="DefinesCapabilities">false</bool>
945
- <Content name="LinkedSource">
973
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
974
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
975
+ <ContentId name="LinkedSource">
946
976
  <null>
947
977
  </null>
948
- </Content>
978
+ </ContentId>
979
+ <bool name="DefinesCapabilities">false</bool>
980
+ <string name="ScriptGuid">{7ab34f07-f6d8-4d43-a55b-e96b6488627a}</string>
949
981
  <string name="Source">local script = script.Parent
950
982
 
951
983
  local Figure = script.Parent
@@ -1458,6 +1490,7 @@ end
1458
1490
 
1459
1491
  local function onHook()
1460
1492
  onUnhook()
1493
+ if not eventHum then return end
1461
1494
 
1462
1495
  pose = Humanoid.Sit and "Seated" or "Standing"
1463
1496
 
@@ -1481,6 +1514,9 @@ RepHumanoid.Changed:Connect(function()
1481
1514
  end)
1482
1515
 
1483
1516
  RepHumanoid.Value = Humanoid
1517
+ if #events == 0 then
1518
+ onHook()
1519
+ end
1484
1520
 
1485
1521
  -- setup emote chat hook
1486
1522
  game:GetService("Players").LocalPlayer.Chatted:connect(function(msg)
@@ -1528,15 +1564,19 @@ return {
1528
1564
  }</string>
1529
1565
  <int64 name="SourceAssetId">-1</int64>
1530
1566
  <BinaryString name="Tags"></BinaryString>
1567
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005dd</UniqueId>
1531
1568
  </Properties>
1532
1569
  </Item>
1533
1570
  <Item class="ObjectValue" referent="8">
1534
1571
  <Properties>
1535
1572
  <string name="Name">ReplicatedHumanoid</string>
1536
1573
  <BinaryString name="AttributesSerialize"></BinaryString>
1574
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1575
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
1537
1576
  <bool name="DefinesCapabilities">false</bool>
1538
1577
  <int64 name="SourceAssetId">-1</int64>
1539
1578
  <BinaryString name="Tags"></BinaryString>
1579
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005de</UniqueId>
1540
1580
  <Ref name="Value">null</Ref>
1541
1581
  </Properties>
1542
1582
  </Item>
@@ -1544,11 +1584,14 @@ return {
1544
1584
  <Properties>
1545
1585
  <string name="Name">VerifyAnims</string>
1546
1586
  <BinaryString name="AttributesSerialize"></BinaryString>
1547
- <bool name="DefinesCapabilities">false</bool>
1548
- <Content name="LinkedSource">
1587
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1588
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
1589
+ <ContentId name="LinkedSource">
1549
1590
  <null>
1550
1591
  </null>
1551
- </Content>
1592
+ </ContentId>
1593
+ <bool name="DefinesCapabilities">false</bool>
1594
+ <string name="ScriptGuid">{0ba70de6-b4f1-4e69-bd8b-61d26d449d79}</string>
1552
1595
  <string name="Source">local LENGTH = string.len("Animation")
1553
1596
 
1554
1597
  local DESC_ANIM_PROPS = {
@@ -1580,6 +1623,7 @@ return function(humanoid, animate)
1580
1623
  end</string>
1581
1624
  <int64 name="SourceAssetId">-1</int64>
1582
1625
  <BinaryString name="Tags"></BinaryString>
1626
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005df</UniqueId>
1583
1627
  </Properties>
1584
1628
  </Item>
1585
1629
  </Item>
@@ -1587,13 +1631,16 @@ end</string>
1587
1631
  <Properties>
1588
1632
  <string name="Name">PlayerScriptsLoader</string>
1589
1633
  <BinaryString name="AttributesSerialize"></BinaryString>
1590
- <bool name="DefinesCapabilities">false</bool>
1634
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1591
1635
  <bool name="Disabled">false</bool>
1592
- <Content name="LinkedSource">
1636
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
1637
+ <ContentId name="LinkedSource">
1593
1638
  <null>
1594
1639
  </null>
1595
- </Content>
1640
+ </ContentId>
1596
1641
  <token name="RunContext">0</token>
1642
+ <bool name="DefinesCapabilities">false</bool>
1643
+ <string name="ScriptGuid">{e0183d8c-3ce2-475d-ae7c-995a9c8941b8}</string>
1597
1644
  <string name="Source">--[[
1598
1645
  PlayerScriptsLoader - This script requires and instantiates the PlayerModule singleton
1599
1646
 
@@ -1697,7 +1744,7 @@ local defaultUpdateMouseBehavior = BaseCamera.UpdateMouseBehavior
1697
1744
 
1698
1745
  function BaseCamera:UpdateMouseBehavior()
1699
1746
  defaultUpdateMouseBehavior(self)
1700
- if UserGameSettings.RotationType == Enum.RotationType.CameraRelative then
1747
+ if _G._gravityControllerActive and UserGameSettings.RotationType == Enum.RotationType.CameraRelative then
1701
1748
  UserGameSettings.RotationType = Enum.RotationType.MovementRelative
1702
1749
  end
1703
1750
  end
@@ -1851,16 +1898,20 @@ end
1851
1898
  require(PlayerModule)</string>
1852
1899
  <int64 name="SourceAssetId">-1</int64>
1853
1900
  <BinaryString name="Tags"></BinaryString>
1901
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e0</UniqueId>
1854
1902
  </Properties>
1855
1903
  <Item class="ModuleScript" referent="11">
1856
1904
  <Properties>
1857
1905
  <string name="Name">CameraInjector</string>
1858
1906
  <BinaryString name="AttributesSerialize"></BinaryString>
1859
- <bool name="DefinesCapabilities">false</bool>
1860
- <Content name="LinkedSource">
1907
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1908
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
1909
+ <ContentId name="LinkedSource">
1861
1910
  <null>
1862
1911
  </null>
1863
- </Content>
1912
+ </ContentId>
1913
+ <bool name="DefinesCapabilities">false</bool>
1914
+ <string name="ScriptGuid">{431f5e99-73c4-410d-92cf-798558b9d76b}</string>
1864
1915
  <string name="Source"><![CDATA[-- Injects into the CameraModule to override for public API access
1865
1916
  -- EgoMoose
1866
1917
 
@@ -1936,17 +1987,21 @@ return result
1936
1987
  ]]></string>
1937
1988
  <int64 name="SourceAssetId">-1</int64>
1938
1989
  <BinaryString name="Tags"></BinaryString>
1990
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e1</UniqueId>
1939
1991
  </Properties>
1940
1992
  </Item>
1941
1993
  <Item class="ModuleScript" referent="12">
1942
1994
  <Properties>
1943
1995
  <string name="Name">FakeUserSettings</string>
1944
1996
  <BinaryString name="AttributesSerialize"></BinaryString>
1945
- <bool name="DefinesCapabilities">false</bool>
1946
- <Content name="LinkedSource">
1997
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1998
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
1999
+ <ContentId name="LinkedSource">
1947
2000
  <null>
1948
2001
  </null>
1949
- </Content>
2002
+ </ContentId>
2003
+ <bool name="DefinesCapabilities">false</bool>
2004
+ <string name="ScriptGuid">{eac70e9a-9f5c-452e-bae3-2826d0735557}</string>
1950
2005
  <string name="Source">local FFLAG_OVERRIDES = {
1951
2006
  ["UserRemoveTheCameraApi"] = false
1952
2007
  }
@@ -1978,6 +2033,7 @@ end
1978
2033
  return FakeUserSettingsFunc</string>
1979
2034
  <int64 name="SourceAssetId">-1</int64>
1980
2035
  <BinaryString name="Tags"></BinaryString>
2036
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e2</UniqueId>
1981
2037
  </Properties>
1982
2038
  </Item>
1983
2039
  </Item>
@@ -1985,13 +2041,16 @@ return FakeUserSettingsFunc</string>
1985
2041
  <Properties>
1986
2042
  <string name="Name">RbxCharacterSounds</string>
1987
2043
  <BinaryString name="AttributesSerialize"></BinaryString>
1988
- <bool name="DefinesCapabilities">false</bool>
2044
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1989
2045
  <bool name="Disabled">false</bool>
1990
- <Content name="LinkedSource">
2046
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2047
+ <ContentId name="LinkedSource">
1991
2048
  <null>
1992
2049
  </null>
1993
- </Content>
2050
+ </ContentId>
1994
2051
  <token name="RunContext">0</token>
2052
+ <bool name="DefinesCapabilities">false</bool>
2053
+ <string name="ScriptGuid">{cdad40e6-3a62-4b8c-b94e-2dd8d206ffdb}</string>
1995
2054
  <string name="Source"><![CDATA[-- Roblox character sound script
1996
2055
 
1997
2056
  local Players = game:GetService("Players")
@@ -2316,16 +2375,20 @@ end
2316
2375
  ]]></string>
2317
2376
  <int64 name="SourceAssetId">-1</int64>
2318
2377
  <BinaryString name="Tags"></BinaryString>
2378
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e3</UniqueId>
2319
2379
  </Properties>
2320
2380
  <Item class="ModuleScript" referent="14">
2321
2381
  <Properties>
2322
2382
  <string name="Name">AnimationState</string>
2323
2383
  <BinaryString name="AttributesSerialize"></BinaryString>
2324
- <bool name="DefinesCapabilities">false</bool>
2325
- <Content name="LinkedSource">
2384
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2385
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2386
+ <ContentId name="LinkedSource">
2326
2387
  <null>
2327
2388
  </null>
2328
- </Content>
2389
+ </ContentId>
2390
+ <bool name="DefinesCapabilities">false</bool>
2391
+ <string name="ScriptGuid">{9fdc64e9-3148-4289-9ab9-51fe1358b8d9}</string>
2329
2392
  <string name="Source">local STATE_MAP = {
2330
2393
  ["climb"] = Enum.HumanoidStateType.Climbing,
2331
2394
  ["fall"] = Enum.HumanoidStateType.Freefall,
@@ -2357,6 +2420,7 @@ return function(animator, callback)
2357
2420
  end</string>
2358
2421
  <int64 name="SourceAssetId">-1</int64>
2359
2422
  <BinaryString name="Tags"></BinaryString>
2423
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e4</UniqueId>
2360
2424
  </Properties>
2361
2425
  </Item>
2362
2426
  </Item>
@@ -2365,11 +2429,14 @@ end</string>
2365
2429
  <Properties>
2366
2430
  <string name="Name">GravityController</string>
2367
2431
  <BinaryString name="AttributesSerialize"></BinaryString>
2368
- <bool name="DefinesCapabilities">false</bool>
2369
- <Content name="LinkedSource">
2432
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2433
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2434
+ <ContentId name="LinkedSource">
2370
2435
  <null>
2371
2436
  </null>
2372
- </Content>
2437
+ </ContentId>
2438
+ <bool name="DefinesCapabilities">false</bool>
2439
+ <string name="ScriptGuid">{f6e2e306-cb25-41c8-91a6-74b8518edd95}</string>
2373
2440
  <string name="Source">local RunService = game:GetService("RunService")
2374
2441
 
2375
2442
  local Utility = script:WaitForChild("Utility")
@@ -2595,6 +2662,7 @@ function init(self)
2595
2662
  self._characterMass = getModelMass(self.Character)
2596
2663
  end))
2597
2664
 
2665
+ _G._gravityControllerActive = true
2598
2666
  self.Humanoid.PlatformStand = true
2599
2667
  self.Maid:Mark(self.Humanoid:GetPropertyChangedSignal("Jump"):Connect(function()
2600
2668
  if self.Humanoid.Jump then
@@ -2652,6 +2720,7 @@ function GravityControllerClass:Destroy()
2652
2720
  RunService:UnbindFromRenderStep("GravityStep")
2653
2721
  self.Maid:Sweep()
2654
2722
  self.Humanoid.PlatformStand = false
2723
+ _G._gravityControllerActive = false
2655
2724
  end
2656
2725
 
2657
2726
  --
@@ -2659,24 +2728,31 @@ end
2659
2728
  return GravityControllerClass</string>
2660
2729
  <int64 name="SourceAssetId">-1</int64>
2661
2730
  <BinaryString name="Tags"></BinaryString>
2731
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e5</UniqueId>
2662
2732
  </Properties>
2663
2733
  <Item class="Folder" referent="16">
2664
2734
  <Properties>
2665
2735
  <string name="Name">CharacterModules</string>
2666
2736
  <BinaryString name="AttributesSerialize"></BinaryString>
2737
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2738
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2667
2739
  <bool name="DefinesCapabilities">false</bool>
2668
2740
  <int64 name="SourceAssetId">-1</int64>
2669
2741
  <BinaryString name="Tags"></BinaryString>
2742
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e6</UniqueId>
2670
2743
  </Properties>
2671
2744
  <Item class="ModuleScript" referent="17">
2672
2745
  <Properties>
2673
2746
  <string name="Name">Camera</string>
2674
2747
  <BinaryString name="AttributesSerialize"></BinaryString>
2675
- <bool name="DefinesCapabilities">false</bool>
2676
- <Content name="LinkedSource">
2748
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2749
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2750
+ <ContentId name="LinkedSource">
2677
2751
  <null>
2678
2752
  </null>
2679
- </Content>
2753
+ </ContentId>
2754
+ <bool name="DefinesCapabilities">false</bool>
2755
+ <string name="ScriptGuid">{76d10358-03a7-4411-9c24-5a674dd6c931}</string>
2680
2756
  <string name="Source">-- Class
2681
2757
 
2682
2758
  local CameraClass = {}
@@ -2723,17 +2799,21 @@ end
2723
2799
  return CameraClass</string>
2724
2800
  <int64 name="SourceAssetId">-1</int64>
2725
2801
  <BinaryString name="Tags"></BinaryString>
2802
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e7</UniqueId>
2726
2803
  </Properties>
2727
2804
  </Item>
2728
2805
  <Item class="ModuleScript" referent="18">
2729
2806
  <Properties>
2730
2807
  <string name="Name">Control</string>
2731
2808
  <BinaryString name="AttributesSerialize"></BinaryString>
2732
- <bool name="DefinesCapabilities">false</bool>
2733
- <Content name="LinkedSource">
2809
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2810
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2811
+ <ContentId name="LinkedSource">
2734
2812
  <null>
2735
2813
  </null>
2736
- </Content>
2814
+ </ContentId>
2815
+ <bool name="DefinesCapabilities">false</bool>
2816
+ <string name="ScriptGuid">{10647ef5-c281-4a40-b0bb-8038409c7a2d}</string>
2737
2817
  <string name="Source">-- Class
2738
2818
 
2739
2819
  local ControlClass = {}
@@ -2771,6 +2851,7 @@ end
2771
2851
  return ControlClass</string>
2772
2852
  <int64 name="SourceAssetId">-1</int64>
2773
2853
  <BinaryString name="Tags"></BinaryString>
2854
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e8</UniqueId>
2774
2855
  </Properties>
2775
2856
  </Item>
2776
2857
  </Item>
@@ -2778,11 +2859,14 @@ return ControlClass</string>
2778
2859
  <Properties>
2779
2860
  <string name="Name">Collider</string>
2780
2861
  <BinaryString name="AttributesSerialize"></BinaryString>
2781
- <bool name="DefinesCapabilities">false</bool>
2782
- <Content name="LinkedSource">
2862
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
2863
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
2864
+ <ContentId name="LinkedSource">
2783
2865
  <null>
2784
2866
  </null>
2785
- </Content>
2867
+ </ContentId>
2868
+ <bool name="DefinesCapabilities">false</bool>
2869
+ <string name="ScriptGuid">{f0cc7fc1-6d6d-4df8-a71d-31e31a485f35}</string>
2786
2870
  <string name="Source"><![CDATA[local Maid = require(script.Parent.Utility.Maid)
2787
2871
 
2788
2872
  local params = RaycastParams.new()
@@ -3019,17 +3103,21 @@ return ColliderClass
3019
3103
  ]]></string>
3020
3104
  <int64 name="SourceAssetId">-1</int64>
3021
3105
  <BinaryString name="Tags"></BinaryString>
3106
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005e9</UniqueId>
3022
3107
  </Properties>
3023
3108
  </Item>
3024
3109
  <Item class="ModuleScript" referent="20">
3025
3110
  <Properties>
3026
3111
  <string name="Name">StateTracker</string>
3027
3112
  <BinaryString name="AttributesSerialize"></BinaryString>
3028
- <bool name="DefinesCapabilities">false</bool>
3029
- <Content name="LinkedSource">
3113
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
3114
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
3115
+ <ContentId name="LinkedSource">
3030
3116
  <null>
3031
3117
  </null>
3032
- </Content>
3118
+ </ContentId>
3119
+ <bool name="DefinesCapabilities">false</bool>
3120
+ <string name="ScriptGuid">{7ebcb610-6d6d-492e-9422-94acd49b969d}</string>
3033
3121
  <string name="Source">local Maid = require(script.Parent.Utility.Maid)
3034
3122
  local Signal = require(script.Parent.Utility.Signal)
3035
3123
 
@@ -3145,25 +3233,32 @@ end
3145
3233
  return StateTrackerClass</string>
3146
3234
  <int64 name="SourceAssetId">-1</int64>
3147
3235
  <BinaryString name="Tags"></BinaryString>
3236
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005ea</UniqueId>
3148
3237
  </Properties>
3149
3238
  </Item>
3150
3239
  <Item class="Folder" referent="21">
3151
3240
  <Properties>
3152
3241
  <string name="Name">Utility</string>
3153
3242
  <BinaryString name="AttributesSerialize"></BinaryString>
3243
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
3244
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
3154
3245
  <bool name="DefinesCapabilities">false</bool>
3155
3246
  <int64 name="SourceAssetId">-1</int64>
3156
3247
  <BinaryString name="Tags"></BinaryString>
3248
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005eb</UniqueId>
3157
3249
  </Properties>
3158
3250
  <Item class="ModuleScript" referent="22">
3159
3251
  <Properties>
3160
3252
  <string name="Name">Maid</string>
3161
3253
  <BinaryString name="AttributesSerialize"></BinaryString>
3162
- <bool name="DefinesCapabilities">false</bool>
3163
- <Content name="LinkedSource">
3254
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
3255
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
3256
+ <ContentId name="LinkedSource">
3164
3257
  <null>
3165
3258
  </null>
3166
- </Content>
3259
+ </ContentId>
3260
+ <bool name="DefinesCapabilities">false</bool>
3261
+ <string name="ScriptGuid">{a0996765-17f7-4168-a2cf-13d8501d57f7}</string>
3167
3262
  <string name="Source">-- CONSTANTS
3168
3263
 
3169
3264
  local FORMAT_STR = "Maid does not support type \"%s\""
@@ -3237,17 +3332,21 @@ MaidClass.Destroy = MaidClass.Sweep
3237
3332
  return MaidClass</string>
3238
3333
  <int64 name="SourceAssetId">-1</int64>
3239
3334
  <BinaryString name="Tags"></BinaryString>
3335
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005ec</UniqueId>
3240
3336
  </Properties>
3241
3337
  </Item>
3242
3338
  <Item class="ModuleScript" referent="23">
3243
3339
  <Properties>
3244
3340
  <string name="Name">Signal</string>
3245
3341
  <BinaryString name="AttributesSerialize"></BinaryString>
3246
- <bool name="DefinesCapabilities">false</bool>
3247
- <Content name="LinkedSource">
3342
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
3343
+ <UniqueId name="HistoryId">00000000000000000000000000000000</UniqueId>
3344
+ <ContentId name="LinkedSource">
3248
3345
  <null>
3249
3346
  </null>
3250
- </Content>
3347
+ </ContentId>
3348
+ <bool name="DefinesCapabilities">false</bool>
3349
+ <string name="ScriptGuid">{8108a59e-e9e8-499c-8311-78d8a49a0a92}</string>
3251
3350
  <string name="Source">-- Taken from Quenty's nevermore engine
3252
3351
  -- https://github.com/Quenty/NevermoreEngine/blob/version2/LICENSE.md
3253
3352
  -- https://github.com/Quenty/NevermoreEngine
@@ -3335,9 +3434,10 @@ end
3335
3434
  return Signal</string>
3336
3435
  <int64 name="SourceAssetId">-1</int64>
3337
3436
  <BinaryString name="Tags"></BinaryString>
3437
+ <UniqueId name="UniqueId">62919a01c8dda09809ca8574000005ed</UniqueId>
3338
3438
  </Properties>
3339
3439
  </Item>
3340
3440
  </Item>
3341
3441
  </Item>
3342
3442
  </Item>
3343
- </roblox>
3443
+ </roblox>
package/README.md CHANGED
@@ -1,11 +1,273 @@
1
1
  # @rbxts/gravity-controller
2
2
 
3
- Typescript bindings for [EgoMoose's Rbx-Gravity-Controller](https://github.com/EgoMoose/Rbx-Gravity-Controller) with
4
- ground normal finding by [EmilyBendsSpace](https://x.com/EmilyBendsSpace)
3
+ TypeScript bindings for [EgoMoose's Rbx-Gravity-Controller](https://github.com/EgoMoose/Rbx-Gravity-Controller) with
4
+ ground-normal-based wall walking by [EmilyBendsSpace](https://x.com/EmilyBendsSpace).
5
5
 
6
- ## Flamework setup
6
+ Players can walk on walls, ceilings, and any arbitrary surface with smooth gravity transitions.
7
7
 
8
- 1. Add `installGravityControllerClass` to the `onInit` of some `@Service`
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @rbxts/gravity-controller
12
+ ```
13
+
14
+ The package ships a `GravityController.rbxmx` model that contains the Lua runtime (camera, collider, state tracker, animations, and character sounds). The TypeScript wrapper in `src/index.ts` handles deploying those scripts at runtime and provides typed access to the controller.
15
+
16
+ ## How it works
17
+
18
+ ### Architecture
19
+
20
+ ```
21
+ Server (onInit) Client (onStart)
22
+ ───────────────── ──────────────────
23
+ installGravityControllerClass() installGravityControllerClass()
24
+ ├─ Copies Client/PlayerScriptsLoader └─ require("GravityController")
25
+ │ → StarterPlayerScripts from ReplicatedStorage
26
+ ├─ Copies Client/RbxCharacterSounds ↓
27
+ │ → StarterPlayerScripts new GravityControllerClass(player)
28
+ ├─ Copies Client/Animate ├─ Camera (custom camera module)
29
+ │ → StarterCharacterScripts ├─ Control (input → move vector)
30
+ └─ Moves GravityController module ├─ Collider (physics body movers)
31
+ → ReplicatedStorage └─ StateTracker (humanoid states)
32
+ ```
33
+
34
+ `installGravityControllerClass()` must be called on **both** the server and the client. On the server it deploys the bundled scripts into `StarterPlayerScripts`, `StarterCharacterScripts`, and `ReplicatedStorage`. On the client it `require`s the `GravityController` ModuleScript from `ReplicatedStorage` and returns the class.
35
+
36
+ ### Permanent runtime modifications
37
+
38
+ The server-side `installGravityControllerClass()` **replaces** scripts in StarterPlayerScripts / StarterCharacterScripts:
39
+
40
+ - **PlayerScriptsLoader** — modified version that monkey-patches `BaseCamera`, `CameraUtils`, `Poppercam`, and the `CameraModule.Update` loop to support arbitrary gravity up-vectors
41
+ - **Animate** — custom version that works with `PlatformStand = true`
42
+ - **RbxCharacterSounds** — custom version driven by the gravity controller's `StateTracker` rather than native `Humanoid` state
43
+
44
+ These replacements are **permanent for the session** — they survive `GravityController:Destroy()`.
45
+
46
+ ### Key monkey-patch: `BaseCamera:UpdateMouseBehavior()`
47
+
48
+ The modified PlayerScriptsLoader overrides `BaseCamera:UpdateMouseBehavior()` to force `UserGameSettings.RotationType` from `CameraRelative` to `MovementRelative`. This is necessary while the gravity controller is active because `CameraRelative` rotation conflicts with the custom `BodyGyro`-driven character orientation.
49
+
50
+ This override is guarded by `_G._gravityControllerActive` so it only applies while a gravity controller instance is alive. Without this guard, any first-person camera system that depends on `CameraRelative` (such as Character-Realism's `FpsCamera`) will break permanently after gravity controller destruction.
51
+
52
+ ### What a GravityController instance does (per-player, temporary)
53
+
54
+ - Sets `Humanoid.PlatformStand = true` (disables the default humanoid physics)
55
+ - Creates collision proxy parts (Sphere, FloorDetector, JumpDetector) welded to HRP
56
+ - Adds `VectorForce`, `BodyGyro`, `BodyPosition` to HRP
57
+ - Binds `"GravityStep"` RenderStep at priority `Camera - 1` (199)
58
+ - Sets `_G._gravityControllerActive = true`
59
+
60
+ ### What `GravityController:Destroy()` does
61
+
62
+ - Unbinds `"GravityStep"` from RenderStep
63
+ - `Maid:Sweep()` — destroys all proxy parts, body movers, disconnects events
64
+ - Sets `Humanoid.PlatformStand = false`
65
+ - Sets `_G._gravityControllerActive = false`
66
+
67
+ It does **not** restore the Animate script or the PlayerScriptsLoader — those persist.
68
+
69
+ ### Gravity step (per frame)
70
+
71
+ Each render frame the controller runs `onGravityStep`:
72
+
73
+ 1. **Query gravity direction** — calls `GetGravityUp(oldGravity)` which you can override. By default it returns the previous gravity (no change). Assign `getGravityControllerUp` to enable surface-following wall walk.
74
+ 2. **Lerp transition** — spherically interpolates from the old gravity direction toward the new one, controlled by `Transition` (default `0.15`).
75
+ 3. **Compute world move vector** — projects the camera-relative input onto the plane perpendicular to gravity so the character always moves along the surface.
76
+ 4. **Compute forces** — calculates a counter-gravity force (`gForce`) and a walk force (`walkForce`) that accelerates the character toward target velocity.
77
+ 5. **Optional horizontal lock** — when `UseBodyPositionLock` is enabled and the character is standing still on an aligned surface, a `BodyPosition` prevents micro-sliding.
78
+ 6. **Update collider and state** — applies the combined force to the character's body movers and updates the state tracker (running, jumping, freefall, etc.).
79
+
80
+ ### Interaction with first-person camera systems
81
+
82
+ If you use a first-person camera module (e.g. Character-Realism's `FpsCamera`) that depends on `UserGameSettings.RotationType` being `CameraRelative`:
83
+
84
+ - While the gravity controller is **active**, `onGravityStep` handles character rotation via `BodyGyro`. The `_G._gravityControllerActive` flag ensures the monkey-patch forces `MovementRelative` only during this time.
85
+ - After `Destroy()`, the flag clears and the monkey-patch becomes a no-op, allowing your first-person system to set `AutoRotate = false` and take over character rotation normally.
86
+
87
+ ### Ground normal detection (`getGroundNormal`)
88
+
89
+ The exported `getGroundNormal` function determines which direction is "up" by casting rays from the character's root part:
90
+
91
+ | Ray group | Count | Purpose |
92
+ |---|---|---|
93
+ | Center ray | 1 | Single downward ray (length 25) to find the surface directly below |
94
+ | Down rays | 24 | Radial ring of rays angled slightly inward/outward, with alternating even/odd radii, to sample the surrounding surface normals |
95
+ | Feeler rays | 9 | Shorter rays (length 2) fanning outward and downward to detect walls and edges the character is approaching |
96
+
97
+ All hit normals are weighted (front-facing rays weighted more heavily, feelers weighted 8x) and summed. The final unit vector becomes the new "up" direction. If no rays hit anything, the previous gravity direction is preserved.
98
+
99
+ ### `GravityController.rbxmx` file manifest
100
+
101
+ The `.rbxmx` model bundles all the Lua scripts needed at runtime. During `installGravityControllerClass()` these are deployed to the correct locations in the Roblox data model.
102
+
103
+ ```
104
+ GravityController (Script) ← server entry point; deploys children at runtime
105
+ ├── Client (Folder) ← scripts that get copied into StarterPlayer
106
+ │ ├── Animate (LocalScript) → StarterCharacterScripts
107
+ │ │ ├── Controller (ModuleScript) — bootstraps R6/R15 animation sets
108
+ │ │ ├── Loaded (BoolValue) — signals when animations are ready
109
+ │ │ ├── PlayEmote (BindableFunction) — emote playback hook
110
+ │ │ ├── R15 (ModuleScript) — full R15 animation state machine
111
+ │ │ ├── R6 (ModuleScript) — full R6 animation state machine
112
+ │ │ ├── ReplicatedHumanoid (ObjectValue) — humanoid reference for replication
113
+ │ │ └── VerifyAnims (ModuleScript) — validates animation assets on the character
114
+ │ ├── PlayerScriptsLoader (LocalScript) → StarterPlayerScripts
115
+ │ │ ├── CameraInjector (ModuleScript) — monkey-patches PlayerModule's CameraModule
116
+ │ │ │ to expose a public GetUpVector API for gravity-aware camera rotation
117
+ │ │ └── FakeUserSettings (ModuleScript) — shims UserSettings() to override feature
118
+ │ │ flags (e.g. disables UserRemoveTheCameraApi) during camera injection
119
+ │ └── RbxCharacterSounds (LocalScript) → StarterPlayerScripts
120
+ │ └── AnimationState (ModuleScript) — maps animation track names to
121
+ │ HumanoidStateTypes so footstep/jump/fall sounds play correctly
122
+ │ under custom gravity
123
+ └── GravityController (ModuleScript) → ReplicatedStorage
124
+ ├── CharacterModules (Folder)
125
+ │ ├── Camera (ModuleScript) — hooks into PlayerModule cameras to override
126
+ │ │ GetUpVector, making the camera orbit around the custom gravity axis
127
+ │ └── Control (ModuleScript) — wraps PlayerModule controls to read the
128
+ │ move vector from keyboard/gamepad/touch input
129
+ ├── Collider (ModuleScript) — creates an invisible Ball Part welded below
130
+ │ the HRP for ground detection, plus VectorForce (gravity + walk),
131
+ │ BodyGyro (orientation), and BodyPosition (optional anti-slide lock)
132
+ ├── StateTracker (ModuleScript) — replaces Humanoid state detection with
133
+ │ velocity-based Running/Jumping/Freefall tracking and fires the
134
+ │ Animate script's callbacks (onRunning, onJumping, onFreeFall, etc.)
135
+ └── Utility (Folder)
136
+ ├── Maid (ModuleScript) — connection/instance cleanup utility
137
+ └── Signal (ModuleScript) — lightweight event/signal implementation
138
+ ```
139
+
140
+ **Where each piece ends up at runtime:**
141
+
142
+ | Script | Deployed to | Role |
143
+ |---|---|---|
144
+ | `Client/PlayerScriptsLoader` | `StarterPlayerScripts` | Replaces the default PlayerScriptsLoader to inject gravity-aware camera and control overrides into the stock `PlayerModule` |
145
+ | `Client/RbxCharacterSounds` | `StarterPlayerScripts` | Replaces default character sounds so audio triggers are driven by the custom `StateTracker` instead of the native `Humanoid` state |
146
+ | `Client/Animate` | `StarterCharacterScripts` | Replaces default Animate script; plays R6/R15 animations driven by `StateTracker.Changed` events rather than native humanoid states |
147
+ | `GravityController` | `ReplicatedStorage` | The core module — `require`d by both the TypeScript wrapper and the PlayerScriptsLoader at runtime |
148
+
149
+ ## API
150
+
151
+ ### `installGravityControllerClass(config?)`
152
+
153
+ Initializes the gravity system. Call on both server and client. Returns the `GravityControllerClass` constructor. Idempotent — calling it again returns the same class (and optionally applies new config).
154
+
155
+ ### `GravityControllerClass`
156
+
157
+ | Member | Type | Description |
158
+ |---|---|---|
159
+ | `new(player)` | constructor | Creates a controller for the given player's current character |
160
+ | `SetConstants(config)` | static method | Updates physics constants globally (see Configuration below) |
161
+
162
+ ### `GravityController` (instance)
163
+
164
+ | Member | Type | Description |
165
+ |---|---|---|
166
+ | `Player` | `Player` | The owning player |
167
+ | `Character` | `Model` | The player's character model |
168
+ | `Humanoid` | `Humanoid` | The character's humanoid |
169
+ | `HRP` | `BasePart` | `HumanoidRootPart` |
170
+ | `Maid` | `{ Mark }` | Cleanup helper — tracks connections for automatic teardown |
171
+ | `GetGravityUp(oldGravity)` | method | Override this to control gravity direction each frame. Default returns `oldGravity` (no change). |
172
+ | `ResetGravity(direction)` | method | Instantly sets the gravity-up vector and resets the fall tracker |
173
+ | `GetFallHeight()` | method | Returns the signed distance fallen along the gravity axis while in freefall; `0` otherwise |
174
+ | `Destroy()` | method | Unbinds the render step, sweeps all connections, and restores `PlatformStand` |
175
+
176
+ ### `GravityManager`
177
+
178
+ A plain TypeScript class (no framework dependencies) that manages the full lifecycle of a `GravityController` instance — waiting for the character and Animate script, constructing the controller, handling timeouts and retries, and tearing down cleanly.
179
+
180
+ ```typescript
181
+ const cls = installGravityControllerClass(config)
182
+ const manager = new GravityManager(cls, logger)
183
+
184
+ manager.enable(getGravityControllerUp) // async 4-phase construction
185
+ manager.disable() // teardown + reset GravityUp attribute
186
+ manager.getController() // live instance or undefined
187
+ manager.getIsEnabling() // true while construction is in-flight
188
+ ```
189
+
190
+ **Constructor:** `new GravityManager(gravityControllerClass, logger?)`
191
+
192
+ - `gravityControllerClass` — the class returned by `installGravityControllerClass()`
193
+ - `logger` — optional `GravityLogger` (see below). If omitted, logging is silently skipped.
194
+
195
+ **`enable(getGravityUp)`** — starts a 4-phase async construction:
196
+
197
+ 1. Waits for `Players.LocalPlayer.Character` to exist
198
+ 2. Waits for the `Animate` script and its `Controller` child (with timeout)
199
+ 3. Waits for the `Animate` module to finish loading (`Loaded.Value = true`)
200
+ 4. Constructs the `GravityController` instance and assigns `GetGravityUp`
201
+
202
+ Each call stores the requested `getGravityUp` function. If `enable()` is called again while construction is in-flight, the new function is saved and will be used when the current construction completes or on retry.
203
+
204
+ A generation counter invalidates stale constructions — if `disable()` is called while construction is running, the in-flight thread is cancelled and any completed-but-stale controller is destroyed immediately.
205
+
206
+ A watchdog fires after the timeout period and hard-cancels stuck construction threads, cleans up partial state (`GravityStep` RenderStep binding, `PlatformStand`), and triggers a retry.
207
+
208
+ **`disable()`** — tears down the active controller:
209
+
210
+ - Increments the generation counter (invalidating any in-flight construction)
211
+ - Cancels the construction thread if running
212
+ - Calls `Destroy()` on the live controller
213
+ - Sets the HRP `GravityUp` attribute to `(0, 1, 0)`
214
+
215
+ **`getController()`** — returns the live `GravityController` instance or `undefined`.
216
+
217
+ **`getIsEnabling()`** — returns `true` while construction is in-flight.
218
+
219
+ ### `GravityLogger`
220
+
221
+ Minimal logging interface so the package doesn't depend on any specific logging library. Any object with `Info`, `Warn`, and `Error` string methods satisfies it — including `@rbxts/log`'s `Logger`.
222
+
223
+ ```typescript
224
+ interface GravityLogger {
225
+ Info(message: string): void
226
+ Warn(message: string): void
227
+ Error(message: string): void
228
+ }
229
+ ```
230
+
231
+ ### `wrapGravityUpSaveAttribute(getGravityUp)`
232
+
233
+ Higher-order function that wraps a `GetGravityUp` function to persist the current gravity direction as an HRP attribute (`GravityUp`). Only writes when the direction actually changes (magnitude delta > 0.001).
234
+
235
+ ```typescript
236
+ manager.enable(wrapGravityUpSaveAttribute(getGravityControllerUp))
237
+ ```
238
+
239
+ ### `GetGravityUp` (type)
240
+
241
+ ```typescript
242
+ type GetGravityUp = (self: GravityController, oldGravityUp: Vector3) => Vector3
243
+ ```
244
+
245
+ The signature for gravity direction functions. Passed to `GravityManager.enable()` or assigned to `controller.GetGravityUp`.
246
+
247
+ ### `getGravityControllerUp(controller, oldGravityUp)`
248
+
249
+ Convenience wrapper that calls `getGroundNormal` using the controller's `HRP.CFrame` with a rig-type-aware origin offset. Assign this to `controller.GetGravityUp` to enable wall walking.
250
+
251
+ ### `getGroundNormal(cframe, originOffset, oldGravityUp)`
252
+
253
+ Low-level raycast function that returns a unit `Vector3` representing the surface normal beneath and around `cframe`. Useful if you want to build your own gravity logic.
254
+
255
+ ## Configuration
256
+
257
+ Pass a config table to `installGravityControllerClass()` or call `SetConstants()` at any time:
258
+
259
+ | Key | Type | Default | Description |
260
+ |---|---|---|---|
261
+ | `Transition` | `number` | `0.15` | Lerp alpha per frame for gravity direction changes. Lower = slower, smoother transitions. |
262
+ | `WalkForce` | `number` | `66.67` | Horizontal acceleration multiplier. Increase for snappier movement. |
263
+ | `JumpModifier` | `number` | `1.2` | Multiplier on `Humanoid.JumpPower` when jumping along the custom gravity axis. |
264
+ | `UseBodyPositionLock` | `boolean` | `false` | When `true`, locks the character's horizontal position with a `BodyPosition` while idle on an aligned surface to prevent sliding. |
265
+
266
+ ## Usage
267
+
268
+ ### With GravityManager (recommended)
269
+
270
+ **Server** — install in a `@Service`:
9
271
 
10
272
  ```typescript
11
273
  import { OnInit, Service } from '@flamework/core'
@@ -14,52 +276,86 @@ import { installGravityControllerClass } from '@rbxts/gravity-controller'
14
276
  @Service()
15
277
  export class GravityService implements OnInit {
16
278
  onInit() {
17
- // Install EgoMoose's Rbx-Gravity-Controller https://github.com/EgoMoose/Rbx-Gravity-Controller
18
279
  installGravityControllerClass()
19
280
  }
20
281
  }
21
282
  ```
22
283
 
23
- 2. Setup a `GravityController` in some `@Controller`
284
+ **Client** use `GravityManager` to handle the full enable/disable lifecycle:
24
285
 
25
286
  ```typescript
26
287
  import { Controller, OnStart } from '@flamework/core'
27
288
  import {
28
289
  getGravityControllerUp,
29
- GravityController,
30
- GravityControllerClass,
290
+ GravityManager,
31
291
  installGravityControllerClass,
292
+ wrapGravityUpSaveAttribute,
32
293
  } from '@rbxts/gravity-controller'
33
- import { Players } from '@rbxts/services'
294
+ import { Logger } from '@rbxts/log'
34
295
 
35
296
  @Controller({})
36
297
  export class PlayerGravityController implements OnStart {
37
- gravityControllerClass: GravityControllerClass | undefined
38
- gravityController: GravityController | undefined
298
+ private gravityManager: GravityManager | undefined
299
+
300
+ constructor(private logger: Logger) {}
39
301
 
40
302
  onStart() {
41
- this.gravityControllerClass = installGravityControllerClass()
42
- Players.LocalPlayer.CharacterAdded.Connect((_character) => {
43
- this.disableGravityController()
44
- this.enableGravityController()
45
- })
46
- if (Players.LocalPlayer.Character) this.enableGravityController()
303
+ const cls = installGravityControllerClass()
304
+ this.gravityManager = new GravityManager(cls, this.logger)
305
+
306
+ // Enable gravity with surface-following wall walk
307
+ this.gravityManager.enable(
308
+ wrapGravityUpSaveAttribute(getGravityControllerUp),
309
+ )
47
310
  }
48
311
 
49
- disableGravityController() {
50
- this.gravityController?.Destroy()
51
- this.gravityController = undefined
312
+ disable() {
313
+ this.gravityManager?.disable()
52
314
  }
315
+ }
316
+ ```
53
317
 
54
- enableGravityController() {
55
- if (this.gravityController || !this.gravityControllerClass) return
56
- const gravityController = new this.gravityControllerClass(Players.LocalPlayer)
318
+ ### Manual lifecycle (without GravityManager)
57
319
 
58
- // Use EmilyBendsSpace's getGroundNormal() to walk up walls
59
- gravityController.GetGravityUp = getGravityControllerUp
320
+ If you need full control over construction timing:
60
321
 
61
- this.gravityController = gravityController
62
- }
63
- }
322
+ ```typescript
323
+ import {
324
+ installGravityControllerClass,
325
+ getGravityControllerUp,
326
+ } from '@rbxts/gravity-controller'
327
+ import { Players } from '@rbxts/services'
328
+
329
+ const GravityControllerClass = installGravityControllerClass()
64
330
 
331
+ Players.LocalPlayer.CharacterAdded.Connect(() => {
332
+ const gc = new GravityControllerClass(Players.LocalPlayer)
333
+ gc.GetGravityUp = getGravityControllerUp
334
+ })
65
335
  ```
336
+
337
+ ### Custom gravity direction
338
+
339
+ If you don't want surface-following wall walk, you can point gravity in any fixed direction:
340
+
341
+ ```typescript
342
+ const gc = new GravityControllerClass(Players.LocalPlayer)
343
+
344
+ // Gravity pulls toward -X (sideways)
345
+ gc.GetGravityUp = () => new Vector3(1, 0, 0)
346
+ ```
347
+
348
+ Or reset gravity imperatively:
349
+
350
+ ```typescript
351
+ gc.ResetGravity(new Vector3(0, -1, 0)) // flip upside down
352
+ ```
353
+
354
+ ## Credits
355
+
356
+ - [EgoMoose](https://github.com/EgoMoose) — original [Rbx-Gravity-Controller](https://github.com/EgoMoose/Rbx-Gravity-Controller) Lua implementation
357
+ - [EmilyBendsSpace](https://x.com/EmilyBendsSpace) — improved ground normal raycasting for smooth wall walking ([DevForum post](https://devforum.roblox.com/t/example-source-smooth-wall-walking-gravity-controller-from-club-raven/440229))
358
+
359
+ ## License
360
+
361
+ MIT
package/out/index.d.ts CHANGED
@@ -28,7 +28,31 @@ export interface GravityControllerConfig {
28
28
  JumpModifier?: number;
29
29
  UseBodyPositionLock?: boolean;
30
30
  }
31
+ export interface GravityLogger {
32
+ Info(message: string): void;
33
+ Warn(message: string): void;
34
+ Error(message: string): void;
35
+ }
36
+ export type GetGravityUp = (self: GravityController, oldGravityUp: Vector3) => Vector3;
31
37
  export declare let gravityControllerClass: GravityControllerClass;
32
38
  export declare function installGravityControllerClass(config?: GravityControllerConfig): GravityControllerClass;
39
+ export declare function wrapGravityUpSaveAttribute(getGravityUp: GetGravityUp): (gravityController: GravityController, oldGravityUp: Vector3) => Vector3;
40
+ export declare class GravityManager {
41
+ private _controller;
42
+ private _enabling;
43
+ private enablingStartedAt;
44
+ private generation;
45
+ private retryCount;
46
+ private constructionThread;
47
+ private pendingGetGravityUp;
48
+ private readonly gravityControllerClass;
49
+ private readonly logger;
50
+ constructor(gravityControllerClass: GravityControllerClass, logger?: GravityLogger);
51
+ getController(): GravityController | undefined;
52
+ getIsEnabling(): boolean;
53
+ disable(): void;
54
+ enable(getGravityUp: GetGravityUp): void;
55
+ private retryEnable;
56
+ }
33
57
  export declare function getGroundNormal(cframe: CFrame, originOffset: Vector3, oldGravityUp: Vector3): Vector3;
34
58
  export declare function getGravityControllerUp(gravityController: GravityController, oldGravityUp: Vector3): Vector3;
package/out/init.lua CHANGED
@@ -6,6 +6,7 @@ local Players = _services.Players
6
6
  local ReplicatedStorage = _services.ReplicatedStorage
7
7
  local RunService = _services.RunService
8
8
  local StarterPlayer = _services.StarterPlayer
9
+ local Workspace = _services.Workspace
9
10
  local function installGravityControllerClass(config)
10
11
  if exports.gravityControllerClass then
11
12
  if config then
@@ -44,6 +45,228 @@ local function installGravityControllerClass(config)
44
45
  end
45
46
  return exports.gravityControllerClass
46
47
  end
48
+ -- ── Utility ──────────────────────────────────────────────────────────
49
+ local function wrapGravityUpSaveAttribute(getGravityUp)
50
+ return function(gravityController, oldGravityUp)
51
+ local up = getGravityUp(gravityController, oldGravityUp)
52
+ local _oldGravityUp = oldGravityUp
53
+ if (up - _oldGravityUp).Magnitude > 0.001 then
54
+ gravityController.HRP:SetAttribute("GravityUp", up)
55
+ end
56
+ return up
57
+ end
58
+ end
59
+ -- ── GravityManager ───────────────────────────────────────────────────
60
+ local ENABLING_STALE_SEC = 5
61
+ local MAX_GRAVITY_RETRIES = 3
62
+ local RETRY_BACKOFF_SEC = 2
63
+ local noopLogger = {
64
+ Info = function(self) end,
65
+ Warn = function(self) end,
66
+ Error = function(self) end,
67
+ }
68
+ local GravityManager
69
+ do
70
+ GravityManager = setmetatable({}, {
71
+ __tostring = function()
72
+ return "GravityManager"
73
+ end,
74
+ })
75
+ GravityManager.__index = GravityManager
76
+ function GravityManager.new(...)
77
+ local self = setmetatable({}, GravityManager)
78
+ return self:constructor(...) or self
79
+ end
80
+ function GravityManager:constructor(gravityControllerClass, logger)
81
+ self._enabling = false
82
+ self.enablingStartedAt = 0
83
+ self.generation = 0
84
+ self.retryCount = 0
85
+ self.gravityControllerClass = gravityControllerClass
86
+ self.logger = logger or noopLogger
87
+ end
88
+ function GravityManager:getController()
89
+ return self._controller
90
+ end
91
+ function GravityManager:getIsEnabling()
92
+ return self._enabling
93
+ end
94
+ function GravityManager:disable()
95
+ local wasActive = self._controller ~= nil
96
+ local wasEnabling = self._enabling
97
+ if not wasActive and not wasEnabling then
98
+ return nil
99
+ end
100
+ local prevGen = self.generation
101
+ self.generation += 1
102
+ self._enabling = false
103
+ self.pendingGetGravityUp = nil
104
+ self.retryCount = 0
105
+ if self.constructionThread then
106
+ pcall(function()
107
+ return task.cancel(self.constructionThread)
108
+ end)
109
+ self.constructionThread = nil
110
+ end
111
+ self.logger:Info(`Disabling gravity controller: wasActive={wasActive}, wasEnabling={wasEnabling}` .. `, gen={prevGen}->{self.generation}`)
112
+ local _result = self._controller
113
+ if _result ~= nil then
114
+ _result:Destroy()
115
+ end
116
+ self._controller = nil
117
+ local _result_1 = Players.LocalPlayer.Character
118
+ if _result_1 ~= nil then
119
+ _result_1 = _result_1:FindFirstChild("HumanoidRootPart")
120
+ end
121
+ local hrp = _result_1
122
+ local _result_2 = hrp
123
+ if _result_2 ~= nil then
124
+ _result_2:SetAttribute("GravityUp", Vector3.new(0, 1, 0))
125
+ end
126
+ end
127
+ function GravityManager:enable(getGravityUp)
128
+ self.pendingGetGravityUp = getGravityUp
129
+ if self._controller then
130
+ return nil
131
+ end
132
+ local now = Workspace:GetServerTimeNow()
133
+ if self._enabling and now - self.enablingStartedAt < ENABLING_STALE_SEC then
134
+ return nil
135
+ end
136
+ self._enabling = true
137
+ self.enablingStartedAt = now
138
+ local generation = self.generation
139
+ local timeout = ENABLING_STALE_SEC + self.retryCount * RETRY_BACKOFF_SEC
140
+ self.logger:Info(`Enabling gravity controller (gen {generation}, timeout {timeout}s)`)
141
+ self.constructionThread = task.spawn(function()
142
+ -- Phase 1: wait for character
143
+ if not Players.LocalPlayer.Character then
144
+ Players.LocalPlayer.CharacterAdded:Wait()
145
+ end
146
+ local character = Players.LocalPlayer.Character
147
+ if generation ~= self.generation then
148
+ self._enabling = false
149
+ local pending = self.pendingGetGravityUp
150
+ if pending then
151
+ self:enable(pending)
152
+ end
153
+ return nil
154
+ end
155
+ -- Phase 2: wait for Animate script + Controller instance
156
+ local animate = character:WaitForChild("Animate", timeout)
157
+ if not animate then
158
+ self:retryEnable(generation, `Animate not found after {timeout}s`)
159
+ return nil
160
+ end
161
+ local animController = animate:WaitForChild("Controller", timeout)
162
+ if not animController then
163
+ self:retryEnable(generation, `Animate.Controller not found after {timeout}s`)
164
+ return nil
165
+ end
166
+ -- Phase 3: wait for Animate module to finish executing
167
+ local loaded = animate:FindFirstChild("Loaded")
168
+ if loaded and not loaded.Value then
169
+ self.logger:Info(`Waiting for Animate to finish loading (gen {generation})`)
170
+ local loadStart = os.clock()
171
+ while not loaded.Value and os.clock() - loadStart < timeout and generation == self.generation do
172
+ task.wait(0.1)
173
+ end
174
+ if not loaded.Value then
175
+ self:retryEnable(generation, `Animate not fully loaded after {timeout}s`)
176
+ return nil
177
+ end
178
+ end
179
+ if generation ~= self.generation then
180
+ self._enabling = false
181
+ local pending = self.pendingGetGravityUp
182
+ if pending then
183
+ self:enable(pending)
184
+ end
185
+ return nil
186
+ end
187
+ -- Phase 4: construct
188
+ self.logger:Info(`Constructing gravity controller (gen {generation})`)
189
+ local ok, result = pcall(function()
190
+ local gc = self.gravityControllerClass.new(Players.LocalPlayer)
191
+ gc.GetGravityUp = getGravityUp
192
+ return gc
193
+ end)
194
+ self._enabling = false
195
+ if generation ~= self.generation then
196
+ self.logger:Info(`Gravity controller construction completed but generation is stale ({generation} vs {self.generation}), destroying`)
197
+ if ok and result then
198
+ result:Destroy()
199
+ end
200
+ local pending = self.pendingGetGravityUp
201
+ if pending then
202
+ self:enable(pending)
203
+ end
204
+ return nil
205
+ end
206
+ if ok and result then
207
+ self._controller = result
208
+ self.pendingGetGravityUp = nil
209
+ self.retryCount = 0
210
+ self.logger:Info(`Gravity controller enabled (gen {generation})`)
211
+ else
212
+ local _result
213
+ if type(result) == "string" then
214
+ _result = result
215
+ else
216
+ local _condition = result
217
+ if _condition == nil then
218
+ _condition = "Unknown error"
219
+ end
220
+ _result = tostring(_condition)
221
+ end
222
+ local err = _result
223
+ self.logger:Error(`Error enabling gravity controller: {err}`)
224
+ end
225
+ end)
226
+ -- Watchdog: hard-cancel + cleanup + retry
227
+ task.delay(timeout, function()
228
+ if not self._enabling or self.generation ~= generation then
229
+ return nil
230
+ end
231
+ if self.constructionThread then
232
+ pcall(function()
233
+ return task.cancel(self.constructionThread)
234
+ end)
235
+ self.constructionThread = nil
236
+ end
237
+ pcall(function()
238
+ return RunService:UnbindFromRenderStep("GravityStep")
239
+ end)
240
+ local _humanoid = Players.LocalPlayer.Character
241
+ if _humanoid ~= nil then
242
+ _humanoid = _humanoid:FindFirstChildOfClass("Humanoid")
243
+ end
244
+ local humanoid = _humanoid
245
+ if humanoid then
246
+ humanoid.PlatformStand = false
247
+ end
248
+ self:retryEnable(generation, `construction timed out after {timeout}s`)
249
+ end)
250
+ end
251
+ function GravityManager:retryEnable(generation, reason)
252
+ if generation ~= self.generation then
253
+ return nil
254
+ end
255
+ self._enabling = false
256
+ self.retryCount += 1
257
+ if self.retryCount > MAX_GRAVITY_RETRIES then
258
+ self.logger:Error(`Gravity controller failed after {self.retryCount} attempts ({reason}), giving up (gen {generation})`)
259
+ return nil
260
+ end
261
+ self.logger:Warn(`Gravity controller: {reason} (gen {generation}), retrying (attempt {self.retryCount}/{MAX_GRAVITY_RETRIES})`)
262
+ self.generation += 1
263
+ local pending = self.pendingGetGravityUp
264
+ if pending then
265
+ self:enable(pending)
266
+ end
267
+ end
268
+ end
269
+ -- ── GetGravityUp implementations ─────────────────────────────────────
47
270
  local PI2 = math.pi * 2
48
271
  local ZERO = Vector3.new(0, 0, 0)
49
272
  local LOWER_RADIUS_OFFSET = 3
@@ -175,6 +398,8 @@ local function getGravityControllerUp(gravityController, oldGravityUp)
175
398
  return getGroundNormal(gravityController.HRP.CFrame, if gravityController.Humanoid.RigType == Enum.HumanoidRigType.R15 then ZERO else oldGravityUp * 0.35, oldGravityUp)
176
399
  end
177
400
  exports.installGravityControllerClass = installGravityControllerClass
401
+ exports.wrapGravityUpSaveAttribute = wrapGravityUpSaveAttribute
178
402
  exports.getGroundNormal = getGroundNormal
179
403
  exports.getGravityControllerUp = getGravityControllerUp
404
+ exports.GravityManager = GravityManager
180
405
  return exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/gravity-controller",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "main": "out/init.lua",
5
5
  "keywords": [
6
6
  "gravity",