@platonic-dice/core 2.0.2 → 2.1.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 +54 -7
- package/dist/analyseModTest.d.ts +126 -0
- package/dist/analyseModTest.d.ts.map +1 -0
- package/dist/analyseModTest.js +181 -0
- package/dist/analyseTest.d.ts +101 -0
- package/dist/analyseTest.d.ts.map +1 -0
- package/dist/analyseTest.js +141 -0
- package/dist/entities/ModifiedTestConditions.d.ts +89 -0
- package/dist/entities/ModifiedTestConditions.d.ts.map +1 -0
- package/dist/entities/ModifiedTestConditions.js +265 -0
- package/dist/entities/index.d.ts +4 -1
- package/dist/entities/index.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/rollModTest.d.ts +72 -0
- package/dist/rollModTest.d.ts.map +1 -0
- package/dist/rollModTest.js +208 -0
- package/dist/rollTest.d.ts +21 -2
- package/dist/rollTest.d.ts.map +1 -1
- package/dist/rollTest.js +28 -6
- package/dist/utils/determineOutcome.d.ts.map +1 -1
- package/dist/utils/determineOutcome.js +26 -0
- package/dist/utils/index.d.ts +4 -1
- package/dist/utils/index.js +8 -0
- package/dist/utils/outcomeMapper.d.ts +29 -0
- package/dist/utils/outcomeMapper.d.ts.map +1 -0
- package/dist/utils/outcomeMapper.js +197 -0
- package/dist-types.d.ts +23 -1
- package/package.json +18 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @platonic-dice/core/src/rollModTest
|
|
3
|
+
* @description
|
|
4
|
+
* Rolls a die with a modifier and evaluates the modified result against test conditions.
|
|
5
|
+
* Combines the logic of {@link rollMod} and {@link rollTest}.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { rollModTest, TestType, DieType } from "@platonic-dice/core";
|
|
9
|
+
*
|
|
10
|
+
* // Roll a D20 with +5 bonus, check if result ≥ 15
|
|
11
|
+
* const result = rollModTest(
|
|
12
|
+
* DieType.D20,
|
|
13
|
+
* (n) => n + 5,
|
|
14
|
+
* { testType: TestType.AtLeast, target: 15 }
|
|
15
|
+
* );
|
|
16
|
+
* console.log(result.base); // e.g., 12
|
|
17
|
+
* console.log(result.modified); // e.g., 17
|
|
18
|
+
* console.log(result.outcome); // e.g., "success"
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Skill check with advantage and modifier
|
|
22
|
+
* const result = rollModTest(
|
|
23
|
+
* DieType.D20,
|
|
24
|
+
* new RollModifier((n) => n + 3),
|
|
25
|
+
* { testType: TestType.Skill, target: 12, critical_success: 20, critical_failure: 1 },
|
|
26
|
+
* RollType.Advantage
|
|
27
|
+
* );
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { normaliseRollModifier } = require("./entities");
|
|
31
|
+
const { ModifiedTestConditions } = require("./entities/ModifiedTestConditions");
|
|
32
|
+
const r = require("./roll.js");
|
|
33
|
+
const utils = require("./utils");
|
|
34
|
+
const { createOutcomeMap } = require("./utils/outcomeMapper");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
38
|
+
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
39
|
+
* @typedef {import("./entities/RollType").RollTypeValue} RollTypeValue
|
|
40
|
+
* @typedef {import("./entities/RollModifier").RollModifierFunction} RollModifierFunction
|
|
41
|
+
* @typedef {import("./entities/RollModifier").RollModifierInstance} RollModifierInstance
|
|
42
|
+
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
43
|
+
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Helper to rank outcomes for comparison.
|
|
48
|
+
* @private
|
|
49
|
+
* @param {OutcomeValue} outcome
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function rankOutcome(outcome) {
|
|
53
|
+
const { Outcome } = require("./entities");
|
|
54
|
+
switch (outcome) {
|
|
55
|
+
case Outcome.CriticalFailure:
|
|
56
|
+
return 0;
|
|
57
|
+
case Outcome.Failure:
|
|
58
|
+
return 1;
|
|
59
|
+
case Outcome.Success:
|
|
60
|
+
return 2;
|
|
61
|
+
case Outcome.CriticalSuccess:
|
|
62
|
+
return 3;
|
|
63
|
+
default:
|
|
64
|
+
return 1; // Default to failure rank
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Rolls a die with a modifier and evaluates the modified result against test conditions.
|
|
70
|
+
*
|
|
71
|
+
* @function rollModTest
|
|
72
|
+
* @param {DieTypeValue} dieType - The type of die to roll (e.g., `DieType.D20`).
|
|
73
|
+
* @param {RollModifierFunction|RollModifierInstance} modifier - The modifier to apply to the roll.
|
|
74
|
+
* Can be either:
|
|
75
|
+
* - A function `(n: number) => number`
|
|
76
|
+
* - A {@link RollModifier} instance
|
|
77
|
+
* @param {TestConditionsInstance|{ testType: TestTypeValue, [key: string]: any }} testConditions
|
|
78
|
+
* Can be:
|
|
79
|
+
* - A `TestConditions` instance
|
|
80
|
+
* - A plain object `{ testType, ...conditions }`
|
|
81
|
+
* @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
|
|
82
|
+
* @param {Object} [options={}] - Optional configuration.
|
|
83
|
+
* @param {boolean} [options.useNaturalCrits] - If true, natural max/min rolls on the die trigger
|
|
84
|
+
* critical success/failure (for Skill tests) or success/failure (for other test types).
|
|
85
|
+
* If undefined, defaults to true for Skill test type and false for all others.
|
|
86
|
+
* @returns {{ base: number, modified: number, outcome: OutcomeValue }}
|
|
87
|
+
* - `base`: The raw die roll
|
|
88
|
+
* - `modified`: The roll after applying the modifier
|
|
89
|
+
* - `outcome`: The success/failure result based on test conditions
|
|
90
|
+
* @throws {TypeError} If `dieType`, `modifier`, or `testConditions` are invalid.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* const result = rollModTest(
|
|
94
|
+
* DieType.D20,
|
|
95
|
+
* (n) => n + 2,
|
|
96
|
+
* { testType: TestType.AtLeast, target: 15 }
|
|
97
|
+
* );
|
|
98
|
+
* console.log(result); // { base: 14, modified: 16, outcome: "success" }
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // With natural crits enabled (TTRPG style)
|
|
102
|
+
* const result = rollModTest(
|
|
103
|
+
* DieType.D20,
|
|
104
|
+
* (n) => n + 5,
|
|
105
|
+
* { testType: TestType.Skill, target: 15, critical_success: 25, critical_failure: 2 },
|
|
106
|
+
* undefined,
|
|
107
|
+
* { useNaturalCrits: true }
|
|
108
|
+
* );
|
|
109
|
+
* // If base roll is 20, outcome is always "critical_success"
|
|
110
|
+
* // If base roll is 1, outcome is always "critical_failure"
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // With advantage - compares outcomes, not just base rolls
|
|
114
|
+
* const result = rollModTest(
|
|
115
|
+
* DieType.D20,
|
|
116
|
+
* (n) => n + 3,
|
|
117
|
+
* { testType: TestType.Skill, target: 12, critical_success: 20, critical_failure: 1 },
|
|
118
|
+
* RollType.Advantage
|
|
119
|
+
* );
|
|
120
|
+
* // Rolls twice, returns the result with the better outcome
|
|
121
|
+
*/
|
|
122
|
+
function rollModTest(
|
|
123
|
+
dieType,
|
|
124
|
+
modifier,
|
|
125
|
+
testConditions,
|
|
126
|
+
rollType = undefined,
|
|
127
|
+
options = {}
|
|
128
|
+
) {
|
|
129
|
+
if (!dieType) throw new TypeError("dieType is required.");
|
|
130
|
+
if (!modifier) throw new TypeError("modifier is required.");
|
|
131
|
+
if (!testConditions) throw new TypeError("testConditions is required.");
|
|
132
|
+
|
|
133
|
+
// Normalise the modifier (skip if already a RollModifier instance)
|
|
134
|
+
const { RollModifier } = require("./entities");
|
|
135
|
+
const mod =
|
|
136
|
+
modifier instanceof RollModifier
|
|
137
|
+
? modifier
|
|
138
|
+
: normaliseRollModifier(modifier);
|
|
139
|
+
|
|
140
|
+
// Create ModifiedTestConditions if input is a plain object
|
|
141
|
+
let conditionSet;
|
|
142
|
+
if (testConditions instanceof ModifiedTestConditions) {
|
|
143
|
+
conditionSet = testConditions;
|
|
144
|
+
} else {
|
|
145
|
+
// Plain object: { testType, ...conditions }
|
|
146
|
+
const { testType, ...rest } = testConditions;
|
|
147
|
+
// @ts-ignore - rest is validated by ModifiedTestConditions constructor
|
|
148
|
+
conditionSet = new ModifiedTestConditions(testType, rest, dieType, mod);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Create outcome map for all possible rolls (with modifier applied)
|
|
152
|
+
const outcomeMap = createOutcomeMap(
|
|
153
|
+
dieType,
|
|
154
|
+
conditionSet.testType,
|
|
155
|
+
// @ts-ignore - ModifiedTestConditions is compatible with TestConditions for outcome mapping
|
|
156
|
+
conditionSet,
|
|
157
|
+
mod, // include modifier
|
|
158
|
+
options.useNaturalCrits
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Handle advantage/disadvantage by comparing outcomes
|
|
162
|
+
if (rollType) {
|
|
163
|
+
const { RollType } = require("./entities");
|
|
164
|
+
|
|
165
|
+
// Roll twice
|
|
166
|
+
const roll1 = utils.generateResult(dieType);
|
|
167
|
+
const roll2 = utils.generateResult(dieType);
|
|
168
|
+
|
|
169
|
+
// Look up outcomes from pre-computed map
|
|
170
|
+
const outcome1 = outcomeMap[roll1];
|
|
171
|
+
const outcome2 = outcomeMap[roll2];
|
|
172
|
+
|
|
173
|
+
// Build result objects
|
|
174
|
+
const result1 = {
|
|
175
|
+
base: roll1,
|
|
176
|
+
modified: mod.apply(roll1),
|
|
177
|
+
outcome: outcome1,
|
|
178
|
+
};
|
|
179
|
+
const result2 = {
|
|
180
|
+
base: roll2,
|
|
181
|
+
modified: mod.apply(roll2),
|
|
182
|
+
outcome: outcome2,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Compare outcomes: higher rank is better
|
|
186
|
+
const rank1 = rankOutcome(outcome1);
|
|
187
|
+
const rank2 = rankOutcome(outcome2);
|
|
188
|
+
|
|
189
|
+
if (rollType === RollType.Advantage) {
|
|
190
|
+
// Return the result with better outcome (higher rank)
|
|
191
|
+
return rank1 >= rank2 ? result1 : result2;
|
|
192
|
+
} else {
|
|
193
|
+
// Disadvantage: return the result with worse outcome (lower rank)
|
|
194
|
+
return rank1 <= rank2 ? result1 : result2;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Normal roll (no advantage/disadvantage)
|
|
199
|
+
const base = r.roll(dieType);
|
|
200
|
+
const outcome = outcomeMap[base];
|
|
201
|
+
const modified = mod.apply(base);
|
|
202
|
+
|
|
203
|
+
return { base, modified, outcome };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
rollModTest,
|
|
208
|
+
};
|
package/dist/rollTest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
declare namespace _exports {
|
|
2
|
-
export { DieTypeValue, OutcomeValue, RollTypeValue, TestTypeValue, TestConditionsInstance };
|
|
2
|
+
export { DieTypeValue, OutcomeValue, RollTypeValue, TestTypeValue, TestConditionsInstance, RollTestOptions };
|
|
3
3
|
}
|
|
4
4
|
declare namespace _exports {
|
|
5
5
|
export { rollTest };
|
|
@@ -10,6 +10,16 @@ type OutcomeValue = import("./entities/Outcome").OutcomeValue;
|
|
|
10
10
|
type RollTypeValue = import("./entities/RollType").RollTypeValue;
|
|
11
11
|
type TestTypeValue = import("./entities/TestType").TestTypeValue;
|
|
12
12
|
type TestConditionsInstance = import("./entities/TestConditions").TestConditionsInstance;
|
|
13
|
+
type RollTestOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* - If true, rolling the die's maximum value
|
|
16
|
+
* triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
|
|
17
|
+
* and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
|
|
18
|
+
* or Success (for AtMost). If undefined, defaults to true for TestType.Skill
|
|
19
|
+
* and false for all other test types.
|
|
20
|
+
*/
|
|
21
|
+
useNaturalCrits?: boolean | undefined;
|
|
22
|
+
};
|
|
13
23
|
/**
|
|
14
24
|
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
15
25
|
* @typedef {import("./entities/Outcome").OutcomeValue} OutcomeValue
|
|
@@ -17,6 +27,14 @@ type TestConditionsInstance = import("./entities/TestConditions").TestConditions
|
|
|
17
27
|
* @typedef {import("./entities/TestType").TestTypeValue} TestTypeValue
|
|
18
28
|
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
19
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} RollTestOptions
|
|
32
|
+
* @property {boolean} [useNaturalCrits] - If true, rolling the die's maximum value
|
|
33
|
+
* triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
|
|
34
|
+
* and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
|
|
35
|
+
* or Success (for AtMost). If undefined, defaults to true for TestType.Skill
|
|
36
|
+
* and false for all other test types.
|
|
37
|
+
*/
|
|
20
38
|
/**
|
|
21
39
|
* Rolls a die and evaluates it against specified test conditions.
|
|
22
40
|
*
|
|
@@ -27,13 +45,14 @@ type TestConditionsInstance = import("./entities/TestConditions").TestConditions
|
|
|
27
45
|
* - A `TestConditions` instance.
|
|
28
46
|
* - A plain object `{ testType, ...conditions }`.
|
|
29
47
|
* @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
|
|
48
|
+
* @param {RollTestOptions} [options={}] - Optional configuration for natural crits
|
|
30
49
|
* @returns {{ base: number, outcome: OutcomeValue }} The raw roll and its evaluated outcome.
|
|
31
50
|
* @throws {TypeError} If `dieType` or `testConditions` are invalid.
|
|
32
51
|
*/
|
|
33
52
|
declare function rollTest(dieType: DieTypeValue, testConditions: TestConditionsInstance | {
|
|
34
53
|
testType: TestTypeValue;
|
|
35
54
|
[key: string]: any;
|
|
36
|
-
}, rollType?: RollTypeValue): {
|
|
55
|
+
}, rollType?: RollTypeValue, options?: RollTestOptions): {
|
|
37
56
|
base: number;
|
|
38
57
|
outcome: OutcomeValue;
|
|
39
58
|
};
|
package/dist/rollTest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rollTest.d.ts","sourceRoot":"","sources":["../src/rollTest.js"],"names":[],"mappings":";;;;;;;oBA6Ba,OAAO,oBAAoB,EAAE,YAAY;oBACzC,OAAO,oBAAoB,EAAE,YAAY;qBACzC,OAAO,qBAAqB,EAAE,aAAa;qBAC3C,OAAO,qBAAqB,EAAE,aAAa;8BAC3C,OAAO,2BAA2B,EAAE,sBAAsB
|
|
1
|
+
{"version":3,"file":"rollTest.d.ts","sourceRoot":"","sources":["../src/rollTest.js"],"names":[],"mappings":";;;;;;;oBA6Ba,OAAO,oBAAoB,EAAE,YAAY;oBACzC,OAAO,oBAAoB,EAAE,YAAY;qBACzC,OAAO,qBAAqB,EAAE,aAAa;qBAC3C,OAAO,qBAAqB,EAAE,aAAa;8BAC3C,OAAO,2BAA2B,EAAE,sBAAsB;;;;;;;;;;;AALvE;;;;;;GAMG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;;;;;GAaG;AACH,mCAVW,YAAY,kBACZ,sBAAsB,GAAC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,aAItE,aAAa,YACb,eAAe,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,CA4BnD"}
|
package/dist/rollTest.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
const { DieType, TestType } = require("./entities");
|
|
25
25
|
const tc = require("./entities/TestConditions.js");
|
|
26
26
|
const r = require("./roll.js");
|
|
27
|
-
const
|
|
27
|
+
const { createOutcomeMap } = require("./utils/outcomeMapper");
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @typedef {import("./entities/DieType").DieTypeValue} DieTypeValue
|
|
@@ -34,6 +34,15 @@ const utils = require("./utils");
|
|
|
34
34
|
* @typedef {import("./entities/TestConditions").TestConditionsInstance} TestConditionsInstance
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} RollTestOptions
|
|
39
|
+
* @property {boolean} [useNaturalCrits] - If true, rolling the die's maximum value
|
|
40
|
+
* triggers CriticalSuccess (for Skill tests) or Success (for AtLeast/AtMost tests),
|
|
41
|
+
* and rolling 1 triggers CriticalFailure (for Skill tests) or Failure (for AtLeast)
|
|
42
|
+
* or Success (for AtMost). If undefined, defaults to true for TestType.Skill
|
|
43
|
+
* and false for all other test types.
|
|
44
|
+
*/
|
|
45
|
+
|
|
37
46
|
/**
|
|
38
47
|
* Rolls a die and evaluates it against specified test conditions.
|
|
39
48
|
*
|
|
@@ -44,20 +53,33 @@ const utils = require("./utils");
|
|
|
44
53
|
* - A `TestConditions` instance.
|
|
45
54
|
* - A plain object `{ testType, ...conditions }`.
|
|
46
55
|
* @param {RollTypeValue} [rollType=undefined] - Optional roll mode (`RollType.Advantage` or `RollType.Disadvantage`).
|
|
56
|
+
* @param {RollTestOptions} [options={}] - Optional configuration for natural crits
|
|
47
57
|
* @returns {{ base: number, outcome: OutcomeValue }} The raw roll and its evaluated outcome.
|
|
48
58
|
* @throws {TypeError} If `dieType` or `testConditions` are invalid.
|
|
49
59
|
*/
|
|
50
|
-
function rollTest(dieType, testConditions, rollType = undefined) {
|
|
60
|
+
function rollTest(dieType, testConditions, rollType = undefined, options = {}) {
|
|
51
61
|
if (!dieType) throw new TypeError("dieType is required.");
|
|
52
62
|
|
|
53
|
-
// Normalise testConditions
|
|
54
|
-
const conditionSet =
|
|
63
|
+
// Normalise testConditions (skip if already a TestConditions instance)
|
|
64
|
+
const conditionSet =
|
|
65
|
+
testConditions instanceof tc.TestConditions
|
|
66
|
+
? testConditions
|
|
67
|
+
: tc.normaliseTestConditions(testConditions, dieType);
|
|
68
|
+
|
|
69
|
+
// Create outcome map for all possible rolls
|
|
70
|
+
const outcomeMap = createOutcomeMap(
|
|
71
|
+
dieType,
|
|
72
|
+
conditionSet.testType,
|
|
73
|
+
conditionSet,
|
|
74
|
+
null, // no modifier
|
|
75
|
+
options.useNaturalCrits
|
|
76
|
+
);
|
|
55
77
|
|
|
56
78
|
// Perform the roll
|
|
57
79
|
const base = r.roll(dieType, rollType);
|
|
58
80
|
|
|
59
|
-
//
|
|
60
|
-
const outcome =
|
|
81
|
+
// Look up outcome from pre-computed map
|
|
82
|
+
const outcome = outcomeMap[base];
|
|
61
83
|
|
|
62
84
|
return { base, outcome };
|
|
63
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"determineOutcome.d.ts","sourceRoot":"","sources":["../../src/utils/determineOutcome.js"],"names":[],"mappings":"2BAWa,OAAO,qBAAqB,EAAE,YAAY;qCAC1C,OAAO,4BAA4B,EAAE,sBAAsB;yBAC3D,OAAO,4BAA4B,EAAE,UAAU;4BAC/C,OAAO,sBAAsB,EAAE,aAAa;2BAC5C,OAAO,qBAAqB,EAAE,YAAY;iCAK1C;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAAG,UAAU;AAV5E;;;;;;GAMG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wCAlBW,MAAM,kBACN,sBAAsB,GAAC,kBAAkB,GACvC,YAAY,
|
|
1
|
+
{"version":3,"file":"determineOutcome.d.ts","sourceRoot":"","sources":["../../src/utils/determineOutcome.js"],"names":[],"mappings":"2BAWa,OAAO,qBAAqB,EAAE,YAAY;qCAC1C,OAAO,4BAA4B,EAAE,sBAAsB;yBAC3D,OAAO,4BAA4B,EAAE,UAAU;4BAC/C,OAAO,sBAAsB,EAAE,aAAa;2BAC5C,OAAO,qBAAqB,EAAE,YAAY;iCAK1C;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,YAAY,CAAA;CAAE,GAAG,UAAU;AAV5E;;;;;;GAMG;AAEH;;;GAGG;AAEH;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wCAlBW,MAAM,kBACN,sBAAsB,GAAC,kBAAkB,GACvC,YAAY,CAmDxB"}
|
|
@@ -46,11 +46,23 @@ function getEntities() {
|
|
|
46
46
|
*/
|
|
47
47
|
function determineOutcome(value, testConditions) {
|
|
48
48
|
const { Outcome, TestType, TestConditions } = getEntities();
|
|
49
|
+
const {
|
|
50
|
+
ModifiedTestConditions,
|
|
51
|
+
} = require("../entities/ModifiedTestConditions");
|
|
49
52
|
|
|
50
53
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
51
54
|
throw new TypeError("value must be a valid number.");
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
// Handle ModifiedTestConditions instances - no need to re-validate
|
|
58
|
+
if (testConditions instanceof ModifiedTestConditions) {
|
|
59
|
+
/** @type {TestConditionsInstance} */
|
|
60
|
+
// @ts-ignore - ModifiedTestConditions has compatible structure for outcome evaluation
|
|
61
|
+
const { testType, conditions } = testConditions;
|
|
62
|
+
// Use the switch statement below to determine outcome
|
|
63
|
+
return evaluateOutcome(value, testType, conditions, Outcome, TestType);
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
// Normalise plain object input into a TestConditions instance
|
|
55
67
|
if (!(testConditions instanceof TestConditions)) {
|
|
56
68
|
const { testType, dieType, ...rest } = /** @type {TestConditionsLike} */ (
|
|
@@ -66,6 +78,20 @@ function determineOutcome(value, testConditions) {
|
|
|
66
78
|
/** @type {TestConditionsInstance} */
|
|
67
79
|
const { testType, conditions } = testConditions;
|
|
68
80
|
|
|
81
|
+
return evaluateOutcome(value, testType, conditions, Outcome, TestType);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Core evaluation logic extracted for reuse.
|
|
86
|
+
* @private
|
|
87
|
+
* @param {number} value
|
|
88
|
+
* @param {TestTypeValue} testType
|
|
89
|
+
* @param {Conditions | any} conditions
|
|
90
|
+
* @param {any} Outcome
|
|
91
|
+
* @param {any} TestType
|
|
92
|
+
* @returns {OutcomeValue}
|
|
93
|
+
*/
|
|
94
|
+
function evaluateOutcome(value, testType, conditions, Outcome, TestType) {
|
|
69
95
|
switch (testType) {
|
|
70
96
|
case TestType.AtLeast:
|
|
71
97
|
case TestType.AtMost:
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { determineOutcome } from "./determineOutcome.js";
|
|
2
2
|
import { generateResult } from "./generateResult.js";
|
|
3
3
|
import { numSides } from "./generateResult.js";
|
|
4
|
-
|
|
4
|
+
import { createOutcomeMap } from "./outcomeMapper.js";
|
|
5
|
+
import { clearOutcomeMapCache } from "./outcomeMapper.js";
|
|
6
|
+
import { getOutcomeMapCacheSize } from "./outcomeMapper.js";
|
|
7
|
+
export { determineOutcome, generateResult, numSides, createOutcomeMap, clearOutcomeMapCache, getOutcomeMapCacheSize };
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/utils/index.js
CHANGED
|
@@ -18,9 +18,17 @@
|
|
|
18
18
|
*/
|
|
19
19
|
const { determineOutcome } = require("./determineOutcome.js");
|
|
20
20
|
const { generateResult, numSides } = require("./generateResult.js");
|
|
21
|
+
const {
|
|
22
|
+
createOutcomeMap,
|
|
23
|
+
clearOutcomeMapCache,
|
|
24
|
+
getOutcomeMapCacheSize,
|
|
25
|
+
} = require("./outcomeMapper.js");
|
|
21
26
|
|
|
22
27
|
module.exports = {
|
|
23
28
|
determineOutcome,
|
|
24
29
|
generateResult,
|
|
25
30
|
numSides,
|
|
31
|
+
createOutcomeMap,
|
|
32
|
+
clearOutcomeMapCache,
|
|
33
|
+
getOutcomeMapCacheSize,
|
|
26
34
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an outcome map for all possible base rolls given the configuration.
|
|
3
|
+
* Uses memoization cache for performance.
|
|
4
|
+
*
|
|
5
|
+
* @param {import("../entities/DieType").DieTypeValue} dieType - The type of die
|
|
6
|
+
* @param {import("../entities/TestType").TestTypeValue} testType - The type of test being performed
|
|
7
|
+
* @param {import("../entities/TestConditions").TestConditionsInstance} testConditions - The test conditions
|
|
8
|
+
* @param {import("../entities/RollModifier").RollModifierInstance|null} modifier - Optional modifier to apply
|
|
9
|
+
* @param {boolean|null} useNaturalCrits - Whether to use natural crits (null = auto-determine)
|
|
10
|
+
* @returns {Object.<number, import("../entities/Outcome").OutcomeValue>} Map of baseRoll -> outcome
|
|
11
|
+
*/
|
|
12
|
+
export function createOutcomeMap(dieType: import("../entities/DieType").DieTypeValue, testType: import("../entities/TestType").TestTypeValue, testConditions: import("../entities/TestConditions").TestConditionsInstance, modifier?: import("../entities/RollModifier").RollModifierInstance | null, useNaturalCrits?: boolean | null): {
|
|
13
|
+
[x: number]: import("../entities").OutcomeValue;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Clears the outcome map cache.
|
|
17
|
+
* Useful for testing or memory management.
|
|
18
|
+
*
|
|
19
|
+
* @function clearOutcomeMapCache
|
|
20
|
+
*/
|
|
21
|
+
export function clearOutcomeMapCache(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Gets the current size of the outcome map cache.
|
|
24
|
+
*
|
|
25
|
+
* @function getOutcomeMapCacheSize
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
export function getOutcomeMapCacheSize(): number;
|
|
29
|
+
//# sourceMappingURL=outcomeMapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outcomeMapper.d.ts","sourceRoot":"","sources":["../../src/utils/outcomeMapper.js"],"names":[],"mappings":"AAsGA;;;;;;;;;;GAUG;AACH,0CAPW,OAAO,qBAAqB,EAAE,YAAY,YAC1C,OAAO,sBAAsB,EAAE,aAAa,kBAC5C,OAAO,4BAA4B,EAAE,sBAAsB,aAC3D,OAAO,0BAA0B,EAAE,oBAAoB,GAAC,IAAI,oBAC5D,OAAO,GAAC,IAAI;;EA4DtB;AAED;;;;;GAKG;AACH,6CAEC;AAED;;;;;GAKG;AACH,0CAFa,MAAM,CAIlB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @platonic-dice/core/src/utils/outcomeMapper
|
|
3
|
+
* @description
|
|
4
|
+
* Creates outcome maps for die rolls, handling natural crits and modifiers.
|
|
5
|
+
* This provides a centralized way to determine all possible outcomes for a
|
|
6
|
+
* given die configuration.
|
|
7
|
+
*
|
|
8
|
+
* Includes memoization cache for performance optimization.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { determineOutcome } = require("./determineOutcome");
|
|
12
|
+
const { numSides } = require("./generateResult");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lazy-loaded entities to avoid circular dependencies.
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
function getEntities() {
|
|
19
|
+
return require("../entities");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Memoization cache for outcome maps.
|
|
24
|
+
* @private
|
|
25
|
+
* @type {Map<string, Object.<number, import("../entities/Outcome").OutcomeValue>>}
|
|
26
|
+
*/
|
|
27
|
+
const outcomeMapCache = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Applies natural crit logic based on test type.
|
|
31
|
+
* Only applies to Skill, AtLeast, and AtMost test types.
|
|
32
|
+
*
|
|
33
|
+
* @private
|
|
34
|
+
* @param {import("../entities/Outcome").OutcomeValue} currentOutcome - The outcome before natural crit override
|
|
35
|
+
* @param {boolean} isNaturalMax - Whether this is the maximum die value
|
|
36
|
+
* @param {boolean} isNaturalMin - Whether this is the minimum die value (1)
|
|
37
|
+
* @param {import("../entities/TestType").TestTypeValue} testType - The type of test
|
|
38
|
+
* @returns {import("../entities/Outcome").OutcomeValue} The potentially overridden outcome
|
|
39
|
+
*/
|
|
40
|
+
function applyNaturalCritOverride(
|
|
41
|
+
currentOutcome,
|
|
42
|
+
isNaturalMax,
|
|
43
|
+
isNaturalMin,
|
|
44
|
+
testType
|
|
45
|
+
) {
|
|
46
|
+
const { TestType, Outcome } = getEntities();
|
|
47
|
+
|
|
48
|
+
// Skill tests: natural crits become Critical outcomes
|
|
49
|
+
if (testType === TestType.Skill) {
|
|
50
|
+
if (isNaturalMax) return Outcome.CriticalSuccess;
|
|
51
|
+
if (isNaturalMin) return Outcome.CriticalFailure;
|
|
52
|
+
return currentOutcome;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// At-Most tests: natural max is BAD (failure), natural min is GOOD (success)
|
|
56
|
+
if (testType === TestType.AtMost) {
|
|
57
|
+
if (isNaturalMax) return Outcome.Failure;
|
|
58
|
+
if (isNaturalMin) return Outcome.Success;
|
|
59
|
+
return currentOutcome;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// At-Least tests: natural max is GOOD (success), natural min is BAD (failure)
|
|
63
|
+
if (testType === TestType.AtLeast) {
|
|
64
|
+
if (isNaturalMax) return Outcome.Success;
|
|
65
|
+
if (isNaturalMin) return Outcome.Failure;
|
|
66
|
+
return currentOutcome;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Other test types (Exact, Within, InList): natural crits don't apply
|
|
70
|
+
return currentOutcome;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a cache key for outcome map memoization.
|
|
75
|
+
*
|
|
76
|
+
* @private
|
|
77
|
+
* @param {import("../entities/DieType").DieTypeValue} dieType
|
|
78
|
+
* @param {import("../entities/TestType").TestTypeValue} testType
|
|
79
|
+
* @param {import("../entities/TestConditions").TestConditionsInstance} testConditions
|
|
80
|
+
* @param {import("../entities/RollModifier").RollModifierInstance|null} modifier
|
|
81
|
+
* @param {boolean} useNaturalCrits
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
function createCacheKey(
|
|
85
|
+
dieType,
|
|
86
|
+
testType,
|
|
87
|
+
testConditions,
|
|
88
|
+
modifier,
|
|
89
|
+
useNaturalCrits
|
|
90
|
+
) {
|
|
91
|
+
// Serialize the conditions object for hashing
|
|
92
|
+
const conditionsKey = JSON.stringify({
|
|
93
|
+
testType: testConditions.testType,
|
|
94
|
+
conditions: testConditions.conditions,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Create modifier key (use function toString for consistent hashing)
|
|
98
|
+
const modifierKey = modifier ? modifier.fn.toString() : "none";
|
|
99
|
+
|
|
100
|
+
return `${dieType}|${testType}|${conditionsKey}|${modifierKey}|${useNaturalCrits}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates an outcome map for all possible base rolls given the configuration.
|
|
105
|
+
* Uses memoization cache for performance.
|
|
106
|
+
*
|
|
107
|
+
* @param {import("../entities/DieType").DieTypeValue} dieType - The type of die
|
|
108
|
+
* @param {import("../entities/TestType").TestTypeValue} testType - The type of test being performed
|
|
109
|
+
* @param {import("../entities/TestConditions").TestConditionsInstance} testConditions - The test conditions
|
|
110
|
+
* @param {import("../entities/RollModifier").RollModifierInstance|null} modifier - Optional modifier to apply
|
|
111
|
+
* @param {boolean|null} useNaturalCrits - Whether to use natural crits (null = auto-determine)
|
|
112
|
+
* @returns {Object.<number, import("../entities/Outcome").OutcomeValue>} Map of baseRoll -> outcome
|
|
113
|
+
*/
|
|
114
|
+
function createOutcomeMap(
|
|
115
|
+
dieType,
|
|
116
|
+
testType,
|
|
117
|
+
testConditions,
|
|
118
|
+
modifier = null,
|
|
119
|
+
useNaturalCrits = null
|
|
120
|
+
) {
|
|
121
|
+
const { TestType } = getEntities();
|
|
122
|
+
|
|
123
|
+
// Auto-determine useNaturalCrits if not specified
|
|
124
|
+
// Default: true for Skill tests, false for all others
|
|
125
|
+
const shouldUseNaturalCrits = useNaturalCrits ?? testType === TestType.Skill;
|
|
126
|
+
|
|
127
|
+
// Check cache first
|
|
128
|
+
const cacheKey = createCacheKey(
|
|
129
|
+
dieType,
|
|
130
|
+
testType,
|
|
131
|
+
testConditions,
|
|
132
|
+
modifier,
|
|
133
|
+
shouldUseNaturalCrits
|
|
134
|
+
);
|
|
135
|
+
const cached = outcomeMapCache.get(cacheKey);
|
|
136
|
+
if (cached) {
|
|
137
|
+
return cached;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sides = numSides(dieType);
|
|
141
|
+
/** @type {Object.<number, import("../entities/Outcome").OutcomeValue>} */
|
|
142
|
+
const outcomeMap = {};
|
|
143
|
+
|
|
144
|
+
for (let baseRoll = 1; baseRoll <= sides; baseRoll++) {
|
|
145
|
+
// Apply modifier if present
|
|
146
|
+
const value = modifier ? modifier.apply(baseRoll) : baseRoll;
|
|
147
|
+
|
|
148
|
+
// Determine base outcome from test evaluation
|
|
149
|
+
let outcome = determineOutcome(value, testConditions);
|
|
150
|
+
|
|
151
|
+
// Apply natural crit overrides if enabled
|
|
152
|
+
if (shouldUseNaturalCrits) {
|
|
153
|
+
const isNaturalMax = baseRoll === sides;
|
|
154
|
+
const isNaturalMin = baseRoll === 1;
|
|
155
|
+
|
|
156
|
+
outcome = applyNaturalCritOverride(
|
|
157
|
+
outcome,
|
|
158
|
+
isNaturalMax,
|
|
159
|
+
isNaturalMin,
|
|
160
|
+
testType
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
outcomeMap[baseRoll] = outcome;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Store in cache before returning
|
|
168
|
+
outcomeMapCache.set(cacheKey, outcomeMap);
|
|
169
|
+
|
|
170
|
+
return outcomeMap;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clears the outcome map cache.
|
|
175
|
+
* Useful for testing or memory management.
|
|
176
|
+
*
|
|
177
|
+
* @function clearOutcomeMapCache
|
|
178
|
+
*/
|
|
179
|
+
function clearOutcomeMapCache() {
|
|
180
|
+
outcomeMapCache.clear();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gets the current size of the outcome map cache.
|
|
185
|
+
*
|
|
186
|
+
* @function getOutcomeMapCacheSize
|
|
187
|
+
* @returns {number}
|
|
188
|
+
*/
|
|
189
|
+
function getOutcomeMapCacheSize() {
|
|
190
|
+
return outcomeMapCache.size;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
createOutcomeMap,
|
|
195
|
+
clearOutcomeMapCache,
|
|
196
|
+
getOutcomeMapCacheSize,
|
|
197
|
+
};
|
package/dist-types.d.ts
CHANGED
|
@@ -22,8 +22,11 @@ export * from "./dist/rollMod";
|
|
|
22
22
|
export * from "./dist/rollDice";
|
|
23
23
|
export * from "./dist/rollDiceMod";
|
|
24
24
|
export * from "./dist/rollTest";
|
|
25
|
+
export * from "./dist/rollModTest";
|
|
26
|
+
export * from "./dist/analyseTest";
|
|
27
|
+
export * from "./dist/analyseModTest";
|
|
25
28
|
|
|
26
|
-
// The `rollMod`/`rollTest` d.ts files are emitted using a CommonJS `export =`
|
|
29
|
+
// The `rollMod`/`rollTest`/`rollModTest` d.ts files are emitted using a CommonJS `export =`
|
|
27
30
|
// shape which doesn't always expose named exports for consumers under some
|
|
28
31
|
// moduleResolution strategies. Provide explicit ESM-style type declarations
|
|
29
32
|
// here so TypeScript imports like `import { rollMod } from '@platonic-dice/core'`
|
|
@@ -46,3 +49,22 @@ export function rollTest(
|
|
|
46
49
|
},
|
|
47
50
|
rollType?: import("./dist/entities/RollType").RollTypeValue
|
|
48
51
|
): { base: number; outcome: import("./dist/entities/Outcome").OutcomeValue };
|
|
52
|
+
|
|
53
|
+
export function rollModTest(
|
|
54
|
+
dieType: import("./dist/entities/DieType").DieTypeValue,
|
|
55
|
+
modifier:
|
|
56
|
+
| import("./dist/entities/RollModifier").RollModifierFunction
|
|
57
|
+
| import("./dist/entities/RollModifier").RollModifierInstance,
|
|
58
|
+
testConditions:
|
|
59
|
+
| import("./dist/entities/TestConditions").TestConditionsInstance
|
|
60
|
+
| {
|
|
61
|
+
testType: import("./dist/entities/TestType").TestTypeValue;
|
|
62
|
+
[k: string]: any;
|
|
63
|
+
},
|
|
64
|
+
rollType?: import("./dist/entities/RollType").RollTypeValue,
|
|
65
|
+
options?: { useNaturalCrits?: boolean }
|
|
66
|
+
): {
|
|
67
|
+
base: number;
|
|
68
|
+
modified: number;
|
|
69
|
+
outcome: import("./dist/entities/Outcome").OutcomeValue;
|
|
70
|
+
};
|