@quenty/camera 14.40.0 → 14.41.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [14.41.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.1...@quenty/camera@14.41.0) (2026-06-03)
7
+
8
+ ### Features
9
+
10
+ - ConstrainedLookCamera ([8247ba8](https://github.com/Quenty/NevermoreEngine/commit/8247ba88b84c8f209bd1f72b7a384b907f735338))
11
+
12
+ ## [14.40.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.0...@quenty/camera@14.40.1) (2026-05-30)
13
+
14
+ **Note:** Version bump only for package @quenty/camera
15
+
6
16
  # [14.40.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.39.0...@quenty/camera@14.40.0) (2026-05-29)
7
17
 
8
18
  **Note:** Version bump only for package @quenty/camera
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/camera",
3
- "version": "14.40.0",
3
+ "version": "14.41.0",
4
4
  "description": "Quenty's camera system for Roblox",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -31,27 +31,27 @@
31
31
  "@quenty/acceltween": "2.7.0",
32
32
  "@quenty/baseobject": "10.13.0",
33
33
  "@quenty/cframeutils": "5.5.6",
34
- "@quenty/cubicspline": "10.12.0",
35
- "@quenty/draw": "7.14.0",
34
+ "@quenty/cubicspline": "10.12.1",
35
+ "@quenty/draw": "7.14.1",
36
36
  "@quenty/ducktype": "5.11.0",
37
- "@quenty/inputobjectutils": "4.30.0",
37
+ "@quenty/inputobjectutils": "4.30.1",
38
38
  "@quenty/loader": "10.11.0",
39
39
  "@quenty/maid": "3.9.0",
40
40
  "@quenty/math": "2.7.5",
41
41
  "@quenty/nevermore-test-runner": "1.4.0",
42
- "@quenty/qframe": "10.22.0",
43
- "@quenty/rx": "13.28.2",
44
- "@quenty/servicebag": "11.18.0",
42
+ "@quenty/qframe": "10.22.1",
43
+ "@quenty/rx": "13.28.3",
44
+ "@quenty/servicebag": "11.18.1",
45
45
  "@quenty/spring": "10.12.0",
46
- "@quenty/valueobject": "13.31.0",
46
+ "@quenty/valueobject": "13.31.1",
47
47
  "@quenty/vector3utils": "10.11.0",
48
48
  "@quentystudios/jest-lua": "3.10.0-quenty.2"
49
49
  },
50
50
  "devDependencies": {
51
- "@quenty/camerastoryutils": "10.21.0"
51
+ "@quenty/camerastoryutils": "10.21.1"
52
52
  },
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "gitHead": "f4a374a0a294ee8900aa5cb68ab138b0acf3e0ae"
56
+ "gitHead": "75291c76133c2b87b71c620125800dd3d72b7fe1"
57
57
  }
@@ -76,7 +76,7 @@ function CameraStack.PrintCameraStack(self: CameraStack): ()
76
76
  end
77
77
 
78
78
  --[=[
79
- Gets the camera current on the top of the stack
79
+ Gets the camera currently on the top of the stack
80
80
  @return CameraEffect
81
81
  ]=]
82
82
  function CameraStack.GetTopCamera(self: CameraStack): CameraEffectUtils.CameraLike
@@ -1,7 +1,7 @@
1
1
  --!nonstrict
2
2
  --[=[
3
3
  Holds camera states and allows for the last camera state to be retrieved. Also
4
- initializes an impulse and default camera as the bottom of the stack. Is a singleton.
4
+ initializes an impulse and default camera at the bottom of the stack. Is a singleton.
5
5
 
6
6
  @class CameraStackService
7
7
  ]=]
@@ -173,7 +173,7 @@ function CameraStackService.GetRawDefaultCamera(self: CameraStackService): Defau
173
173
  end
174
174
 
175
175
  --[=[
176
- Gets the camera current on the top of the stack
176
+ Gets the camera currently on the top of the stack
177
177
  @return CameraEffect
178
178
  ]=]
179
179
  function CameraStackService.GetTopCamera(self: CameraStackService): CameraEffectUtils.CameraLike
