@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 +11 -0
- package/LICENSE.md +21 -0
- package/README.md +34 -0
- package/default.project.json +6 -0
- package/package.json +42 -0
- package/src/Shared/AdorneeConditionUtils.lua +506 -0
- package/src/node_modules.project.json +7 -0
- package/test/default.project.json +21 -0
- package/test/scripts/Client/ClientMain.client.lua +13 -0
- package/test/scripts/Server/ServerMain.server.lua +66 -0
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
|
+
|
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,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
|