@platonic-dice/core 2.1.2 → 3.0.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.
Files changed (66) hide show
  1. package/README.md +11 -2
  2. package/dist/analyseModTest.js +12 -10
  3. package/dist/analyseTest.js +43 -11
  4. package/dist/entities/DiceTestConditions.js +174 -0
  5. package/dist/entities/ModifiedTestConditions.js +30 -41
  6. package/dist/entities/RollModifier.js +15 -1
  7. package/dist/entities/TestConditions.js +26 -182
  8. package/dist/entities/TestConditionsArray.js +87 -0
  9. package/dist/entities/index.js +250 -39
  10. package/dist/index.js +42 -25
  11. package/dist/rollDiceMod.js +30 -17
  12. package/dist/rollDiceTest.js +68 -0
  13. package/dist/rollMod.js +3 -4
  14. package/dist/rollModTest.js +40 -13
  15. package/dist/rollTest.js +50 -15
  16. package/dist/utils/determineOutcome.js +35 -7
  17. package/dist/utils/getArrayEvaluator.js +48 -0
  18. package/dist/utils/getEvaluator.js +84 -0
  19. package/dist/utils/index.js +244 -16
  20. package/dist/utils/outcomeMapper.js +62 -15
  21. package/dist/utils/testRegistry.js +91 -0
  22. package/dist/utils/testValidators.js +190 -0
  23. package/package.json +15 -17
  24. package/dist/analyseModTest.d.ts +0 -126
  25. package/dist/analyseModTest.d.ts.map +0 -1
  26. package/dist/analyseTest.d.ts +0 -101
  27. package/dist/analyseTest.d.ts.map +0 -1
  28. package/dist/entities/DieType.d.ts +0 -35
  29. package/dist/entities/DieType.d.ts.map +0 -1
  30. package/dist/entities/ModifiedTestConditions.d.ts +0 -89
  31. package/dist/entities/ModifiedTestConditions.d.ts.map +0 -1
  32. package/dist/entities/Outcome.d.ts +0 -26
  33. package/dist/entities/Outcome.d.ts.map +0 -1
  34. package/dist/entities/RollModifier.d.ts +0 -115
  35. package/dist/entities/RollModifier.d.ts.map +0 -1
  36. package/dist/entities/RollType.d.ts +0 -24
  37. package/dist/entities/RollType.d.ts.map +0 -1
  38. package/dist/entities/TestConditions.d.ts +0 -97
  39. package/dist/entities/TestConditions.d.ts.map +0 -1
  40. package/dist/entities/TestType.d.ts +0 -28
  41. package/dist/entities/TestType.d.ts.map +0 -1
  42. package/dist/entities/index.d.ts +0 -19
  43. package/dist/entities/index.d.ts.map +0 -1
  44. package/dist/index.d.ts +0 -5
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/roll.d.ts +0 -66
  47. package/dist/roll.d.ts.map +0 -1
  48. package/dist/rollDice.d.ts +0 -48
  49. package/dist/rollDice.d.ts.map +0 -1
  50. package/dist/rollDiceMod.d.ts +0 -54
  51. package/dist/rollDiceMod.d.ts.map +0 -1
  52. package/dist/rollMod.d.ts +0 -52
  53. package/dist/rollMod.d.ts.map +0 -1
  54. package/dist/rollModTest.d.ts +0 -72
  55. package/dist/rollModTest.d.ts.map +0 -1
  56. package/dist/rollTest.d.ts +0 -59
  57. package/dist/rollTest.d.ts.map +0 -1
  58. package/dist/utils/determineOutcome.d.ts +0 -45
  59. package/dist/utils/determineOutcome.d.ts.map +0 -1
  60. package/dist/utils/generateResult.d.ts +0 -30
  61. package/dist/utils/generateResult.d.ts.map +0 -1
  62. package/dist/utils/index.d.ts +0 -8
  63. package/dist/utils/index.d.ts.map +0 -1
  64. package/dist/utils/outcomeMapper.d.ts +0 -29
  65. package/dist/utils/outcomeMapper.d.ts.map +0 -1
  66. package/dist-types.d.ts +0 -70
