@quenty/rogue-properties 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 RogueProperty package ([87114a1](https://github.com/Quenty/NevermoreEngine/commit/87114a11e33015e4eccf7d907ac2fce2e2d889c0))
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,35 @@
1
+ ## RogueProperties
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
+ <div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/RogueProperty">View docs →</a></div>
15
+
16
+ Roguelike properties which can be modified by external provides -- and that modification can be attributed to a source.
17
+
18
+ ## Installation
19
+ ```
20
+ npm install @quenty/rogueproperties --save
21
+ ```
22
+
23
+ ## Design goals
24
+ We need a property system for a rogue-like or MMORPG style game that offers the following attributes.
25
+
26
+ 1. Modifiable - Can modify a base property in a variety of ways (additive, multiplicative, et cetera)
27
+ 2. Attributable - Can attribute the source of final computation, especially for UI.
28
+ 3. Extensible - Can combine properties
29
+ 4. Grounded in Roblox datamodel - Source of truth exists in Roblox so other people can modify it
30
+ 5. Performant - Needs to run fast
31
+ 6. Agnostic to server/client - Needs to be able to centralize in datatable.
32
+ 7. Scriptable - Need to be able to apply weird scripts to data.
33
+ 8. Levelable - Can scale with level!
34
+ 9. Usable for other people - The computed value is just .Value under the object!
35
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "rogue-properties",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@quenty/rogue-properties",
3
+ "version": "1.0.1-canary.256.8cd6f5a.0",
4
+ "description": "Roguelike properties which can be modified by external provides",
5
+ "keywords": [
6
+ "Roblox",
7
+ "Nevermore",
8
+ "Lua"
9
+ ],
10
+ "bugs": {
11
+ "url": "https://github.com/Quenty/NevermoreEngine/issues"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Quenty/NevermoreEngine.git",
16
+ "directory": "src/rogue-properties/"
17
+ },
18
+ "funding": {
19
+ "type": "patreon",
20
+ "url": "https://www.patreon.com/quenty"
21
+ },
22
+ "license": "MIT",
23
+ "contributors": [
24
+ "Quenty"
25
+ ],
26
+ "dependencies": {
27
+ "@quenty/baseobject": "4.1.1-canary.256.8cd6f5a.0",
28
+ "@quenty/binder": "5.1.1-canary.256.8cd6f5a.0",
29
+ "@quenty/brio": "5.1.1-canary.256.8cd6f5a.0",
30
+ "@quenty/instanceutils": "4.1.1-canary.256.8cd6f5a.0",
31
+ "@quenty/jsonutils": "4.1.1-canary.256.8cd6f5a.0",
32
+ "@quenty/linkutils": "4.1.1-canary.256.8cd6f5a.0",
33
+ "@quenty/loader": "4.0.1-canary.256.8cd6f5a.0",
34
+ "@quenty/maid": "2.2.1-canary.256.8cd6f5a.0",
35
+ "@quenty/rx": "4.1.1-canary.256.8cd6f5a.0",
36
+ "@quenty/rxbinderutils": "5.1.1-canary.256.8cd6f5a.0",
37
+ "@quenty/servicebag": "4.1.1-canary.256.8cd6f5a.0",
38
+ "@quenty/signal": "2.1.1-canary.256.8cd6f5a.0",
39
+ "@quenty/table": "2.1.2-canary.256.8cd6f5a.0",
40
+ "@quenty/valuebaseutils": "4.1.1-canary.256.8cd6f5a.0",
41
+ "@quentystudios/t": "^1.2.5"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "gitHead": "8cd6f5a870e3e400eb53d147873062111e6ae8ed"
47
+ }
@@ -0,0 +1,97 @@
1
+ --[=[
2
+ @class RoguePropertyDefinition
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local BaseObject = require("BaseObject")
8
+ local RogueProperty = require("RogueProperty")
9
+ local ServiceBag = require("ServiceBag")
10
+ local RoguePropertyUtils = require("RoguePropertyUtils")
11
+
12
+ local RoguePropertyDefinition = setmetatable({}, BaseObject)
13
+ RoguePropertyDefinition.ClassName = "RoguePropertyDefinition"
14
+ RoguePropertyDefinition.__index = RoguePropertyDefinition
15
+
16
+ function RoguePropertyDefinition.new(name, defaultValue, roguePropertyTableDefinition)
17
+ local self = setmetatable(BaseObject.new(), RoguePropertyDefinition)
18
+
19
+ assert(defaultValue ~= nil, "Bad defaultValue")
20
+
21
+ self._name = assert(name, "Bad name")
22
+ self._defaultValue = defaultValue
23
+ self._valueType = typeof(self._defaultValue)
24
+ self._storageType = self:_computeStorageInstanceType()
25
+ self._roguePropertyTableDefinition = roguePropertyTableDefinition or nil
26
+ self._encodedDefaultValue = RoguePropertyUtils.encodeProperty(self, self._defaultValue)
27
+
28
+ return self
29
+ end
30
+
31
+ --[=[
32
+ @param serviceBag ServiceBag
33
+ @param adornee Instance
34
+ @return RogueProperty
35
+ ]=]
36
+ function RoguePropertyDefinition:Get(serviceBag, adornee)
37
+ assert(ServiceBag.isServiceBag(serviceBag), "Bad serviceBag")
38
+ assert(typeof(adornee) == "Instance", "Bad adornee")
39
+
40
+ return RogueProperty.new(adornee, serviceBag, self)
41
+ end
42
+
43
+
44
+ function RoguePropertyDefinition:GetPropertyTableDefinition()
45
+ return self._roguePropertyTableDefinition
46
+ end
47
+
48
+ --[=[
49
+ Gets the name of the rogue property
50
+ @return string
51
+ ]=]
52
+ function RoguePropertyDefinition:GetName(): string
53
+ return self._name
54
+ end
55
+
56
+ --[=[
57
+ Gets the default value for the property
58
+ @return TProperty
59
+ ]=]
60
+ function RoguePropertyDefinition:GetDefaultValue()
61
+ return self._defaultValue
62
+ end
63
+
64
+ function RoguePropertyDefinition:GetValueType()
65
+ return self._valueType
66
+ end
67
+
68
+ function RoguePropertyDefinition:GetStorageInstanceType()
69
+ return self._storageType
70
+ end
71
+
72
+ function RoguePropertyDefinition:GetEncodedDefaultValue()
73
+ return rawget(self, "_encodedDefaultValue")
74
+ end
75
+
76
+ function RoguePropertyDefinition:_computeStorageInstanceType()
77
+ if self._valueType == "string" or self._valueType == "table" then
78
+ return "StringValue"
79
+ elseif self._valueType == "number" then
80
+ return "NumberValue"
81
+ elseif self._valueType == "boolean" then
82
+ return "BoolValue"
83
+ elseif self._valueType == "Color3" then
84
+ return "Color3Value"
85
+ elseif self._valueType == "BrickColor" then
86
+ return "BrickColorValue"
87
+ elseif self._valueType == "Vector3" then
88
+ return "Vector3Value"
89
+ elseif self._valueType == "CFrame" then
90
+ return "CFrameValue"
91
+ else
92
+ error(("Unknown valueType %q"):format(tostring(self._valueType)))
93
+ end
94
+ end
95
+
96
+
97
+ return RoguePropertyDefinition
@@ -0,0 +1,139 @@
1
+ --[=[
2
+ This holds the definition for a variety of tables.
3
+ @class RoguePropertyTableDefinition
4
+ ]=]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local RoguePropertyTable = require("RoguePropertyTable")
9
+ local ServiceBag = require("ServiceBag")
10
+ local RoguePropertyDefinition = require("RoguePropertyDefinition")
11
+ local RxInstanceUtils = require("RxInstanceUtils")
12
+ local RoguePropertyService = require("RoguePropertyService")
13
+
14
+ local RoguePropertyTableDefinition = {}
15
+ RoguePropertyTableDefinition.ClassName = "RoguePropertyTableDefinition"
16
+ RoguePropertyTableDefinition.__index = RoguePropertyTableDefinition
17
+
18
+ function RoguePropertyTableDefinition.new(tableName: string, propertyDefinition: {[string]: any})
19
+ local self = setmetatable({}, RoguePropertyTableDefinition)
20
+
21
+ assert(type(tableName) == "string", "Bad tableName")
22
+ assert(type(propertyDefinition) == "table", "Bad propertyDefinition")
23
+
24
+ self._tableName = tableName
25
+ self._definitionMap = {}
26
+
27
+ for key, defaultValue in pairs(propertyDefinition) do
28
+ self._definitionMap[key] = RoguePropertyDefinition.new(key, defaultValue, self)
29
+ end
30
+
31
+ return self
32
+ end
33
+
34
+ --[=[
35
+ Gets the name of the rogue property table
36
+ @return string
37
+ ]=]
38
+ function RoguePropertyTableDefinition:GetName(): string
39
+ return self._tableName
40
+ end
41
+
42
+ function RoguePropertyTableDefinition:GetDefinitionMap()
43
+ return self._definitionMap
44
+ end
45
+
46
+ --[=[
47
+ Gets the RoguePropertyDefinition if it exists
48
+ @param propertyName
49
+ @return RoguePropertyDefinition?
50
+ ]=]
51
+ function RoguePropertyTableDefinition:GetDefinition(propertyName: string)
52
+ assert(type(propertyName) == "string", "Bad propertyName")
53
+
54
+ local definitions = rawget(self, "_definitionMap")
55
+ return definitions[propertyName]
56
+ end
57
+
58
+ --[=[
59
+ Gets a new property table for a given object, which can compute the modified
60
+ value of the adornee. This will initialize the properties on the server.
61
+
62
+ @param serviceBag ServiceBag
63
+ @param adornee Instance
64
+ @return RoguePropertyTable
65
+ ]=]
66
+ function RoguePropertyTableDefinition:GetPropertyTable(serviceBag, adornee)
67
+ assert(ServiceBag.isServiceBag(serviceBag), "Bad serviceBag")
68
+ assert(typeof(adornee) == "Instance", "Bad adornee")
69
+
70
+ return RoguePropertyTable.new(adornee, serviceBag, self)
71
+ end
72
+
73
+ --[=[
74
+ Observes the current container while it exists for the given adornee.
75
+
76
+ @param serviceBag ServiceBag
77
+ @param adornee Instance
78
+ @return Observable<Brio<Folder>>
79
+ ]=]
80
+ function RoguePropertyTableDefinition:ObserveContainerBrio(serviceBag, adornee)
81
+ assert(ServiceBag.isServiceBag(serviceBag), "Bad serviceBag")
82
+ assert(typeof(adornee) == "Instance", "Bad adornee")
83
+
84
+ -- TODO: Optimize so we aren't duplcating this logic each time we index a property
85
+ if serviceBag:GetService(RoguePropertyService):CanInitializeProperties() then
86
+ self:_ensureContainer(adornee)
87
+ end
88
+
89
+ return RxInstanceUtils.observeLastNamedChildBrio(adornee, "Folder", self._tableName)
90
+ end
91
+
92
+ --[=[
93
+ Gets the current container for the given adornee.
94
+ @param serviceBag ServiceBag
95
+ @param adornee Instance
96
+ @return Folder?
97
+ ]=]
98
+ function RoguePropertyTableDefinition:GetContainer(serviceBag, adornee)
99
+ assert(ServiceBag.isServiceBag(serviceBag), "Bad serviceBag")
100
+ assert(typeof(adornee) == "Instance", "Bad adornee")
101
+
102
+ if serviceBag:GetService(RoguePropertyService):CanInitializeProperties() then
103
+ return self:_ensureContainer(adornee)
104
+ else
105
+ return adornee:FindFirstChild(self._tableName)
106
+ end
107
+ end
108
+
109
+ function RoguePropertyTableDefinition:_ensureContainer(adornee)
110
+ local existing = adornee:FindFirstChild(self._tableName)
111
+ if existing then
112
+ return existing
113
+ end
114
+
115
+ local folder = Instance.new("Folder")
116
+ folder.Name = self._tableName
117
+ folder.Parent = adornee
118
+ return folder
119
+ end
120
+
121
+ function RoguePropertyTableDefinition:__index(index)
122
+ assert(type(index) == "string", "Bad index")
123
+
124
+ if index == "_definitionMap" then
125
+ return rawget(self, "_definitionMap")
126
+ elseif RoguePropertyTableDefinition[index] then
127
+ return RoguePropertyTableDefinition[index]
128
+ else
129
+ local definitions = rawget(self, "_definitionMap")
130
+
131
+ if definitions[index] then
132
+ return definitions[index]
133
+ else
134
+ error(("Bad definition %q"):format(tostring(index)))
135
+ end
136
+ end
137
+ end
138
+
139
+ return RoguePropertyTableDefinition
@@ -0,0 +1,34 @@
1
+ --[=[
2
+ @class RogueAdditive
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local BaseObject = require("BaseObject")
8
+ local RxValueBaseUtils = require("RxValueBaseUtils")
9
+
10
+ local RogueAdditive = setmetatable({}, BaseObject)
11
+ RogueAdditive.ClassName = "RogueAdditive"
12
+ RogueAdditive.__index = RogueAdditive
13
+
14
+ function RogueAdditive.new(obj, serviceBag)
15
+ local self = setmetatable(BaseObject.new(obj), RogueAdditive)
16
+
17
+ self._serviceBag = assert(serviceBag, "No serviceBag")
18
+
19
+ return self
20
+ end
21
+
22
+ function RogueAdditive:GetObject()
23
+ return self._obj
24
+ end
25
+
26
+ function RogueAdditive:ObserveAdditive()
27
+ return RxValueBaseUtils.observeValue(self._obj)
28
+ end
29
+
30
+ function RogueAdditive:GetAdditive()
31
+ return self._obj.Value
32
+ end
33
+
34
+ return RogueAdditive
@@ -0,0 +1,91 @@
1
+ --[=[
2
+ @class RogueAdditiveProvider
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local Rx = require("Rx")
8
+ local RxBinderUtils = require("RxBinderUtils")
9
+ local RxBrioUtils = require("RxBrioUtils")
10
+ local RogueBindersShared = require("RogueBindersShared")
11
+ local BinderUtils = require("BinderUtils")
12
+ local RoguePropertyModifierUtils = require("RoguePropertyModifierUtils")
13
+ local RoguePropertyService = require("RoguePropertyService")
14
+
15
+ local RogueAdditiveProvider = {}
16
+
17
+ function RogueAdditiveProvider:Init(serviceBag)
18
+ assert(not self._serviceBag, "Already initialized")
19
+ self._serviceBag = assert(serviceBag, "No serviceBag")
20
+
21
+ -- Internal
22
+ self._roguePropetyService = self._serviceBag:GetService(RoguePropertyService)
23
+ self._rogueBinders = self._serviceBag:GetService(RogueBindersShared)
24
+
25
+ self._roguePropetyService:AddProvider(self)
26
+ end
27
+
28
+ function RogueAdditiveProvider:GetBinder()
29
+ return self._rogueBinders.RogueAdditive
30
+ end
31
+
32
+ function RogueAdditiveProvider:Create(additive, source)
33
+ assert(type(additive) == "number", "Bad additive")
34
+ assert(typeof(source) == "Instance" or source == nil, "Bad source")
35
+
36
+ local obj = Instance.new("NumberValue")
37
+ obj.Name = "Additive"
38
+ obj.Value = additive
39
+
40
+ if source then
41
+ RoguePropertyModifierUtils.createSourceLink(obj, source)
42
+ end
43
+
44
+ self._rogueBinders.RogueAdditive:Bind(obj)
45
+
46
+ return obj
47
+ end
48
+
49
+ function RogueAdditiveProvider:GetModifiedVersion(propObj, rogueProperty, baseValue)
50
+ if rogueProperty:GetDefinition():GetValueType() == "number" then
51
+ local value = baseValue
52
+
53
+ for _, item in pairs(self:_getAdditives(propObj)) do
54
+ value = value + item:GetAdditive()
55
+ end
56
+
57
+ return value
58
+ else
59
+ return baseValue
60
+ end
61
+ end
62
+
63
+ function RogueAdditiveProvider:ObserveModifiedVersion(propObj, rogueProperty, observeBaseValue)
64
+ if rogueProperty:GetDefinition():GetValueType() == "number" then
65
+ return RxBrioUtils.flatCombineLatest({
66
+ value = observeBaseValue;
67
+ multiplier = self:_observeMultipliers(propObj):Pipe({
68
+ RxBrioUtils.flatMapBrio(function(item)
69
+ return item:ObserveAdditive();
70
+ end); -- this gets us a list of multipliers which should mutate pretty frequently.
71
+ Rx.defaultsToNil;
72
+ });
73
+ }):Pipe({
74
+ Rx.map(function(state)
75
+ return self:GetModifiedVersion(propObj, rogueProperty, state.value)
76
+ end);
77
+ })
78
+ else
79
+ return observeBaseValue
80
+ end
81
+ end
82
+
83
+ function RogueAdditiveProvider:_observeMultipliers(propObj)
84
+ return RxBinderUtils.observeBoundChildClassBrio(self._rogueBinders.RogueAdditive, propObj)
85
+ end
86
+
87
+ function RogueAdditiveProvider:_getAdditives(propObj)
88
+ return BinderUtils.getChildren(self._rogueBinders.RogueAdditive, propObj)
89
+ end
90
+
91
+ return RogueAdditiveProvider
@@ -0,0 +1,34 @@
1
+ --[=[
2
+ @class RogueMultiplier
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local BaseObject = require("BaseObject")
8
+ local RxValueBaseUtils = require("RxValueBaseUtils")
9
+
10
+ local RogueMultiplier = setmetatable({}, BaseObject)
11
+ RogueMultiplier.ClassName = "RogueMultiplier"
12
+ RogueMultiplier.__index = RogueMultiplier
13
+
14
+ function RogueMultiplier.new(obj, serviceBag)
15
+ local self = setmetatable(BaseObject.new(obj), RogueMultiplier)
16
+
17
+ self._serviceBag = assert(serviceBag, "No serviceBag")
18
+
19
+ return self
20
+ end
21
+
22
+ function RogueMultiplier:ObserveMultiplier()
23
+ return RxValueBaseUtils.observeValue(self._obj)
24
+ end
25
+
26
+ function RogueMultiplier:GetMultiplier()
27
+ return self._obj.Value
28
+ end
29
+
30
+ function RogueMultiplier:GetObject()
31
+ return self._obj
32
+ end
33
+
34
+ return RogueMultiplier
@@ -0,0 +1,91 @@
1
+ --[=[
2
+ @class RogueMultiplierProvider
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local Rx = require("Rx")
8
+ local RxBinderUtils = require("RxBinderUtils")
9
+ local RxBrioUtils = require("RxBrioUtils")
10
+ local RogueBindersShared = require("RogueBindersShared")
11
+ local BinderUtils = require("BinderUtils")
12
+ local RoguePropertyModifierUtils = require("RoguePropertyModifierUtils")
13
+ local RoguePropertyService = require("RoguePropertyService")
14
+
15
+ local RogueMultiplierProvider = {}
16
+
17
+ function RogueMultiplierProvider:Init(serviceBag)
18
+ assert(not self._serviceBag, "Already initialized")
19
+ self._serviceBag = assert(serviceBag, "No serviceBag")
20
+
21
+ -- Internal
22
+ self._roguePropetyService = self._serviceBag:GetService(RoguePropertyService)
23
+ self._rogueBinders = self._serviceBag:GetService(RogueBindersShared)
24
+
25
+ self._roguePropetyService:AddProvider(self)
26
+ end
27
+
28
+ function RogueMultiplierProvider:GetBinder()
29
+ return self._rogueBinders.RogueMultiplier
30
+ end
31
+
32
+ function RogueMultiplierProvider:Create(multiplier, source)
33
+ assert(type(multiplier) == "number", "Bad multiplier")
34
+ assert(typeof(source) == "Instance" or source == nil, "Bad source")
35
+
36
+ local obj = Instance.new("NumberValue")
37
+ obj.Name = "Multiplier"
38
+ obj.Value = multiplier
39
+
40
+ if source then
41
+ RoguePropertyModifierUtils.createSourceLink(obj, source)
42
+ end
43
+
44
+ self._rogueBinders.RogueMultiplier:Bind(obj)
45
+
46
+ return obj
47
+ end
48
+
49
+ function RogueMultiplierProvider:GetModifiedVersion(propObj, rogueProperty, baseValue)
50
+ if rogueProperty:GetDefinition():GetValueType() == "number" then
51
+ local multiplier = 1
52
+
53
+ for _, item in pairs(self:_getMultipliers(propObj)) do
54
+ multiplier = multiplier*item:GetMultiplier()
55
+ end
56
+
57
+ return baseValue*multiplier
58
+ else
59
+ return baseValue
60
+ end
61
+ end
62
+
63
+ function RogueMultiplierProvider:ObserveModifiedVersion(propObj, rogueProperty, observeBaseValue)
64
+ if rogueProperty:GetDefinition():GetValueType() == "number" then
65
+ return RxBrioUtils.flatCombineLatest({
66
+ value = observeBaseValue;
67
+ multiplier = self:_observeMultipliers(propObj):Pipe({
68
+ RxBrioUtils.flatMapBrio(function(item)
69
+ return item:ObserveMultiplier();
70
+ end); -- this gets us a list of multipliers which should mutate pretty frequently.
71
+ Rx.defaultsToNil;
72
+ });
73
+ }):Pipe({
74
+ Rx.map(function(state)
75
+ return self:GetModifiedVersion(propObj, rogueProperty, state.value)
76
+ end);
77
+ })
78
+ else
79
+ return observeBaseValue
80
+ end
81
+ end
82
+
83
+ function RogueMultiplierProvider:_observeMultipliers(propObj)
84
+ return RxBinderUtils.observeBoundChildClassBrio(self._rogueBinders.RogueMultiplier, propObj)
85
+ end
86
+
87
+ function RogueMultiplierProvider:_getMultipliers(propObj)
88
+ return BinderUtils.getChildren(self._rogueBinders.RogueMultiplier, propObj)
89
+ end
90
+
91
+ return RogueMultiplierProvider
@@ -0,0 +1,11 @@
1
+ --[=[
2
+ @class RoguePropertyModifierConstants
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local Table = require("Table")
8
+
9
+ return Table.readonly({
10
+ PROPERTY_SOURCE_LINK_NAME = "RoguePropertySourceLink";
11
+ })
@@ -0,0 +1,21 @@
1
+ --[=[
2
+ @class RoguePropertyModifierUtils
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local LinkUtils = require("LinkUtils")
8
+ local RoguePropertyModifierConstants = require("RoguePropertyModifierConstants")
9
+ local RxLinkUtils = require("RxLinkUtils")
10
+
11
+ local RoguePropertyModifierUtils = {}
12
+
13
+ function RoguePropertyModifierUtils.createSourceLink(modifier, source)
14
+ return LinkUtils.createLink(RoguePropertyModifierConstants.PROPERTY_SOURCE_LINK_NAME, modifier, source)
15
+ end
16
+
17
+ function RoguePropertyModifierUtils.observeSourceLinksBrio(modifier)
18
+ return RxLinkUtils.observeValidLinksBrio(RoguePropertyModifierConstants.PROPERTY_SOURCE_LINK_NAME, modifier)
19
+ end
20
+
21
+ return RoguePropertyModifierUtils
@@ -0,0 +1,219 @@
1
+ --[=[
2
+ @class RogueProperty
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local RogueAdditiveProvider = require("RogueAdditiveProvider")
8
+ local RogueMultiplierProvider = require("RogueMultiplierProvider")
9
+ local RoguePropertyBinderGroups = require("RoguePropertyBinderGroups")
10
+ local RoguePropertyModifierUtils = require("RoguePropertyModifierUtils")
11
+ local RoguePropertyService = require("RoguePropertyService")
12
+ local Rx = require("Rx")
13
+ local RxBinderUtils = require("RxBinderUtils")
14
+ local RxBrioUtils = require("RxBrioUtils")
15
+ local RxInstanceUtils = require("RxInstanceUtils")
16
+ local RxValueBaseUtils = require("RxValueBaseUtils")
17
+ local ValueBaseUtils = require("ValueBaseUtils")
18
+ local RoguePropertyUtils = require("RoguePropertyUtils")
19
+
20
+ local RogueProperty = {}
21
+ RogueProperty.ClassName = "RogueProperty"
22
+ RogueProperty.__index = RogueProperty
23
+
24
+ function RogueProperty.new(adornee, serviceBag, definition)
25
+ local self = setmetatable({}, RogueProperty)
26
+
27
+ self._serviceBag = assert(serviceBag, "No serviceBag")
28
+ self._roguePropertyBinderGroups = self._serviceBag:GetService(RoguePropertyBinderGroups)
29
+ self._roguePropertyService = self._serviceBag:GetService(RoguePropertyService)
30
+
31
+ self._adornee = assert(adornee, "Bad adornee")
32
+ self._definition = assert(definition, "Bad definition")
33
+
34
+ if self._roguePropertyService:CanInitializeProperties() then
35
+ self:_getBaseValueObject()
36
+ end
37
+
38
+ return self
39
+ end
40
+
41
+ function RogueProperty:_getBaseValueObject()
42
+ local parent
43
+ local tableDefinition = self._definition:GetPropertyTableDefinition()
44
+ if tableDefinition then
45
+ parent = tableDefinition:GetContainer(self._serviceBag, self._adornee)
46
+ else
47
+ parent = self._adornee
48
+ end
49
+
50
+ if not parent then
51
+ return nil
52
+ end
53
+
54
+ if self._roguePropertyService:CanInitializeProperties() then
55
+ return ValueBaseUtils.getOrCreateValue(
56
+ parent,
57
+ self._definition:GetStorageInstanceType(),
58
+ self._definition:GetName(),
59
+ self._definition:GetEncodedDefaultValue())
60
+ else
61
+ return parent:FindFirstChild(self._definition:GetName())
62
+ end
63
+ end
64
+
65
+ function RogueProperty:_observeBaseValueBrio()
66
+ local tableDefinition = self._definition:GetPropertyTableDefinition()
67
+ if tableDefinition then
68
+ return tableDefinition:ObserveContainerBrio(self._serviceBag, self._adornee)
69
+ :Pipe({
70
+ RxBrioUtils.switchMapBrio(function(container)
71
+ return RxInstanceUtils.observeLastNamedChildBrio(
72
+ container,
73
+ self._definition:GetStorageInstanceType(),
74
+ self._definition:GetName())
75
+ end);
76
+ })
77
+ else
78
+ return RxInstanceUtils.observeLastNamedChildBrio(self._adornee, self._definition:GetStorageInstanceType(), self._definition:GetName())
79
+ end
80
+ end
81
+
82
+ function RogueProperty:SetBaseValue(value)
83
+ local baseValue = self:_getBaseValueObject()
84
+ if baseValue then
85
+ baseValue.Value = self:_encodeValue(value)
86
+ else
87
+ warn("Failed to get the baseValue to parent")
88
+ end
89
+ end
90
+
91
+ function RogueProperty:GetValue()
92
+ local propObj = self:_getBaseValueObject()
93
+ if not propObj then
94
+ return self._definition:GetDefaultValue()
95
+ end
96
+
97
+ local current = self:_decodeValue(propObj.Value)
98
+
99
+ for _, item in pairs(self._roguePropertyService:GetProviders()) do
100
+ current = item:GetModifiedVersion(propObj, self, current)
101
+ end
102
+
103
+ return current
104
+ end
105
+
106
+ function RogueProperty:GetDefinition()
107
+ return self._definition
108
+ end
109
+
110
+
111
+ function RogueProperty:ObserveModifiersBrio()
112
+ return self:_observeBaseValueBrio()
113
+ :Pipe({
114
+ RxBrioUtils.flatMapBrio(function(baseValue)
115
+ if baseValue then
116
+ return RxBinderUtils.observeBoundChildClassesBrio(self._roguePropertyBinderGroups.RogueModifiers:GetBinders(), baseValue)
117
+ else
118
+ return Rx.EMPTY
119
+ end
120
+ end);
121
+ })
122
+ end
123
+
124
+ function RogueProperty:ObserveSourcesBrio()
125
+ return self:ObserveModifiersBrio()
126
+ :Pipe({
127
+ RxBrioUtils.flatMapBrio(function(rogueModifier)
128
+ return RoguePropertyModifierUtils.observeSourceLinksBrio(rogueModifier:GetObject())
129
+ end);
130
+ })
131
+ end
132
+
133
+ function RogueProperty:Observe()
134
+ return self._roguePropertyService:ObserveProviderList():Pipe({
135
+ RxBrioUtils.toBrio();
136
+ RxBrioUtils.switchMapBrio(function()
137
+ return self:_observeBaseValueBrio()
138
+ end);
139
+ RxBrioUtils.switchMapBrio(function(baseValue)
140
+ local current
141
+ if baseValue then
142
+ current = RxValueBaseUtils.observeValue(baseValue)
143
+
144
+ if self._definition:GetValueType() == "table" then
145
+ current = current:Pipe({
146
+ Rx.map(function(value)
147
+ return self:_decodeValue(value)
148
+ end)
149
+ })
150
+ end
151
+ else
152
+ current = Rx.of(self._definition:GetDefaultValue())
153
+ end
154
+
155
+ for _, item in pairs(self._roguePropertyService:GetProviders()) do
156
+ current = item:ObserveModifiedVersion(baseValue, self, current)
157
+ end
158
+
159
+ return current
160
+ end);
161
+ RxBrioUtils.emitOnDeath(self._definition:GetDefaultValue());
162
+ })
163
+ end
164
+
165
+ function RogueProperty:CreateMultiplier(amount, source)
166
+ assert(type(amount) == "number", "Bad amount")
167
+
168
+ local provider = self._serviceBag:GetService(RogueMultiplierProvider)
169
+ local baseValue = self:_getBaseValueObject()
170
+
171
+ if not baseValue then
172
+ warn("Failed to get the baseValue to parent")
173
+ end
174
+
175
+ local multiplier = provider:Create(amount, source)
176
+ multiplier.Parent = baseValue
177
+
178
+ return multiplier
179
+ end
180
+
181
+ function RogueProperty:CreateAdditive(amount, source)
182
+ assert(type(amount) == "number", "Bad amount")
183
+
184
+ local provider = self._serviceBag:GetService(RogueAdditiveProvider)
185
+ local baseValue = self:_getBaseValueObject()
186
+
187
+ if not baseValue then
188
+ warn("Failed to get the baseValue to parent")
189
+ end
190
+
191
+ local multiplier = provider:Create(amount, source)
192
+ multiplier.Parent = baseValue
193
+
194
+ return multiplier
195
+ end
196
+
197
+ function RogueProperty:_observeModifiersBrio()
198
+ return RxBinderUtils.observeBoundChildClassesBrio(self._roguePropertyBinderGroups.RogueModifiers:GetAll(), self._adornee)
199
+ end
200
+
201
+ function RogueProperty:__index(index)
202
+ if index == "Value" then
203
+ return self:GetValue()
204
+ elseif RogueProperty[index] then
205
+ return RogueProperty[index]
206
+ else
207
+ error(("Bad index %q"):format(tostring(index)))
208
+ end
209
+ end
210
+
211
+ function RogueProperty:_decodeValue(current)
212
+ return RoguePropertyUtils.decodeProperty(self._definition, current)
213
+ end
214
+
215
+ function RogueProperty:_encodeValue(current)
216
+ return RoguePropertyUtils.encodeProperty(self._definition, current)
217
+ end
218
+
219
+ return RogueProperty
@@ -0,0 +1,39 @@
1
+ --[=[
2
+ @class RoguePropertyUtils
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local JSONUtils = require("JSONUtils")
8
+
9
+ local RoguePropertyUtils = {}
10
+
11
+ function RoguePropertyUtils.decodeProperty(definition, value)
12
+ if definition:GetValueType() == "table" then
13
+ local ok, decoded, err = JSONUtils.jsonDecode(value)
14
+ if not ok then
15
+ warn(("Failed to decode current value of %s. %q"):format(definition:GetName(), tostring(err)))
16
+ return definition:GetDefaultValue()
17
+ end
18
+
19
+ return decoded
20
+ else
21
+ return value
22
+ end
23
+ end
24
+
25
+ function RoguePropertyUtils.encodeProperty(definition, value)
26
+ if definition:GetValueType() == "table" then
27
+ local ok, encoded, err = JSONUtils.jsonEncode(value)
28
+ if not ok then
29
+ warn(("Failed to encode current value of %s. %q"):format(definition:GetName(), tostring(err)))
30
+ return definition:GetEncodedDefaultValue()
31
+ end
32
+
33
+ return encoded
34
+ else
35
+ return value
36
+ end
37
+ end
38
+
39
+ return RoguePropertyUtils
@@ -0,0 +1,13 @@
1
+ --[=[
2
+ @class RogueBindersShared
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local BinderProvider = require("BinderProvider")
8
+ local Binder = require("Binder")
9
+
10
+ return BinderProvider.new(function(self, serviceBag)
11
+ self:Add(Binder.new("RogueMultiplier", require("RogueMultiplier"), serviceBag))
12
+ self:Add(Binder.new("RogueAdditive", require("RogueAdditive"), serviceBag))
13
+ end)
@@ -0,0 +1,17 @@
1
+ --[=[
2
+ @class RoguePropertyBinderGroups
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local BinderGroup = require("BinderGroup")
8
+ local t = require("t")
9
+
10
+ return require("BinderGroupProvider").new(function(self)
11
+ self:Add("RogueModifiers", BinderGroup.new(
12
+ {},
13
+ t.interface({
14
+ GetObject = t.callback;
15
+ })
16
+ ))
17
+ end)
@@ -0,0 +1,69 @@
1
+ --[=[
2
+ @class RoguePropertyService
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local RunService = game:GetService("RunService")
8
+
9
+ local Signal = require("Signal")
10
+ local Maid = require("Maid")
11
+ local Observable = require("Observable")
12
+
13
+ local RoguePropertyService = {}
14
+
15
+ function RoguePropertyService:Init(serviceBag)
16
+ assert(not self._serviceBag, "Already initialized")
17
+ self._serviceBag = assert(serviceBag, "No serviceBag")
18
+
19
+ self._maid = Maid.new()
20
+
21
+ -- Internal
22
+ self._serviceBag:GetService(require("RogueBindersShared"))
23
+ self._roguePropertyBinderGroups = self._serviceBag:GetService(require("RoguePropertyBinderGroups"))
24
+
25
+ self._providers = {}
26
+
27
+ self.ProviderAddedEvent = Signal.new()
28
+ self._maid:GiveTask(self.ProviderAddedEvent)
29
+
30
+ -- Internal providers
31
+ self._serviceBag:GetService(require("RogueAdditiveProvider"))
32
+ self._serviceBag:GetService(require("RogueMultiplierProvider"))
33
+ end
34
+
35
+ function RoguePropertyService:AddProvider(provider)
36
+ self._roguePropertyBinderGroups.RogueModifiers:Add(provider:GetBinder())
37
+
38
+ table.insert(self._providers, provider)
39
+
40
+ self.ProviderAddedEvent:Fire(provider)
41
+ end
42
+
43
+ function RoguePropertyService:GetProviders()
44
+ return self._providers
45
+ end
46
+
47
+ function RoguePropertyService:ObserveProviderList()
48
+ return Observable.new(function(sub)
49
+ local maid = Maid.new()
50
+
51
+ sub:Fire(self._providers)
52
+
53
+ maid:GiveTask(self.ProviderAddedEvent:Connect(function()
54
+ sub:Fire(self._providers)
55
+ end))
56
+
57
+ return maid
58
+ end)
59
+ end
60
+
61
+ function RoguePropertyService:CanInitializeProperties()
62
+ return RunService:IsServer()
63
+ end
64
+
65
+ function RoguePropertyService:Destroy()
66
+ self._maid:DoCleaning()
67
+ end
68
+
69
+ return RoguePropertyService
@@ -0,0 +1,87 @@
1
+ --[=[
2
+ @class RoguePropertyTable
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local RunService = game:GetService("RunService")
8
+
9
+ local RoguePropertyTable = {}
10
+ RoguePropertyTable.ClassName = "RoguePropertyTable"
11
+ RoguePropertyTable.__index = RoguePropertyTable
12
+
13
+ function RoguePropertyTable.new(adornee, serviceBag, roguePropertyTableDefinition)
14
+ local self = setmetatable({}, RoguePropertyTable)
15
+
16
+ self._adornee = assert(adornee, "No roguePropertyTableDefinition")
17
+ self._serviceBag = assert(serviceBag, "No serviceBag")
18
+ self._definition = assert(roguePropertyTableDefinition, "No roguePropertyTableDefinition")
19
+
20
+ self._properties = {}
21
+
22
+ if RunService:IsServer() then
23
+ self:_setup()
24
+ end
25
+
26
+ return self
27
+ end
28
+
29
+ function RoguePropertyTable:ObserveContainerBrio()
30
+ return self._definition:ObserveContainerBrio(self._serviceBag, self._adornee)
31
+ end
32
+
33
+ function RoguePropertyTable:GetContainer()
34
+ return self._definition:GetContainer(self._serviceBag, self._adornee)
35
+ end
36
+
37
+ function RoguePropertyTable:Set(newBaseValues)
38
+ assert(type(newBaseValues) == "table", "Bad newBaseValues")
39
+
40
+ for propertyName, value in pairs(newBaseValues) do
41
+ local rogueProperty = self:GetRogueProperty(propertyName)
42
+ if not rogueProperty then
43
+ error(("Bad property %q"):format(tostring(propertyName)))
44
+ end
45
+
46
+ rogueProperty:SetBaseValue(value)
47
+ end
48
+ end
49
+
50
+ function RoguePropertyTable:_setup()
51
+ for definitionName, _ in pairs(self._definition:GetDefinitionMap()) do
52
+ self:GetRogueProperty(definitionName)
53
+ end
54
+ end
55
+
56
+ function RoguePropertyTable:GetRogueProperty(name)
57
+ -- Caching these things doesn't do a whole lot, but saves on table allocation.
58
+ if self._properties[name] then
59
+ return self._properties[name]
60
+ end
61
+
62
+ local definition = self._definition:GetDefinition(name)
63
+ if definition then
64
+ self._properties[name] = definition:Get(self._serviceBag, self._adornee)
65
+ return self._properties[name]
66
+ else
67
+ return nil
68
+ end
69
+ end
70
+
71
+ function RoguePropertyTable:__index(index)
72
+ assert(type(index) == "string", "Bad index")
73
+
74
+ if RoguePropertyTable[index] then
75
+ return RoguePropertyTable[index]
76
+ elseif type(index) == "string" then
77
+ local property = self:GetRogueProperty(index)
78
+ if not property then
79
+ error(("Bad index %q"):format(tostring(index)))
80
+ end
81
+ return property
82
+ else
83
+ error(("Bad index %q"):format(tostring(index)))
84
+ end
85
+ end
86
+
87
+ return RoguePropertyTable
@@ -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": "RoguePropertiesTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "rogueproperties": {
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,11 @@
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.RoguePropertyService)
8
+
9
+ -- Start game
10
+ serviceBag:Init()
11
+ serviceBag:Start()
@@ -0,0 +1,41 @@
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.rogueproperties)
8
+
9
+ local serviceBag = require(packages.ServiceBag).new()
10
+ serviceBag:GetService(packages.RoguePropertyService)
11
+
12
+ -- Start game
13
+ serviceBag:Init()
14
+ serviceBag:Start()
15
+
16
+ local RoguePropertyTableDefinition = require(packages.RoguePropertyTableDefinition)
17
+ local RoguePropertyDefinition = require(packages.RoguePropertyDefinition)
18
+
19
+ local properties = RoguePropertyTableDefinition.new({
20
+ RoguePropertyDefinition.new("AttackDamage", 30);
21
+ RoguePropertyDefinition.new("AbilityPower", 30);
22
+ })
23
+
24
+ local attackDamage = properties.AttackDamage:Get(serviceBag, workspace)
25
+ local abilityPower = properties.AbilityPower:Get(serviceBag, workspace)
26
+
27
+ attackDamage:Observe():Subscribe(function(value)
28
+ print("Attack damage", value)
29
+ end)
30
+ abilityPower:Observe():Subscribe(function(value)
31
+ print("Ability power", value)
32
+ end)
33
+
34
+ attackDamage:CreateMultiplier(2, workspace)
35
+ attackDamage:CreateAdditive(100, workspace)
36
+
37
+ print(attackDamage:GetValue())
38
+
39
+ attackDamage:ObserveSourcesBrio():Subscribe(function(value)
40
+ print(value:GetValue())
41
+ end)