mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-07-23 10:59:54 +02:00
[Feature] ComboDice Modifier (#2061)
This commit is contained in:
parent
7aa4101889
commit
b7f03f7a2d
7 changed files with 208 additions and 19 deletions
|
|
@ -3289,6 +3289,7 @@
|
||||||
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
|
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
|
||||||
"noTokenTargeted": "No token is targeted",
|
"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",
|
||||||
"reloadRequired": "The {weapon} must be reloaded to be used!"
|
"reloadRequired": "The {weapon} must be reloaded to be used!"
|
||||||
},
|
},
|
||||||
"Progress": {
|
"Progress": {
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ export { default as DamageRoll } from './damageRoll.mjs';
|
||||||
export { default as DHRoll } from './dhRoll.mjs';
|
export { default as DHRoll } from './dhRoll.mjs';
|
||||||
export { default as DualityRoll } from './dualityRoll.mjs';
|
export { default as DualityRoll } from './dualityRoll.mjs';
|
||||||
export { default as FateRoll } from './fateRoll.mjs';
|
export { default as FateRoll } from './fateRoll.mjs';
|
||||||
|
export { BaseDie } from './die/_module.mjs';
|
||||||
export { diceTypes } from './die/_module.mjs';
|
export { diceTypes } from './die/_module.mjs';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
|
import { adjustDice, triggerChatRollFx } from '../../helpers/utils.mjs';
|
||||||
|
|
||||||
export default class BaseDie extends foundry.dice.terms.Die {
|
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];
|
const result = this.results[resultIndex];
|
||||||
result.rerolled = true;
|
result.rerolled = true;
|
||||||
result.active = false;
|
result.active = false;
|
||||||
|
|
@ -8,5 +17,174 @@ export default class BaseDie extends foundry.dice.terms.Die {
|
||||||
const rerolledResult = this.results[this.results.length - 1];
|
const rerolledResult = this.results[this.results.length - 1];
|
||||||
this.results.splice(this.results.length - 1, 1);
|
this.results.splice(this.results.length - 1, 1);
|
||||||
this.results.splice(resultIndex, 0, rerolledResult);
|
this.results.splice(resultIndex, 0, rerolledResult);
|
||||||
|
|
||||||
|
if (['c', 'cc'].some(x => this.modifiers.includes(x))) {
|
||||||
|
await this.handleComboDiceReroll(resultIndex, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
getResultCSS(result) {
|
||||||
|
// Accomodating ComboDie as a result can have a different denomination than the die as a whole
|
||||||
|
const css = super.getResultCSS(result);
|
||||||
|
const idx = css.findIndex(c => /d\d+/.test(c));
|
||||||
|
css[idx] = result.denomination ?? this.denomination;
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
/* 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) =>
|
||||||
|
x.active && (!rerollStartIndex || index === rerollStartIndex || index > initialResultsLength - 1));
|
||||||
|
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 */
|
||||||
|
const promises = rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true));
|
||||||
|
if (rerollStartIndex !== undefined || this._root.dice.length <= 1) {
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */
|
||||||
|
const isFinalHigher =
|
||||||
|
rerollGroupingIndex === resultGroupingIndexes.length - 1 && rerolledResult.result >= previousResult?.result;
|
||||||
|
const isSemifinalLower =
|
||||||
|
rerollGroupingIndex === resultGroupingIndexes.length - 2 && rerolledResult.result < nextResult?.result;
|
||||||
|
if (isFinalHigher || isSemifinalLower) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import BaseDie from './baseDie.mjs';
|
|
||||||
import { updateResourcesForDualityReroll } from '../helpers.mjs';
|
import { updateResourcesForDualityReroll } from '../helpers.mjs';
|
||||||
|
import BaseDie from './baseDie.mjs';
|
||||||
|
|
||||||
export default class DualityDie extends BaseDie {
|
export default class DualityDie extends BaseDie {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,20 @@ export default class RegisterHandlebarsHelpers {
|
||||||
static registerHelpers() {
|
static registerHelpers() {
|
||||||
Handlebars.registerHelper({
|
Handlebars.registerHelper({
|
||||||
add: this.add,
|
add: this.add,
|
||||||
includes: this.includes,
|
coalesce: this.coalesce,
|
||||||
times: this.times,
|
|
||||||
damageFormula: this.damageFormula,
|
damageFormula: this.damageFormula,
|
||||||
formulaValue: this.formulaValue,
|
|
||||||
damageSymbols: this.damageSymbols,
|
damageSymbols: this.damageSymbols,
|
||||||
rollParsed: this.rollParsed,
|
|
||||||
hasProperty: foundry.utils.hasProperty,
|
|
||||||
getProperty: foundry.utils.getProperty,
|
|
||||||
setVar: this.setVar,
|
|
||||||
empty: this.empty,
|
empty: this.empty,
|
||||||
|
formulaValue: this.formulaValue,
|
||||||
|
getProperty: foundry.utils.getProperty,
|
||||||
|
hasProperty: foundry.utils.hasProperty,
|
||||||
|
includes: this.includes,
|
||||||
|
isNullish: this.isNullish,
|
||||||
pluralize: this.pluralize,
|
pluralize: this.pluralize,
|
||||||
positive: this.positive,
|
positive: this.positive,
|
||||||
isNullish: this.isNullish
|
rollParsed: this.rollParsed,
|
||||||
|
setVar: this.setVar,
|
||||||
|
times: this.times
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
static add(a, b) {
|
static add(a, b) {
|
||||||
|
|
@ -25,6 +26,10 @@ export default class RegisterHandlebarsHelpers {
|
||||||
return (Number.isNaN(aNum) ? 0 : aNum) + (Number.isNaN(bNum) ? 0 : bNum);
|
return (Number.isNaN(aNum) ? 0 : aNum) + (Number.isNaN(bNum) ? 0 : bNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static coalesce(...args) {
|
||||||
|
return args.find(a => a !== undefined && a !== null);
|
||||||
|
}
|
||||||
|
|
||||||
static includes(list, item) {
|
static includes(list, item) {
|
||||||
return list.includes(item);
|
return list.includes(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,11 @@
|
||||||
{{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }}
|
{{> damage label=(localize "DAGGERHEART.GENERAL.damage") roll=damage.main isDirect=isDirect }}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#each damage.resources as | roll index | }}
|
{{#each damage.resources as |roll index| }}
|
||||||
{{> damage
|
{{> damage
|
||||||
label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')))
|
label=(ifThen ../hasHealing (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.name')) (localize (concat 'DAGGERHEART.CONFIG.HealingType.' index '.inChatRoll')))
|
||||||
roll=roll
|
roll=roll
|
||||||
isResource=true
|
resource=index
|
||||||
}}
|
}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,13 +57,13 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="roll-dice">
|
<div class="roll-dice">
|
||||||
{{#if roll.dice.length}}
|
{{#if roll.dice.length}}
|
||||||
{{#each roll.dice}}
|
{{#each roll.dice as |dice diceKey|}}
|
||||||
{{#each results}}
|
{{#each results as |dieResult resultKey|}}
|
||||||
{{#if active}}
|
{{#if active}}
|
||||||
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
|
<div class="roll-die{{#unless @../first}} has-plus{{/unless}}">
|
||||||
<div
|
<div
|
||||||
class="dice reroll-button {{../denomination}}"
|
class="dice reroll-button {{coalesce dieResult.denomination dice.denomination}}"
|
||||||
data-type="damage" data-damage-type="{{@../../key}}" data-dice="{{@../key}}" data-result="{{@key}}" {{#if ../../isResource}}data-is-resource="true"{{/if}}
|
data-type="damage" data-dice="{{diceKey}}" data-result="{{resultKey}}" {{#if ../../resource}}data-damage-type="{{../../resource}}" data-is-resource="true"{{/if}}
|
||||||
>
|
>
|
||||||
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
|
{{#if hasRerolls}}<i class="fa-solid fa-dice dice-rerolled" data-tooltip="{{localize "DAGGERHEART.GENERAL.rerolled"}}"></i>{{/if}}
|
||||||
{{result}}
|
{{result}}
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,14 @@
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#each roll.dice}}
|
{{#each roll.dice as |dice|}}
|
||||||
{{#each results}}
|
{{#each results as |dieResult|}}
|
||||||
<div class="roll-die {{#unless (or @../first discarded)}} has-plus{{/unless}}">
|
<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
|
||||||
|
{{coalesce dieResult.denomination 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}}
|
||||||
|
">
|
||||||
{{result}}
|
{{result}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue