Skip to content

Commit f989eff

Browse files
authored
Merge pull request #648 from Quenty/users/unrooot/holdable-input-keymap
feat: Add Holdable option to InputKeyMapList
2 parents 678ed19 + 7c77d1f commit f989eff

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
--!strict
2+
--[=[
3+
Tracks hold state for an input. Handles the timing logic
4+
and exposes observables for hold progress.
5+
6+
```lua
7+
local holdableInputModel = HoldableInputModel.new(1.5)
8+
-- Or set it later:
9+
-- holdableInputModel:SetMaxHoldDuration(1.5)
10+
11+
maid:GiveTask(holdableInputModel.HoldReleased:Connect(function(holdPercent)
12+
print("Released at", holdPercent)
13+
end))
14+
15+
-- When input begins
16+
holdableInputModel:StartHold()
17+
18+
-- When input ends
19+
holdableInputModel:StopHold()
20+
```
21+
22+
@class HoldableInputModel
23+
]=]
24+
25+
local require = require(script.Parent.loader).load(script)
26+
27+
local RunService = game:GetService("RunService")
28+
29+
local BaseObject = require("BaseObject")
30+
local Maid = require("Maid")
31+
local Observable = require("Observable")
32+
local Signal = require("Signal")
33+
local ValueObject = require("ValueObject")
34+
35+
local HoldableInputModel = setmetatable({}, BaseObject)
36+
HoldableInputModel.ClassName = "HoldableInputModel"
37+
HoldableInputModel.__index = HoldableInputModel
38+
39+
export type HoldableInputModel =
40+
typeof(setmetatable(
41+
{} :: {
42+
_maxHoldDuration: ValueObject.ValueObject<number>,
43+
_holdPercent: ValueObject.ValueObject<number>,
44+
_isHolding: ValueObject.ValueObject<boolean>,
45+
HoldStarted: Signal.Signal<()>,
46+
HoldUpdated: Signal.Signal<number>,
47+
HoldReleased: Signal.Signal<number>,
48+
},
49+
{} :: typeof({ __index = HoldableInputModel })
50+
))
51+
& BaseObject.BaseObject
52+
53+
--[=[
54+
Constructs a new HoldableInputModel
55+
56+
@param maxHoldDuration number? -- Optional max hold duration in seconds (defaults to 1)
57+
@return HoldableInputModel
58+
]=]
59+
function HoldableInputModel.new(maxHoldDuration: number?): HoldableInputModel
60+
local self = setmetatable(BaseObject.new() :: any, HoldableInputModel)
61+
62+
self._maxHoldDuration = self._maid:Add(ValueObject.new(maxHoldDuration or 1, "number"))
63+
self._holdPercent = self._maid:Add(ValueObject.new(0, "number"))
64+
self._isHolding = self._maid:Add(ValueObject.new(false, "boolean"))
65+
66+
--[=[
67+
Fires when a hold begins
68+
@prop HoldStarted Signal<>
69+
@within HoldableInputModel
70+
]=]
71+
self.HoldStarted = self._maid:Add(Signal.new())
72+
73+
--[=[
74+
Fires when the hold percent updates
75+
@prop HoldUpdated Signal<number>
76+
@within HoldableInputModel
77+
]=]
78+
self.HoldUpdated = self._maid:Add(Signal.new())
79+
80+
--[=[
81+
Fires when a hold is released with the final hold percent
82+
@prop HoldReleased Signal<number>
83+
@within HoldableInputModel
84+
]=]
85+
self.HoldReleased = self._maid:Add(Signal.new())
86+
87+
return self
88+
end
89+
90+
--[=[
91+
Sets the maximum hold duration in seconds
92+
93+
@param duration number | Observable<number>
94+
@return MaidTask
95+
]=]
96+
function HoldableInputModel.SetMaxHoldDuration(
97+
self: HoldableInputModel,
98+
duration: number | Observable.Observable<number>
99+
)
100+
return self._maxHoldDuration:Mount(duration)
101+
end
102+
103+
--[=[
104+
Gets the maximum hold duration
105+
106+
@return number
107+
]=]
108+
function HoldableInputModel.GetMaxHoldDuration(self: HoldableInputModel): number
109+
return self._maxHoldDuration.Value
110+
end
111+
112+
--[=[
113+
Observes the maximum hold duration
114+
115+
@return Observable<number>
116+
]=]
117+
function HoldableInputModel.ObserveMaxHoldDuration(self: HoldableInputModel): Observable.Observable<number>
118+
return self._maxHoldDuration:Observe()
119+
end
120+
121+
--[=[
122+
Observes the current hold percent (0-1)
123+
124+
@return Observable<number>
125+
]=]
126+
function HoldableInputModel.ObserveHoldPercent(self: HoldableInputModel): Observable.Observable<number>
127+
return self._holdPercent:Observe()
128+
end
129+
130+
--[=[
131+
Gets the current hold percent (0-1)
132+
133+
@return number
134+
]=]
135+
function HoldableInputModel.GetHoldPercent(self: HoldableInputModel): number
136+
return self._holdPercent.Value
137+
end
138+
139+
--[=[
140+
Observes whether currently holding
141+
142+
@return Observable<boolean>
143+
]=]
144+
function HoldableInputModel.ObserveIsHolding(self: HoldableInputModel): Observable.Observable<boolean>
145+
return self._isHolding:Observe()
146+
end
147+
148+
--[=[
149+
Returns whether currently holding
150+
151+
@return boolean
152+
]=]
153+
function HoldableInputModel.IsHolding(self: HoldableInputModel): boolean
154+
return self._isHolding.Value
155+
end
156+
157+
--[=[
158+
Starts tracking a hold. Call this when input begins.
159+
]=]
160+
function HoldableInputModel.StartHold(self: HoldableInputModel): ()
161+
self._maid._holdMaid = nil
162+
163+
local maid = Maid.new()
164+
local elapsed = 0
165+
local maxDuration = self._maxHoldDuration.Value or 1
166+
167+
self._isHolding.Value = true
168+
self._holdPercent.Value = 0
169+
self.HoldStarted:Fire()
170+
171+
maid:GiveTask(RunService.Heartbeat:Connect(function(dt)
172+
elapsed += dt
173+
local newPercent = math.clamp(elapsed / maxDuration, 0, 1)
174+
if self._holdPercent.Value ~= newPercent then
175+
self._holdPercent.Value = newPercent
176+
self.HoldUpdated:Fire(newPercent)
177+
end
178+
end))
179+
180+
maid:GiveTask(function()
181+
local finalPercent = self._holdPercent.Value
182+
self._holdPercent.Value = 0
183+
self._isHolding.Value = false
184+
self.HoldReleased:Fire(finalPercent)
185+
end)
186+
187+
self._maid._holdMaid = maid
188+
end
189+
190+
--[=[
191+
Stops tracking a hold and fires HoldReleased with the final percent.
192+
Call this when input ends.
193+
]=]
194+
function HoldableInputModel.StopHold(self: HoldableInputModel): ()
195+
self._maid._holdMaid = nil
196+
end
197+
198+
--[=[
199+
Cancels a hold without firing HoldReleased.
200+
Use this when the hold should be aborted (e.g., interrupted by stun).
201+
]=]
202+
function HoldableInputModel.CancelHold(self: HoldableInputModel): ()
203+
if self._maid._holdMaid then
204+
self._isHolding.Value = false
205+
self._holdPercent.Value = 0
206+
-- Clear without triggering cleanup function
207+
local holdMaid = self._maid._holdMaid
208+
self._maid._holdMaid = nil
209+
if holdMaid and holdMaid.Destroy then
210+
-- Destroy without the cleanup function firing HoldReleased
211+
holdMaid._tasks = {}
212+
holdMaid:Destroy()
213+
end
214+
end
215+
end
216+
217+
return HoldableInputModel

0 commit comments

Comments
 (0)