import {localizeWithFallback} from "../other/utility-functions.js" export async function sendPercentileTestToChat(actor, skill, target, rollMode){ //let roll = new Roll('1D100', actor.data.data).evaluate(false); let roll = new Roll('1D100', actor.system) await roll.evaluate({async: true}); let total = roll.total; let isCritical = false; let isSuccess = false; let html = ''; let label = ''; let resultString = ''; let styleOverride = ''; if(rollMode == null || rollMode === ""){ rollMode = game.settings.get("core", "rollMode"); } // if using private san rolls, must hide any SAN role unless user is a GM let setting = false; setting = game.settings.get("deltagreen", "keepSanityPrivate"); if(setting === true && (skill === 'SAN' || skill === 'RITUAL') && !game.user.isGM){ rollMode = 'blindroll'; } // "Inhuman" stat being rolled, logic is different per page 188 of the Handler's Guide. // Note - originally implemented by Uriele, but my attempt at merging conficts went poorly, so re-implementing. // For an inhuman check, the roll succeeds except on a roll of 100 which fails AND fumbles. // If the roll is a matching digit roll, it is a critical as normal. // Also, if the roll is below the regular (non-x5) value of the stat, it is a critical. E.g. a CON of 25, a d100 roll of 21 would be a critical. if(target > 99 && skillIsStatTest(skill)){ label = `${game.i18n.localize("DG.Roll.Rolling")} ${skill} [${game.i18n.localize("DG.Roll.Inhuman").toUpperCase()}] ${game.i18n.localize("DG.Roll.Target")} ${Math.floor(target / 5)}`; if(total === 100){ // only possible fail criteria, and also a fumble. isSuccess = false; isCritical = true; } else{ isSuccess = true; if(total <= (target / 5.0)){ isCritical = true; } else if(skillCheckResultIsCritical(total)){ isCritical = true; } else{ isCritical = false; } } } else{ label = `${game.i18n.localize("DG.Roll.Rolling")} ${skill} ${game.i18n.localize("DG.Roll.Target")} ${target}`; isCritical = skillCheckResultIsCritical(total); if(total <= target){ isSuccess = true; } } if(isCritical){ resultString = `${game.i18n.localize("DG.Roll.Critical")} `; } if(isSuccess){ resultString += `${game.i18n.localize("DG.Roll.Success")}`; if(isCritical){ resultString = resultString.toUpperCase() + '!'; styleOverride="color: green"; } } else{ resultString += `${game.i18n.localize("DG.Roll.Failure")}`; if(isCritical){ resultString = resultString.toUpperCase() + '!'; styleOverride="color: red"; } } html = `
` html += `
` html += `
${resultString}
` html += `
` html += `
` html += `
` html += `

` html += ` ${roll.formula}` html += ` ${roll.total}` html += `

` html += `
    ` html += `
  1. ${roll.total}
  2. ` html += `
` html += `
` html += `
` html += `
` html += `

${roll.total}

` html += `
` let chatData = { speaker: ChatMessage.getSpeaker({actor: actor}), content: html, flavor: label, type: 5, //CHAT_MESSAGE_TYPES.ROLL, roll: roll, rollMode: rollMode }; // play the dice rolling sound, like a regular in-chat roll AudioHelper.play({src: "sounds/dice.wav", volume: 0.8, autoplay: true, loop: false}, true); ChatMessage.create(chatData, {}); } export function skillCheckResultIsCritical(rollResult){ let isCritical = false; if(rollResult === 1 || rollResult === 100){ // really good, or reeaaaally bad isCritical = true; } else if(rollResult % 11 === 0){ // any matching dice are a crit, i.e. 11, 22, 33...99. isCritical = true; } return isCritical; } export async function sendLethalityTestToChat(actor, weaponName, target, rollMode){ //let roll = new Roll('1D100', actor.data.data).evaluate(false); let roll = new Roll('1D100', actor.system); await roll.evaluate({async: true}); let isCritical = false; let skillCheckTotal = roll.total; if(rollMode == null || rollMode === ""){ rollMode = game.settings.get("core", "rollMode"); } // page 57 of agent's handbook // For a failed lethality check, the damage on the tens die is a 10 for a roll of '0'. E.g. '30' + '0' = 13 damage. // Lethality rolls do not fumble or critically succeed, but the attack roll can. // In this situation, the lethality threshold doubles (e.g. 20% would become 40%), // and if the lethality roll fails, the damage is doubled per normal. However right now // the attack roll and damage roll are completely separate... Perhaps in the future, use a // button in the skill check roll to allow doubling this logic? // try to determine what the d100 result would be as if it was two d10's being rolled let damageDie1 = Math.floor(skillCheckTotal / 10); let damageDie2 = skillCheckTotal - (damageDie1 * 10); if(damageDie2 === 0){ // if the result is evenly divisible by 10 (e.g. 30, 40, 50...) // then the percentile die result was actually one lower than the calculation above would show. // For example, a '70' is actually a 60 + 10, so the damage die should be 6 + 10 = 16, not 7 + 0 = 7. damageDie1 -= 1; damageDie2 = 10; } let damageDie1Label = damageDie1.toString(); let damageDie2Label = damageDie2.toString(); if(damageDie1 === 0){ // a roll of 00 on the tens part of the d100 counts as a ten if it becomes a damage die // Will leave the labeling as a '0' though, to be consistent with a regular d10 roll damageDie1 = 10; } if(damageDie2 === 10){ // keep the labelling consistent, show a '0' for a ten damageDie2Label = '0'; } let nonlethalDamage = damageDie1 + damageDie2; let isLethal = false; let html = ''; let label = `${game.i18n.localize("DG.Roll.Rolling")} ${game.i18n.localize("DG.Roll.Lethality").toUpperCase()} ${game.i18n.localize("DG.Roll.For")} ${weaponName.toUpperCase()} ${game.i18n.localize("DG.Roll.Target")} ${target}`; let resultString = ''; let styleOverride = ''; if(skillCheckTotal <= target){ isLethal = true; resultString = `${game.i18n.localize("DG.Roll.Lethal").toUpperCase()}`; styleOverride="color: red"; } else{ resultString = `${game.i18n.localize("DG.Roll.Failure")}`; } html = `
`; html += `
`; html += `
${resultString}
`; html += `
`; html += `
`; html += `
`; html += `

`; html += ` d100 ${game.i18n.localize("DG.Roll.Or").toUpperCase()} d10 + d10`; html += ` ${roll.total}`; html += `

`; html += `
    `; html += `
  1. ${damageDie1Label}
  2. `; html += `
  3. ${damageDie2Label}
  4. `; html += `
`; html += `
`; html += `
`; html += `
`; html += `

${roll.total} (${nonlethalDamage} ${game.i18n.localize("DG.Roll.Damage")})

`; html += `
`; let chatData = { speaker: ChatMessage.getSpeaker({actor: actor}), content: html, flavor: label, type: 5, //CHAT_MESSAGE_TYPES.ROLL, roll: roll, rollMode: rollMode }; // play the dice rolling sound, like a regular in-chat roll AudioHelper.play({src: "sounds/dice.wav", volume: 0.8, autoplay: true, loop: false}, true); ChatMessage.create(chatData, {}); } export async function sendDamageRollToChat(actor, label, diceFormula, rollMode){ if(rollMode == null || rollMode === ""){ rollMode = game.settings.get("core", "rollMode"); } //diceFormula += actor.data.data.statistics.str.meleeDamageBonusFormula; let roll = new Roll(diceFormula, actor.system); await roll.evaluate({async: true}); try{ label = `${game.i18n.localize("DG.Roll.Rolling")} ${game.i18n.localize("DG.Roll.Damage").toUpperCase()} ${game.i18n.localize("DG.Roll.For")} ${label.toUpperCase()}`; } catch{ label = `Rolling DAMAGE for ${label.toUpperCase()}` } let chatData = { speaker: ChatMessage.getSpeaker({actor: actor}), content: roll.total, flavor: label, type: 5, //CHAT_MESSAGE_TYPES.ROLL, roll: roll, rollMode: rollMode }; // play the dice rolling sound, like a regular in-chat roll AudioHelper.play({src: "sounds/dice.wav", volume: 0.8, autoplay: true, loop: false}, true); ChatMessage.create(chatData, {}); } export async function showModifyPercentileTestDialogue(actor, label, originalTarget, isLethalityTest){ let template = "systems/deltagreen/templates/dialog/modify-percentile-roll.html"; let isSanCheck = false; let hideSanTarget = false; if(label === 'SAN' || label === 'RITUAL'){ isSanCheck = true; hideSanTarget = game.settings.get("deltagreen", "keepSanityPrivate"); if(game.user.isGM){ hideSanTarget = false; } } let backingData = { data:{ label: label, originalTarget: originalTarget, targetModifier: 20, isSanCheck: isSanCheck, hideTarget: hideSanTarget }, }; let html = await renderTemplate(template, backingData); new Dialog({ content: html, title: localizeWithFallback("DG.ModifySkillRollDialogue.Title", "Modify Roll"), default: "roll", buttons: { roll:{ label: localizeWithFallback("DG.Roll.Roll", "Roll"), callback: html => { try{ let targetModifier = html.find("[name='targetModifier']").val(); // this is text as a heads up let rollMode = html.find("[name='targetRollMode']").val(); let plusMinus = html.find("[name='plusOrMinus']").val(); let newTarget = parseInt(originalTarget); // this should be an int, but technically the incoming value is text, so parse it just to be safe if(targetModifier.trim() != "" && !isNaN(targetModifier)){ let numericTargetModifier = Math.abs(parseInt(targetModifier)); if(plusMinus === "-"){ numericTargetModifier = -1 * numericTargetModifier; } newTarget += numericTargetModifier; } if(isLethalityTest){ sendLethalityTestToChat(actor, label, newTarget, rollMode); } else{ sendPercentileTestToChat(actor, label, newTarget, rollMode); } } catch(ex){ console.log(ex); } } } } }).render(true); } export async function showModifyDamageRollDialogue(actor, label, originalFormula){ let template = "systems/deltagreen/templates/dialog/modify-damage-roll.html"; let backingData = { data:{ label: label, originalFormula: originalFormula, outerModifier: "2 * ", innerModifier: "+ 0" }, }; let html = await renderTemplate(template, backingData); new Dialog({ content: html, title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"), default: "roll", buttons: { roll:{ label: game.i18n.translations.DG.Roll.Roll, callback: html => { try{ let outerModifier = html.find("[name='outerModifier']").val(); // this is text as a heads up let innerModifier = html.find("[name='innerModifier']").val(); // this is text as a heads up let modifiedBaseRoll = html.find("[name='originalFormula']").val(); // this is text as a heads up let rollMode = html.find("[name='targetRollMode']").val(); if(innerModifier.replace(" ", "") === "+0"){ innerModifier = ""; } let newRoll = ""; if(outerModifier.trim() != ""){ newRoll += outerModifier + "(" + modifiedBaseRoll + innerModifier.trim() + ")"; } else{ newRoll += modifiedBaseRoll + innerModifier.trim(); } sendDamageRollToChat(actor, label, newRoll, rollMode); } catch(ex){ console.log(ex); } } } } } ).render(true); } export function skillIsStatTest(skillName){ try{ if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.str").toUpperCase()){ return true; } if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.con").toUpperCase()){ return true; } if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.dex").toUpperCase()){ return true; } if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.int").toUpperCase()){ return true; } if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.pow").toUpperCase()){ return true; } if(skillName.toUpperCase() === game.i18n.localize("DG.Attributes.cha").toUpperCase()){ return true; } else{ return false; } } catch(ex){ return false; } }