@quenty/camera 14.40.1 → 14.41.1

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,18 @@
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.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.41.0...@quenty/camera@14.41.1) (2026-06-05)
7
+
8
+ ### Bug Fixes
9
+
10
+ - Pass input ([faf1d56](https://github.com/Quenty/NevermoreEngine/commit/faf1d567726e8d640d8fd0060342fc200840a640))
11
+
12
+ # [14.41.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.1...@quenty/camera@14.41.0) (2026-06-03)
13
+
14
+ ### Features
15
+
16
+ - ConstrainedLookCamera ([8247ba8](https://github.com/Quenty/NevermoreEngine/commit/8247ba88b84c8f209bd1f72b7a384b907f735338))
17
+
6
18
  ## [14.40.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/camera@14.40.0...@quenty/camera@14.40.1) (2026-05-30)
7
19
 
8
20
  **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.1",
3
+ "version": "14.41.1",
4
4
  "description": "Quenty's camera system for Roblox",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -53,5 +53,5 @@
53
53
  "publishConfig": {
54
54
  "access": "public"
55
55
  },
56
- "gitHead": "598b2b62b36bdcbdbbd56f7db10c399831cc6eba"
56
+ "gitHead": "4f028984d278f4c3c77ab60ae1ce11cbaf00cb6d"
57
57
  }
@@ -0,0 +1,222 @@
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
+ return Enum.ContextActionResult.Pass
105
+ end, false, unpack(self._dragBeginTypes))
106
+
107
+ ContextActionService:BindAction(self._key .. "Rotate", function(_, _, inputObject)
108
+ self._gamepadRotateModel:HandleThumbstickInput(inputObject)
109
+ return Enum.ContextActionResult.Pass
110
+ end, false, Enum.KeyCode.Thumbstick2)
111
+
112
+ maid:GiveTask(function()
113
+ ContextActionService:UnbindAction(self._key .. "Drag")
114
+ ContextActionService:UnbindAction(self._key .. "Rotate")
115
+ end)
116
+
117
+ maid:GiveTask(function()
118
+ self._camera:Release()
119
+ end)
120
+ end
121
+
122
+ --[=[
123
+ Disables input and releases the camera.
124
+ ]=]
125
+ function ConstrainedLookControls.Disable(self: ConstrainedLookControls)
126
+ if not self._enabled then
127
+ return
128
+ end
129
+ assert(self._maid, "Must be enabled")
130
+ self._enabled = false
131
+
132
+ self._maid:DoCleaning()
133
+ self._maid = nil
134
+ self._lastMousePosition = nil
135
+ end
136
+
137
+ function ConstrainedLookControls._beginDrag(self: ConstrainedLookControls, beginInputObject: InputObject)
138
+ assert(self._maid, "Must be enabled")
139
+
140
+ local maid = Maid.new()
141
+ self._lastMousePosition = beginInputObject.Position
142
+
143
+ local isMouse = InputObjectUtils.isMouseUserInputType(beginInputObject.UserInputType)
144
+ if isMouse then
145
+ UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
146
+ maid:GiveTask(function()
147
+ UserInputService.MouseBehavior = Enum.MouseBehavior.Default
148
+ end)
149
+ end
150
+
151
+ maid:GiveTask(UserInputService.InputEnded:Connect(function(inputObject: InputObject)
152
+ if inputObject == beginInputObject then
153
+ self:_endDrag()
154
+ end
155
+ end))
156
+
157
+ maid:GiveTask(UserInputService.InputChanged:Connect(function(inputObject: InputObject)
158
+ if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) or inputObject == beginInputObject then
159
+ self:_handleDragMovement(inputObject)
160
+ end
161
+ end))
162
+
163
+ maid:GiveTask(function()
164
+ self._lastMousePosition = nil
165
+ self._camera:Release()
166
+ end)
167
+
168
+ self._maid._dragMaid = maid
169
+ end
170
+
171
+ function ConstrainedLookControls._endDrag(self: ConstrainedLookControls)
172
+ assert(self._maid, "Must be enabled")
173
+ self._maid._dragMaid = nil
174
+ end
175
+
176
+ function ConstrainedLookControls._handleDragMovement(self: ConstrainedLookControls, inputObject: InputObject)
177
+ if not self._lastMousePosition then
178
+ return
179
+ end
180
+
181
+ local delta = -inputObject.Delta
182
+ if InputObjectUtils.isMouseUserInputType(inputObject.UserInputType) then
183
+ delta += self._lastMousePosition - inputObject.Position
184
+ end
185
+
186
+ local xTheta = delta.X / 1920
187
+ local yTheta = delta.Y / 1200
188
+ local deltaAngle = Vector2.new(xTheta, yTheta) * self._mouseSensitivity
189
+ self._camera:RotateXY(deltaAngle)
190
+
191
+ self._lastMousePosition = inputObject.Position
192
+ end
193
+
194
+ function ConstrainedLookControls._handleGamepadRotateStart(self: ConstrainedLookControls)
195
+ assert(self._maid, "Must be enabled")
196
+
197
+ local maid = Maid.new()
198
+
199
+ maid:GiveTask(RunService.Stepped:Connect(function()
200
+ local deltaAngle = self._gamepadSensitivity * self._gamepadRotateModel:GetThumbstickDeltaAngle()
201
+ self._camera:RotateXY(deltaAngle)
202
+ end))
203
+
204
+ maid:GiveTask(function()
205
+ self._camera:Release()
206
+ end)
207
+
208
+ self._maid._dragMaid = maid
209
+ end
210
+
211
+ function ConstrainedLookControls._handleGamepadRotateStop(self: ConstrainedLookControls)
212
+ assert(self._maid, "Must be enabled")
213
+ self._maid._dragMaid = nil
214
+ end
215
+
216
+ function ConstrainedLookControls.Destroy(self: ConstrainedLookControls)
217
+ self:Disable()
218
+ self._gamepadRotateModel:Destroy()
219
+ setmetatable(self :: any, nil)
220
+ end
221
+
222
+ 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