Skip to content

Commit 03b6d67

Browse files
committed
FIX - Multiple xa rolls in the same formula
1 parent 369f091 commit 03b6d67

2 files changed

Lines changed: 144 additions & 111 deletions

File tree

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,67 @@
1+
// ABFExploderRoll/ABFExploderRoll.js
12
import ABFFoundryRoll from '../ABFFoundryRoll';
23
import { ABFRoll } from '../ABFRoll';
34

45
export default class ABFExploderRoll extends ABFRoll {
5-
lastOpenRange = this.openRollRange;
6-
7-
async canExplode() {
8-
const lastResult = this.firstDice.results[this.firstDice.results.length - 1];
9-
10-
if (this.openOnDoubles && lastResult.result % 11 === 0) {
11-
const newRoll = new ABFFoundryRoll('1d10');
12-
await newRoll.evaluate();
13-
14-
if (newRoll.total === lastResult.result / 11) {
15-
this.firstDice.results[this.firstDice.results.length - 1] = {
16-
...lastResult,
17-
success: true,
18-
exploded: true,
19-
count: 100
20-
};
21-
return true;
22-
}
23-
}
24-
let exploded = lastResult.result >= this.lastOpenRange;
25-
lastResult.success = exploded;
26-
return exploded;
27-
}
28-
6+
// Expose same API as before
297
get fumbled() {
308
return this.foundryRoll.firstResult <= this.fumbleRange;
319
}
3210

33-
/** @param {number} result */
34-
checkDoubles(result) {
35-
if (result % 11 === 0) {
36-
const newRoll = new ABFFoundryRoll('1d10');
37-
newRoll.evaluate();
38-
39-
return newRoll.total === result / 11;
40-
}
41-
return false;
42-
}
43-
4411
/** @returns {Promise<ABFFoundryRoll>} */
4512
async evaluate() {
46-
if (await this.canExplode()) {
47-
await this.explodeDice(this.lastOpenRange + 1);
48-
}
13+
// Safety: ensure there is at least one result
14+
if (!this.firstDice || !this.firstDice.results?.length) return this.foundryRoll;
4915

16+
// Fumble only on the first base die (same behavior as before)
5017
this.firstDice.results[0].failure =
5118
this.firstDice.results[0].result <= this.fumbleRange;
5219

53-
this.foundryRoll.recalculateTotal();
20+
// Snapshot of base results count (e.g., 2 for "2d100")
21+
const baseCount = this.firstDice.results.length;
22+
23+
// Process each base result independently
24+
for (let i = 0; i < baseCount; i++) {
25+
await this._explodeChainFromIndex(i);
26+
}
5427

55-
return new Promise((resolve, reject) => {
56-
resolve(this.foundryRoll);
57-
});
28+
// Sum again after appending extra dice
29+
this.foundryRoll.recalculateTotal();
30+
return this.foundryRoll;
5831
}
5932

60-
/** @param {number} openRange */
61-
async explodeDice(openRange) {
62-
this.lastOpenRange = Math.min(openRange, 100);
33+
/**
34+
* Start an open-roll chain from the base result at `index`.
35+
* Uses openRollRange and increases it by +1 per explosion (capped at 100).
36+
*/
37+
async _explodeChainFromIndex(index) {
38+
let threshold = this.openRollRange;
39+
let currentObj = this.firstDice.results[index];
6340

64-
const newRoll = new ABFFoundryRoll('1d100');
65-
await newRoll.evaluate();
41+
while (
42+
currentObj.result >= threshold ||
43+
(this.openOnDoubles && (await this._checkDoubles(currentObj.result)))
44+
) {
45+
// Mark the triggering result for UI
46+
currentObj.success = true;
47+
currentObj.exploded = true;
6648

67-
const newResult = this.addRoll(newRoll);
49+
// Roll extra 1d100 and append to the same Die (same chain)
50+
const extra = new ABFFoundryRoll('1d100');
51+
await extra.evaluate();
52+
this.addRoll(extra);
6853

69-
if (await this.canExplode()) {
70-
await this.explodeDice(openRange + 1);
54+
// Next link in the chain: raise threshold and point to the new last result
55+
threshold = Math.min(threshold + 1, 100);
56+
currentObj = this.firstDice.results[this.firstDice.results.length - 1];
7157
}
7258
}
59+
60+
/** Optional "open on doubles" rule: roll 1d10 and compare to the repeated digit */
61+
async _checkDoubles(value) {
62+
if (value % 11 !== 0) return false;
63+
const d10 = new ABFFoundryRoll('1d10');
64+
await d10.evaluate();
65+
return d10.total === value / 11;
66+
}
7367
}
Lines changed: 100 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,116 @@
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';
64

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;
128

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;
3312
}
3413

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;
3720
}
3821

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+
}
4229

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;
4632

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+
}
5037

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+
}
5442

55-
get firstDice() {
56-
return this.dice[0];
43+
return this.foundryRoll;
5744
}
5845

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+
}
7180

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+
}
74105

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+
}
76115
}
77116
}

0 commit comments

Comments
 (0)