diff --git a/module/dice/die/baseDie.mjs b/module/dice/die/baseDie.mjs index 7b9c865a..26ed6d7f 100644 --- a/module/dice/die/baseDie.mjs +++ b/module/dice/die/baseDie.mjs @@ -1,7 +1,14 @@ -import { adjustDice } from '../../helpers/utils.mjs'; +import { adjustDice, triggerChatRollFx } from '../../helpers/utils.mjs'; export default class BaseDie extends foundry.dice.terms.Die { - async rerollResult(resultIndex) { + static MODIFIERS = { + ...foundry.dice.terms.Die.MODIFIERS, + cc: 'compoundComboDice', + c: 'comboDice' + }; + + async rerollResult(resultToReroll) { + const resultIndex = Number(resultToReroll); const result = this.results[resultIndex]; result.rerolled = true; result.active = false; @@ -10,75 +17,10 @@ export default class BaseDie extends foundry.dice.terms.Die { const rerolledResult = this.results[this.results.length - 1]; this.results.splice(this.results.length - 1, 1); this.results.splice(resultIndex, 0, rerolledResult); - } - static MODIFIERS = { - ...foundry.dice.terms.Die.MODIFIERS, - cc: 'compoundComboDice', - c: 'comboDice' - }; - - async compoundComboDice() { - return this.handleComboDice(true); - } - - async comboDice() { - return this.handleComboDice(false); - } - - async handleComboDice(maxIncreasesDiceSize) { - /* ComboDice only works with exactly two dice and both have to be the same denomination */ - if (this.number !== 2) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.comboDiceOnlyTwoDiceError')); - return false; - } - - const result = await this.continueCombo(maxIncreasesDiceSize); - - /* The flow of DiceSoNice has no way of knowign that some of the results of a Die should be a different denomination - We solve this by marking the results as hidden so they're not picked up by the auto roll of DiceSoNice. - The actual rolls are done here in place so every dice gets the correct denomination. - */ - if (game.modules.get('dice-so-nice')?.active) { - const rolls = []; - for (const result of this.results) { - const roll = await (new Roll(`1${result.denomination ?? this.denomination}`)).evaluate(); - roll.terms[0].results = [result]; - roll._evaluateTotal(); - rolls.push(roll); - } - - /* If there are other dice that will be rolled we cannot await here. The other dice will be awaited in the normal flow */ - if (this._root.dice.length > 1) { - for (const roll of rolls) { - game.dice3d.showForRoll(roll, game.user, true); - } - } else { - await Promise.allSettled(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); - } - } - - for (const result of this.results) - result.hidden = true; - - return result; - } - - async continueCombo(maxIncreasesDiceSize) { - const lastIndex = this.results.length - 1; - - /* The Combo only continues if the latest roll was higher or equal to the previous */ - if (this.results[lastIndex].result < this.results[lastIndex - 1].result) return false; - - const lastFaces = this.results[lastIndex].denomination?.slice(1) ?? this.faces; - const increaseDiceSize = maxIncreasesDiceSize && lastFaces < 12 && this.results[lastIndex].result === lastFaces; - const denomination = increaseDiceSize ? adjustDice(this.denomination, false) : this.denomination; - - const newRoll = await (new Roll(`1${denomination}`)).evaluate(); - this.results.push({ result: newRoll.total, denomination: denomination, active: true }); - this.number += 1; - - return this.continueCombo(maxIncreasesDiceSize); + if (['c', 'cc'].some(x => this.modifiers.includes(x))) { + await this.handleComboDiceReroll(resultIndex); + } } /** @override */ @@ -99,4 +41,146 @@ export default class BaseDie extends foundry.dice.terms.Die { !(hasSuccess || hasFailure) && isMax ? 'max' : null ]; } + + /* -------------------------------------------- */ + /* Modifier Logic */ + /* -------------------------------------------- */ + + async compoundComboDice() { + return this.handleComboDice({ maxIncreasesDiceSize: true}); + } + + async comboDice() { + return this.handleComboDice({ maxIncreasesDiceSize: false }); + } + + async handleComboDice(maxIncreasesDiceSize) { + /* ComboDice only works with exactly two dice and both have to be the same denomination */ + if (this.number !== 2) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.comboDiceOnlyTwoDiceError')); + return false; + } + + return this.rollComboDice(maxIncreasesDiceSize); + } + + async rollComboDice(options) { + const { maxIncreasesDiceSize, rerollStartIndex } = options; + const initialResultsLength = this.results.length; + const result = await this.continueCombo(maxIncreasesDiceSize); + + /* The flow of DiceSoNice has no way of knowing that some of the results of a Die should be a different denomination + We solve this by marking the results as hidden so they're not picked up by the auto roll of DiceSoNice. + The actual rolls are done here in place so every dice gets the correct denomination. + */ + if (game.modules.get('dice-so-nice')?.active) { + const resultsToRoll = this.results.filter((x, index) => { + if (!x.active) return false; + if (rerollStartIndex) + return index === rerollStartIndex || index > initialResultsLength - 1; + + return true; + }); + const rolls = []; + for (const result of resultsToRoll) { + const roll = await (new Roll(`1${result.denomination ?? this.denomination}`)).evaluate(); + roll.terms[0].results = [result]; + roll._evaluateTotal(); + rolls.push(roll); + } + + /* If there are other dice that will be rolled we cannot await here. The other dice will be awaited in the normal flow */ + if (rerollStartIndex === undefined && this._root.dice.length > 1) { + for (const roll of rolls) { + game.dice3d.showForRoll(roll, game.user, true); + } + } else { + await Promise.allSettled(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true))); + } + } + + for (const result of this.results) + result.hidden = true; + + const modifier = maxIncreasesDiceSize ? 'cc' : 'c'; + if (!this.modifiers.includes(modifier)) + this.modifiers.push(modifier); + + return result; + } + + async continueCombo(maxIncreasesDiceSize) { + const activeResults = this.results.filter(x => x.active); + const lastIndex = activeResults.length - 1; + + /* The Combo only continues if the latest roll was higher or equal to the previous */ + if (activeResults[lastIndex].result < activeResults[lastIndex - 1].result) return false; + + const lastFaces = activeResults[lastIndex].denomination?.slice(1) ?? this.faces; + const increaseDiceSize = + maxIncreasesDiceSize && lastFaces < 12 && + activeResults[lastIndex].result === lastFaces; + const denomination = increaseDiceSize ? adjustDice(this.denomination, false) : this.denomination; + + const newRoll = await (new Roll(`1${denomination}`)).evaluate(); + this.results.push({ result: newRoll.total, denomination: denomination, active: true }); + this.number += 1; + + return this.continueCombo(maxIncreasesDiceSize); + } + + async handleComboDiceReroll(rerolledIndex) { + const resultGroupingIndexes = this.results.map((x, index) => ({ index, active: x.active })) + .filter(x => x.active).map(x => x.index); + if (resultGroupingIndexes.length <= 1) return; + + const rerolledResult = this.results[rerolledIndex]; + const compoundCombo = this.modifiers.includes('cc'); + const rerollGroupingIndex = resultGroupingIndexes.indexOf(rerolledIndex); + + const previousIndex = resultGroupingIndexes[rerollGroupingIndex - 1]; + const previousResult = this.results[previousIndex]; + const nextIndex = resultGroupingIndexes[rerollGroupingIndex + 1]; + const nextResult = this.results[nextIndex]; + + /* Rerolling any of the last two dice might introduce new results */ + if ( + rerollGroupingIndex === resultGroupingIndexes.length - 1 && + rerolledResult.result >= previousResult?.result + ) { + return await this.rollComboDice({ + maxIncreasesDiceSize: compoundCombo, + rerollStartIndex: rerollGroupingIndex + }); + } else if ( + rerollGroupingIndex === resultGroupingIndexes.length - 2 && + rerolledResult.result < nextResult?.result + ) { + return await this.rollComboDice({ + maxIncreasesDiceSize: compoundCombo, + rerollStartIndex: rerollGroupingIndex + }); + } + /* Rerolling a preceeding dice might invalidate later dice which should then be dropped */ + else if (rerolledResult.result < previousResult?.result){ + const cutoffIndex = rerollGroupingIndex === 0 ? resultGroupingIndexes[1] + 1 : rerolledIndex + 1; + this.results = this.results.slice(0, cutoffIndex); + this.number = this.results.filter(x => x.active).length; + } + + const fakeRollFaces = + rerolledResult.denomination ? rerolledResult.denomination.slice(1) : this.faces; + const fakeRoll = { + _evaluated: true, + dice: [new foundry.dice.terms.Die({ + ...this, + results: [rerolledResult], + total: rerolledResult.result, + faces: fakeRollFaces + })], + options: { appearance: {} } + }; + await triggerChatRollFx([fakeRoll]); + rerolledResult.hidden = true; + } } \ No newline at end of file