Skip to content

Commit 0422fb6

Browse files
committed
feat: Add Holdable option to InputKeyMapList
1 parent 28054a1 commit 0422fb6

File tree

2 files changed

+244
-0
lines changed

2 files changed

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

src/inputkeymaputils/src/Shared/InputKeyMapList.lua

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export type InputKeyMapList =
8181
export type InputKeyMapListOptions = {
8282
bindingName: string,
8383
rebindable: boolean,
84+
holdable: boolean?,
85+
maxHoldDuration: number?,
8486
}
8587

8688
--[=[
@@ -171,6 +173,38 @@ function InputKeyMapList.IsUserRebindable(self: InputKeyMapList): boolean
171173
return self._options.rebindable == true
172174
end
173175

176+
--[=[
177+
Returns whether this input is holdable
178+
@return boolean
179+
]=]
180+
function InputKeyMapList.IsHoldable(self: InputKeyMapList): boolean
181+
return self._options.holdable == true
182+
end
183+
184+
--[=[
185+
Gets the maximum hold duration in seconds
186+
@return number
187+
]=]
188+
function InputKeyMapList.GetMaxHoldDuration(self: InputKeyMapList): number
189+
return self._options.maxHoldDuration or 1
190+
end
191+
192+
--[=[
193+
Observes whether this input is holdable
194+
@return Observable<boolean>
195+
]=]
196+
function InputKeyMapList.ObserveIsHoldable(self: InputKeyMapList): Observable.Observable<boolean>
197+
return Rx.of(self:IsHoldable())
198+
end
199+
200+
--[=[
201+
Observes the maximum hold duration
202+
@return Observable<number>
203+
]=]
204+
function InputKeyMapList.ObserveMaxHoldDuration(self: InputKeyMapList): Observable.Observable<number>
205+
return Rx.of(self:GetMaxHoldDuration())
206+
end
207+
174208
--[=[
175209
Gets the english name
176210
@return string

0 commit comments

Comments
 (0)