@@ -0,0 +1,220 @@
1
+ --!strict
2
+ --[=[
3
+ Input handler that drives a [ConstrainedLookCamera]. Reacts to right-click
4
+ drag on mouse, drag on touchscreen, and right thumbstick on gamepad.
5
+
6
+ @class ConstrainedLookControls
7
+ ]=]
8
+
9
+ local require = require(script.Parent.loader).load(script)
10
+
11
+ local ContextActionService = game:GetService("ContextActionService")
12
+ local RunService = game:GetService("RunService")
13
+ local UserInputService = game:GetService("UserInputService")
14
+
15
+ local ConstrainedLookCamera = require("ConstrainedLookCamera")
16
+ local GamepadRotateModel = require("GamepadRotateModel")
17
+ local InputObjectUtils = require("InputObjectUtils")
18
+ local Maid = require("Maid")
19
+
20
+ local ConstrainedLookControls = {}
21
+ ConstrainedLookControls.__index = ConstrainedLookControls
22
+ ConstrainedLookControls.ClassName = "ConstrainedLookControls"
23
+
24
+ ConstrainedLookControls.MOUSE_SENSITIVITY = Vector2.new(math.pi * 4, math.pi * 1.9)
25
+ ConstrainedLookControls.GAMEPAD_SENSITIVITY = 0.1
26
+ ConstrainedLookControls._dragBeginTypes = { Enum.UserInputType.MouseButton2, Enum.UserInputType.Touch }
27
+
28
+ export type ConstrainedLookControls = typeof(setmetatable(
29
+ {} :: {
30
+ _camera: ConstrainedLookCamera.ConstrainedLookCamera,
31
+ _enabled: boolean,
32
+ _key: string,
33
+ _maid: Maid.Maid?,
34
+ _gamepadRotateModel: GamepadRotateModel.GamepadRotateModel,
35
+ _lastMousePosition: Vector3?,
36
+ _mouseSensitivity: Vector2,
37
+ _gamepadSensitivity: number,
38
+ },
39
+ {} :: typeof({ __index = ConstrainedLookControls })
40
+ ))
41
+
42
+ --[=[
43
+ Constructs a new ConstrainedLookControls bound to the given camera.
44
+ ]=]
45
+ function ConstrainedLookControls.new(camera: ConstrainedLookCamera.ConstrainedLookCamera): ConstrainedLookControls
46
+ local self: ConstrainedLookControls = setmetatable({} :: any, ConstrainedLookControls)
47
+
48
+ self._camera = assert(camera, "Bad camera")
49
+ self._enabled = false
50
+ self._key = tostring(self) .. "ConstrainedLookControls"
51
+ self._mouseSensitivity = ConstrainedLookControls.MOUSE_SENSITIVITY
52
+ self._gamepadSensitivity = ConstrainedLookControls.GAMEPAD_SENSITIVITY
53
+ self._gamepadRotateModel = GamepadRotateModel.new()
54
+
55
+ return self
56
+ end
57
+
58
+ --[=[
59
+ Sets mouse sensitivity.
60
+ ]=]
61
+ function ConstrainedLookControls.SetMouseSensitivity(self: ConstrainedLookControls, sensitivity: Vector2)
62
+ self._mouseSensitivity = sensitivity
63
+ end
64
+
65
+ --[=[
66
+ Sets gamepad sensitivity.
67
+ ]=]
68
+ function ConstrainedLookControls.SetGamepadSensitivity(self: ConstrainedLookControls, sensitivity: number)
69
+ self._gamepadSensitivity = sensitivity
70
+ end
71
+
72
+ --[=[
73
+ Returns whether controls are enabled.
74
+ ]=]
75
+ function ConstrainedLookControls.IsEnabled(self: ConstrainedLookControls): boolean
76
+ return self._enabled
77
+ end
78
+
79
+ --[=[
80
+ Enables input. Binds drag (MouseButton2 / Touch) and gamepad Thumbstick2.
81
+ ]=]
82
+ function ConstrainedLookControls.Enable(self: ConstrainedLookControls)
83
+ if self._enabled then
84
+ return
85
+ end
86
+ assert(not self._maid, "Maid already defined")
87
+ self._enabled = true
88
+
89
+ local maid = Maid.new()
90
+ self._maid = maid
91
+
92
+ maid:GiveTask(self._gamepadRotateModel.IsRotating.Changed:Connect(function()
93
+ if self._gamepadRotateModel.IsRotating.Value then
94
+ self:_handleGamepadRotateStart()
95
+ else
96
+ self:_handleGamepadRotateStop()
97
+ end
98
+ end))
99
+
100
+ ContextActionService:BindAction(self._key .. "Drag", function(_, userInputState, inputObject)
101
+ if userInputState == Enum.UserInputState.Begin then
102
+ self:_beginDrag(inputObject)
103
+ end
104
+ end, false, unpack(self._dragBeginTypes))
105
+
106
+ ContextActionService:BindAction(self._key .. "Rotate", function(_, _, inputObject)
107
+ self._gamepadRotateModel:HandleThumbstickInput(inputObject)
108
+ end, false, Enum.KeyCode.Thumbstick2)
109
+
110
+ maid:GiveTask(function()
111
+ ContextActionService:UnbindAction(self._key .. "Drag")
112
+ ContextActionService:UnbindAction(self._key .. "Rotate")
113
+ end)
114
+
115
+ maid:GiveTask(function()
116
+ self._camera:Release()
117
+ end)
118
+ end
119
+
120
+ --[=[
121
+ Disables input and releases the camera.
122
+ ]=]
123
+ function ConstrainedLookControls.Disable(self: ConstrainedLookControls)
124
+ if not self._enabled then
125
+ return
126
+ end
127
+ assert(self._maid, "Must be enabled")
128
+ self._enabled = false
129
+
130
+ self._maid:DoCleaning()
131
+ self._maid = nil
132
+ self._lastMousePosition = nil
133
+ end
134
+
135
+ function ConstrainedLookControls._beginDrag(self: ConstrainedLookControls, beginInputObject: InputObject)
136
+ assert(self._maid, "Must be enabled")
137
+
138
+ local maid = Maid.new()
139
+ self._lastMousePosition = beginInputObject.Position
140
+
141
+ local isMouse = InputObjectUtils.isMouseUserInputType(beginInputObject.UserInputType)
142
+ if isMouse then
143
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
144
+ maid:GiveTask(function()
145
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
146
+ end)
147
+ end
148
+
149
+ maid:GiveTask(UserInputService.InputEnded:Connect(function(inputObject: InputObject)
150
+ if inputObject == beginInputObject then
151
+ self:_endDrag()
152
+ end
153
+ end))
154
+
155
+ maid:GiveTask(UserInputService.InputChanged:Connect(function(inputObject: InputObject)
156
+ if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) or inputObject == beginInputObject then
157
+ self:_handleDragMovement(inputObject)
158
+ end
159
+ end))
160
+
161
+ maid:GiveTask(function()
162
+ self._lastMousePosition = nil
163
+ self._camera:Release()
164
+ end)
165
+
166
+ self._maid._dragMaid = maid
167
+ end
168
+
169
+ function ConstrainedLookControls._endDrag(self: ConstrainedLookControls)
170
+ assert(self._maid, "Must be enabled")
171
+ self._maid._dragMaid = nil
172
+ end
173
+
174
+ function ConstrainedLookControls._handleDragMovement(self: ConstrainedLookControls, inputObject: InputObject)
175
+ if not self._lastMousePosition then
176
+ return
177
+ end
178
+
179
+ local delta = -inputObject.Delta
180
+ if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) then
181
+ delta += self._lastMousePosition - inputObject.Position
182
+ end
183
+
184
+ local xTheta = delta.X / 1920
185
+ local yTheta = delta.Y / 1200
186
+ local deltaAngle = Vector2.new(xTheta, yTheta) * self._mouseSensitivity
187
+ self._camera:RotateXY(deltaAngle)
188
+
189
+ self._lastMousePosition = inputObject.Position
190
+ end
191
+
192
+ function ConstrainedLookControls._handleGamepadRotateStart(self: ConstrainedLookControls)
193
+ assert(self._maid, "Must be enabled")
194
+
195
+ local maid = Maid.new()
196
+
197
+ maid:GiveTask(RunService.Stepped:Connect(function()
198
+ local deltaAngle = self._gamepadSensitivity * self._gamepadRotateModel:GetThumbstickDeltaAngle()
199
+ self._camera:RotateXY(deltaAngle)
200
+ end))
201
+
202
+ maid:GiveTask(function()
203
+ self._camera:Release()
204
+ end)
205
+
206
+ self._maid._dragMaid = maid
207
+ end
208
+
209
+ function ConstrainedLookControls._handleGamepadRotateStop(self: ConstrainedLookControls)
210
+ assert(self._maid, "Must be enabled")
211
+ self._maid._dragMaid = nil
212
+ end
213
+
214
+ function ConstrainedLookControls.Destroy(self: ConstrainedLookControls)
215
+ self:Disable()
216
+ self._gamepadRotateModel:Destroy()
217
+ setmetatable(self :: any, nil)
218
+ end
219
+
220
+ return ConstrainedLookControls
@@ -0,0 +1,143 @@
1
+ --!strict
2
+ --[=[
3
+ Constrains pitch and yaw within a cone.
4
+
5
+ @class ConstrainedLookCamera
6
+ ]=]
7
+
8
+ local require = require(script.Parent.loader).load(script)
9
+
10
+ local CameraEffectUtils = require("CameraEffectUtils")
11
+ local CameraState = require("CameraState")
12
+ local Spring = require("Spring")
13
+ local SummedCamera = require("SummedCamera")
14
+
15
+ local ConstrainedLookCamera = {}
16
+ ConstrainedLookCamera.ClassName = "ConstrainedLookCamera"
17
+
18
+ export type ConstrainedLookCamera =
19
+ typeof(setmetatable(
20
+ {} :: {
21
+ CameraState: CameraState.CameraState,
22
+ CFrame: CFrame,
23
+ AngleYaw: number,
24
+ AnglePitch: number,
25
+ TargetAngleYaw: number,
26
+ TargetAnglePitch: number,
27
+ MaxYawOffset: number,
28
+ MaxPitchOffset: number,
29
+ Speed: number,
30
+ SpeedYaw: number,
31
+ SpeedPitch: number,
32
+ Damper: number,
33
+ SpringYaw: Spring.Spring<number>,
34
+ SpringPitch: Spring.Spring<number>,
35
+ },
36
+ {} :: typeof({ __index = ConstrainedLookCamera })
37
+ ))
38
+ & CameraEffectUtils.CameraEffect
39
+
40
+ ConstrainedLookCamera._maxYawOffset = math.rad(20)
41
+ ConstrainedLookCamera._maxPitchOffset = math.rad(15)
42
+
43
+ --[=[
44
+ Constructs a new ConstrainedLookCamera.
45
+ ]=]
46
+ function ConstrainedLookCamera.new(): ConstrainedLookCamera
47
+ local self: ConstrainedLookCamera = setmetatable({} :: any, ConstrainedLookCamera)
48
+
49
+ self.SpringYaw = Spring.new(0)
50
+ self.SpringPitch = Spring.new(0)
51
+ self.Speed = 15
52
+ self.Damper = 1
53
+
54
+ return self
55
+ end
56
+
57
+ function ConstrainedLookCamera.__add(self: ConstrainedLookCamera, other: CameraEffectUtils.CameraEffect)
58
+ return SummedCamera.new(self, other)
59
+ end
60
+
61
+ --[=[
62
+ Rotates the target yaw (X) and pitch (Y) by the given delta, clamped within the max offsets.
63
+ ]=]
64
+ function ConstrainedLookCamera.RotateXY(self: ConstrainedLookCamera, delta: Vector2)
65
+ self.TargetAngleYaw += delta.X
66
+ self.TargetAnglePitch += delta.Y
67
+ end
68
+
69
+ --[=[
70
+ Releases input, sending spring targets back to the origin.
71
+ ]=]
72
+ function ConstrainedLookCamera.Release(self: ConstrainedLookCamera)
73
+ self.TargetAngleYaw = 0
74
+ self.TargetAnglePitch = 0
75
+ end
76
+
77
+ --[=[
78
+ Snaps springs to the origin without animating.
79
+ ]=]
80
+ function ConstrainedLookCamera.SnapToOrigin(self: ConstrainedLookCamera)
81
+ self.SpringYaw:SetTarget(0, true)
82
+ self.SpringPitch:SetTarget(0, true)
83
+ end
84
+
85
+ function ConstrainedLookCamera.__newindex(self: ConstrainedLookCamera, index, value)
86
+ if index == "AngleYaw" then
87
+ self.SpringYaw.Position = math.clamp(value, -self.MaxYawOffset, self.MaxYawOffset)
88
+ elseif index == "AnglePitch" then
89
+ self.SpringPitch.Position = math.clamp(value, -self.MaxPitchOffset, self.MaxPitchOffset)
90
+ elseif index == "TargetAngleYaw" then
91
+ self.SpringYaw.Target = math.clamp(value, -self.MaxYawOffset, self.MaxYawOffset)
92
+ elseif index == "TargetAnglePitch" then
93
+ self.SpringPitch.Target = math.clamp(value, -self.MaxPitchOffset, self.MaxPitchOffset)
94
+ elseif index == "MaxYawOffset" then
95
+ assert(value >= 0, "MaxYawOffset must be non-negative")
96
+ self._maxYawOffset = value
97
+ self.TargetAngleYaw = self.SpringYaw.Target
98
+ elseif index == "MaxPitchOffset" then
99
+ assert(value >= 0, "MaxPitchOffset must be non-negative")
100
+ self._maxPitchOffset = value
101
+ self.TargetAnglePitch = self.SpringPitch.Target
102
+ elseif index == "SpeedYaw" then
103
+ self.SpringYaw.Speed = value
104
+ elseif index == "SpeedPitch" then
105
+ self.SpringPitch.Speed = value
106
+ elseif index == "Speed" then
107
+ self.SpringYaw.Speed = value
108
+ self.SpringPitch.Speed = value
109
+ elseif index == "Damper" then
110
+ self.SpringYaw.Damper = value
111
+ self.SpringPitch.Damper = value
112
+ elseif ConstrainedLookCamera[index] ~= nil or index == "SpringYaw" or index == "SpringPitch" then
113
+ rawset(self, index, value)
114
+ else
115
+ error(`{tostring(index)} is not a valid member of ConstrainedLookCamera`)
116
+ end
117
+ end
118
+
119
+ function ConstrainedLookCamera.__index(self: ConstrainedLookCamera, index)
120
+ if index == "CameraState" then
121
+ local state = CameraState.new()
122
+ state.CFrame = self.CFrame
123
+ return state
124
+ elseif index == "CFrame" then
125
+ return CFrame.Angles(0, self.AngleYaw, 0) * CFrame.Angles(self.AnglePitch, 0, 0)
126
+ elseif index == "AngleYaw" then
127
+ return self.SpringYaw.Position
128
+ elseif index == "AnglePitch" then
129
+ return self.SpringPitch.Position
130
+ elseif index == "TargetAngleYaw" then
131
+ return self.SpringYaw.Target
132
+ elseif index == "TargetAnglePitch" then
133
+ return self.SpringPitch.Target
134
+ elseif index == "MaxYawOffset" then
135
+ return self._maxYawOffset
136
+ elseif index == "MaxPitchOffset" then
137
+ return self._maxPitchOffset
138
+ else
139
+ return ConstrainedLookCamera[index]
140
+ end
141
+ end
142
+
143
+ return ConstrainedLookCamera
@@ -1,6 +1,6 @@
1
1
  --!strict
