@quenty/conditions 1.0.1-canary.256.8cd6f5a.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 ADDED
@@ -0,0 +1,11 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## 1.0.1-canary.256.8cd6f5a.0 (2022-03-27)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add conditions package ([354f533](https://github.com/Quenty/NevermoreEngine/commit/354f5332c1d63b858ff984509ce2ae9820e1a70f))
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2014-2021 Quenty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ ## Conditions
2
+ <div align="center">
3
+ <a href="http://quenty.github.io/NevermoreEngine/">
4
+ <img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
5
+ </a>
6
+ <a href="https://discord.gg/mhtGUS8">
7
+ <img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
8
+ </a>
9
+ <a href="https://github.com/Quenty/NevermoreEngine/actions">
10
+ <img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
11
+ </a>
12
+ </div>
13
+
14
+ Adornee based conditional system that is sufficiently generic to script gameplay.
15
+
16
+ <div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/AdorneeConditionUtils">View docs →</a></div>
17
+
18
+ ## Installation
19
+ ```
20
+ npm install @quenty/conditions --save
21
+ ```
22
+
23
+ ## Design challenges
24
+
25
+ The issue for conditions is they must be scriptable enough that anything can implement them, including external consumers, but flexibile enough that any gameplay can be constructed with them.
26
+
27
+ Additionally, we want to observe conditions for an adornee over specifically having them on a loop, so we want to definitely leverage Observables/Rx here.
28
+
29
+ Finally, there's a networking component here, where we want to define code once, but network it across many places. But we're also latency sensitive for conditions, that is, we should be able to deny actions as soon as they occur on the client.
30
+
31
+ ## Design results
32
+
33
+ For this reason, we end up with basically just a way to bind functions to a folder. Thus, we're scriptable. To allow replication/avoid duplicate implementation at the server/client layer we'll add another layer here where we'll bind conditions to the client/server.
34
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "conditions",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@quenty/conditions",
3
+ "version": "1.0.1-canary.256.8cd6f5a.0",
4
+ "description": "Adornee based conditional system that is sufficiently generic to script gameplay.",
5
+ "keywords": [
6
+ "Roblox",
7
+ "Nevermore",
8
+ "Lua",
9
+ "Conditions",
10
+ "Rx"
11
+ ],
12
+ "bugs": {
13
+ "url": "https://github.com/Quenty/NevermoreEngine/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Quenty/NevermoreEngine.git",
18
+ "directory": "src/conditions/"
19
+ },
20
+ "funding": {
21
+ "type": "patreon",
22
+ "url": "https://www.patreon.com/quenty"
23
+ },
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@quenty/attributeutils": "5.1.1-canary.256.8cd6f5a.0",
27
+ "@quenty/brio": "5.1.1-canary.256.8cd6f5a.0",
28
+ "@quenty/instanceutils": "4.1.1-canary.256.8cd6f5a.0",
29
+ "@quenty/loader": "4.0.1-canary.256.8cd6f5a.0",
30
+ "@quenty/maid": "2.2.1-canary.256.8cd6f5a.0",
31
+ "@quenty/rx": "4.1.1-canary.256.8cd6f5a.0",
32
+ "@quenty/statestack": "5.2.1-canary.256.8cd6f5a.0",
33
+ "@quenty/tie": "1.0.1-canary.256.8cd6f5a.0"
34
+ },
35
+ "contributors": [
36
+ "Quenty"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "gitHead": "8cd6f5a870e3e400eb53d147873062111e6ae8ed"
42
+ }
@@ -0,0 +1,506 @@
1
+ --[=[
2
+ Utility library that defines a generalized interface for scriptable conditions. These conditions are rooted in [Rx] and
3
+ can be scripted by a variety of systems. For example, we may have conditions on what ammo we can consume, or whether
4
+ or not an action can be activated.
5
+
6
+ This library provides an interface for this to happen in. Conditions are marshalled through a BindableFunction and
7
+ must be defined on both the client and server. However, these conditions can be reactive. It would be very simple
8
+ to create a condition that also just links to a bool value if conditions should be only on the server.
9
+
10
+ ```lua
11
+ local conditionFolder = AdorneeConditionUtils.createConditionContainer()
12
+ conditionFolder.Parent = workspace
13
+
14
+ local orGroup = AdorneeConditionUtils.createOrConditionGroup()
15
+ orGroup.Parent = conditionFolder
16
+
17
+ AdorneeConditionUtils.createRequiredProperty("Name", "Allowed").Parent = orGroup
18
+
19
+ local andGroup = AdorneeConditionUtils.createAndConditionGroup()
20
+ andGroup.Parent = orGroup
21
+
22
+ AdorneeConditionUtils.createRequiredProperty("Name", "Allow").Parent = andGroup
23
+ AdorneeConditionUtils.createRequiredAttribute("IsEnabled", true).Parent = andGroup
24
+
25
+ local testInst = Instance.new("Folder")
26
+ testInst.Name = "Deny"
27
+ testInst:SetAttribute("IsEnabled", false)
28
+ testInst.Parent = workspace
29
+
30
+ AdorneeConditionUtils.observeConditionsMet(conditionFolder, testInst):Subscribe(function(isAllowed)
31
+ print("Is allowed", isAllowed)
32
+ end) --> Is allowed false
33
+
34
+ task.delay(0.1, function()
35
+ testInst.Name = "Allowed" --> Is allowed true
36
+ end)
37
+ ```
38
+
39
+ @class AdorneeConditionUtils
40
+ ]=]
41
+
42
+ local require = require(script.Parent.loader).load(script)
43
+
44
+ local CollectionService = game:GetService("CollectionService")
45
+ local RunService = game:GetService("RunService")
46
+
47
+ local Maid = require("Maid")
48
+ local Observable = require("Observable")
49
+ local Rx = require("Rx")
50
+ local RxAttributeUtils = require("RxAttributeUtils")
51
+ local RxBrioUtils = require("RxBrioUtils")
52
+ local RxInstanceUtils = require("RxInstanceUtils")
53
+ local StateStack = require("StateStack")
54
+ local TieUtils = require("TieUtils")
55
+ local AttributeUtils = require("AttributeUtils")
56
+
57
+ local VALUE_WHEN_EMPTY_ATTRIBUTE = "ValueWhenEmpty"
58
+ local DEFAULT_VALUE_WHEN_EMPTY_WHEN_UNDEFINED = true
59
+
60
+ local AdorneeConditionUtils = {}
61
+
62
+ --[=[
63
+ Observes whether conditions are met or not.
64
+
65
+ ```lua
66
+ AdorneeConditionUtils.observeConditionsMet(gun.AmmoAllowedConditions, ammo):Subscribe(function(allowed)
67
+ if allowed then
68
+ print("Can use this ammo to refill this gun")
69
+ end
70
+ end)
71
+ ```
72
+
73
+ @param conditionObj -- Condition to invoke
74
+ @param adornee -- Adornee to check conditions on
75
+ @return Observable<boolean>
76
+ ]=]
77
+ function AdorneeConditionUtils.observeConditionsMet(conditionObj: BindableFunction, adornee: Instance)
78
+ assert(typeof(conditionObj) == "Instance" and conditionObj:IsA("BindableFunction"), "Bad conditionObj")
79
+ assert(typeof(adornee) == "Instance", "Bad adornee")
80
+
81
+ return AdorneeConditionUtils._getObservableFromConditionObj(conditionObj, adornee)
82
+ end
83
+
84
+ --[=[
85
+ Promises a query result of whether conditions are met or not. Unlike the observable, this will return
86
+ a result once all combinations are met.
87
+
88
+ @param conditionObj -- Condition to invoke
89
+ @param adornee -- Adornee to check conditions on
90
+ @param cancelToken CancelToken?
91
+ @return Promise<boolean>
92
+ ]=]
93
+ function AdorneeConditionUtils.promiseQueryConditionsMet(conditionObj: BindableFunction, adornee: Instance, cancelToken)
94
+ assert(typeof(conditionObj) == "Instance" and conditionObj:IsA("BindableFunction"), "Bad condition")
95
+ assert(typeof(adornee) == "Instance", "Bad adornee")
96
+
97
+ return Rx.toPromise(AdorneeConditionUtils.observeConditionsMet(conditionObj, adornee, cancelToken))
98
+ end
99
+
100
+ --[=[
101
+ Creates a new condition container which conditions can be parented to. By default this is an
102
+ "and" container, that is, all conditions underneath this container must be met for something to be allowed.
103
+
104
+ @return BindableFunction
105
+ ]=]
106
+ function AdorneeConditionUtils.createConditionContainer(): BindableFunction
107
+ local container = AdorneeConditionUtils.createAndConditionGroup()
108
+ container.Name = "Condition" .. AdorneeConditionUtils.getConditionNamePostfix()
109
+
110
+ -- allow by default
111
+ AdorneeConditionUtils.setValueWhenEmpty(container, true)
112
+
113
+ return container
114
+ end
115
+
116
+ --[=[
117
+ Creates a new adornee condition
118
+ @param observeCallback function
119
+ @return BindableFunction
120
+ ]=]
121
+ function AdorneeConditionUtils.create(observeCallback): BindableFunction
122
+ assert(type(observeCallback) == "function", "Bad observeCallback")
123
+
124
+ local condition = Instance.new("BindableFunction")
125
+ condition.Name = "CustomAdorneeCondition" .. AdorneeConditionUtils.getConditionNamePostfix()
126
+ condition.OnInvoke = TieUtils.encodeCallback(observeCallback)
127
+
128
+ CollectionService:AddTag(condition, AdorneeConditionUtils.getRequiredTag())
129
+
130
+ return condition
131
+ end
132
+
133
+ --[=[
134
+ Creates a new condition that a property is a set value.
135
+ @param propertyName string
136
+ @param requiredValue any
137
+ @return BindableFunction
138
+ ]=]
139
+ function AdorneeConditionUtils.createRequiredProperty(propertyName: string, requiredValue: any): BindableFunction
140
+ assert(type(propertyName) == "string", "Bad propertyName")
141
+
142
+ local condition = AdorneeConditionUtils.create(function(adornee: Instance)
143
+ return RxInstanceUtils.observeProperty(adornee, propertyName):Pipe({
144
+ Rx.map(function(value)
145
+ return value == requiredValue
146
+ end);
147
+ })
148
+ end)
149
+ condition.Name = ("RequiredProperty%s_%s_%s"):format(
150
+ AdorneeConditionUtils.getConditionNamePostfix(),
151
+ tostring(propertyName),
152
+ tostring(requiredValue))
153
+
154
+ return condition
155
+ end
156
+
157
+ --[=[
158
+ Creates a new condition that an attribute must be set to the current value
159
+ @param attributeName string
160
+ @param attributeValue any
161
+ @return BindableFunction
162
+ ]=]
163
+ function AdorneeConditionUtils.createRequiredAttribute(attributeName: string, attributeValue: any): BindableFunction
164
+ assert(type(attributeName) == "string", "Bad attributeName")
165
+
166
+ local condition = AdorneeConditionUtils.create(function(adornee: Instance)
167
+ return RxAttributeUtils.observeAttribute(adornee, attributeName):Pipe({
168
+ Rx.map(function(value)
169
+ return value == attributeValue
170
+ end);
171
+ })
172
+ end)
173
+
174
+ condition.Name = ("RequiredAttribute%s_%s_%s"):format(
175
+ AdorneeConditionUtils.getConditionNamePostfix(),
176
+ tostring(attributeName),
177
+ tostring(attributeValue))
178
+ return condition
179
+ end
180
+
181
+ --[=[
182
+ Creates a new condition that a tie interface must be implemented for the object
183
+ @param tieInterfaceDefinition TieDefinition
184
+ @return BindableFunction
185
+ ]=]
186
+ function AdorneeConditionUtils.createRequiredTieInterface(tieInterfaceDefinition): BindableFunction
187
+ assert(tieInterfaceDefinition, "Bad tieInterfaceDefinition")
188
+
189
+ local condition = AdorneeConditionUtils.create(function(adornee: Instance)
190
+ return tieInterfaceDefinition:ObserveIsImplemented(adornee)
191
+ end)
192
+
193
+ condition.Name = ("RequiredInterface%s_%s"):format(
194
+ AdorneeConditionUtils.getConditionNamePostfix(),
195
+ tieInterfaceDefinition:GetName())
196
+
197
+ return condition
198
+ end
199
+
200
+ --[=[
201
+ Creates a new "or" condition group where conditions are "or"ed together.
202
+ Conditions should be parented underneath this BindableFunction.
203
+ @return BindableFunction
204
+ ]=]
205
+ function AdorneeConditionUtils.createOrConditionGroup(): BindableFunction
206
+ local container
207
+ container = AdorneeConditionUtils.create(function(adornee: Instance)
208
+ assert(container, "Should not be invoking this on construction before container is assigned")
209
+
210
+ return AdorneeConditionUtils._observeConditionObservablesBrio(container, adornee)
211
+ :Pipe({
212
+ AdorneeConditionUtils._mapToOr(AdorneeConditionUtils.observeValueWhenEmpty(container))
213
+ })
214
+ end)
215
+
216
+ container.Name = ("OrConditionGroup%s"):format(AdorneeConditionUtils.getConditionNamePostfix())
217
+ AdorneeConditionUtils.setValueWhenEmpty(container, true)
218
+
219
+ return container
220
+ end
221
+
222
+ --[=[
223
+ Creates a new "and" condition group where conditions are "and"ed together.
224
+ Conditions should be parented underneath this BindableFunction.
225
+ @return BindableFunction
226
+ ]=]
227
+ function AdorneeConditionUtils.createAndConditionGroup(): BindableFunction
228
+ local container
229
+ container = AdorneeConditionUtils.create(function(adornee: Instance)
230
+ assert(container, "Should not be invoking this on construction before container is assigned")
231
+
232
+ return AdorneeConditionUtils._observeConditionObservablesBrio(container, adornee)
233
+ :Pipe({
234
+ AdorneeConditionUtils._mapToAnd(AdorneeConditionUtils.observeValueWhenEmpty(container))
235
+ })
236
+ end)
237
+
238
+ container.Name = ("AndConditionGroup%s"):format(AdorneeConditionUtils.getConditionNamePostfix())
239
+ AdorneeConditionUtils.setValueWhenEmpty(container, true)
240
+
241
+ return container
242
+ end
243
+
244
+ --[=[
245
+ Conditions must be tagged with a specific tag to make sure we don't invoke random code,
246
+ or code on the wrong host.
247
+
248
+ @return string
249
+ ]=]
250
+ function AdorneeConditionUtils.getRequiredTag(): string
251
+ if RunService:IsClient() then
252
+ return "AdorneeConditionClient"
253
+ else
254
+ return "AdorneeCondition"
255
+ end
256
+ end
257
+
258
+ --[=[
259
+ Gets the postfix for the condition name to make it clear to users
260
+ if this condition is on the server or the client.
261
+
262
+ @return string
263
+ ]=]
264
+ function AdorneeConditionUtils.getConditionNamePostfix(): string
265
+ if RunService:IsClient() then
266
+ return "Client"
267
+ else
268
+ return "Server"
269
+ end
270
+ end
271
+
272
+ --[=[
273
+ Sets the default value if we have no value
274
+
275
+ @param container BindableFunction
276
+ @param value boolean -- Value to default to
277
+ ]=]
278
+ function AdorneeConditionUtils.setValueWhenEmpty(container, value)
279
+ assert(typeof(container) == "Instance", "Bad container")
280
+ assert(type(value) == "boolean", "Bad value")
281
+
282
+ container:SetAttribute(VALUE_WHEN_EMPTY_ATTRIBUTE, value)
283
+ end
284
+
285
+ --[=[
286
+ Gets the default value on a container when empty
287
+
288
+ @param container BindableFunction
289
+ @return boolean
290
+ ]=]
291
+ function AdorneeConditionUtils.getValueWhenEmpty(container: BindableFunction)
292
+ assert(typeof(container) == "Instance", "Bad container")
293
+
294
+ return AttributeUtils.getAttribute(container, VALUE_WHEN_EMPTY_ATTRIBUTE, DEFAULT_VALUE_WHEN_EMPTY_WHEN_UNDEFINED)
295
+ end
296
+
297
+ --[=[
298
+ Sets the default value if we have no value
299
+ @param container BindableFunction
300
+ @return Observable<boolean>
301
+ ]=]
302
+ function AdorneeConditionUtils.observeValueWhenEmpty(container: BindableFunction)
303
+ assert(typeof(container) == "Instance", "Bad container")
304
+
305
+ return RxAttributeUtils.observeAttribute(container, VALUE_WHEN_EMPTY_ATTRIBUTE, DEFAULT_VALUE_WHEN_EMPTY_WHEN_UNDEFINED)
306
+ end
307
+
308
+ --[[
309
+ Maps the given object to "and"
310
+ ]]
311
+ function AdorneeConditionUtils._mapToAnd(observeValueWhenEmpty)
312
+ assert(observeValueWhenEmpty, "Bad observeValueWhenEmpty")
313
+
314
+ return function(source)
315
+ assert(source, "No source")
316
+
317
+ return Observable.new(function(sub)
318
+ local topMaid = Maid.new()
319
+
320
+ local isDisabled = StateStack.new(false)
321
+ topMaid:GiveTask(isDisabled)
322
+
323
+ local activeSourceCount = Instance.new("IntValue")
324
+ activeSourceCount.Value = 0
325
+ topMaid:GiveTask(activeSourceCount)
326
+
327
+ local totalState = Instance.new("BoolValue")
328
+ totalState.Value = false
329
+ topMaid:GiveTask(totalState)
330
+
331
+ local valueWhenEmpty = Instance.new("BoolValue")
332
+ valueWhenEmpty.Value = DEFAULT_VALUE_WHEN_EMPTY_WHEN_UNDEFINED
333
+ topMaid:GiveTask(observeValueWhenEmpty:Subscribe(function(defaultValue)
334
+ valueWhenEmpty.Value = defaultValue
335
+ end))
336
+
337
+ local function update()
338
+ if activeSourceCount.Value == 0 then
339
+ totalState.Value = valueWhenEmpty.Value
340
+ else
341
+ totalState.Value = not isDisabled:GetState()
342
+ end
343
+ end
344
+ topMaid:GiveTask(valueWhenEmpty.Changed:Connect(update))
345
+ topMaid:GiveTask(activeSourceCount.Changed:Connect(update))
346
+ topMaid:GiveTask(isDisabled.Changed:Connect(update))
347
+ update()
348
+
349
+ topMaid:GiveTask(source:Subscribe(function(observableBrio)
350
+ if observableBrio:IsDead() then
351
+ return
352
+ end
353
+
354
+ local observable = observableBrio:GetValue()
355
+ local observableMaid = observableBrio:ToMaid()
356
+
357
+ activeSourceCount.Value = activeSourceCount.Value + 1
358
+ observableMaid:GiveTask(function()
359
+ activeSourceCount.Value = activeSourceCount.Value - 1
360
+ end)
361
+
362
+ observableMaid:GiveTask(observable:Subscribe(function(state)
363
+ if state then
364
+ observableMaid._state = nil
365
+ else
366
+ observableMaid._state = isDisabled:PushState(true)
367
+ end
368
+ end))
369
+
370
+ end), sub:GetFailComplete())
371
+
372
+ topMaid:GiveTask(totalState.Changed:Connect(function()
373
+ sub:Fire(totalState.Value)
374
+ end))
375
+ sub:Fire(totalState.Value)
376
+
377
+ return topMaid
378
+ end)
379
+ end
380
+ end
381
+
382
+ --[[
383
+ Maps the given object to "or"
384
+ ]]
385
+ function AdorneeConditionUtils._mapToOr(observeValueWhenEmpty)
386
+ assert(observeValueWhenEmpty, "Bad observeValueWhenEmpty")
387
+
388
+ return function(source)
389
+ assert(source, "No source")
390
+
391
+ return Observable.new(function(sub)
392
+ local topMaid = Maid.new()
393
+
394
+ local isEnabled = StateStack.new(false)
395
+ topMaid:GiveTask(isEnabled)
396
+
397
+ local activeSourceCount = Instance.new("IntValue")
398
+ activeSourceCount.Value = 0
399
+ topMaid:GiveTask(activeSourceCount)
400
+
401
+ local totalState = Instance.new("BoolValue")
402
+ totalState.Value = false
403
+ topMaid:GiveTask(totalState)
404
+
405
+ local valueWhenEmpty = Instance.new("BoolValue")
406
+ valueWhenEmpty.Value = DEFAULT_VALUE_WHEN_EMPTY_WHEN_UNDEFINED
407
+ topMaid:GiveTask(observeValueWhenEmpty:Subscribe(function(defaultValue)
408
+ valueWhenEmpty.Value = defaultValue
409
+ end))
410
+
411
+ local function update()
412
+ if activeSourceCount.Value == 0 then
413
+ totalState.Value = valueWhenEmpty.Value
414
+ else
415
+ totalState.Value = isEnabled:GetState()
416
+ end
417
+ end
418
+ topMaid:GiveTask(valueWhenEmpty.Changed:Connect(update))
419
+ topMaid:GiveTask(activeSourceCount.Changed:Connect(update))
420
+ topMaid:GiveTask(isEnabled.Changed:Connect(update))
421
+ update()
422
+
423
+ topMaid:GiveTask(source:Subscribe(function(observableBrio)
424
+ if observableBrio:IsDead() then
425
+ return
426
+ end
427
+
428
+ local observable = observableBrio:GetValue()
429
+ local observableMaid = observableBrio:ToMaid()
430
+
431
+ activeSourceCount.Value = activeSourceCount.Value + 1
432
+ observableMaid:GiveTask(function()
433
+ activeSourceCount.Value = activeSourceCount.Value - 1
434
+ end)
435
+
436
+ observableMaid:GiveTask(observable:Subscribe(function(state)
437
+ if state then
438
+ observableMaid._state = isEnabled:PushState(true)
439
+ else
440
+ observableMaid._state = nil
441
+ end
442
+ end))
443
+
444
+ end), sub:GetFailComplete())
445
+
446
+ topMaid:GiveTask(totalState.Changed:Connect(function()
447
+ sub:Fire(totalState.Value)
448
+ end))
449
+ sub:Fire(totalState.Value)
450
+
451
+ return topMaid
452
+ end)
453
+ end
454
+ end
455
+
456
+ --[[
457
+ Observes all of the conditions under a given parent
458
+
459
+ @return Observable<Observable<boolean>>
460
+ ]]
461
+ function AdorneeConditionUtils._observeConditionObservablesBrio(parent: Instance, adornee: Instance)
462
+ assert(typeof(parent) == "Instance", "Bad parent")
463
+ assert(typeof(adornee) == "Instance", "Bad adornee")
464
+
465
+ return RxInstanceUtils.observeChildrenBrio(parent, function(child)
466
+ return child:IsA("BindableFunction") and CollectionService:HasTag(child, AdorneeConditionUtils.getRequiredTag())
467
+ end)
468
+ :Pipe({
469
+ RxBrioUtils.map(function(conditionObj)
470
+ return AdorneeConditionUtils._getObservableFromConditionObj(conditionObj, adornee)
471
+ end);
472
+ });
473
+ end
474
+
475
+ --[[
476
+ Tries to extract the condition observable from a given condition. This invokes the bindable
477
+ and will warn if the invocation yields.
478
+
479
+ @return Observable<boolean>
480
+ ]]
481
+ function AdorneeConditionUtils._getObservableFromConditionObj(conditionObj: Instance, adornee: Instance)
482
+ local observable
483
+ local current
484
+ task.spawn(function()
485
+ current = coroutine.running()
486
+ observable = TieUtils.invokeEncodedBindableFunction(conditionObj, adornee)
487
+ end)
488
+
489
+ -- TODO: Allow yielding here
490
+ if coroutine.status(current) ~= "dead" then
491
+ warn(("[AdorneeConditionUtils.observeAllowed] - Getting condition yielded from %q")
492
+ :format(conditionObj:GetFullName()))
493
+ return Rx.EMPTY
494
+ end
495
+
496
+ -- TODO: Allow non-observables.
497
+ if not (observable and Observable.isObservable(observable)) then
498
+ warn(("[AdorneeConditionUtils.observeAllowed] - Failed to get observable from %q. Got %q")
499
+ :format(conditionObj:GetFullName(), tostring(observable)))
500
+ return Rx.EMPTY
501
+ end
502
+
503
+ return observable
504
+ end
505
+
506
+ return AdorneeConditionUtils
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "ConditionTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "conditions": {
7
+ "$path": ".."
8
+ },
9
+ "Script": {
10
+ "$path": "scripts/Server"
11
+ }
12
+ },
13
+ "StarterPlayer": {
14
+ "StarterPlayerScripts": {
15
+ "Main": {
16
+ "$path": "scripts/Client"
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ --[[
2
+ @class ClientMain
3
+ ]]
4
+ local packages = game:GetService("ReplicatedStorage"):WaitForChild("Packages")
5
+
6
+ local serviceBag = require(packages.ServiceBag).new()
7
+ serviceBag:GetService(packages.AimLockServiceClient)
8
+
9
+ -- Start game
10
+ serviceBag:Init()
11
+ serviceBag:Start()
12
+
13
+ serviceBag:GetService(packages.AimLockServiceClient):SetUserControlsEnabled(true)
@@ -0,0 +1,66 @@
1
+ --[[
2
+ @class ServerMain
3
+ ]]
4
+ local ServerScriptService = game:GetService("ServerScriptService")
5
+
6
+ local loader = ServerScriptService:FindFirstChild("LoaderUtils", true).Parent
7
+ local packages = require(loader).bootstrapGame(ServerScriptService.conditions)
8
+
9
+ local AdorneeConditionUtils = require(packages.AdorneeConditionUtils)
10
+ local TieDefinition = require(packages.TieDefinition)
11
+ local AttributeValue = require(packages.AttributeValue)
12
+
13
+ do
14
+ local conditionFolder = AdorneeConditionUtils.createConditionContainer()
15
+ conditionFolder.Parent = workspace
16
+
17
+ local orGroup = AdorneeConditionUtils.createOrConditionGroup()
18
+ orGroup.Parent = conditionFolder
19
+
20
+ AdorneeConditionUtils.createRequiredProperty("Name", "Allowed").Parent = orGroup
21
+
22
+ local andGroup = AdorneeConditionUtils.createAndConditionGroup()
23
+ andGroup.Parent = orGroup
24
+
25
+ AdorneeConditionUtils.createRequiredProperty("Name", "Allow").Parent = andGroup
26
+ AdorneeConditionUtils.createRequiredAttribute("IsEnabled", true).Parent = andGroup
27
+
28
+ local testInst = Instance.new("Folder")
29
+ testInst.Name = "Deny"
30
+ testInst:SetAttribute("IsEnabled", false)
31
+ testInst.Parent = workspace
32
+
33
+ AdorneeConditionUtils.observeConditionsMet(conditionFolder, testInst):Subscribe(function(isAllowed)
34
+ print("Is allowed", isAllowed)
35
+ end)
36
+
37
+ task.delay(0.1, function()
38
+ testInst.Name = "Allowed"
39
+ end)
40
+ end
41
+
42
+ -- Test tie integration
43
+ do
44
+ local door = Instance.new("Folder")
45
+ door.Name = "Door"
46
+ door.Parent = workspace
47
+
48
+
49
+ local openableInterface = TieDefinition.new("Openable", {
50
+ IsOpen = TieDefinition.Types.PROPERTY;
51
+ })
52
+ openableInterface:Implement(door, {
53
+ IsOpen = AttributeValue.new(door, "IsOpen", false);
54
+ })
55
+
56
+ local canOpenCondition = AdorneeConditionUtils.createConditionContainer()
57
+ canOpenCondition.Name = "CanOpenCondition"
58
+ canOpenCondition.Parent = workspace
59
+
60
+ AdorneeConditionUtils.createRequiredTieInterface(openableInterface).Parent = canOpenCondition
61
+ AdorneeConditionUtils.createRequiredAttribute("IsOpen", false).Parent = canOpenCondition
62
+
63
+ AdorneeConditionUtils.observeConditionsMet(canOpenCondition, door):Subscribe(function(isAllowed)
64
+ print("Is door opening allowed", isAllowed)
65
+ end)
66
+ end