|
1 | | -/** |
2 | | - * Custom implementation of Roll from foundry.js |
3 | | - * Test methods are unique methods used for unit testing |
4 | | - */ |
5 | | -import { nextValueService } from './nextValueService'; |
| 1 | +// ABFExploderRoll/ABFExploderRoll.js |
| 2 | +import ABFFoundryRoll from '../ABFFoundryRoll'; |
| 3 | +import { ABFRoll } from '../ABFRoll'; |
6 | 4 |
|
7 | | -export default class ABFFoundryRoll { |
8 | | - /** @type {string} */ |
9 | | - _formula; |
10 | | - /** @type {Record<string,unknown> | undefined} */ |
11 | | - system; |
| 5 | +export default class ABFExploderRoll extends ABFRoll { |
| 6 | + // Keep for backward compatibility, not used by the new flow |
| 7 | + lastOpenRange = this.openRollRange; |
12 | 8 |
|
13 | | - _rolled = false; |
14 | | - /** @type {number} */ |
15 | | - _total; |
16 | | - |
17 | | - /** @type {DiceTerm[]} */ |
18 | | - dice; |
19 | | - |
20 | | - // Test variable |
21 | | - /** @type {number | null} */ |
22 | | - static nextValue; |
23 | | - |
24 | | - /** |
25 | | - * @param {string} formula |
26 | | - * @param {Record<string,unknown>} [data] |
27 | | - */ |
28 | | - constructor(formula, data) { |
29 | | - this._formula = formula; |
30 | | - this.system = data; |
31 | | - |
32 | | - this.dice = []; |
| 9 | + get fumbled() { |
| 10 | + // Preserve previous public API |
| 11 | + return this.foundryRoll.firstResult <= this.fumbleRange; |
33 | 12 | } |
34 | 13 |
|
35 | | - recalculateTotal(mod = 0) { |
36 | | - this._total = this.getResults().reduce((prev, curr) => prev + curr) + mod; |
| 14 | + /** Check "open on doubles" rule (optional setting) */ |
| 15 | + async checkDoubles(result) { |
| 16 | + if (result % 11 !== 0) return false; |
| 17 | + const d10 = new ABFFoundryRoll('1d10'); |
| 18 | + await d10.evaluate(); |
| 19 | + return d10.total === result / 11; |
37 | 20 | } |
38 | 21 |
|
39 | | - get total() { |
40 | | - return this._total; |
41 | | - } |
| 22 | + /** @returns {Promise<ABFFoundryRoll>} */ |
| 23 | + async evaluate() { |
| 24 | + // Mark fumble only on the very first base die (same behavior as before) |
| 25 | + if (this.firstDice?.results?.length > 0) { |
| 26 | + this.firstDice.results[0].failure = |
| 27 | + this.firstDice.results[0].result <= this.fumbleRange; |
| 28 | + } |
42 | 29 |
|
43 | | - get firstResult() { |
44 | | - return this.getResults()[0]; |
45 | | - } |
| 30 | + // Number of base dice in the term (e.g., 2 for "2d100") |
| 31 | + const baseCount = this.firstDice?.results?.length ?? 0; |
46 | 32 |
|
47 | | - get lastResult() { |
48 | | - return this.getResults()[this.getResults().length - 1]; |
49 | | - } |
| 33 | + // Process each base result independently and append its own open chain |
| 34 | + for (let i = 0; i < baseCount; i++) { |
| 35 | + await this._explodeChainFromIndex(i); |
| 36 | + } |
50 | 37 |
|
51 | | - getResults() { |
52 | | - return this.dice.map(d => d.results.map(res => res.result)).flat(); |
53 | | - } |
| 38 | + // Recalculate total if the wrapper provides it (used in tests/custom roll) |
| 39 | + if (typeof this.foundryRoll.recalculateTotal === 'function') { |
| 40 | + this.foundryRoll.recalculateTotal(); |
| 41 | + } |
54 | 42 |
|
55 | | - get firstDice() { |
56 | | - return this.dice[0]; |
| 43 | + return this.foundryRoll; |
57 | 44 | } |
58 | 45 |
|
59 | | - evaluate() { |
60 | | - if (this._rolled) throw new Error('Already rolled'); |
61 | | - |
62 | | - const value = |
63 | | - nextValueService.getNextValue() ?? Math.min(1, Math.floor(Math.random() * 100)); |
64 | | - |
65 | | - /** @type {DiceTerm} */ |
66 | | - const diceTerm = { results: [{ result: value, active: true }] }; |
67 | | - |
68 | | - this.dice.push(diceTerm); |
69 | | - |
70 | | - this.recalculateTotal(); |
| 46 | + /** |
| 47 | + * Explode chain starting from base result at `index`. |
| 48 | + * Uses openRollRange threshold, increasing by +1 per explosion (capped at 100). |
| 49 | + */ |
| 50 | + async _explodeChainFromIndex(index) { |
| 51 | + if (!this.firstDice || !this.firstDice.results?.[index]) return; |
| 52 | + |
| 53 | + let threshold = this.openRollRange; |
| 54 | + // Pointer to the current (last) result object in this chain |
| 55 | + let currentObj = this.firstDice.results[index]; |
| 56 | + |
| 57 | + // Keep exploding as long as we meet threshold or pass the doubles rule |
| 58 | + // Note: we check doubles on each link in the chain if the setting is enabled. |
| 59 | + // If both conditions pass, it still only produces a single extra die per loop. |
| 60 | + while ( |
| 61 | + currentObj.result >= threshold || |
| 62 | + (this.openOnDoubles && (await this.checkDoubles(currentObj.result))) |
| 63 | + ) { |
| 64 | + // Mark for UI (flags already used in your codebase) |
| 65 | + currentObj.success = true; |
| 66 | + currentObj.exploded = true; |
| 67 | + |
| 68 | + // Roll extra 1d100 and append it to the same Die (same chain) |
| 69 | + const extra = new ABFFoundryRoll('1d100'); |
| 70 | + await extra.evaluate(); |
| 71 | + this.addRoll(extra); |
| 72 | + |
| 73 | + // Move pointer to the brand-new last result we just appended |
| 74 | + currentObj = this.firstDice.results[this.firstDice.results.length - 1]; |
| 75 | + |
| 76 | + // Raise threshold for subsequent explosions in this chain |
| 77 | + threshold = Math.min(threshold + 1, 100); |
| 78 | + } |
| 79 | + } |
71 | 80 |
|
72 | | - this._rolled = true; |
73 | | - nextValueService.setNextValue(undefined); |
| 81 | + // ------------------------------------------------------------------------- |
| 82 | + // Legacy helpers kept for compatibility (not used by the new evaluate flow) |
| 83 | + // ------------------------------------------------------------------------- |
| 84 | + |
| 85 | + /** @deprecated Legacy single-chain check against the last result */ |
| 86 | + async canExplode() { |
| 87 | + if (!this.firstDice?.results?.length) return false; |
| 88 | + const lastResult = this.firstDice.results[this.firstDice.results.length - 1]; |
| 89 | + |
| 90 | + if (this.openOnDoubles && lastResult.result % 11 === 0) { |
| 91 | + const newRoll = new ABFFoundryRoll('1d10'); |
| 92 | + await newRoll.evaluate(); |
| 93 | + if (newRoll.total === lastResult.result / 11) { |
| 94 | + lastResult.success = true; |
| 95 | + lastResult.exploded = true; |
| 96 | + lastResult.count = 100; |
| 97 | + return true; |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + const exploded = lastResult.result >= this.openRollRange; |
| 102 | + lastResult.success = exploded; |
| 103 | + return exploded; |
| 104 | + } |
74 | 105 |
|
75 | | - return this; |
| 106 | + /** @deprecated Legacy recursive single-chain exploder */ |
| 107 | + async explodeDice(openRange) { |
| 108 | + this.lastOpenRange = Math.min(openRange, 100); |
| 109 | + const newRoll = new ABFFoundryRoll('1d100'); |
| 110 | + await newRoll.evaluate(); |
| 111 | + this.addRoll(newRoll); |
| 112 | + if (await this.canExplode()) { |
| 113 | + await this.explodeDice(openRange + 1); |
| 114 | + } |
76 | 115 | } |
77 | 116 | } |
0 commit comments