export class AcksDice { static digestResult(data, roll) { let result = { isSuccess: false, isFailure: false, target: data.roll.target, total: roll.total, }; let die = roll.terms[0].total; if (data.roll.type == "above") { // SAVING THROWS if (roll.total >= result.target) { result.isSuccess = true; } else { result.isFailure = true; } } else if (data.roll.type == "below") { // ? if (roll.total <= result.target) { result.isSuccess = true; } else { result.isFailure = true; } } else if (data.roll.type == "check") { // SCORE CHECKS (1s and 20s), EXPLORATION if (die == 1 || (roll.total <= result.target && die < 20)) { result.isSuccess = true; } else { result.isFailure = true; } } else if (data.roll.type == "hitdice") { // RESULT CAN BE NO LOWER THAN 1 if (roll.total < 1) { roll._total = 1; } } else if (data.roll.type == "table") { // Reaction, MORALE // Roll cannot be less than 2 on a 2d6 roll if (roll.total < 2) { roll._total = 2 } let table = data.roll.table; let output = ""; for (let i = 0; i <= roll.total; i++) { if (table[i]) { output = table[i]; } } result.details = output; } return result; } static async sendRoll({ parts = [], data = {}, title = null, flavor = null, speaker = null, form = null, } = {}) { const template = "systems/acks/templates/chat/roll-result.html"; let chatData = { user: game.user._id, speaker: speaker, }; let templateData = { title: title, flavor: flavor, data: data, }; // Optionally include a situational bonus if (form !== null && form.bonus.value) { parts.push(form.bonus.value); } const roll = new Roll(parts.join("+"), data); await roll.evaluate({ async: true, }); // Convert the roll to a chat message and return the roll let rollMode = game.settings.get("core", "rollMode"); rollMode = form ? form.rollMode.value : rollMode; // Force blind roll (ability formulas) if (data.roll.blindroll) { rollMode = game.user.isGM ? "selfroll" : "blindroll"; } if (["gmroll", "blindroll"].includes(rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM"); if (rollMode === "selfroll") chatData["whisper"] = [game.user._id]; if (rollMode === "blindroll") { chatData["blind"] = true; data.roll.blindroll = true; } templateData.result = AcksDice.digestResult(data, roll); return new Promise((resolve) => { roll.render().then((r) => { templateData.rollACKS = r; renderTemplate(template, templateData).then((content) => { chatData.content = content; // Dice So Nice if (game.dice3d) { game.dice3d .showForRoll( roll, game.user, true, chatData.whisper, chatData.blind ) .then((displayed) => { ChatMessage.create(chatData); resolve(roll); }); } else { chatData.sound = CONFIG.sounds.dice; ChatMessage.create(chatData); resolve(roll); } }); }); }); } static digestAttackResult(data, roll) { let result = { isSuccess: false, isFailure: false, target: "", total: roll.total, }; result.target = data.roll.thac0; const targetAc = data.roll.target ? data.roll.target.actor.data.data.ac.value : 9; const targetAac = data.roll.target ? data.roll.target.actor.data.data.aac.value : 0; result.victim = data.roll.target ? data.roll.target.data.name : null; const hfh = game.settings.get("acks", "exploding20s") const die = roll.dice[0].total if (game.settings.get("acks", "ascendingAC")) { if (die == 1 && !hfh) { result.details = game.i18n.format( "ACKS.messages.Fumble", { result: roll.total, bonus: result.target, } ); return result; } else if (roll.total < targetAac + 10 && die < 20) { result.details = game.i18n.format( "ACKS.messages.AttackAscendingFailure", { result: roll.total - 10, bonus: result.target, } ); return result; } else if (roll.total < targetAac + 10 && hfh) { result.details = game.i18n.format( "ACKS.messages.AttackAscendingFailure", { result: roll.total - 10, bonus: result.target, } ); return result; } if (!hfh && die == 20) { result.details = game.i18n.format("ACKS.messages.Critical", { result: roll.total, }); } else { result.details = game.i18n.format("ACKS.messages.AttackAscendingSuccess", { result: roll.total - 10, }); } result.isSuccess = true; } else { // B/X Historic THAC0 Calculation if (result.target - roll.total > targetAc) { result.details = game.i18n.format("ACKS.messages.AttackFailure", { bonus: result.target, }); return result; } result.isSuccess = true; let value = Math.clamped(result.target - roll.total, -3, 9); result.details = game.i18n.format("ACKS.messages.AttackSuccess", { result: value, bonus: result.target, }); } return result; } static async sendAttackRoll({ parts = [], data = {}, title = null, flavor = null, speaker = null, form = null, } = {}) { const template = "systems/acks/templates/chat/roll-attack.html"; let chatData = { user: game.user._id, speaker: speaker, }; let templateData = { title: title, flavor: flavor, data: data, config: CONFIG.ACKS, }; // Optionally include a situational bonus if (form !== null && form.bonus.value) parts.push(form.bonus.value); const roll = new Roll(parts.join("+"), data); await roll.evaluate({ async: true, }); const dmgRoll = new Roll(data.roll.dmg.join("+"), data); await dmgRoll.evaluate({ async: true, }); // Add minimal damage of 1 if (dmgRoll.total < 1) { dmgRoll._total = 1; } // Convert the roll to a chat message and return the roll let rollMode = game.settings.get("core", "rollMode"); rollMode = form ? form.rollMode.value : rollMode; // Force blind roll (ability formulas) if (data.roll.blindroll) { rollMode = game.user.isGM ? "selfroll" : "blindroll"; } if (["gmroll", "blindroll"].includes(rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM"); if (rollMode === "selfroll") chatData["whisper"] = [game.user._id]; if (rollMode === "blindroll") { chatData["blind"] = true; data.roll.blindroll = true; } templateData.result = AcksDice.digestAttackResult(data, roll); return new Promise((resolve) => { roll.render().then((r) => { templateData.rollACKS = r; dmgRoll.render().then((dr) => { templateData.rollDamage = dr; renderTemplate(template, templateData).then((content) => { chatData.content = content; // 2 Step Dice So Nice if (game.dice3d) { game.dice3d .showForRoll( roll, game.user, true, chatData.whisper, chatData.blind ) .then(() => { if (templateData.result.isSuccess) { templateData.result.dmg = dmgRoll.total; game.dice3d .showForRoll( dmgRoll, game.user, true, chatData.whisper, chatData.blind ) .then(() => { ChatMessage.create(chatData); resolve(roll); }); } else { ChatMessage.create(chatData); resolve(roll); } }); } else { chatData.sound = CONFIG.sounds.dice; ChatMessage.create(chatData); resolve(roll); } }); }); }); }); } static async RollSave({ parts = [], data = {}, skipDialog = false, speaker = null, flavor = null, title = null, } = {}) { let rolled = false; const template = "systems/acks/templates/chat/roll-dialog.html"; let dialogData = { formula: parts.join(" "), data: data, rollMode: game.settings.get("core", "rollMode"), rollModes: CONFIG.Dice.rollModes, }; let rollData = { parts: parts, data: data, title: title, flavor: flavor, speaker: speaker, }; let buttons = {} if (skipDialog) { return AcksDice.sendRoll(rollData); } if (game.settings.get("acks", "removeMagicBonus") == false) { buttons = { ok: { label: game.i18n.localize("ACKS.Roll"), icon: '', callback: (html) => { rolled = true; rollData.form = html[0].querySelector("form"); roll = AcksDice.sendRoll(rollData); }, }, magic: { label: game.i18n.localize("ACKS.saves.magic.short"), icon: '', callback: (html) => { rolled = true; rollData.form = html[0].querySelector("form"); rollData.parts.push(`${rollData.data.roll.magic}`); rollData.title += ` ${game.i18n.localize("ACKS.saves.magic.short")} (${rollData.data.roll.magic})`; roll = AcksDice.sendRoll(rollData); }, }, cancel: { icon: '', label: game.i18n.localize("ACKS.Cancel"), callback: (html) => { }, }, }; } else { buttons = { ok: { label: game.i18n.localize("ACKS.Roll"), icon: '', callback: (html) => { rolled = true; rollData.form = html[0].querySelector("form"); rollData.parts.push(`${rollData.data.roll.magic}`); roll = AcksDice.sendRoll(rollData); }, }, cancel: { icon: '', label: game.i18n.localize("ACKS.Cancel"), callback: (html) => { }, }, }; } const html = await renderTemplate(template, dialogData); let roll; //Create Dialog window return new Promise((resolve) => { new Dialog({ title: title, content: html, buttons: buttons, default: "ok", close: () => { resolve(rolled ? roll : false); }, }).render(true); }); } static async Roll({ parts = [], data = {}, skipDialog = false, speaker = null, flavor = null, title = null, } = {}) { let rolled = false; const template = "systems/acks/templates/chat/roll-dialog.html"; let dialogData = { formula: parts.join(" "), data: data, rollMode: game.settings.get("core", "rollMode"), rollModes: CONFIG.Dice.rollModes, }; let rollData = { parts: parts, data: data, title: title, flavor: flavor, speaker: speaker, }; if (skipDialog) { return ["melee", "missile", "attack"].includes(data.roll.type) ? AcksDice.sendAttackRoll(rollData) : AcksDice.sendRoll(rollData); } let buttons = { ok: { label: game.i18n.localize("ACKS.Roll"), icon: '', callback: (html) => { rolled = true; rollData.form = html[0].querySelector("form"); roll = ["melee", "missile", "attack"].includes(data.roll.type) ? AcksDice.sendAttackRoll(rollData) : AcksDice.sendRoll(rollData); }, }, cancel: { icon: '', label: game.i18n.localize("ACKS.Cancel"), callback: (html) => { }, }, }; const html = await renderTemplate(template, dialogData); let roll; //Create Dialog window return new Promise((resolve) => { new Dialog({ title: title, content: html, buttons: buttons, default: "ok", close: () => { resolve(rolled ? roll : false); }, }).render(true); }); } }