|
| 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