This commit is contained in:
WBHarry 2026-07-20 14:27:43 +00:00 committed by GitHub
commit 15b67d016c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 215 additions and 5 deletions

View file

@ -3258,7 +3258,8 @@
"knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM",
"comboDiceOnlyTwoDiceError": "Combo dice functionality only works on a single pair of two dice"
},
"Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry."

View file

@ -4,4 +4,5 @@ export { default as DamageRoll } from './damageRoll.mjs';
export { default as DHRoll } from './dhRoll.mjs';
export { default as DualityRoll } from './dualityRoll.mjs';
export { default as FateRoll } from './fateRoll.mjs';
export { BaseDie } from './die/_module.mjs';
export { diceTypes } from './die/_module.mjs';

View file

@ -1,5 +1,14 @@
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;
@ -8,5 +17,199 @@ 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);
if (['c', 'cc'].some(x => this.modifiers.includes(x))) {
await this.handleComboDiceReroll(resultIndex, result);
}
}
/** @override */
getResultCSS(result) {
const hasSuccess = result.success !== undefined;
const hasFailure = result.failure !== undefined;
const isMax = result.result === this.faces;
const isMin = result.result === 1;
return [
this.constructor.name.toLowerCase(),
result.denomination ?? this.denomination, // Accomodating ComboDie as a result can have a different denomination than the die as a whole
result.success ? 'success' : null,
result.failure ? 'failure' : null,
result.rerolled ? 'rerolled' : null,
result.exploded ? 'exploded' : null,
result.discarded ? 'discarded' : null,
!(hasSuccess || hasFailure) && isMin ? 'min' : null,
!(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.filter(x => x.active).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;
const lastResult = activeResults[lastIndex];
/* The Combo only continues if the latest roll was higher or equal to the previous */
if (lastResult.result < activeResults[lastIndex - 1].result) return false;
const lastFaces = lastResult.denomination ?
Number(lastResult.denomination.slice(1)) : this.faces;
const currentDenomination = `d${lastFaces}`
const increaseDiceSize =
maxIncreasesDiceSize && lastFaces < 12 &&
lastResult.result === lastFaces;
const denomination = increaseDiceSize ? adjustDice(currentDenomination, false) : currentDenomination;
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, originalResult) {
/* Potentially sliced when correcting compound dice at (1) */
let 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];
const dropDice = preceeding => {
const cutoffIndex = preceeding ? resultGroupingIndexes[rerollGroupingIndex + 1] + 1 : rerolledIndex + 1;
this.results = this.results.slice(0, cutoffIndex);
this.number = this.results.filter(x => x.active).length;
};
/* (1) If subsequent size increases from the compound effect are now incorrect, drop subsequent dice */
const lastFaces = Number((originalResult.denomination ?? this.denomination).slice(1));
if (
compoundCombo &&
(
(originalResult.result === lastFaces &&
rerolledResult.result !== lastFaces)
||
(originalResult.result !== lastFaces &&
rerolledResult.result === lastFaces)
)
) {
dropDice(false);
resultGroupingIndexes = resultGroupingIndexes.slice(0, this.number);
}
/* (2) 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
});
}
/* (3) Rerolling a subsequent dice might invalidate later dice which should then be dropped */
else if (rerolledResult.result < previousResult?.result){
dropDice(false);
}
/* (4) Rerolling a preceeding dice might invalidate later dice which should then be dropped */
else if (rerolledResult.result >= nextResult?.result) {
dropDice(true);
}
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;
}
}

View file

@ -1,5 +1,5 @@
import BaseDie from './baseDie.mjs';
import { updateResourcesForDualityReroll } from '../helpers.mjs';
import BaseDie from './baseDie.mjs';
export default class DualityDie extends BaseDie {
constructor(options) {

View file

@ -62,7 +62,7 @@
{{#if active}}
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
<div
class="dice reroll-button {{../denomination}}"
class="dice reroll-button {{ifThen denomination denomination ../denomination}}"
data-type="damage" data-damage-type="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}" {{#if ../../isResource}}data-is-resource="true"{{/if}}
>
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}

View file

@ -84,8 +84,13 @@
{{else}}
{{#each roll.dice}}
{{#each results}}
{{debug this}}
<div class="roll-die {{#unless (or @../first discarded)}} has-plus{{/unless}}">
<div class="dice {{../denomination}}{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}">
<div class="
dice
{{ifThen denomination denomination ../denomination}}
{{#if discarded}} discarded{{else}}{{#if (and @../first ../../roll.advantage.type)}}{{#if (eq ../../roll.advantage.type 1)}} color-adv{{else}} color-dis{{/if}}{{/if}}{{#if success}} color-adv{{/if}}{{/if}}
">
{{result}}
</div>
</div>