@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.
- 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 +15 -17
- 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/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:
|
package/dist/analyseModTest.js
CHANGED
|
@@ -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").
|
|
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 {
|
|
58
|
-
* @
|
|
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
|
-
//
|
|
115
|
-
const
|
|
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,
|
|
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
|
package/dist/analyseTest.js
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
//
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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}
|
|
231
|
+
* @typedef {Object} ModifiedBaseTestCondition
|
|
243
232
|
* @property {{ min: number, max: number }} modifiedRange
|
|
244
233
|
*/
|
|
245
234
|
|
|
246
235
|
/**
|
|
247
|
-
* @typedef {
|
|
248
|
-
* @typedef {
|
|
249
|
-
* @typedef {
|
|
250
|
-
* @typedef {
|
|
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 {
|
|
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
|
|
98
|
+
* (Useful if modifiers are loaded dynamically or serialised.)
|
|
85
99
|
* @throws {TypeError} If the modifier is invalid.
|
|
86
100
|
*/
|
|
87
101
|
validate() {
|