package/README.md CHANGED
@@ -9,7 +9,7 @@ This package contains the pure logic used by higher-level packages (for example
9
9
  Install from npm:
10
10
 
11
11
  ```bash
12
- npm install @platonic-dice/core
12
+ npm install @platonic-dice/core @types/platonic-dice__core
13
13
  ```
14
14
 
15
15
  ## Quick usage
@@ -34,7 +34,7 @@ const result = rollModTest(DieType.D20, (n) => n + 5, {
34
34
  target: 15,
35
35
  });
36
36
  console.log(
37
- `Roll: ${result.base}, Modified: ${result.modified}, Outcome: ${result.outcome}`
37
+ `Roll: ${result.base}, Modified: ${result.modified}, Outcome: ${result.outcome}`,
38
38
  );
39
39
  ```
40
40
 
@@ -69,6 +69,15 @@ cd packages/core
69
69
  npm test
70
70
  ```
71
71
 
72
+ ### Type Testing
73
+
74
+ Type definitions are provided by the separate `@types/platonic-dice__core` package. To test the type surface:
75
+
76
+ ```bash
77
+ cd packages/types-core
78
+ pnpm run test:types
79
+ ```
80
+
72
81
  ## Examples
73
82
 
74
83
  The `examples/` directory contains comprehensive examples for all major functions. Run them to see the library in action:
@@ -21,12 +21,12 @@ const { normaliseRollModifier } = require("./entities");
21
21
  const { ModifiedTestConditions } = require("./entities/ModifiedTestConditions");
22
22
  const { createOutcomeMap } = require("./utils/outcomeMapper");
23
23
  const { numSides } = require("./utils");
24
+ const { getEvaluator } = require("./utils/getEvaluator");
24
25
 
25
26
  /**
26
27
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
27
28
  * @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
28
- * @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
29
- * @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
29
+ * @typedef {import("./entities/RollModifier").RollModifierLike} RollModifierLike
30
30
  * @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
31
31
  * @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
32
32
  */
@@ -54,8 +54,9 @@ const { numSides } = require("./utils");
54
54
  *
55
55
  * @function analyseModTest
56
56
  * @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
57
- * @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply to the roll.
58
- * @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
57
+ * @param {RollModifierLike} modifier - The modifier to apply to the roll.
58
+ * @typedef {import("./entities/TestConditions").TestConditionsLike} TestConditionsLike
59
+ * @param {TestConditionsLike} testConditions
59
60
  * Can be:
60
61
  * - A `TestConditions` instance
61
62
  * - A plain object `{ testType, ...conditions }`
@@ -98,7 +99,7 @@ function analyseModTest(dieType, modifier, testConditions, options = {}) {
98
99
  const mod =
99
100
  modifier instanceof RollModifier
100
101
  ? modifier
101
- : normaliseRollModifier(modifier);
102
+ : normaliseRollModifier(/** @type {any} */ (modifier));
102
103
 
103
104
  // Create ModifiedTestConditions if input is a plain object
104
105
  let conditionSet;
@@ -111,17 +112,18 @@ function analyseModTest(dieType, modifier, testConditions, options = {}) {
111
112
  conditionSet = new ModifiedTestConditions(testType, rest, dieType, mod);
112
113
  }
113
114
 
114
- // Create outcome map for all possible rolls (with modifier applied)
115
- const outcomeMap = createOutcomeMap(
115
+ // Obtain evaluator (registry or fallback) and build outcome map
116
+ const evaluator = getEvaluator(
116
117
  dieType,
117
- conditionSet.testType,
118
118
  // @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
119
119
  conditionSet,
120
- mod, // include modifier
120
+ mod,
121
121
  options.useNaturalCrits
122
122
  );
123
-
124
123
  const sides = numSides(dieType);
124
+ /** @type {Object.<number, OutcomeValue>} */
125
+ const outcomeMap = {};
126
+ for (let roll = 1; roll <= sides; roll++) outcomeMap[roll] = evaluator(roll);
125
127
  const totalPossibilities = sides;
126
128
 
127
129
  // Calculate modified values for each roll
@@ -22,6 +22,7 @@ const { DieType, TestType } = require("./entities");
22
22
  const tc = require("./entities/TestConditions.js");
23
23
  const { createOutcomeMap } = require("./utils/outcomeMapper");
24
24
  const { numSides } = require("./utils");
25
+ const { getEvaluator } = require("./utils/getEvaluator");
25
26
 
26
27
  /**
27
28
  * @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
@@ -51,7 +52,8 @@ const { numSides } = require("./utils");
51
52
  *
52
53
  * @function analyseTest
53
54
  * @param {DieTypeValue} dieType - The type of die (e.g., `DieType.D20`).
54
- * @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
55
+ * @typedef {import("./entities/TestConditions").TestConditionsLike} TestConditionsLike
56
+ * @param {TestConditionsLike} testConditions
55
57
  * Can be:
56
58
  * - A `TestConditions` instance.
57
59
  * - A plain object `{ testType, ...conditions }`.
@@ -83,21 +85,51 @@ function analyseTest(dieType, testConditions, options = {}) {
83
85
  if (!dieType) throw new TypeError("dieType is required.");
84
86
 
85
87
  // Normalise testConditions (skip if already a TestConditions instance)
86
- const conditionSet =
87
- testConditions instanceof tc.TestConditions
88
- ? testConditions
89
- : tc.normaliseTestConditions(testConditions, dieType);
88
+ let conditionSet;
89
+ if (testConditions instanceof tc.TestConditions) {
90
+ conditionSet = testConditions;
91
+ } else {
92
+ // Plain object: validate early for clearer errors, then normalise.
93
+ // We still call `normaliseTestConditions` so tests and any instrumentation
94
+ // that spy on it will observe the delegation (and the constructor remains
95
+ // the final authority for detailed RangeErrors).
96
+ const { testType, ...rest } = testConditions;
97
+ const fullConditions = { ...rest, dieType };
98
+ const validators = require("./utils/testValidators");
99
+ const { isValidTestType } = require("./entities/TestType");
90
100
 
91
- // Create outcome map for all possible rolls
92
- const outcomeMap = createOutcomeMap(
101
+ if (!isValidTestType(testType)) {
102
+ try {
103
+ tc.normaliseTestConditions(testConditions, dieType);
104
+ } catch (err) {
105
+ // ignore deeper error
106
+ }
107
+ throw new TypeError(`Invalid test type: ${testType}`);
108
+ }
109
+
110
+ if (!validators.areValidTestConditions(fullConditions, testType)) {
111
+ try {
112
+ tc.normaliseTestConditions(testConditions, dieType);
113
+ } catch (err) {
114
+ // ignore deeper error
115
+ }
116
+ throw new TypeError("Invalid test conditions shape.");
117
+ }
118
+
119
+ conditionSet = tc.normaliseTestConditions(testConditions, dieType);
120
+ }
121
+
122
+ // Obtain evaluator (registry or fallback) and build outcome map
123
+ const evaluator = getEvaluator(
93
124
  dieType,
94
- conditionSet.testType,
95
125
  conditionSet,
96
- null, // no modifier
97
- options.useNaturalCrits
126
+ undefined,
127
+ options.useNaturalCrits,
98
128
  );
99
-
100
129
  const sides = numSides(dieType);
130
+ /** @type {Object.<number, OutcomeValue>} */
131
+ const outcomeMap = {};
132
+ for (let roll = 1; roll <= sides; roll++) outcomeMap[roll] = evaluator(roll);
101
133
  const totalPossibilities = sides;
102
134
 
103
135
  // Count outcomes
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @module @platonic-dice/core/src/entities/DiceTestConditions
3
+ * @description
4
+ * Aggregates `TestConditionsArray` evaluations across multiple dice and
5
+ * applies simple aggregation rules such as "at least N dice equal X" or
6
+ * "at least N dice satisfy condition #k".
7
+ */
8
+
9
+ const { TestConditionsArray } = require("./TestConditionsArray");
10
+ const { getArrayEvaluator } = require("../utils/getArrayEvaluator");
11
+ const { Outcome } = require("./Outcome");
12
+ const { DieType } = require("./DieType");
13
+
14
+ /**
15
+ * @typedef {import("./TestConditions").TestConditionsLike} TestConditionsLike
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} Rule
20
+ * @property {"value_count"|"condition_count"} type
21
+ * @property {number} [value]
22
+ * @property {number} [conditionIndex]
23
+ * @property {number} [exact]
24
+ * @property {number} [atLeast]
25
+ * @property {number} [atMost]
26
+ */
27
+
28
+ class DiceTestConditions {
29
+ /**
30
+ * @param {{ count?: number, conditions?: TestConditionsLike[]|TestConditionsArray, rules?: Rule[], dieType?: string }} [opts]
31
+ */
32
+ constructor(opts = {}) {
33
+ const { count, conditions, rules = [], dieType = undefined } = opts;
34
+ if (typeof count !== "number" || !Number.isInteger(count) || count < 1) {
35
+ throw new TypeError("count must be a positive integer");
36
+ }
37
+ this.count = count;
38
+
39
+ // Normalise conditions into TestConditionsArray
40
+ if (conditions instanceof TestConditionsArray) {
41
+ this.tcArray = conditions;
42
+ } else if (Array.isArray(conditions)) {
43
+ // If caller didn't supply a default die type, assume D6 for value-based
44
+ // multi-dice tests (most common simple use-case in tests).
45
+ const defaultDie = dieType || DieType.D6;
46
+ this.tcArray = new TestConditionsArray(
47
+ conditions,
48
+ /** @type {any} */ (defaultDie),
49
+ );
50
+ } else {
51
+ throw new TypeError("conditions must be an array or TestConditionsArray");
52
+ }
53
+
54
+ // Rules are simple objects. We'll validate minimally here.
55
+ this.rules = rules.map((r, idx) => {
56
+ if (!r || typeof r !== "object")
57
+ throw new TypeError(`rule ${idx} must be an object`);
58
+ // Ensure required shape
59
+ if (r.type !== "value_count" && r.type !== "condition_count")
60
+ throw new TypeError(`unsupported rule type: ${r.type}`);
61
+ return /** @type {Rule} */ (r);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Returns an evaluator function that accepts an array of rolled values
67
+ * and returns an aggregated result object.
68
+ *
69
+ * @param {import("./RollModifier").RollModifierInstance} [modifier]
70
+ * @param {boolean} [useNaturalCrits]
71
+ * @returns {(rolls: number[]) => Object}
72
+ */
73
+ toEvaluator(modifier = undefined, useNaturalCrits = undefined) {
74
+ const tcArray = this.tcArray;
75
+ const arrayEvaluator = getArrayEvaluator(
76
+ tcArray,
77
+ modifier,
78
+ useNaturalCrits,
79
+ );
80
+ const expectedCount = this.count;
81
+ const rules = this.rules;
82
+
83
+ return /** @param {number[]} rolls */ (rolls) => {
84
+ if (!Array.isArray(rolls))
85
+ throw new TypeError("rolls must be an array of numbers");
86
+ if (rolls.length !== expectedCount)
87
+ throw new TypeError(
88
+ `rolls length ${rolls.length} does not match expected count ${expectedCount}`,
89
+ );
90
+
91
+ // Build matrix: for each die -> array of outcomes per condition
92
+ const matrix = rolls.map((v) => arrayEvaluator(v));
93
+
94
+ // Count successes per condition (treat success or critical_success as match)
95
+ /** @type {{[k:number]: number}} */
96
+ const condCount = {};
97
+ for (let i = 0; i < tcArray.toArray().length; i++) condCount[i] = 0;
98
+
99
+ matrix.forEach((outcomes) => {
100
+ outcomes.forEach((o, condIdx) => {
101
+ if (o === Outcome.Success || o === Outcome.CriticalSuccess)
102
+ condCount[condIdx]++;
103
+ });
104
+ });
105
+
106
+ // Count literal values matches (raw equality)
107
+ /** @type {{[k:number]: number}} */
108
+ const valueCounts = {};
109
+ rolls.forEach((v) => {
110
+ valueCounts[v] = (valueCounts[v] || 0) + 1;
111
+ });
112
+
113
+ // Evaluate rules
114
+ const ruleResults = rules.map((r, idx) => {
115
+ if (r.type === "value_count") {
116
+ const val = /** @type {number|undefined} */ (r.value);
117
+ const cnt = val == null ? 0 : valueCounts[val] || 0;
118
+ const passed = _checkThreshold(cnt, r);
119
+ return { id: idx, rule: r, count: cnt, passed };
120
+ }
121
+
122
+ // condition_count
123
+ if (r.type === "condition_count") {
124
+ const ci = /** @type {number|undefined} */ (r.conditionIndex);
125
+ if (typeof ci !== "number" || !(ci in condCount)) {
126
+ throw new TypeError(`invalid conditionIndex in rule ${idx}`);
127
+ }
128
+ const cnt = condCount[ci] || 0;
129
+ const passed = _checkThreshold(cnt, r);
130
+ return { id: idx, rule: r, count: cnt, passed };
131
+ }
132
+ // unreachable
133
+ return { id: idx, rule: r, passed: false };
134
+ });
135
+
136
+ const passed = ruleResults.every((rr) => rr.passed);
137
+
138
+ return {
139
+ matrix,
140
+ condCount,
141
+ valueCounts,
142
+ ruleResults,
143
+ passed,
144
+ };
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Convenience: evaluate immediately against provided rolls
150
+ * @param {number[]} rolls
151
+ * @param {import("./RollModifier").RollModifierInstance|undefined} [modifier=undefined]
152
+ * @param {boolean|undefined} [useNaturalCrits=undefined]
153
+ */
154
+ evaluateRolls(rolls, modifier = undefined, useNaturalCrits = undefined) {
155
+ return this.toEvaluator(modifier, useNaturalCrits)(rolls);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * @param {number} count
161
+ * @param {Rule} rule
162
+ * @returns {boolean}
163
+ */
164
+ function _checkThreshold(count, rule) {
165
+ if (rule.exact != null) return count === rule.exact;
166
+ if (rule.atLeast != null) return count >= rule.atLeast;
167
+ if (rule.atMost != null) return count <= rule.atMost;
168
+ // default: require atLeast 1 if nothing provided
169
+ return count >= 1;
170
+ }
171
+
172
+ module.exports = {
173
+ DiceTestConditions,
174
+ };
@@ -21,12 +21,18 @@ const { isValidDieType } = require("./DieType");
21
21
  const { isValidTestType } = require("./TestType");
22
22
  const { normaliseRollModifier } = require("./RollModifier");
23
23
  const { numSides } = require("../utils");
24
+ const validators = require("../utils/testValidators");
24
25
 
25
26
  /**
26
27
  * @typedef {import("./TestType").TestTypeValue} TestTypeValue
27
28
  * @typedef {import("./DieType").DieTypeValue} DieTypeValue
28
29
  * @typedef {import("./RollModifier").RollModifierFunction} RollModifierFunction
29
30
  * @typedef {import("./RollModifier").RollModifierInstance} RollModifierInstance
31
+ * @typedef {import("./RollModifier").RollModifierLike} RollModifierLike
32
+ */
33
+ /**
34
+ * @typedef {import("../utils/testValidators").Conditions} Conditions
35
+ * @typedef {import("../utils/testValidators").PlainObject} PlainObject
30
36
  */
31
37
 
32
38
  /**
@@ -64,7 +70,7 @@ class ModifiedTestConditions {
64
70
  * @param {TestTypeValue} testType - The test type.
65
71
  * @param {Conditions} conditions - The test conditions object.
66
72
  * @param {DieTypeValue} dieType - The base die type.
67
- * @param {RollModifierFunction | RollModifierInstance} modifier - The modifier to apply.
73
+ * @param {RollModifierLike} modifier - The modifier to apply.
68
74
  * @throws {TypeError|RangeError} If the test type or conditions are invalid.
69
75
  */
70
76
  constructor(testType, conditions, dieType, modifier) {
@@ -84,8 +90,8 @@ class ModifiedTestConditions {
84
90
  throw new TypeError("modifier is required.");
85
91
  }
86
92
 
87
- // Normalize the modifier
88
- const mod = normaliseRollModifier(modifier);
93
+ // normalise the modifier
94
+ const mod = normaliseRollModifier(/** @type {any} */ (modifier));
89
95
 
90
96
  // Compute the achievable range with this modifier
91
97
  const range = computeModifiedRange(dieType, mod);
@@ -153,7 +159,7 @@ class ModifiedTestConditions {
153
159
  * Validates test conditions against a modified range.
154
160
  *
155
161
  * @private
156
- * @param {Conditions & Record<string, any>} c - Conditions with modifiedRange
162
+ * @param {ModifiedConditions & PlainObject} c - Conditions with modifiedRange
157
163
  * @param {TestTypeValue} testType
158
164
  * @returns {boolean}
159
165
  */
@@ -185,13 +191,12 @@ function areValidModifiedTestConditions(c, testType) {
185
191
  );
186
192
 
187
193
  case "in_list":
194
+ // Use shared validator helper for range checks (accepts arrays)
188
195
  return (
189
196
  Array.isArray(c.values) &&
190
- c.values.length > 0 &&
191
- c.values.every(
192
- (v) =>
193
- typeof v === "number" && Number.isInteger(v) && v >= min && v <= max
194
- )
197
+ validators.areValidValuesInRange({ values: c.values }, min, max, [
198
+ "values",
199
+ ])
195
200
  );
196
201
 
197
202
  case "skill":
@@ -205,33 +210,17 @@ function areValidModifiedTestConditions(c, testType) {
205
210
  return false;
206
211
  }
207
212
 
208
- // critical_success is optional but must be valid if present
209
- if (c.critical_success !== undefined) {
210
- if (
211
- typeof c.critical_success !== "number" ||
212
- !Number.isInteger(c.critical_success) ||
213
- c.critical_success < min ||
214
- c.critical_success > max ||
215
- c.critical_success < c.target
216
- ) {
217
- return false;
218
- }
219
- }
220
-
221
- // critical_failure is optional but must be valid if present
222
- if (c.critical_failure !== undefined) {
223
- if (
224
- typeof c.critical_failure !== "number" ||
225
- !Number.isInteger(c.critical_failure) ||
226
- c.critical_failure < min ||
227
- c.critical_failure > max ||
228
- c.critical_failure > c.target
229
- ) {
230
- return false;
231
- }
232
- }
213
+ // Validate critical thresholds are within range if present using helper
214
+ if (
215
+ !validators.areValidValuesInRange(c, min, max, [
216
+ "critical_success",
217
+ "critical_failure",
218
+ ])
219
+ )
220
+ return false;
233
221
 
234
- return true;
222
+ // Check logical ordering using shared helper
223
+ return validators.isValidThresholdOrder(c);
235
224
 
236
225
  default:
237
226
  return false;
@@ -239,19 +228,19 @@ function areValidModifiedTestConditions(c, testType) {
239
228
  }
240
229
 
241
230
  /**
242
- * @typedef {Object} BaseTestCondition
231
+ * @typedef {Object} ModifiedBaseTestCondition
243
232
  * @property {{ min: number, max: number }} modifiedRange
244
233
  */
245
234
 
246
235
  /**
247
- * @typedef {BaseTestCondition & { target: number }} TargetConditions
248
- * @typedef {BaseTestCondition & { min: number, max: number }} WithinConditions
249
- * @typedef {BaseTestCondition & { values: number[] }} SpecificListConditions
250
- * @typedef {BaseTestCondition & { target: number, critical_success?: number, critical_failure?: number }} SkillConditions
236
+ * @typedef {ModifiedBaseTestCondition & { target: number }} ModifiedTargetConditions
237
+ * @typedef {ModifiedBaseTestCondition & { min: number, max: number }} ModifiedWithinConditions
238
+ * @typedef {ModifiedBaseTestCondition & { values: number[] }} ModifiedSpecificListConditions
239
+ * @typedef {ModifiedBaseTestCondition & { target: number, critical_success?: number, critical_failure?: number }} ModifiedSkillConditions
251
240
  */
252
241
 
253
242
  /**
254
- * @typedef {TargetConditions | SkillConditions | WithinConditions | SpecificListConditions} Conditions
243
+ * @typedef {ModifiedTargetConditions | ModifiedSkillConditions | ModifiedWithinConditions | ModifiedSpecificListConditions} ModifiedConditions
255
244
  */
256
245
 
257
246
  /**
@@ -11,6 +11,11 @@
11
11
  * const result = bonus.apply(10); // 12
12
12
  */
13
13
 
14
+ /*
15
+ * Typedef ownership:
16
+ * - `RollModifierLike`, `RollModifierFunction`, `DiceModifier`, `RollModifierInstance`
17
+ */
18
+
14
19
  /**
15
20
  * @typedef {(n: number) => number} RollModifierFunction
16
21
  * @description
@@ -54,6 +59,15 @@
54
59
  /**
55
60
  * Represents a numeric modifier applied to dice rolls.
56
61
  */
62
+
63
+ /**
64
+ * Public 'like' type for modifiers accepted across APIs.
65
+ * - a `RollModifier` instance
66
+ * - a plain function `(n:number)=>number`
67
+ * - a composite `DiceModifier` object with optional `each`/`net`
68
+ *
69
+ * @typedef {RollModifier | RollModifierFunction | DiceModifier} RollModifierLike
70
+ */
57
71
  class RollModifier {
58
72
  /**
59
73
  * @param {RollModifierFunction} fn - Modifier function.
@@ -81,7 +95,7 @@ class RollModifier {
81
95
 
82
96
  /**
83
97
  * Validates that this modifier still conforms to spec.
84
- * (Useful if modifiers are loaded dynamically or serialized.)
98
+ * (Useful if modifiers are loaded dynamically or serialised.)
85
99
  * @throws {TypeError} If the modifier is invalid.
86
100
  */
87
101
  validate() {