import DamageDialog from '../applications/dialogs/damageDialog.mjs'; import { RefreshType, socketEvent } from '../systemRegistration/socket.mjs'; import DHRoll from './dhRoll.mjs'; export default class DamageRoll extends DHRoll { constructor(formula, data = {}, options = {}) { super(formula, data, options); } static DefaultDialog = DamageDialog; static async buildEvaluate(roll, config = {}, message = {}) { if (config.dialog.configure === false) roll.constructFormula(config); if (config.evaluate !== false) for (const roll of config.roll) await roll.roll.evaluate(); roll._evaluated = true; const parts = config.roll.map(r => this.postEvaluate(r)); config.damage = this.unifyDamageRoll(parts); } static postEvaluate(roll, config = {}) { return { ...roll, ...super.postEvaluate(roll.roll, config), damageTypes: [...(roll.damageTypes ?? [])], roll: roll.roll, type: config.type, modifierTotal: this.calculateTotalModifiers(roll.roll) }; } static async buildPost(roll, config, message) { const chatMessage = config.source?.message ? ui.chat.collection.get(config.source.message) : getDocumentClass('ChatMessage').applyRollMode({}, config.rollMode); if (game.modules.get('dice-so-nice')?.active) { const pool = foundry.dice.terms.PoolTerm.fromRolls( Object.values(config.damage).flatMap(r => r.parts.map(p => p.roll)) ), diceRoll = Roll.fromTerms([pool]); await game.dice3d.showForRoll( diceRoll, game.user, true, chatMessage.whisper?.length > 0 ? chatMessage.whisper : null, chatMessage.blind ); } await super.buildPost(roll, config, message); if (config.source?.message) chatMessage.update({ 'system.damage': config.damage }); } static unifyDamageRoll(rolls) { const unified = {}; rolls.forEach(r => { const resource = unified[r.applyTo] ?? { formula: '', total: 0, parts: [] }; resource.formula += `${resource.formula !== '' ? ' + ' : ''}${r.formula}`; resource.total += r.total; resource.parts.push(r); unified[r.applyTo] = resource; }); return unified; } static formatGlobal(rolls) { let formula, total; const applyTo = new Set(rolls.flatMap(r => r.applyTo)); if (applyTo.size > 1) { const data = {}; rolls.forEach(r => { if (data[r.applyTo]) { data[r.applyTo].formula += ` + ${r.formula}`; data[r.applyTo].total += r.total; } else { data[r.applyTo] = { formula: r.formula, total: r.total }; } }); formula = Object.entries(data).reduce((a, [k, v]) => a + ` ${k}: ${v.formula}`, ''); total = Object.entries(data).reduce((a, [k, v]) => a + ` ${k}: ${v.total}`, ''); } else { formula = rolls.map(r => r.formula).join(' + '); total = rolls.reduce((a, c) => a + c.total, 0); } return { formula, total }; } applyBaseBonus(part) { const modifiers = [], type = this.options.messageType ?? (this.options.hasHealing ? 'healing' : 'damage'), options = part ?? this.options; modifiers.push(...this.getBonus(`${type}`, `${type.capitalize()} Bonus`)); if (!this.options.hasHealing) { options.damageTypes?.forEach(t => { modifiers.push(...this.getBonus(`${type}.${t}`, `${t.capitalize()} ${type.capitalize()} Bonus`)); }); const weapons = ['primaryWeapon', 'secondaryWeapon']; weapons.forEach(w => { if (this.options.source.item && this.options.source.item === this.data[w]?.id) modifiers.push(...this.getBonus(`${type}.${w}`, 'Weapon Bonus')); }); } return modifiers; } constructFormula(config) { this.options.roll.forEach((part, index) => { part.roll = new Roll(Roll.replaceFormulaData(part.formula, config.data)); this.constructFormulaPart(config, part, index); }); return this.options.roll; } constructFormulaPart(config, part, index) { part.roll.terms = Roll.parse(part.roll.formula, config.data); if (part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { part.modifiers = this.applyBaseBonus(part); this.addModifiers(part); part.modifiers?.forEach(m => { part.roll.terms.push(...this.formatModifier(m.value)); }); } /* To Remove When Reaction System */ if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { for (const mod in config.modifiers) { const modifier = config.modifiers[mod]; if (modifier.beforeCrit === true && (modifier.enabled || modifier.value)) modifier.callback(part); } } if (part.extraFormula) { part.roll.terms.push( new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.constructor.parse(part.extraFormula, this.options.data) ); } if (config.isCritical && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { const total = part.roll.dice.reduce((acc, term) => acc + term._faces * term._number, 0); if (total > 0) { part.roll.terms.push(...this.formatModifier(total)); } } /* To Remove When Reaction System */ if (index === 0 && part.applyTo === CONFIG.DH.GENERAL.healingTypes.hitPoints.id) { for (const mod in config.modifiers) { const modifier = config.modifiers[mod]; if (!modifier.beforeCrit && (modifier.enabled || modifier.value)) modifier.callback(part); } } return (part.roll._formula = this.constructor.getFormula(part.roll.terms)); } /* To Remove When Reaction System */ static temporaryModifierBuilder(config) { const mods = {}; if (config.data?.parent) { if (config.data.parent.appliedEffects) { // Bardic Rally const rallyChoices = config.data?.parent?.appliedEffects.reduce((a, c) => { const change = c.changes.find(ch => ch.key === 'system.bonuses.rally'); if (change) a.push({ value: c.id, label: change.value }); return a; }, []); if (rallyChoices.length) { mods.rally = { label: 'DAGGERHEART.CLASS.Feature.rallyDice', values: rallyChoices, value: null, beforeCrit: true, callback: part => { const rallyFaces = config.modifiers.rally.values.find( r => r.value === config.modifiers.rally.value )?.label; part.roll.terms.push( new foundry.dice.terms.OperatorTerm({ operator: '+' }), ...this.parse(`1${rallyFaces}`) ); } }; } } const item = config.data.parent.items?.get(config.source.item); if (item) { // Massive (Weapon Feature) if (item.system.itemFeatures.find(f => f.value === 'massive')) mods.massive = { label: CONFIG.DH.ITEM.weaponFeatures.massive.label, enabled: true, callback: part => { part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); part.roll.terms[0].number += 1; } }; // Powerful (Weapon Feature) if (item.system.itemFeatures.find(f => f.value === 'powerful')) mods.powerful = { label: CONFIG.DH.ITEM.weaponFeatures.powerful.label, enabled: true, callback: part => { part.roll.terms[0].modifiers.push(`kh${part.roll.terms[0].number}`); part.roll.terms[0].number += 1; } }; // Brutal (Weapon Feature) if (item.system.itemFeatures.find(f => f.value === 'brutal')) mods.brutal = { label: CONFIG.DH.ITEM.weaponFeatures.brutal.label, enabled: true, beforeCrit: true, callback: part => { part.roll.terms[0].modifiers.push(`x${part.roll.terms[0].faces}`); } }; // Serrated (Weapon Feature) if (item.system.itemFeatures.find(f => f.value === 'serrated')) mods.serrated = { label: CONFIG.DH.ITEM.weaponFeatures.serrated.label, enabled: true, callback: part => { part.roll.terms[0].modifiers.push(`sc8`); } }; // Self-Correcting (Weapon Feature) if (item.system.itemFeatures.find(f => f.value === 'selfCorrecting')) mods.selfCorrecting = { label: CONFIG.DH.ITEM.weaponFeatures.selfCorrecting.label, enabled: true, callback: part => { part.roll.terms[0].modifiers.push(`sc6`); } }; } } config.modifiers = mods; return mods; } static async reroll(target, message) { const { damageType, part, dice, result } = target.dataset; const rollPart = message.system.damage[damageType].parts[part]; let diceIndex = 0; let parsedRoll = game.system.api.dice.DamageRoll.fromData({ ...rollPart.roll, terms: rollPart.roll.terms.map(term => { const isDie = term.class === 'Die'; const fixedTerm = { ...term, ...(isDie ? { results: rollPart.dice[diceIndex].results } : {}) }; if (isDie) diceIndex++; return fixedTerm; }), class: 'DamageRoll', evaluated: false }); const parsedDiceTerms = Object.keys(parsedRoll.terms).reduce((acc, key) => { const term = parsedRoll.terms[key]; if (term instanceof CONFIG.Dice.termTypes.DiceTerm) acc[Object.keys(acc).length] = term; return acc; }, {}); const term = parsedDiceTerms[dice]; const termResult = parsedDiceTerms[dice].results[result]; const newIndex = parsedDiceTerms[dice].results.length; await term.reroll(`/r1=${termResult.result}`); if (game.modules.get('dice-so-nice')?.active) { const newResult = parsedDiceTerms[dice].results[newIndex]; const diceSoNiceRoll = { _evaluated: true, dice: [ new foundry.dice.terms.Die({ ...term, total: newResult.result, faces: term._faces, results: [newResult] }) ], options: { appearance: {} } }; await game.dice3d.showForRoll(diceSoNiceRoll, game.user, true); } await parsedRoll.evaluate(); const results = parsedRoll.dice[dice].results.map(result => ({ ...result, discarded: !result.active })); const newResult = results.splice(results.length - 1, 1); results.splice(Number(result) + 1, 0, newResult[0]); const rerolledDice = parsedRoll.dice.map((x, index) => { const isRerollDice = index === Number(dice); if (!isRerollDice) return { ...x, dice: x.denomination }; return { dice: parsedRoll.dice[dice].denomination, total: parsedRoll.dice[dice].total, results: results.map(result => ({ ...result, hasRerolls: result.hasRerolls || isRerollDice })) }; }); const updateMessage = game.messages.get(message._id); const damageParts = updateMessage.system.damage[damageType].parts.map((damagePart, index) => { if (index !== Number(part)) return damagePart; return { ...rollPart, total: parsedRoll.total, dice: rerolledDice }; }); await updateMessage.update({ [`system.damage.${damageType}`]: { ...updateMessage, total: parsedRoll.total, parts: damageParts } }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.TagTeamRoll }); await game.socket.emit(`system.${CONFIG.DH.id}`, { action: socketEvent.Refresh, data: { refreshType: RefreshType.TagTeamRoll } }); } }