@platonic-dice/core 2.2.2 → 3.0.2
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/README.md +11 -2
- package/dist/analyseModTest.js +12 -10
- package/dist/analyseTest.js +43 -11
- package/dist/entities/DiceTestConditions.js +174 -0
- package/dist/entities/ModifiedTestConditions.js +30 -41
- package/dist/entities/RollModifier.js +15 -1
- package/dist/entities/TestConditions.js +26 -182
- package/dist/entities/TestConditionsArray.js +87 -0
- package/dist/entities/index.js +250 -39
- package/dist/index.js +42 -25
- package/dist/rollDiceMod.js +30 -17
- package/dist/rollDiceTest.js +68 -0
- package/dist/rollMod.js +3 -4
- package/dist/rollModTest.js +40 -13
- package/dist/rollTest.js +50 -15
- package/dist/utils/determineOutcome.js +35 -7
- package/dist/utils/getArrayEvaluator.js +48 -0
- package/dist/utils/getEvaluator.js +84 -0
- package/dist/utils/index.js +244 -16
- package/dist/utils/outcomeMapper.js +62 -15
- package/dist/utils/testRegistry.js +91 -0
- package/dist/utils/testValidators.js +190 -0
- package/package.json +12 -15
- package/dist/analyseModTest.d.ts +0 -126
- package/dist/analyseModTest.d.ts.map +0 -1
- package/dist/analyseTest.d.ts +0 -101
- package/dist/analyseTest.d.ts.map +0 -1
- package/dist/entities/DieType.d.ts +0 -35
- package/dist/entities/DieType.d.ts.map +0 -1
- package/dist/entities/ModifiedTestConditions.d.ts +0 -89
- package/dist/entities/ModifiedTestConditions.d.ts.map +0 -1
- package/dist/entities/Outcome.d.ts +0 -26
- package/dist/entities/Outcome.d.ts.map +0 -1
- package/dist/entities/RollModifier.d.ts +0 -115
- package/dist/entities/RollModifier.d.ts.map +0 -1
- package/dist/entities/RollType.d.ts +0 -24
- package/dist/entities/RollType.d.ts.map +0 -1
- package/dist/entities/TestConditions.d.ts +0 -97
- package/dist/entities/TestConditions.d.ts.map +0 -1
- package/dist/entities/TestType.d.ts +0 -28
- package/dist/entities/TestType.d.ts.map +0 -1
- package/dist/entities/index.d.ts +0 -19
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/roll.d.ts +0 -66
- package/dist/roll.d.ts.map +0 -1
- package/dist/rollDice.d.ts +0 -48
- package/dist/rollDice.d.ts.map +0 -1
- package/dist/rollDiceMod.d.ts +0 -54
- package/dist/rollDiceMod.d.ts.map +0 -1
- package/dist/rollMod.d.ts +0 -52
- package/dist/rollMod.d.ts.map +0 -1
- package/dist/rollModTest.d.ts +0 -72
- package/dist/rollModTest.d.ts.map +0 -1
- package/dist/rollTest.d.ts +0 -59
- package/dist/rollTest.d.ts.map +0 -1
- package/dist/utils/determineOutcome.d.ts +0 -45
- package/dist/utils/determineOutcome.d.ts.map +0 -1
- package/dist/utils/generateResult.d.ts +0 -30
- package/dist/utils/generateResult.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/outcomeMapper.d.ts +0 -29
- package/dist/utils/outcomeMapper.d.ts.map +0 -1
- package/dist-types.d.ts +0 -70
package/dist/utils/index.js
CHANGED
|
@@ -16,19 +16,247 @@
|
|
|
16
16
|
* // Internal usage only — not part of the public API
|
|
17
17
|
* import { generateDieResult, determineOutcome } from "../utils";
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
19
|
+
// Lazy-loaded per-export getters to avoid circular require during
|
|
20
|
+
// module initialization. Each exported name is resolved on first
|
|
21
|
+
// access and cached locally. Properties are `configurable: true`
|
|
22
|
+
// so test frameworks (e.g., Jest) can spy/mock them.
|
|
23
|
+
|
|
24
|
+
/** @type {typeof import("./determineOutcome").determineOutcome | undefined} */
|
|
25
|
+
let _determineOutcome;
|
|
26
|
+
Object.defineProperty(exports, "determineOutcome", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
get() {
|
|
30
|
+
if (_determineOutcome !== undefined) return _determineOutcome;
|
|
31
|
+
_determineOutcome = require("./determineOutcome.js").determineOutcome;
|
|
32
|
+
return _determineOutcome;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** @type {typeof import("./generateResult").generateResult | undefined} */
|
|
37
|
+
let _generateResult;
|
|
38
|
+
Object.defineProperty(exports, "generateResult", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
get() {
|
|
42
|
+
if (_generateResult !== undefined) return _generateResult;
|
|
43
|
+
_generateResult = require("./generateResult.js").generateResult;
|
|
44
|
+
return _generateResult;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** @type {typeof import("./generateResult").numSides | undefined} */
|
|
49
|
+
let _numSides;
|
|
50
|
+
Object.defineProperty(exports, "numSides", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
get() {
|
|
54
|
+
if (_numSides !== undefined) return _numSides;
|
|
55
|
+
_numSides = require("./generateResult.js").numSides;
|
|
56
|
+
return _numSides;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** @type {typeof import("./outcomeMapper").createOutcomeMap | undefined} */
|
|
61
|
+
let _createOutcomeMap;
|
|
62
|
+
Object.defineProperty(exports, "createOutcomeMap", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
get() {
|
|
66
|
+
if (_createOutcomeMap !== undefined) return _createOutcomeMap;
|
|
67
|
+
_createOutcomeMap = require("./outcomeMapper.js").createOutcomeMap;
|
|
68
|
+
return _createOutcomeMap;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** @type {typeof import("./outcomeMapper").clearOutcomeMapCache | undefined} */
|
|
73
|
+
let _clearOutcomeMapCache;
|
|
74
|
+
Object.defineProperty(exports, "clearOutcomeMapCache", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
get() {
|
|
78
|
+
if (_clearOutcomeMapCache !== undefined) return _clearOutcomeMapCache;
|
|
79
|
+
_clearOutcomeMapCache = require("./outcomeMapper.js").clearOutcomeMapCache;
|
|
80
|
+
return _clearOutcomeMapCache;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** @type {typeof import("./outcomeMapper").getOutcomeMapCacheSize | undefined} */
|
|
85
|
+
let _getOutcomeMapCacheSize;
|
|
86
|
+
Object.defineProperty(exports, "getOutcomeMapCacheSize", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
get() {
|
|
90
|
+
if (_getOutcomeMapCacheSize !== undefined) return _getOutcomeMapCacheSize;
|
|
91
|
+
_getOutcomeMapCacheSize =
|
|
92
|
+
require("./outcomeMapper.js").getOutcomeMapCacheSize;
|
|
93
|
+
return _getOutcomeMapCacheSize;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** @type {typeof import("./getEvaluator").getEvaluator | undefined} */
|
|
98
|
+
let _getEvaluator;
|
|
99
|
+
Object.defineProperty(exports, "getEvaluator", {
|
|
100
|
+
enumerable: true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
get() {
|
|
103
|
+
if (_getEvaluator !== undefined) return _getEvaluator;
|
|
104
|
+
_getEvaluator = require("./getEvaluator.js").getEvaluator;
|
|
105
|
+
return _getEvaluator;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// testRegistry exports
|
|
110
|
+
/** @type {typeof import("./testRegistry").registerTestType | undefined} */
|
|
111
|
+
let _registerTestType;
|
|
112
|
+
Object.defineProperty(exports, "registerTestType", {
|
|
113
|
+
enumerable: true,
|
|
114
|
+
configurable: true,
|
|
115
|
+
get() {
|
|
116
|
+
if (_registerTestType !== undefined) return _registerTestType;
|
|
117
|
+
_registerTestType = require("./testRegistry.js").registerTestType;
|
|
118
|
+
return _registerTestType;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/** @type {typeof import("./testRegistry").getRegistration | undefined} */
|
|
123
|
+
let _getRegistration;
|
|
124
|
+
Object.defineProperty(exports, "getRegistration", {
|
|
125
|
+
enumerable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
get() {
|
|
128
|
+
if (_getRegistration !== undefined) return _getRegistration;
|
|
129
|
+
_getRegistration = require("./testRegistry.js").getRegistration;
|
|
130
|
+
return _getRegistration;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/** @type {typeof import("./testRegistry").registry | undefined} */
|
|
135
|
+
let _registry;
|
|
136
|
+
Object.defineProperty(exports, "registry", {
|
|
137
|
+
enumerable: true,
|
|
138
|
+
configurable: true,
|
|
139
|
+
get() {
|
|
140
|
+
if (_registry !== undefined) return _registry;
|
|
141
|
+
_registry = require("./testRegistry.js").registry;
|
|
142
|
+
return _registry;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// testValidators exports
|
|
147
|
+
/** @type {typeof import("./testValidators").isValidFaceValue | undefined} */
|
|
148
|
+
let _isValidFaceValue;
|
|
149
|
+
Object.defineProperty(exports, "isValidFaceValue", {
|
|
150
|
+
enumerable: true,
|
|
151
|
+
configurable: true,
|
|
152
|
+
get() {
|
|
153
|
+
if (_isValidFaceValue !== undefined) return _isValidFaceValue;
|
|
154
|
+
_isValidFaceValue = require("./testValidators.js").isValidFaceValue;
|
|
155
|
+
return _isValidFaceValue;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
/** @type {typeof import("./testValidators").areValidFaceValues | undefined} */
|
|
160
|
+
let _areValidFaceValues;
|
|
161
|
+
Object.defineProperty(exports, "areValidFaceValues", {
|
|
162
|
+
enumerable: true,
|
|
163
|
+
configurable: true,
|
|
164
|
+
get() {
|
|
165
|
+
if (_areValidFaceValues !== undefined) return _areValidFaceValues;
|
|
166
|
+
_areValidFaceValues = require("./testValidators.js").areValidFaceValues;
|
|
167
|
+
return _areValidFaceValues;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** @type {typeof import("./testValidators").isValidThresholdOrder | undefined} */
|
|
172
|
+
let _isValidThresholdOrder;
|
|
173
|
+
Object.defineProperty(exports, "isValidThresholdOrder", {
|
|
174
|
+
enumerable: true,
|
|
175
|
+
configurable: true,
|
|
176
|
+
get() {
|
|
177
|
+
if (_isValidThresholdOrder !== undefined) return _isValidThresholdOrder;
|
|
178
|
+
_isValidThresholdOrder =
|
|
179
|
+
require("./testValidators.js").isValidThresholdOrder;
|
|
180
|
+
return _isValidThresholdOrder;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/** @type {typeof import("./testValidators").isValidTargetConditions | undefined} */
|
|
185
|
+
let _isValidTargetConditions;
|
|
186
|
+
Object.defineProperty(exports, "isValidTargetConditions", {
|
|
187
|
+
enumerable: true,
|
|
188
|
+
configurable: true,
|
|
189
|
+
get() {
|
|
190
|
+
if (_isValidTargetConditions !== undefined) return _isValidTargetConditions;
|
|
191
|
+
_isValidTargetConditions =
|
|
192
|
+
require("./testValidators.js").isValidTargetConditions;
|
|
193
|
+
return _isValidTargetConditions;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
/** @type {typeof import("./testValidators").isValidSkillTestCondition | undefined} */
|
|
198
|
+
let _isValidSkillTestCondition;
|
|
199
|
+
Object.defineProperty(exports, "isValidSkillTestCondition", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
configurable: true,
|
|
202
|
+
get() {
|
|
203
|
+
if (_isValidSkillTestCondition !== undefined)
|
|
204
|
+
return _isValidSkillTestCondition;
|
|
205
|
+
_isValidSkillTestCondition =
|
|
206
|
+
require("./testValidators.js").isValidSkillTestCondition;
|
|
207
|
+
return _isValidSkillTestCondition;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/** @type {typeof import("./testValidators").isValidWithinConditions | undefined} */
|
|
212
|
+
let _isValidWithinConditions;
|
|
213
|
+
Object.defineProperty(exports, "isValidWithinConditions", {
|
|
214
|
+
enumerable: true,
|
|
215
|
+
configurable: true,
|
|
216
|
+
get() {
|
|
217
|
+
if (_isValidWithinConditions !== undefined) return _isValidWithinConditions;
|
|
218
|
+
_isValidWithinConditions =
|
|
219
|
+
require("./testValidators.js").isValidWithinConditions;
|
|
220
|
+
return _isValidWithinConditions;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
/** @type {typeof import("./testValidators").isValidSpecificListConditions | undefined} */
|
|
225
|
+
let _isValidSpecificListConditions;
|
|
226
|
+
Object.defineProperty(exports, "isValidSpecificListConditions", {
|
|
227
|
+
enumerable: true,
|
|
228
|
+
configurable: true,
|
|
229
|
+
get() {
|
|
230
|
+
if (_isValidSpecificListConditions !== undefined)
|
|
231
|
+
return _isValidSpecificListConditions;
|
|
232
|
+
_isValidSpecificListConditions =
|
|
233
|
+
require("./testValidators.js").isValidSpecificListConditions;
|
|
234
|
+
return _isValidSpecificListConditions;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
/** @type {typeof import("./testValidators").areValidValuesInRange | undefined} */
|
|
239
|
+
let _areValidValuesInRange;
|
|
240
|
+
Object.defineProperty(exports, "areValidValuesInRange", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
configurable: true,
|
|
243
|
+
get() {
|
|
244
|
+
if (_areValidValuesInRange !== undefined) return _areValidValuesInRange;
|
|
245
|
+
_areValidValuesInRange =
|
|
246
|
+
require("./testValidators.js").areValidValuesInRange;
|
|
247
|
+
return _areValidValuesInRange;
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/** @type {typeof import("./testValidators").areValidTestConditions | undefined} */
|
|
252
|
+
let _areValidTestConditions;
|
|
253
|
+
Object.defineProperty(exports, "areValidTestConditions", {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
configurable: true,
|
|
256
|
+
get() {
|
|
257
|
+
if (_areValidTestConditions !== undefined) return _areValidTestConditions;
|
|
258
|
+
_areValidTestConditions =
|
|
259
|
+
require("./testValidators.js").areValidTestConditions;
|
|
260
|
+
return _areValidTestConditions;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module @platonic-dice/core/src/utils/outcomeMapper
|
|
3
3
|
* @description
|
|
4
4
|
* Creates outcome maps for die rolls, handling natural crits and modifiers.
|
|
5
|
-
* This provides a
|
|
5
|
+
* This provides a centralised way to determine all possible outcomes for a
|
|
6
6
|
* given die configuration.
|
|
7
7
|
*
|
|
8
8
|
* Includes memoization cache for performance optimization.
|
|
@@ -41,7 +41,7 @@ function applyNaturalCritOverride(
|
|
|
41
41
|
currentOutcome,
|
|
42
42
|
isNaturalMax,
|
|
43
43
|
isNaturalMin,
|
|
44
|
-
testType
|
|
44
|
+
testType,
|
|
45
45
|
) {
|
|
46
46
|
const { TestType, Outcome } = getEntities();
|
|
47
47
|
|
|
@@ -77,8 +77,8 @@ function applyNaturalCritOverride(
|
|
|
77
77
|
* @param {import("../entities/DieType").DieTypeValue} dieType
|
|
78
78
|
* @param {import("../entities/TestType").TestTypeValue} testType
|
|
79
79
|
* @param {import("../entities/TestConditions").TestConditionsInstance} testConditions
|
|
80
|
-
* @param {import("../entities/RollModifier").RollModifierInstance|
|
|
81
|
-
* @param {boolean} useNaturalCrits
|
|
80
|
+
* @param {import("../entities/RollModifier").RollModifierInstance|undefined} modifier
|
|
81
|
+
* @param {boolean|undefined} useNaturalCrits
|
|
82
82
|
* @returns {string}
|
|
83
83
|
*/
|
|
84
84
|
function createCacheKey(
|
|
@@ -86,9 +86,9 @@ function createCacheKey(
|
|
|
86
86
|
testType,
|
|
87
87
|
testConditions,
|
|
88
88
|
modifier,
|
|
89
|
-
useNaturalCrits
|
|
89
|
+
useNaturalCrits,
|
|
90
90
|
) {
|
|
91
|
-
//
|
|
91
|
+
// serialise the conditions object for hashing
|
|
92
92
|
const conditionsKey = JSON.stringify({
|
|
93
93
|
testType: testConditions.testType,
|
|
94
94
|
conditions: testConditions.conditions,
|
|
@@ -107,22 +107,45 @@ function createCacheKey(
|
|
|
107
107
|
* @param {import("../entities/DieType").DieTypeValue} dieType - The type of die
|
|
108
108
|
* @param {import("../entities/TestType").TestTypeValue} testType - The type of test being performed
|
|
109
109
|
* @param {import("../entities/TestConditions").TestConditionsInstance} testConditions - The test conditions
|
|
110
|
-
* @param {import("../entities/RollModifier").RollModifierInstance|
|
|
111
|
-
* @param {boolean|
|
|
110
|
+
* @param {import("../entities/RollModifier").RollModifierInstance|undefined} modifier - Optional modifier to apply
|
|
111
|
+
* @param {boolean|undefined} useNaturalCrits - Whether to use natural crits (undefined = auto-determine)
|
|
112
112
|
* @returns {Object.<number, import("../entities/Outcome").OutcomeValue>} Map of baseRoll -> outcome
|
|
113
113
|
*/
|
|
114
114
|
function createOutcomeMap(
|
|
115
115
|
dieType,
|
|
116
116
|
testType,
|
|
117
117
|
testConditions,
|
|
118
|
-
modifier =
|
|
119
|
-
useNaturalCrits =
|
|
118
|
+
modifier = undefined,
|
|
119
|
+
useNaturalCrits = undefined,
|
|
120
120
|
) {
|
|
121
121
|
const { TestType } = getEntities();
|
|
122
122
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
123
|
+
// Resolve `useNaturalCrits` with the following precedence:
|
|
124
|
+
// 1. Caller-provided `useNaturalCrits` (boolean)
|
|
125
|
+
// 2. Registry-provided `defaultUseNaturalCrits` (optional boolean)
|
|
126
|
+
// 3. Fallback default: true for Skill tests, false otherwise
|
|
127
|
+
let shouldUseNaturalCrits = useNaturalCrits;
|
|
128
|
+
// Try to get registry metadata (if registry is available) to consult any
|
|
129
|
+
// declared `defaultUseNaturalCrits` for this testType. We swallow errors
|
|
130
|
+
// to remain resilient if the registry cannot be loaded.
|
|
131
|
+
let reg;
|
|
132
|
+
try {
|
|
133
|
+
const tr = require("./testRegistry");
|
|
134
|
+
reg = tr.getRegistration(testType);
|
|
135
|
+
if (
|
|
136
|
+
shouldUseNaturalCrits == null &&
|
|
137
|
+
reg &&
|
|
138
|
+
typeof reg.defaultUseNaturalCrits === "boolean"
|
|
139
|
+
) {
|
|
140
|
+
shouldUseNaturalCrits = reg.defaultUseNaturalCrits;
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// ignore registry errors and fall back to heuristic below
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (shouldUseNaturalCrits == null) {
|
|
147
|
+
shouldUseNaturalCrits = testType === TestType.Skill;
|
|
148
|
+
}
|
|
126
149
|
|
|
127
150
|
// Check cache first
|
|
128
151
|
const cacheKey = createCacheKey(
|
|
@@ -130,13 +153,37 @@ function createOutcomeMap(
|
|
|
130
153
|
testType,
|
|
131
154
|
testConditions,
|
|
132
155
|
modifier,
|
|
133
|
-
shouldUseNaturalCrits
|
|
156
|
+
shouldUseNaturalCrits,
|
|
134
157
|
);
|
|
135
158
|
const cached = outcomeMapCache.get(cacheKey);
|
|
136
159
|
if (cached) {
|
|
137
160
|
return cached;
|
|
138
161
|
}
|
|
139
162
|
|
|
163
|
+
// Prefer registry builder when available to keep behavior consistent
|
|
164
|
+
// with registered evaluators. Fall back to per-base determineOutcome loop.
|
|
165
|
+
try {
|
|
166
|
+
if (reg && typeof reg.buildEvaluator === "function") {
|
|
167
|
+
const evaluator = reg.buildEvaluator(
|
|
168
|
+
dieType,
|
|
169
|
+
testConditions,
|
|
170
|
+
modifier,
|
|
171
|
+
shouldUseNaturalCrits,
|
|
172
|
+
);
|
|
173
|
+
const sides = numSides(dieType);
|
|
174
|
+
/** @type {Object.<number, import("../entities/Outcome").OutcomeValue>} */
|
|
175
|
+
const outcomeMap = {};
|
|
176
|
+
for (let baseRoll = 1; baseRoll <= sides; baseRoll++) {
|
|
177
|
+
outcomeMap[baseRoll] = evaluator(baseRoll);
|
|
178
|
+
}
|
|
179
|
+
outcomeMapCache.set(cacheKey, outcomeMap);
|
|
180
|
+
return outcomeMap;
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// If registry cannot be loaded for any reason, gracefully fall back
|
|
184
|
+
// to the existing per-base logic below.
|
|
185
|
+
}
|
|
186
|
+
|
|
140
187
|
const sides = numSides(dieType);
|
|
141
188
|
/** @type {Object.<number, import("../entities/Outcome").OutcomeValue>} */
|
|
142
189
|
const outcomeMap = {};
|
|
@@ -157,7 +204,7 @@ function createOutcomeMap(
|
|
|
157
204
|
outcome,
|
|
158
205
|
isNaturalMax,
|
|
159
206
|
isNaturalMin,
|
|
160
|
-
testType
|
|
207
|
+
testType,
|
|
161
208
|
);
|
|
162
209
|
}
|
|
163
210
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight registry for test types.
|
|
3
|
+
*
|
|
4
|
+
* Allows future registration of new test types with associated
|
|
5
|
+
* `validateShape` and `buildEvaluator` functions. For now, only
|
|
6
|
+
* `validateShape` is populated using `testValidators`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const validators = require("./testValidators");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import("./testValidators").Conditions} Conditions
|
|
13
|
+
* @typedef {Record<string, unknown>} PlainObject
|
|
14
|
+
* @typedef {Conditions|PlainObject} ConditionsLike
|
|
15
|
+
*
|
|
16
|
+
* Evaluator: function that maps a rolled base value (1..sides) to an OutcomeValue.
|
|
17
|
+
* @typedef {(base: number) => import("../entities/Outcome").OutcomeValue} Evaluator
|
|
18
|
+
*
|
|
19
|
+
* BuildEvaluator: factory that builds an Evaluator for a specific die/conditions.
|
|
20
|
+
* @typedef {(dieType: import("../entities/DieType").DieTypeValue, testConditions: ConditionsLike, modifier?: import("../entities/RollModifier").RollModifierInstance, useNaturalCrits?: boolean) => Evaluator} BuildEvaluator
|
|
21
|
+
*
|
|
22
|
+
* RegistryEntry: describes the shape validator, optional evaluator builder, and
|
|
23
|
+
* optional default for `useNaturalCrits` for that test type.
|
|
24
|
+
* @typedef {{ validateShape: (c: ConditionsLike) => boolean, buildEvaluator?: BuildEvaluator, defaultUseNaturalCrits?: boolean }} RegistryEntry
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Internal registry map: testType -> { validateShape, buildEvaluator? } */
|
|
28
|
+
const registry = new Map();
|
|
29
|
+
|
|
30
|
+
// Populate with built-in test types using validators
|
|
31
|
+
const builtIns = ["at_least", "at_most", "exact", "within", "in_list", "skill"];
|
|
32
|
+
|
|
33
|
+
for (const t of builtIns) {
|
|
34
|
+
registry.set(t, {
|
|
35
|
+
/** @param {ConditionsLike} c */
|
|
36
|
+
validateShape: (c) => validators.areValidTestConditions(c, t),
|
|
37
|
+
// buildEvaluator: returns an evaluator function (base:number)=>outcome
|
|
38
|
+
/**
|
|
39
|
+
* @param {import("../entities/DieType").DieTypeValue} dieType
|
|
40
|
+
* @param {ConditionsLike} testConditions
|
|
41
|
+
* @param {import("../entities/RollModifier").RollModifierInstance} [modifier]
|
|
42
|
+
* @param {boolean} [useNaturalCrits]
|
|
43
|
+
* @returns {(base: number) => import("../entities/Outcome").OutcomeValue}
|
|
44
|
+
*/
|
|
45
|
+
/** @type {import("./testRegistry").BuildEvaluator} */
|
|
46
|
+
buildEvaluator: (
|
|
47
|
+
dieType,
|
|
48
|
+
testConditions,
|
|
49
|
+
modifier = undefined,
|
|
50
|
+
useNaturalCrits = undefined,
|
|
51
|
+
) => {
|
|
52
|
+
// require lazily to avoid circular requires at module init
|
|
53
|
+
const { createOutcomeMap } = require("./outcomeMapper");
|
|
54
|
+
// t is a string matching TestType values; TS/JSDoc will accept via runtime checks
|
|
55
|
+
const outcomeMap = createOutcomeMap(
|
|
56
|
+
dieType,
|
|
57
|
+
/** @type {import("../entities/TestType").TestTypeValue} */ (t),
|
|
58
|
+
/** @type {any} */ (testConditions),
|
|
59
|
+
modifier,
|
|
60
|
+
useNaturalCrits,
|
|
61
|
+
);
|
|
62
|
+
return /** @param {number} base */ (base) => outcomeMap[base];
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register a new test type.
|
|
69
|
+
* @param {string} name
|
|
70
|
+
* @param {{ validateShape: (c: ConditionsLike) => boolean, buildEvaluator?: Function }} opts
|
|
71
|
+
*/
|
|
72
|
+
function registerTestType(name, { validateShape, buildEvaluator = undefined }) {
|
|
73
|
+
if (!name || typeof name !== "string") {
|
|
74
|
+
throw new TypeError("test type name must be a non-empty string");
|
|
75
|
+
}
|
|
76
|
+
registry.set(name, { validateShape, buildEvaluator });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} name
|
|
81
|
+
* @returns {RegistryEntry|undefined}
|
|
82
|
+
*/
|
|
83
|
+
function getRegistration(name) {
|
|
84
|
+
return registry.get(name) || undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
registerTestType,
|
|
89
|
+
getRegistration,
|
|
90
|
+
registry, // exported for introspection in tests/tools
|
|
91
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { isValidDieType } = require("../entities/DieType");
|
|
2
|
+
const { numSides } = require("./generateResult.js");
|
|
3
|
+
|
|
4
|
+
/* Typedef ownership:
|
|
5
|
+
* - `PlainObject`, `Conditions`, `ConditionsLike`
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("../entities/TestType").TestTypeValue} TestTypeValue
|
|
10
|
+
* @typedef {import("../entities/DieType").DieTypeValue} DieTypeValue
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Object} BaseTestCondition
|
|
13
|
+
* @property {DieTypeValue} dieType
|
|
14
|
+
*
|
|
15
|
+
* @typedef {BaseTestCondition & { target: number }} TargetConditions
|
|
16
|
+
* @typedef {BaseTestCondition & { min: number, max: number }} WithinConditions
|
|
17
|
+
* @typedef {BaseTestCondition & { values: number[] }} SpecificListConditions
|
|
18
|
+
* @typedef {BaseTestCondition & { target: number, critical_success?: number, critical_failure?: number }} SkillConditions
|
|
19
|
+
*
|
|
20
|
+
* @typedef {TargetConditions | SkillConditions | WithinConditions | SpecificListConditions} Conditions
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Record<string, any>} PlainObject
|
|
23
|
+
* @typedef {Conditions|PlainObject} ConditionsLike
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a number is a valid face value for a die with the given sides.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* @param {number} n
|
|
31
|
+
* @param {number} sides
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isValidFaceValue(n, sides) {
|
|
35
|
+
return Number.isInteger(n) && n >= 1 && n <= sides;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Checks multiple keys in an object for valid face values.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @template T
|
|
43
|
+
* @param {T} obj
|
|
44
|
+
* @param {number} sides
|
|
45
|
+
* @param {(keyof T)[]} keys
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function areValidFaceValues(obj, sides, keys) {
|
|
49
|
+
return keys.every((key) => {
|
|
50
|
+
const value = obj[key];
|
|
51
|
+
return (
|
|
52
|
+
value == null || isValidFaceValue(/** @type {number} */ (value), sides)
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates the ordering of target and critical thresholds.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* @param {SkillConditions|PlainObject} thresholds
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function isValidThresholdOrder({ target, critical_success, critical_failure }) {
|
|
65
|
+
if (critical_failure != null && critical_failure >= target) return false;
|
|
66
|
+
if (critical_success != null && critical_success < target) return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {TargetConditions|PlainObject} c
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
function isValidTargetConditions(c) {
|
|
75
|
+
if (!c || !isValidDieType(c.dieType)) return false;
|
|
76
|
+
return isValidFaceValue(c.target, numSides(c.dieType));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {SkillConditions|PlainObject} c
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isValidSkillTestCondition(c) {
|
|
84
|
+
if (!c || !isValidDieType(c.dieType)) return false;
|
|
85
|
+
const sides = numSides(c.dieType);
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
!areValidFaceValues(c, sides, [
|
|
89
|
+
"target",
|
|
90
|
+
"critical_success",
|
|
91
|
+
"critical_failure",
|
|
92
|
+
])
|
|
93
|
+
)
|
|
94
|
+
return false;
|
|
95
|
+
if (!isValidThresholdOrder(c)) return false;
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {WithinConditions|PlainObject} c
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isValidWithinConditions(c) {
|
|
105
|
+
if (!c || !isValidDieType(c.dieType)) return false;
|
|
106
|
+
const sides = numSides(c.dieType);
|
|
107
|
+
|
|
108
|
+
if (!areValidFaceValues(c, sides, ["min", "max"])) return false;
|
|
109
|
+
if (c.min > c.max) return false;
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {SpecificListConditions|PlainObject} c
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function isValidSpecificListConditions(c) {
|
|
119
|
+
if (!c || !isValidDieType(c.dieType)) return false;
|
|
120
|
+
const sides = numSides(c.dieType);
|
|
121
|
+
if (!Array.isArray(c.values) || c.values.length === 0) return false;
|
|
122
|
+
return c.values.every((v) => isValidFaceValue(v, sides));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Validates that the given keys on an object are integer values within
|
|
127
|
+
* an explicit inclusive range `[min, max]`.
|
|
128
|
+
*
|
|
129
|
+
* This is useful for modified-range validation where the permissible
|
|
130
|
+
* faces aren't 1..sides but an arbitrary min..max after applying modifiers.
|
|
131
|
+
*
|
|
132
|
+
* @param {PlainObject} obj
|
|
133
|
+
* @param {number} min
|
|
134
|
+
* @param {number} max
|
|
135
|
+
* @param {(string)[]} keys
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
function areValidValuesInRange(obj, min, max, keys) {
|
|
139
|
+
if (!obj) return false;
|
|
140
|
+
if (!Number.isInteger(min) || !Number.isInteger(max) || min > max)
|
|
141
|
+
return false;
|
|
142
|
+
return keys.every((key) => {
|
|
143
|
+
const v = obj[key];
|
|
144
|
+
if (v == null) return true;
|
|
145
|
+
if (Array.isArray(v)) {
|
|
146
|
+
if (v.length === 0) return false;
|
|
147
|
+
return v.every(
|
|
148
|
+
(item) => Number.isInteger(item) && item >= min && item <= max
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return Number.isInteger(v) && v >= min && v <= max;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Master validation function for all test conditions.
|
|
157
|
+
*/
|
|
158
|
+
/**
|
|
159
|
+
* @param {Conditions|PlainObject} c
|
|
160
|
+
* @param {TestTypeValue|string} testType
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function areValidTestConditions(c, testType) {
|
|
164
|
+
switch (testType) {
|
|
165
|
+
case "at_least":
|
|
166
|
+
case "at_most":
|
|
167
|
+
case "exact":
|
|
168
|
+
return isValidTargetConditions(c);
|
|
169
|
+
case "within":
|
|
170
|
+
return isValidWithinConditions(c);
|
|
171
|
+
case "in_list":
|
|
172
|
+
return isValidSpecificListConditions(c);
|
|
173
|
+
case "skill":
|
|
174
|
+
return isValidSkillTestCondition(c);
|
|
175
|
+
default:
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
isValidFaceValue,
|
|
182
|
+
areValidFaceValues,
|
|
183
|
+
isValidThresholdOrder,
|
|
184
|
+
isValidTargetConditions,
|
|
185
|
+
isValidSkillTestCondition,
|
|
186
|
+
isValidWithinConditions,
|
|
187
|
+
isValidSpecificListConditions,
|
|
188
|
+
areValidTestConditions,
|
|
189
|
+
areValidValuesInRange,
|
|
190
|
+
};
|