2
2
  --[=[
3
- Hack to maintain default camera control by binding before and after the camera update cycle
3
+ Hack to maintain default camera control by binding before and after the camera update cycle.
4
4
  This allows other cameras to build off of the "default" camera while maintaining the same Roblox control scheme.
5
5
 
6
6
  This camera is automatically setup by the [CameraStackService](/api/CameraStackService).
@@ -1,8 +1,8 @@
1
1
  --!strict
2
2
  --[=[
3
3
  Allow freedom of movement around a current place, much like the classic script works now.
4
- Not intended to be use with the current character script
5
- Intended to be used with a SummedCamera, relative.
4
+ Not intended to be used with the current character script.
5
+ Intended to be used with a [SummedCamera], relative.
6
6
 
7
7
  ```lua
8
8
  local zoom = ZoomedCamera.new()
@@ -1,8 +1,8 @@
1
1
  --!strict
2
2
  --[=[
3
3
  Makes transitions between states easier. Uses the `CameraStackService` to tween in and
4
- out a new camera state Call `:Show()` and `:Hide()` to do so, and make sure to
5
- call `:Destroy()` after usage
4
+ out a new camera state. Call `:Show()` and `:Hide()` to do so, and make sure to
5
+ call `:Destroy()` after usage.
6
6
 
7
7
  @class CameraStateTweener
8
8
  ]=]
@@ -89,7 +89,7 @@ end
89
89
 
90
90
  --[=[
91
91
  Shows the camera to fade in.
92
- @param doNotAnimate? boolean -- Optional, defaults to animating
92
+ @param doNotAnimate boolean? -- Optional, defaults to animating
93
93
  ]=]
94
94
  function CameraStateTweener.Show(self: CameraStateTweener, doNotAnimate: boolean?)
95
95
  self:SetTarget(1, doNotAnimate)
@@ -97,7 +97,7 @@ end
97
97
 
98
98
  --[=[
99
99
  Hides the camera to fade in.
100
- @param doNotAnimate? boolean -- Optional, defaults to animating
100
+ @param doNotAnimate boolean? -- Optional, defaults to animating
101
101
  ]=]
102
102
  function CameraStateTweener.Hide(self: CameraStateTweener, doNotAnimate: boolean?)
103
103
  self:SetTarget(0, doNotAnimate)
@@ -158,6 +158,10 @@ function CameraStateTweener.GetCameraBelow(self: CameraStateTweener): CameraEffe
158
158
  return self._cameraBelow
159
159
  end
160
160
 
161
+ --[=[
162
+ Sets the epsilon to stop animating
163
+ @param epsilon number?
164
+ ]=]
161
165
  function CameraStateTweener:SetEpsilon(epsilon: number?)
162
166
  self._fadeBetween.Epsilon = epsilon
163
167
  end