mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-11 19:25:21 +01:00
316 lines
11 KiB
JavaScript
316 lines
11 KiB
JavaScript
import D20RollDialog from '../applications/dialogs/d20RollDialog.mjs';
|
|
|
|
export default class DHRoll extends Roll {
|
|
baseTerms = [];
|
|
constructor(formula, data = {}, options = {}) {
|
|
super(formula, data, options);
|
|
if (!this.data || !Object.keys(this.data).length) this.data = options.data;
|
|
}
|
|
|
|
get title() {
|
|
return game.i18n.localize('DAGGERHEART.GENERAL.Roll.basic');
|
|
}
|
|
|
|
static messageType = 'adversaryRoll';
|
|
|
|
static CHAT_TEMPLATE = 'systems/daggerheart/templates/ui/chat/roll.hbs';
|
|
|
|
static DefaultDialog = D20RollDialog;
|
|
|
|
static async build(config = {}, message = {}) {
|
|
const roll = await this.buildConfigure(config, message);
|
|
if (!roll) return;
|
|
await this.buildEvaluate(roll, config, (message = {}));
|
|
await this.buildPost(roll, config, (message = {}));
|
|
return config;
|
|
}
|
|
|
|
static async buildConfigure(config = {}, message = {}) {
|
|
config.hooks = [...this.getHooks(), ''];
|
|
config.dialog ??= {};
|
|
|
|
const actorIdSplit = config.source?.actor?.split('.');
|
|
if (actorIdSplit) {
|
|
const tagTeamSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.TagTeamRoll);
|
|
config.tagTeamSelected = Boolean(tagTeamSettings.members[actorIdSplit[actorIdSplit.length - 1]]);
|
|
}
|
|
|
|
for (const hook of config.hooks) {
|
|
if (Hooks.call(`${CONFIG.DH.id}.preRoll${hook.capitalize()}`, config, message) === false) return null;
|
|
}
|
|
|
|
this.applyKeybindings(config);
|
|
|
|
this.temporaryModifierBuilder(config);
|
|
|
|
let roll = new this(config.roll.formula, config.data, config);
|
|
if (config.dialog.configure !== false) {
|
|
// Open Roll Dialog
|
|
const DialogClass = config.dialog?.class ?? this.DefaultDialog;
|
|
const configDialog = await DialogClass.configure(roll, config, message);
|
|
if (!configDialog) return;
|
|
}
|
|
|
|
for (const hook of config.hooks) {
|
|
if (
|
|
Hooks.call(`${CONFIG.DH.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false
|
|
)
|
|
return [];
|
|
}
|
|
return roll;
|
|
}
|
|
|
|
static async buildEvaluate(roll, config = {}, message = {}) {
|
|
if (config.evaluate !== false) {
|
|
await roll.evaluate();
|
|
config.roll = this.postEvaluate(roll, config);
|
|
}
|
|
}
|
|
|
|
static async buildPost(roll, config, message) {
|
|
for (const hook of config.hooks) {
|
|
if (Hooks.call(`${CONFIG.DH.id}.postRoll${hook.capitalize()}`, config, message) === false) return null;
|
|
}
|
|
|
|
if (config.skips?.createMessage) {
|
|
if (game.modules.get('dice-so-nice')?.active) {
|
|
await game.dice3d.showForRoll(roll, game.user, true);
|
|
}
|
|
} else if (!config.source?.message) {
|
|
config.message = await this.toMessage(roll, config);
|
|
}
|
|
}
|
|
|
|
static postEvaluate(roll, config = {}) {
|
|
return {
|
|
total: roll.total,
|
|
formula: roll.formula,
|
|
dice: roll.dice.map(d => ({
|
|
dice: d.denomination,
|
|
total: d.total,
|
|
formula: d.formula,
|
|
results: d.results
|
|
}))
|
|
};
|
|
}
|
|
|
|
static async toMessage(roll, config) {
|
|
const cls = getDocumentClass('ChatMessage'),
|
|
msgData = {
|
|
type: this.messageType,
|
|
user: game.user.id,
|
|
title: roll.title,
|
|
speaker: cls.getSpeaker({ actor: roll.data?.parent }),
|
|
sound: config.mute ? null : CONFIG.sounds.dice,
|
|
system: config,
|
|
rolls: [roll]
|
|
};
|
|
|
|
config.selectedRollMode ??= game.settings.get('core', 'rollMode');
|
|
|
|
if (roll._evaluated) {
|
|
const message = await cls.create(msgData, { rollMode: config.selectedRollMode });
|
|
|
|
if (config.tagTeamSelected) {
|
|
game.system.api.applications.dialogs.TagTeamDialog.assignRoll(message.speakerActor, message);
|
|
}
|
|
|
|
if (roll.formula !== '' && game.modules.get('dice-so-nice')?.active) {
|
|
await game.dice3d.waitFor3DAnimationByMessageID(message.id);
|
|
}
|
|
|
|
return message;
|
|
} else return msgData;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
async render({ flavor, template = this.constructor.CHAT_TEMPLATE, isPrivate = false, ...options } = {}) {
|
|
if (!this._evaluated) return;
|
|
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
|
|
return foundry.applications.handlebars.renderTemplate(template, chatData);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
async _prepareChatRenderContext({ flavor, isPrivate = false, ...options } = {}) {
|
|
if (isPrivate) {
|
|
return {
|
|
user: game.user.id,
|
|
flavor: null,
|
|
title: '???',
|
|
roll: {
|
|
total: '??'
|
|
},
|
|
hasRoll: true,
|
|
isPrivate
|
|
};
|
|
} else {
|
|
options.message.system.user = game.user.id;
|
|
return options.message.system;
|
|
}
|
|
}
|
|
|
|
static applyKeybindings(config) {
|
|
if (config.event)
|
|
config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey);
|
|
}
|
|
|
|
static getHooks(hooks) {
|
|
return hooks ?? [];
|
|
}
|
|
|
|
formatModifier(modifier) {
|
|
if (Array.isArray(modifier)) {
|
|
return [
|
|
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
|
|
...this.constructor.parse(modifier.join(' + '), this.options.data)
|
|
];
|
|
} else {
|
|
const numTerm = modifier < 0 ? '-' : '+';
|
|
return [
|
|
new foundry.dice.terms.OperatorTerm({ operator: numTerm }),
|
|
new foundry.dice.terms.NumericTerm({ number: Math.abs(modifier) })
|
|
];
|
|
}
|
|
}
|
|
|
|
applyBaseBonus() {
|
|
return [];
|
|
}
|
|
|
|
addModifiers(roll) {
|
|
roll = roll ?? this.options.roll;
|
|
roll.modifiers?.forEach(m => {
|
|
this.terms.push(...this.formatModifier(m.value));
|
|
});
|
|
}
|
|
|
|
getBonus(path, label) {
|
|
const bonus = foundry.utils.getProperty(this.data.bonuses, path),
|
|
modifiers = [];
|
|
if (bonus?.bonus)
|
|
modifiers.push({
|
|
label: label,
|
|
value: bonus?.bonus
|
|
});
|
|
if (bonus?.dice?.length)
|
|
modifiers.push({
|
|
label: label,
|
|
value: bonus?.dice
|
|
});
|
|
return modifiers;
|
|
}
|
|
|
|
getFaces(faces) {
|
|
return Number(faces.startsWith('d') ? faces.replace('d', '') : faces);
|
|
}
|
|
|
|
constructFormula(config) {
|
|
this.terms = Roll.parse(this.options.roll.formula, config.data);
|
|
|
|
this.options.roll.modifiers = this.applyBaseBonus();
|
|
this.addModifiers();
|
|
|
|
if (this.options.extraFormula) {
|
|
this.terms.push(
|
|
new foundry.dice.terms.OperatorTerm({ operator: '+' }),
|
|
...this.constructor.parse(this.options.extraFormula, this.options.data)
|
|
);
|
|
}
|
|
return (this._formula = this.constructor.getFormula(this.terms));
|
|
}
|
|
|
|
static calculateTotalModifiers(roll) {
|
|
let modifierTotal = 0;
|
|
for (let i = 0; i < roll.terms.length; i++) {
|
|
if (
|
|
roll.terms[i] instanceof foundry.dice.terms.NumericTerm &&
|
|
!!roll.terms[i - 1] &&
|
|
roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm
|
|
)
|
|
modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`);
|
|
}
|
|
return modifierTotal;
|
|
}
|
|
|
|
static temporaryModifierBuilder(config) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function automateHopeFear(config) {
|
|
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
|
const hopeFearAutomation = automationSettings.hopeFear;
|
|
if (!config.source?.actor ||
|
|
(game.user.isGM ? !hopeFearAutomation.gm : !hopeFearAutomation.players) ||
|
|
config.actionType === 'reaction' ||
|
|
config.tagTeamSelected ||
|
|
config.skips?.resources)
|
|
return;
|
|
const actor = await fromUuid(config.source.actor);
|
|
let updates = [];
|
|
if (!actor) return;
|
|
|
|
if (config.rerolledRoll) {
|
|
if (config.roll.result.duality != config.rerolledRoll.result.duality) {
|
|
const hope = (config.roll.isCritical || config.roll.result.duality === 1 ? 1 : 0)
|
|
- (config.rerolledRoll.isCritical || config.rerolledRoll.result.duality === 1 ? 1 : 0);
|
|
const stress = (config.roll.isCritical ? 1 : 0) - (config.rerolledRoll.isCritical ? 1 : 0);
|
|
const fear = (config.roll.result.duality === -1 ? 1 : 0)
|
|
- (config.rerolledRoll.result.duality === -1 ? 1 : 0)
|
|
|
|
if (hope !== 0)
|
|
updates.push({ key: 'hope', value: hope, total: -1 * hope, enabled: true });
|
|
if (stress !== 0)
|
|
updates.push({ key: 'stress', value: -1 * stress, total: stress, enabled: true });
|
|
if (fear !== 0)
|
|
updates.push({ key: 'fear', value: fear, total: -1 * fear, enabled: true });
|
|
}
|
|
} else {
|
|
if (config.roll.isCritical || config.roll.result.duality === 1)
|
|
updates.push({ key: 'hope', value: 1, total: -1, enabled: true });
|
|
if (config.roll.isCritical)
|
|
updates.push({ key: 'stress', value: -1, total: 1, enabled: true });
|
|
if (config.roll.result.duality === -1)
|
|
updates.push({ key: 'fear', value: 1, total: -1, enabled: true });
|
|
}
|
|
|
|
if (updates.length) {
|
|
const target = actor.system.partner ?? actor;
|
|
if (!['dead', 'defeated', 'unconscious'].some(x => actor.statuses.has(x))) {
|
|
await target.modifyResource(updates);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const registerRollDiceHooks = () => {
|
|
Hooks.on(`${CONFIG.DH.id}.postRollDuality`, async (config, message) => {
|
|
const automationSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation);
|
|
if (
|
|
automationSettings.countdownAutomation &&
|
|
config.actionType !== CONFIG.DH.ITEM.actionTypes.reaction.id &&
|
|
!config.tagTeamSelected &&
|
|
!config.skips?.updateCountdowns
|
|
) {
|
|
const { updateCountdowns } = game.system.api.applications.ui.DhCountdowns;
|
|
|
|
if (config.roll.result.duality === -1) {
|
|
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id,
|
|
CONFIG.DH.GENERAL.countdownProgressionTypes.fear.id);
|
|
} else {
|
|
await updateCountdowns(CONFIG.DH.GENERAL.countdownProgressionTypes.actionRoll.id);
|
|
}
|
|
}
|
|
|
|
await automateHopeFear(config);
|
|
|
|
if (!config.roll.hasOwnProperty('success') && !config.targets?.length) return;
|
|
|
|
const rollResult = config.roll.success || config.targets.some(t => t.hit),
|
|
looseSpotlight = !rollResult || config.roll.result.duality === -1;
|
|
|
|
if (looseSpotlight && game.combat?.active) {
|
|
const currentCombatant = game.combat.combatants.get(game.combat.current?.combatantId);
|
|
if (currentCombatant?.actorId == actor.id) ui.combat.setCombatantSpotlight(currentCombatant.id);
|
|
}
|
|
});
|
|
};
|