// This code is licensed under the same terms as Habitica: // https://raw.githubusercontent.com/HabitRPG/habitrpg/develop/LICENSE /* ========================================== */ /* [Users] Required script data to fill in */ /* ========================================== */ const USER_ID = "PasteYourUserIdHere" const API_TOKEN = "PasteYourApiTokenHere" // Do not share this to anyone const WEB_APP_URL = "PasteGeneratedWebAppUrlHere" /* ========================================== */ /* [Users] Required customizations to fill in */ /* ========================================== */ // For each skill you want to add, change the 0 to a 1. If you don't want it, keep it as a 0 const CREATE_PICKPOCKET = 0 // Gain GP based on Perception. 15 MP. Unlocks at level 11. const CREATE_BACKSTAB = 0 // Gain GP and XP based on Strength. 20 MP. Unlocks at level 12. const CREATE_STEALTH = 0 // Take no damage from some undone Dailies, based on Perception. 50 MP. Unlocks at level 14. const CREATE_HEALING_LIGHT = 0 // Gain HP based on Intelligence and Constitution. 20 MP. Unlocks at level 11. const CREATE_CHILLING_FROST = 0 // Freeze streaks of all Dailies so they don't reset if incomplete. 45 MP. Unlocks at level 14. const CREATE_BURST_OF_FLAMES_XP_ONLY = 0 // Gain XP based on Intelligence. 10 MP. Unlocks at level 11. const CREATE_BURST_OF_FLAMES_DAMAGE_ONLY = 0 // Deal boss damage based on Intelligence. 10 MP per use, multi-use only (to deal 90+ damage). Unlocks at level 11. const CREATE_BURST_OF_FLAMES_XP_PLUS_DAMAGE = 0 // Deal boss damage and gain XP based on Intelligence. 15 MP per use, multi-use only (to deal 90+ damage). Unlocks at level 11. const CREATE_BRUTAL_SMASH = 0 // Deal boss damage based on Strength. 10 MP per use, multi-use only (to deal 90+ damage). Unlocks at level 11. const CREATE_RADIANT_SHIELD = 0 // Deal boss damage based on Constitution. 7.5 MP per use, multi-use only (to deal 90+ damage). Unlocks at level 11. const CREATE_SNEAK_ATTACK = 0 // Deal boss damage based on Perception. 10 MP per use, multi-use only (to deal 90+ damage). Unlocks at level 11. // Which version of the skill do you want to use? (you can do the same or different for each skill) // Choose 2 if you want to set a task value and use it rather than using a specific task. // Choose 3 if you want to average the task values of all your Dailies and use that. // Choose 4 if you want to select one task and always use that task with this skill. const PICKPOCKET_VERSION = 2 const BACKSTAB_VERSION = 2 const BURST_OF_FLAMES_VERSION = 2 // Based on the version you picked, scroll to the relevant section below // If you picked version 2 for any of them, select what task value you want to use // For example, a green task can have a task value between 1 and 5 // A light blue task and have a task value between 6 and 11 // A bright blue task has a value above 12 // The maximum task value is 21.27 const PICKPOCKET_TASK_VALUE = 10 const BACKSTAB_TASK_VALUE = 10 const BURST_OF_FLAMES_TASK_VALUE = 10 // If you picked version 3 for any of them, you don't have to do anything else in this section. // If you picked version 4 for any of them, find the task ID of the task you want to use and paste below // Instructions for this are found in the wiki page const PICKPOCKET_TASK_ID = "620dc42a-b258-47a9-84fa-a5c437345a9" const BACKSTAB_TASK_ID = "620dc42a-b258-47a9-84fa-a5c437345a9" const BURST_OF_FLAMES_TASK_ID = "620dc42a-b258-47a9-84fa-a5c437345a9" /* ========================================== */ /* [Users] Optional customizations to fill in */ /* ========================================== */ // Do you want to get private message notifications for having insufficient mana or being too low of a level to use the skill? // If you don't want them, change the 1 to a 0 in the line below const NOTIFICATIONS_ON = 1 /* ========================================== */ /* [Users] Do not edit code below this line */ /* ========================================== */ const AUTHOR_ID = "0034eb14-b4d8-494e-8386-d3f33cff7922" const SCRIPT_NAME = "Cross-Class Skills" const HEADERS = { "x-client" : AUTHOR_ID + " - " + SCRIPT_NAME, "x-api-user" : USER_ID, "x-api-key" : API_TOKEN, } const scriptProperties = PropertiesService.getScriptProperties() // Constants can have properties changed const LEVEL_LOCK_PICKPOCKET = 11 const LEVEL_LOCK_BACKSTAB = 12 const LEVEL_LOCK_STEALTH = 14 const LEVEL_LOCK_HEALING_LIGHT = 11 const LEVEL_LOCK_CHILLING_FROST = 14 const LEVEL_LOCK_BURST_OF_FLAMES = 11 const LEVEL_LOCK_BRUTAL_SMASH = 11 const LEVEL_LOCK_RADIANT_SHIELD = 11 const LEVEL_LOCK_SNEAK_ATTACK = 11 // Level lock messages const MSG_LEVEL_LOCK_FAIL_START = "Insufficient level: you must be at least level " const MSG_LEVEL_LOCK_FAIL_END = " to use this" // Messages const MSG_ALL_DAILIES_AVOIDED = "You've already avoided all your Dailies, so additional uses of this skill have no effect." const MSG_ALREADY_AT_MAX_HP = "You are already at max HP." const MSG_ALREADY_USED_CHILLING_FROST = "You've already used this skill. Additional uses have no effect." const MSG_INSUFFICIENT_MANA_SKILL = "Insufficient mana to use this skill." const MSG_MULTI_USE_MANA_CHECK_1 = "Insufficient Mana. You will need " const MSG_MULTI_USE_MANA_CHECK_2 = " MP to use this skill, and it will do " const MSG_MULTI_USE_MANA_CHECK_3 = "damage" const MSG_MULTI_USE_MANA_CHECK_4A_1 = " and you will gain " const MSG_MULTI_USE_MANA_CHECK_4A_2 = " XP." const MSG_MULTI_USE_MANA_CHECK_4B = "." const MSG_TASK_ID_NOT_FOUND = "Task ID not found, so this script defaulted to the preset task value (version 2 in the Required Customization section). To find task value, see the [wiki page](https://habitica.fandom.com/wiki/Google_Apps_Script#Cross-Class_Skills) section on Required Customizations and then update the script using the instructions at the bottom of the wiki page." const MSG_ERROR_DAMAGE_SNEAK_ATTACK = "Error calculating Sneak Attack damage, please contact the author of this script so he can debug." const MSG_ERROR_DAMAGE_FINAL_ITERATION = "Error computing damage. Please contact the author of the script to debug (note to author: final iteration triggered this message)." const MSG_ERROR_DAMAGE_SETPOINT = "Error computing damage. Please contact the author of the script to debug (note to author: target damage not between low and high setpoints)." const MP_COST_PICKPOCKET = 15 const MP_COST_BACKSTAB = 20 const MP_COST_STEALTH = 50 const MP_COST_HEALING_LIGHT = 20 const MP_COST_CHILLING_FROST = 45 const MP_COST_BURST_OF_FLAMES_XP_ONLY = 10 const MP_COST_BURST_OF_FLAMES_DAMAGE_ONLY = 10 const MP_COST_BURST_OF_FLAMES_XP_PLUS_DAMAGE = 15 const MP_COST_BRUTAL_SMASH = 10 const MP_COST_RADIANT_SHIELD = 7.5 const MP_COST_SNEAK_ATTACK = 10 const PICKPOCKET_TEXT = "**Cross-Class Pickpocket**" const PICKPOCKET_ALIAS = "ccPickpocket" const PICKPOCKET_NOTES = "You rob a nearby task and gain gold! (Based on: PER). " + MP_COST_PICKPOCKET + " MP." const PICKPOCKET_VALUE = "0" const PICKPOCKET_BUTTON = { "text": PICKPOCKET_TEXT, "type": "reward", "alias": PICKPOCKET_ALIAS, "notes": PICKPOCKET_NOTES, "value": PICKPOCKET_VALUE, } const BACKSTAB_TEXT = "**Cross-Class Backstab**" const BACKSTAB_ALIAS = "ccBackstab" const BACKSTAB_NOTES = "You betray a foolish task and gain gold and XP! (Based on: STR). " + MP_COST_BACKSTAB + " MP." const BACKSTAB_VALUE = "0" const BACKSTAB_BUTTON = { "text": BACKSTAB_TEXT, "type": "reward", "alias": BACKSTAB_ALIAS, "notes": BACKSTAB_NOTES, "value": BACKSTAB_VALUE, } const STEALTH_TEXT = "**Cross-Class Stealth**" const STEALTH_ALIAS = "ccStealth" const STEALTH_NOTES = "With each cast, a few of your undone Dailies won't cause damage tonight. Their streaks and colors won't change. (Based on: PER) (Cast multiple times to affect more Dailies.). " + MP_COST_STEALTH + " MP." const STEALTH_VALUE = "0" const STEALTH_BUTTON = { "text": STEALTH_TEXT, "type": "reward", "alias": STEALTH_ALIAS, "notes": STEALTH_NOTES, "value": STEALTH_VALUE, } const HEALING_LIGHT_TEXT = "**Cross-Class Healing Light**" const HEALING_LIGHT_ALIAS = "ccHealingLight" const HEALING_LIGHT_NOTES = "Shining light restores your health! (Based on: CON and INT). " + MP_COST_HEALING_LIGHT + " MP." const HEALING_LIGHT_VALUE = "0" const HEALING_LIGHT_BUTTON = { "text": HEALING_LIGHT_TEXT, "type": "reward", "alias": HEALING_LIGHT_ALIAS, "notes": HEALING_LIGHT_NOTES, "value": HEALING_LIGHT_VALUE, } const CHILLING_FROST_TEXT = "**Cross-Class Chilling Frost**" const CHILLING_FROST_ALIAS = "ccChillingFrost" const CHILLING_FROST_NOTES = "With one cast, ice freezes all your streaks so they won't reset to zero tomorrow! " + MP_COST_CHILLING_FROST + " MP." const CHILLING_FROST_VALUE = "0" const CHILLING_FROST_BUTTON = { "text": CHILLING_FROST_TEXT, "type": "reward", "alias": CHILLING_FROST_ALIAS, "notes": CHILLING_FROST_NOTES, "value": CHILLING_FROST_VALUE, } const BURST_OF_FLAMES_XP_ONLY_TEXT = "**Cross-Class Burst of Flames (XP only, single use)**" const BURST_OF_FLAMES_XP_ONLY_ALIAS = "ccBurstOfFlamesXp" const BURST_OF_FLAMES_XP_ONLY_NOTES = "You summon fiery XP! (Based on: INT). " + MP_COST_BURST_OF_FLAMES_XP_ONLY + " MP." const BURST_OF_FLAMES_XP_ONLY_VALUE = "0" const BURST_OF_FLAMES_XP_ONLY_BUTTON = { "text": BURST_OF_FLAMES_XP_ONLY_TEXT, "type": "reward", "alias": BURST_OF_FLAMES_XP_ONLY_ALIAS, "notes": BURST_OF_FLAMES_XP_ONLY_NOTES, "value": BURST_OF_FLAMES_XP_ONLY_VALUE, } const BURST_OF_FLAMES_DAMAGE_ONLY_TEXT = "**Cross-Class Burst of Flames (Damage only, multi-use)**" const BURST_OF_FLAMES_DAMAGE_ONLY_ALIAS = "ccBurstOfFlamesDamage" const BURST_OF_FLAMES_DAMAGE_ONLY_NOTES = "You summon a burst of fiery damage to Bosses! (Based on: INT). " + MP_COST_BURST_OF_FLAMES_DAMAGE_ONLY + " MP per use." const BURST_OF_FLAMES_DAMAGE_ONLY_VALUE = "0" const BURST_OF_FLAMES_DAMAGE_ONLY_BUTTON = { "text": BURST_OF_FLAMES_DAMAGE_ONLY_TEXT, "type": "reward", "alias": BURST_OF_FLAMES_DAMAGE_ONLY_ALIAS, "notes": BURST_OF_FLAMES_DAMAGE_ONLY_NOTES, "value": BURST_OF_FLAMES_DAMAGE_ONLY_VALUE, } const BURST_OF_FLAMES_XP_PLUS_DAMAGE_TEXT = "**Cross-Class Burst of Flames (XP and damage, multi-use)**" const BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS = "ccBurstOfFlamesBoth" const BURST_OF_FLAMES_XP_PLUS_DAMAGE_NOTES = "You summon a burst of flames and deal fiery damage to Bosses! (Based on: INT). " + MP_COST_BURST_OF_FLAMES_XP_PLUS_DAMAGE + " MP per use." const BURST_OF_FLAMES_XP_PLUS_DAMAGE_VALUE = "0" const BURST_OF_FLAMES_XP_PLUS_DAMAGE_BUTTON = { "text": BURST_OF_FLAMES_XP_PLUS_DAMAGE_TEXT, "type": "reward", "alias": BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS, "notes": BURST_OF_FLAMES_XP_PLUS_DAMAGE_NOTES, "value": BURST_OF_FLAMES_XP_PLUS_DAMAGE_VALUE, } const BRUTAL_SMASH_TEXT = "**Cross-Class Brutal Smash (damage only, multi-use)**" const BRUTAL_SMASH_ALIAS = "ccBrutalSmash" const BRUTAL_SMASH_NOTES = "You deal extra damage to Bosses! (Based on: STR). " + MP_COST_BRUTAL_SMASH + " MP per use." const BRUTAL_SMASH_VALUE = "0" const BRUTAL_SMASH_BUTTON = { "text": BRUTAL_SMASH_TEXT, "type": "reward", "alias": BRUTAL_SMASH_ALIAS, "notes": BRUTAL_SMASH_NOTES, "value": BRUTAL_SMASH_VALUE, } const RADIANT_SHIELD_TEXT = "**Radiant Shield (multi-use)**" const RADIANT_SHIELD_ALIAS = "ccRadiantShield" const RADIANT_SHIELD_NOTES = "A burst of light redirects some of your foes' attack back on them! Deals boss damage (Based on: CON). " + MP_COST_RADIANT_SHIELD + " MP per use." const RADIANT_SHIELD_VALUE = "0" const RADIANT_SHIELD_BUTTON = { "text": RADIANT_SHIELD_TEXT, "type": "reward", "alias": RADIANT_SHIELD_ALIAS, "notes": RADIANT_SHIELD_NOTES, "value": RADIANT_SHIELD_VALUE, } const SNEAK_ATTACK_TEXT = "**Sneak Attack (multi-use)**" const SNEAK_ATTACK_ALIAS = "ccSneakAttack" const SNEAK_ATTACK_NOTES = "Your keen eyes notice a weak spot and deal damage to Bosses! (Based on: PER). " + MP_COST_SNEAK_ATTACK + " MP per use." const SNEAK_ATTACK_VALUE = "0" const SNEAK_ATTACK_BUTTON = { "text": SNEAK_ATTACK_TEXT, "type": "reward", "alias": SNEAK_ATTACK_ALIAS, "notes": SNEAK_ATTACK_NOTES, "value": SNEAK_ATTACK_VALUE, } const DAMAGE_TEXT = "Boss damage" const DAMAGE_NOTES = "used to create boss damage as part of a custom script" const DAMAGE_BUTTON = { "text": DAMAGE_TEXT, "type": "todo", "notes": DAMAGE_NOTES, "priority": 0.1, } const TIMESTAMP_KEY = "TIMESTAMP_KEY" var timestampKey = "" function doOneTimeSetup() { // These are not "else if" statements on purpose if (CREATE_SNEAK_ATTACK == 1) { api_createNewTaskForUser([SNEAK_ATTACK_BUTTON]) } if (CREATE_RADIANT_SHIELD == 1) { api_createNewTaskForUser([RADIANT_SHIELD_BUTTON]) } if (CREATE_BRUTAL_SMASH == 1) { api_createNewTaskForUser([BRUTAL_SMASH_BUTTON]) } if (CREATE_BURST_OF_FLAMES_XP_PLUS_DAMAGE == 1) { api_createNewTaskForUser([BURST_OF_FLAMES_XP_PLUS_DAMAGE_BUTTON]) } if (CREATE_BURST_OF_FLAMES_DAMAGE_ONLY == 1) { api_createNewTaskForUser([BURST_OF_FLAMES_DAMAGE_ONLY_BUTTON]) } if (CREATE_BURST_OF_FLAMES_XP_ONLY == 1) { api_createNewTaskForUser([BURST_OF_FLAMES_XP_ONLY_BUTTON]) } if (CREATE_CHILLING_FROST == 1) { api_createNewTaskForUser([CHILLING_FROST_BUTTON]) } if (CREATE_HEALING_LIGHT == 1) { api_createNewTaskForUser([HEALING_LIGHT_BUTTON]) } if (CREATE_STEALTH == 1) { api_createNewTaskForUser([STEALTH_BUTTON]) } if (CREATE_BACKSTAB == 1) { api_createNewTaskForUser([BACKSTAB_BUTTON]) } if (CREATE_PICKPOCKET == 1) { api_createNewTaskForUser([PICKPOCKET_BUTTON]) } // Next, create the webhook const options = { "scored" : true, } const payload = { "url" : WEB_APP_URL, "label" : SCRIPT_NAME + " Webhook", "type" : "taskActivity", "options" : options, } apiMult_createNewWebhookNoDuplicates(payload) // set script properties so they carry over to next session initScriptProperties() } // do things when the webhook runs function doPost(e) { const dataContents = JSON.parse(e.postData.contents) const type = dataContents.type const task = dataContents.task // Sanitize task alias let sanitizedAlias = "sanitized" // This will be the value if undefined, null, or blank if ( (task.alias != undefined) && (task.alias != null) && (task.alias != "") ) { sanitizedAlias = task.alias } if (type == "scored") { // Check if the alias matches any of them if ( (sanitizedAlias == PICKPOCKET_ALIAS) || (sanitizedAlias == BACKSTAB_ALIAS) || (sanitizedAlias == STEALTH_ALIAS) || (sanitizedAlias == HEALING_LIGHT_ALIAS) || (sanitizedAlias == CHILLING_FROST_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_XP_ONLY_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_DAMAGE_ONLY_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS) || (sanitizedAlias == BRUTAL_SMASH_ALIAS) || (sanitizedAlias == RADIANT_SHIELD_ALIAS) || (sanitizedAlias == SNEAK_ATTACK_ALIAS) ) { var timestampKey = TIMESTAMP_KEY var timeStart = Number(scriptProperties.getProperty(timestampKey)) if ( (timeStart == 0) || (timeStart == "") || (timeStart == undefined) || (timeStart == null) ) { timeStart = Date.now() } var timeEnd = Date.now() // Rate limiting: If it's been less than 30 seconds since they last clicked one of these buttons, do nothing if ( (timeEnd - timeStart) >= 30000 ) { // Get user info const responseUser = api_getAuthenticatedUserProfile("stats,tasksOrder,items.gear.equipped") user = JSON.parse(responseUser).data // Get equipment stats info const responseContent = apiFree_getAllAvailableContentObjects() content = JSON.parse(responseContent).data let lvl = user.stats.lvl let mp = user.stats.mp let hp = user.stats.hp let xp = user.stats.exp let gp = user.stats.gp let totalDailies = user.tasksOrder.dailys.length let stealthedDailies = user.stats.buffs.stealth if (!user.stats.buffs.stealth) {stealthedDailies = 0} let frostCast = user.stats.buffs.streaks let strength = calculateStrength() let intelligence = calculateIntelligence() let constitution = calculateConstitution() let perception = calculatePerception() let buffsStr = user.stats.buffs.str if (!user.stats.buffs.str) {buffsStr = 0} let buffsInt = user.stats.buffs.int if (!user.stats.buffs.int) {buffsInt = 0} let buffsCon = user.stats.buffs.con if (!user.stats.buffs.con) {buffsCon = 0} let buffsPer = user.stats.buffs.per if (!user.stats.buffs.per) {buffsPer = 0} let unbuffedStr = strength - buffsStr let unbuffedInt = intelligence - buffsInt let unbuffedCon = constitution - buffsCon let unbuffedPer = perception - buffsPer // Switch-case based on which button was pressed. All button-click actions are in a separate function. switch (sanitizedAlias){ case PICKPOCKET_ALIAS: doButtonPickpocket(lvl, mp, gp, perception) break case BACKSTAB_ALIAS: doButtonBackstab(lvl, mp, gp, xp, strength) break case STEALTH_ALIAS: doButtonStealth(lvl, stealthedDailies, mp, totalDailies, perception) break case HEALING_LIGHT_ALIAS: doButtonHealingLight(lvl, mp, hp, constitution, intelligence) break case CHILLING_FROST_ALIAS: doButtonChillingFrost(lvl, mp, frostCast) break case BURST_OF_FLAMES_XP_ONLY_ALIAS: doButtonBurstOfFlamesXpOnly(lvl, mp, xp, intelligence, perception) break case BURST_OF_FLAMES_DAMAGE_ONLY_ALIAS: doButtonBurstOfFlamesDamageOnly(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, buffsStr) break case BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS: doButtonBurstOfFlamesXpAndDamage(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, buffsStr) break case BRUTAL_SMASH_ALIAS: doButtonBrutalSmash(lvl, hp, xp, mp, gp, strength, constitution, intelligence, perception, unbuffedStr, buffsStr) break case RADIANT_SHIELD_ALIAS: doButtonRadiantShield(lvl, hp, xp, mp, gp, constitution, intelligence, perception, unbuffedStr, buffsStr) break case SNEAK_ATTACK_ALIAS: doButtonSneakAttack(lvl, hp, xp, mp, gp, perception, intelligence, strength, unbuffedStr, buffsStr) break } // Save values to non-volatile memory, but only from within the rate-limited if-block (we don't want to save the timestamp unless the script ran) timeStart = timeEnd scriptProperties.setProperty(timestampKey, timeStart) } } } return HtmlService.createHtmlOutput() } // When the Pickpocket button is clicked function doButtonPickpocket(lvl, mp, gp, perception){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_PICKPOCKET, "skill.") if (sufficientLevel){ let enoughMana = checkMana(mp, MP_COST_PICKPOCKET, false) if (enoughMana) { pickpocket(gp, perception) } } } // When the Backstab button is clicked function doButtonBackstab(lvl, mp, gp, xp, strength){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BACKSTAB, "skill.") if (sufficientLevel){ let enoughMana = checkMana(mp, MP_COST_BACKSTAB, false) if (enoughMana) { backstab(gp, xp, strength) } } } // When the Stealth button is clicked function doButtonStealth(lvl, stealthedDailies, mp, totalDailies, perception){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_STEALTH, "skill.") if (sufficientLevel){ // Check how many Dailies due but aren't complete const responseTaskStealth = api_getUserTasks("dailys") const tasksStealth = JSON.parse(responseTaskStealth).data var tasksDueAndNotComplete = 0 for (var i in tasksStealth) { if ( (tasksStealth[i].isDue == true) && (tasksStealth[i].completed == false) ) { tasksDueAndNotComplete++ } } // If already avoided all dailies that are due but not complete, send message and don't subtract MP cost if (stealthedDailies >= tasksDueAndNotComplete){ api_sendPrivateMessageAlways({"message" : MSG_ALL_DAILIES_AVOIDED, "toUserId" : USER_ID}) } // Do skill as normal else { let enoughMana = checkMana(mp, MP_COST_STEALTH, false) if (enoughMana) { stealth (totalDailies, stealthedDailies, perception) } } } } // When the Healing Light button is clicked function doButtonHealingLight(lvl, mp, hp, constitution, intelligence){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_HEALING_LIGHT, "skill.") if (sufficientLevel){ // If not at max HP, subtract mana cost and do the skill if (hp < 50) { let enoughMana = checkMana(mp, MP_COST_HEALING_LIGHT, false) if (enoughMana) { healingLight(hp, constitution, intelligence) } } else { // if already at max HP, do nothing api_sendPrivateMessage({"message" : MSG_ALREADY_AT_MAX_HP, "toUserId" : USER_ID}) } } } // When the Chilling Frost button is clicked function doButtonChillingFrost(lvl, mp, frostCast){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_CHILLING_FROST, "skill.") if (sufficientLevel){ let enoughMana = checkMana(mp, MP_COST_CHILLING_FROST, true) if (enoughMana) { // Check if already cast, send message if yes if (frostCast == true){ api_sendPrivateMessageAlways({"message" : MSG_ALREADY_USED_CHILLING_FROST, "toUserId" : USER_ID}) } else { chillingFrost(mp, MP_COST_CHILLING_FROST) // includes deducting MP cost } } } } // When the Burst of Flames (XP only) button is clicked function doButtonBurstOfFlamesXpOnly(lvl, mp, xp, intelligence, perception){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BURST_OF_FLAMES, "skill.") if (sufficientLevel){ let enoughMana = checkMana(mp, MP_COST_BURST_OF_FLAMES_XP_ONLY, false) if (enoughMana) { var xpGain = burstOfFlamesXp(intelligence, perception, 1) api_updateUser({"stats.exp" : xp + xpGain}) } } } // When the Burst of Flames (damage only) button is clicked function doButtonBurstOfFlamesDamageOnly(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, buffsStr){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BURST_OF_FLAMES, "skill.") if (sufficientLevel){ // Check how much damage it will deal once, and then whether they have enough MP for multi-use (to exceed 90.71 damage) let damageOnce = flamesDamage(intelligence) let numberTimes = checkManaMultiUse(mp, MP_COST_BURST_OF_FLAMES_DAMAGE_ONLY, damageOnce, false, intelligence, perception) // If mana is sufficient if (numberTimes > 0) { let damageDealt = damageOnce * numberTimes doDamage(damageDealt, MP_COST_BURST_OF_FLAMES_DAMAGE_ONLY, numberTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, false, 0) } } } // When the Burst of Flames (XP and damage) button is clicked function doButtonBurstOfFlamesXpAndDamage(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, buffsStr){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BURST_OF_FLAMES, "skill.") if (sufficientLevel){ // Check how much damage it will deal once, and then whether they have enough MP for multi-use (to exceed 90.71 damage) let damageOnce = flamesDamage(intelligence) let numberTimes = checkManaMultiUse(mp, MP_COST_BURST_OF_FLAMES_XP_PLUS_DAMAGE, damageOnce, true, intelligence, perception) // If mana is sufficient if (numberTimes > 0) { let xpGain = burstOfFlamesXp(intelligence, perception, numberTimes); let damageDealt = damageOnce * numberTimes doDamage(damageDealt, MP_COST_BURST_OF_FLAMES_XP_PLUS_DAMAGE, numberTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, true, xpGain) } } } // When the Brutal Smash button is clicked function doButtonBrutalSmash(lvl, hp, xp, mp, gp, strength, constitution, intelligence, perception, unbuffedStr, buffsStr){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BRUTAL_SMASH, "skill.") if (sufficientLevel){ // Check how much damage it will deal once, and then whether they have enough MP for multi-use (to exceed 90.71 damage) let damageOnce = smashDamageNoCrit(strength) let numberTimes = checkManaMultiUse(mp, MP_COST_BRUTAL_SMASH, damageOnce, false, intelligence, perception) // If mana is sufficient if (numberTimes > 0) { let damageDealt = smashDamageActual(strength, constitution, numberTimes) doDamage(damageDealt, MP_COST_BRUTAL_SMASH, numberTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, false, 0) } } } // When the Radiant Shield button is clicked function doButtonRadiantShield(lvl, hp, xp, mp, gp, constitution, intelligence, perception, unbuffedStr, buffsStr){ let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_RADIANT_SHIELD, "skill.") if (sufficientLevel){ // Check how much damage it will deal once, and then whether they have enough MP for multi-use (to exceed 90.71 damage) let damageOnce = radiantDamageNoCrit(constitution) let numberTimes = checkManaMultiUse(mp, MP_COST_RADIANT_SHIELD, damageOnce, false, intelligence, perception) // If mana is sufficient if (numberTimes > 0) { let damageDealt = radiantDamageActual(constitution, intelligence, numberTimes) doDamage(damageDealt, MP_COST_RADIANT_SHIELD, numberTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, false, 0) } } } // When the Sneak Attack button is clicked function doButtonSneakAttack(lvl, hp, xp, mp, gp, perception, intelligence, strength, unbuffedStr, buffsStr) { let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_SNEAK_ATTACK, "skill.") if (sufficientLevel){ // Check how much damage it will deal once, and then whether they have enough MP for multi-use (to exceed 90.71 damage) let damageOnce = sneakDamageNoCrit(perception) let numberTimes = checkManaMultiUse(mp, MP_COST_SNEAK_ATTACK, damageOnce, false, intelligence, perception) // If mana is sufficient if (numberTimes > 0) { let damageDealt = sneakDamageActual(perception, strength, numberTimes) doDamage(damageDealt, MP_COST_SNEAK_ATTACK, numberTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, false, 0) } } } // Create custom reward buttons function api_createNewTaskForUser(payload) { var params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), // Rightmost button goes on top "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/tasks/user" UrlFetchApp.fetch(url, params) } // Create a webhook if no duplicate exists function apiMult_createNewWebhookNoDuplicates(payload) { const response = api_getWebhooks() const webhooks = JSON.parse(response).data var duplicateExists = 0 for (var i in webhooks) { if (webhooks[i].label == payload.label) { duplicateExists = 1 } } // If webhook to be created doesn't exist yet if (!duplicateExists) { api_createNewWebhook(payload) } } // Used to see existing webhooks, and therefore if there's a duplicate function api_getWebhooks() { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook" return UrlFetchApp.fetch(url, params) } // Creates a webhook (as part of the "don't make it if there's a duplicate" function) function api_createNewWebhook(payload) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook" return UrlFetchApp.fetch(url, params) } // Sets initial properties that will be used/saved later. function initScriptProperties() { var timestampInit = Date.now() scriptProperties.setProperty(TIMESTAMP_KEY, timestampInit) } // Gets user info so I can use it, especially stats like mana, experience, and level function api_getAuthenticatedUserProfile(userFields) { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/user" if (userFields != "") { url += "?userFields=" + userFields } return UrlFetchApp.fetch(url, params) } function apiFree_getAllAvailableContentObjects() { const params = { "method" : "get", "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/content" return UrlFetchApp.fetch(url, params) } // Send a notification as a private message, only if they're enabled function api_sendPrivateMessage(payload) { switch (NOTIFICATIONS_ON){ // Check if notifications are on, send message if yes case 0: break case 1: const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/members/send-private-message" return UrlFetchApp.fetch(url, params) break } } // Send a notification as a private message regardless of if they're enabled function api_sendPrivateMessageAlways(payload) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/members/send-private-message" return UrlFetchApp.fetch(url, params) } function calculateStrength() { const levelStrRaw = Math.floor(user.stats.lvl / 2) const levelStr = (levelStrRaw > 50) ? 50 : levelStrRaw var totalEquipmentAndClassStr = 0 const allocatedStr = user.stats.str const buffsStr = user.stats.buffs.str // Get STR from equipped gear totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.weapon]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.shield]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.head]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.armor]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.headAccessory]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.eyewear]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.body]) totalEquipmentAndClassStr += calcEquipmentAndClassStr(content.gear.flat[user.items.gear.equipped.back]) return levelStr + totalEquipmentAndClassStr + allocatedStr + buffsStr } function calcEquipmentAndClassStr(equipment) { var equipmentAndClassStr = 0 if (equipment != undefined) { equipmentAndClassStr += equipment.str if ( (equipment.klass == user.stats.class) || ( (equipment.klass == "special") && (equipment.specialClass == user.stats.class) ) ) { equipmentAndClassStr += equipment.str / 2 } } return equipmentAndClassStr } function calculateIntelligence() { const levelIntRaw = Math.floor(user.stats.lvl / 2) const levelInt = (levelIntRaw > 50) ? 50 : levelIntRaw var totalEquipmentAndClassInt = 0 const allocatedInt = user.stats.int const buffsInt = user.stats.buffs.int // Get INT from equipped gear totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.weapon]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.shield]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.head]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.armor]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.headAccessory]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.eyewear]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.body]) totalEquipmentAndClassInt += calcEquipmentAndClassInt(content.gear.flat[user.items.gear.equipped.back]) return levelInt + totalEquipmentAndClassInt + allocatedInt + buffsInt } function calcEquipmentAndClassInt(equipment) { var equipmentAndClassInt = 0 if (equipment != undefined) { equipmentAndClassInt += equipment.int if ( (equipment.klass == user.stats.class) || ( (equipment.klass == "special") && (equipment.specialClass == user.stats.class) ) ) { equipmentAndClassInt += equipment.int / 2 } } return equipmentAndClassInt } function calculateConstitution() { const levelConRaw = Math.floor(user.stats.lvl / 2) const levelCon = (levelConRaw > 50) ? 50 : levelConRaw var totalEquipmentAndClassCon = 0 const allocatedCon = user.stats.con const buffsCon = user.stats.buffs.con // Get CON from equipped gear totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.weapon]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.shield]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.head]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.armor]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.headAccessory]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.eyewear]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.body]) totalEquipmentAndClassCon += calcEquipmentAndClassCon(content.gear.flat[user.items.gear.equipped.back]) return levelCon + totalEquipmentAndClassCon + allocatedCon + buffsCon } function calcEquipmentAndClassCon(equipment) { var equipmentAndClassCon = 0 if (equipment != undefined) { equipmentAndClassCon += equipment.con if ( (equipment.klass == user.stats.class) || ( (equipment.klass == "special") && (equipment.specialClass == user.stats.class) ) ) { equipmentAndClassCon += equipment.con / 2 } } return equipmentAndClassCon } function calculatePerception() { const levelPerRaw = Math.floor(user.stats.lvl / 2) const levelPer = (levelPerRaw > 50) ? 50 : levelPerRaw var totalEquipmentAndClassPer = 0 const allocatedPer = user.stats.per const buffsPer = user.stats.buffs.per // Get PER from equipped gear totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.weapon]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.shield]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.head]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.armor]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.headAccessory]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.eyewear]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.body]) totalEquipmentAndClassPer += calcEquipmentAndClassPer(content.gear.flat[user.items.gear.equipped.back]) return levelPer + totalEquipmentAndClassPer + allocatedPer + buffsPer } function calcEquipmentAndClassPer(equipment) { var equipmentAndClassPer = 0 if (equipment != undefined) { equipmentAndClassPer += equipment.per if ( (equipment.klass == user.stats.class) || ( (equipment.klass == "special") && (equipment.specialClass == user.stats.class) ) ) { equipmentAndClassPer += equipment.per / 2 } } return equipmentAndClassPer } // Checks if sufficient mana, sends message if not. Choose whether to deduct costs now or later. function checkMana(mp, mpCost, deductLater){ if (mp < mpCost) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_MANA_SKILL, "toUserId" : USER_ID}) return false } else { if (deductLater) { return true } else { api_updateUser({"stats.mp" : mp - mpCost}) return true } } } // How many uses of the skill are required to exceed 90.71 damage? At that damage or higher, crit is guaranteed and thus the script can accurately dial in damage function checkManaMultiUse(mp, mpCost, damageOnce, isFlamesXp, int, per) { let numberTimes = Math.ceil(90.71/damageOnce) let damageMulti = damageOnce * numberTimes let multiMpCost = mpCost * numberTimes // If insufficient MP for multi use, send an error message including expected damage (and if Flames with XP, expected XP) if ( multiMpCost > mp ) { // round the damage to one decimal point (for the sake of the message) let damageMultiForMessage = Math.round(damageMulti*10) / 10 if (isFlamesXp) { let xpGain = burstOfFlamesXp(int, per, numberTimes) api_sendPrivateMessage({"message" : MSG_MULTI_USE_MANA_CHECK_1 + multiMpCost + MSG_MULTI_USE_MANA_CHECK_2 + damageMultiForMessage + MSG_MULTI_USE_MANA_CHECK_3 + MSG_MULTI_USE_MANA_CHECK_4A_1 + xpGain + MSG_MULTI_USE_MANA_CHECK_4A_2, "toUserId" : USER_ID}) return 0 } else { api_sendPrivateMessage({"message" : MSG_MULTI_USE_MANA_CHECK_1 + multiMpCost + MSG_MULTI_USE_MANA_CHECK_2 + damageMultiForMessage + MSG_MULTI_USE_MANA_CHECK_3 + MSG_MULTI_USE_MANA_CHECK_4B, "toUserId" : USER_ID}) return 0 } } else { return numberTimes } } // Check if sufficient level, send message if not. function checkLevelLock(level, levelLock, messageEndVariable){ if (level < levelLock){ let errorMsg = MSG_LEVEL_LOCK_START + levelLock + MSG_LEVEL_LOCK_END + messageEndVariable api_sendPrivateMessage({"message" : errorMsg, "toUserId" : USER_ID}) return false } else { return true } } // Changes stats function api_updateUser(payload) { const params = { "method" : "put", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user" return UrlFetchApp.fetch(url, params) } function getTaskValue(version, presetTaskValue, taskId) { switch (version){ case 2: let value = sanitizeInput(presetTaskValue, 0, 21.27) return value break case 3: const responseV3 = api_getUserTasks("dailys") const tasksV3 = JSON.parse(responseV3).data // loop over all Dailies, then average the task value let taskCount = 0 let taskValueTotal = 0 for (var i in tasksV3) { taskValueTotal += tasksV3[i].value taskCount++ } let average = sanitizeInput((taskValueTotal/taskCount), 0, 21.27) return average break case 4: const responseV4 = api_getUserTasks("dailys") const tasksV4 = JSON.parse(responseV4).data let valueV4 = -100 for (var j in tasksV4) { if (tasksV4[j]._id == taskId) { valueV4 = tasksV4[j].value } } if (valueV4 == -100) { api_sendPrivateMessageAlways({"message" : MSG_TASK_ID_NOT_FOUND, "toUserId" : USER_ID}) let valueIfError = sanitizeInput(presetTaskValue, 0, 21.27) return valueIfError break } else { // grab value of task ID let valueById = sanitizeInput(valueV4, 0, 21.27) return valueById break } } } // Ensures that the input is between the minimum and maximum. Can use "none" in either argument to indicate if there is either no minimum or no maximum. function sanitizeInput(input, minimum, maximum) { if (minimum == "none"){ // Condition for if there is no minimum if ( input > maximum ) { return maximum } else { return input } } else if (maximum == "none"){ // Condition for if there is no maximum if ( input < minimum ) { return minimum } else { return input } } else { // Condition where there is both minimum and maximum if ( input < minimum ) { return minimum } else if ( input > maximum ) { return maximum } else { return input } } } function criticalHit (stat, chance) { let targetToBeat = chance * ( (1 + stat ) / 100 ) if (Math.random() <= targetToBeat) { return (1.5 + ( 4 * stat ) / ( stat + 200 ) ) } else { return 1 } } function pickpocket (gp, per) { let taskValue = getTaskValue(PICKPOCKET_VERSION, PICKPOCKET_TASK_VALUE, PICKPOCKET_TASK_ID) let bonus = ( (taskValue + 1) + (per * 0.5 ) ) let newGp = gp + ( ( 25 * bonus ) / ( bonus + 75) ) api_updateUser({"stats.gp" : newGp}) } function backstab (gp, xp, str) { let multiplier = criticalHit(str, 0.3) let modifiedStr = str * multiplier let taskValue = getTaskValue(BACKSTAB_VERSION, BACKSTAB_TASK_VALUE, BACKSTAB_TASK_ID) let bonus = ( (taskValue + 1) + (modifiedStr * 0.5 ) ) let newXp = xp + ( ( 75 * bonus ) / ( bonus + 50) ) let newGp = gp + ( ( 18 * bonus ) / ( bonus + 75) ) api_updateUser({"stats.gp" : newGp, "stats.exp" : newXp}) } function stealth (totalDailies, prevStealthedDailies, per) { let newDailiesDodged = Math.ceil( ( 0.64 * totalDailies * ( per / (per + 55) ) ) ) // If they'd Stealth more than total Dailies, instead only Stealth the total if ( ( prevStealthedDailies + newDailiesDodged ) >= totalDailies ) { api_updateUser({"stats.buffs.stealth" : totalDailies}) } else { api_updateUser({"stats.buffs.stealth" : prevStealthedDailies + newDailiesDodged}) } } function healingLight (hp, con, int) { let healing = ( ( con + int + 5 ) * 0.075 ) if (hp + healing >= 50) { api_updateUser({"stats.hp" : 50}) } else { api_updateUser({"stats.hp" : hp + healing}) } } function chillingFrost(mp, mpCost) { api_updateUser({"stats.buffs.streaks" : true, "stats.mp" : mp - mpCost}) } function burstOfFlamesXp(int, per, nTimes) { let taskValue = getTaskValue(BURST_OF_FLAMES_VERSION, BURST_OF_FLAMES_TASK_VALUE, BURST_OF_FLAMES_TASK_ID) let multiplier = 1 let modifiedInt = int let bonus = 1 let newXpRunningTotal = 0 for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(per, 0.03) modifiedInt = int * multiplier bonus = Math.ceil( (taskValue + 1) * (modifiedInt * 0.075 ) ) newXpRunningTotal += ( ( 75 * bonus ) / ( bonus + 37.5) ) } return newXpRunningTotal } // Expected and actual damage for Burst of Flames function flamesDamage (int) { return Math.ceil(int/10) } // Expected damage for Brutal Smash. Since this is expected damage only (not actual), don't crit. function smashDamageNoCrit (str) { return (55*str)/(str+70) } // Actual damage for Brutal Smash, including crit. Since there's a chance of crit at each iteration, loop through for each use of the skill rather than doing it once and multiplying by number of times function smashDamageActual (str, con, nTimes) { let smashRunningTotal = 0 let multiplier = 1 let modifiedStr = str for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(con, 0.03) modifiedStr = str * multiplier smashRunningTotal += (55*modifiedStr)/(modifiedStr+70) } return smashRunningTotal } // Expected damage for Radiant Shield. Since this is expected damage only (not actual), don't crit. function radiantDamageNoCrit(con) { // CON above 225 has no additional in-game benefits elsewhere in Habitica (reducing damage from your own tasks), so I will cap damage here as well if (con > 225) { return 25 } else { // base damage of 10 ensure this skill isn't too weak at low levels return 10 + (con/15) } } // Actual damage for Radiant Shield, including crit. Since there's a chance of crit at each iteration, loop through for each use of the skill rather than doing it once and multiplying by number of times function radiantDamageActual(con, int, nTimes) { let radiantRunningTotal = 0 let multiplier = 1 let modifiedCon = con for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(int, 0.03) modifiedCon = con * multiplier if ( modifiedCon > 225) { // Max damage of 25 occurs at 225 CON. Since this is weaker than the other damage skills, the skill has a lower MP cost radiantRunningTotal += 25 } else { // based damage of 10 ensures it's not too weak at low levels radiantRunningTotal += ( 10 + (modifiedCon/15) ) } } return radiantRunningTotal } // Expected damage for Sneak Attack. Since this is expected damage only (not actual), don't crit. function sneakDamageNoCrit(per) { // Goal: keep this skill as similar to Brutal Smash as possible. // Problem: Tools of the Trade buffs PER more than Valorous Presence buffs STR. So, it can't be swap of PER = STR. // Solution: scale damage based on how many buffs it takes. Keep it the same formula up to 35 damage (112.5 STR/PER). // Based on Brutal Smash / Valorous Presence, 16 buffs goes from 35 to 45 damage, 32 further buffs to go from 45 to 50 damage. // Up to 35 damage (PER = 122.5), Sneak Attack is identical to Brutal Smash if (per <= 122.5) { return (55 * per) / (per + 70) } // 35 damage from the previous "tier" formula rolls over, then aim to be at 45 total damage at 1486 PER else if ( (per > 122.5) && (per <= 1486) ) { return 35 + (20 * per) / (per + 1486) } // 45 damage from the previous "tier" formula rolls over, then aim to be at 50 total damage at 4213 PER else if (per > 1486) { return 45 + (10 * per) / (per + 4213) } else { // this scenario shouldn't be possible, but just in case… api_sendPrivateMessage({"message" : MSG_ERROR_DAMAGE_SNEAK_ATTACK, "toUserId" : USER_ID}) } } // Actual damage for Sneak Attack, including crit. Since there's a chance of crit at each iteration, loop through for each use of the skill rather than doing it once and multiplying by number of times function sneakDamageActual(per, str, nTimes) { let sneakRunningTotal = 0 let multiplier = 1 let modifiedPer = per for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(str, 0.03) modifiedPer = per * multiplier // Instead of using PER for the cutoff points, use modifiedPer because it's PER times crit multiplier if (modifiedPer <= 122.5) { sneakRunningTotal += (55*modifiedPer)/(modifiedPer+70) } else if ( (modifiedPer > 122.5) && (modifiedPer <= 1486) ) { sneakRunningTotal += 35 + ( (20*modifiedPer)/(modifiedPer+1486) ) } else if (modifiedPer > 1486) { sneakRunningTotal += 45 + ( (10*modifiedPer)/(modifiedPer+4213) ) } } return sneakRunningTotal } // Deals boss damage function doDamage(damage, mpCost, nTimes, unbuffedStr, buffsStr, hp, xp, mp, gp, lvl, isFlamesXp, valueToChange) { let totalMpCost = mpCost * nTimes // calculate STR setpoint needed to achieve expected damage. Damage = (1+(STR*0.005)) * (1.5 + ((4*STR)/(200+STR)) ) * 1, since crit is 100% guaranteed and task value delta resolves to 1 // lowest STR setpoint is 3244, which guarantees a crit, for 90.71 damage. Highest setpoint is 6500, for 180.25 damage, since that's the most this script will do in one use. let setpoint = setpointIteration(3244, 6500, damage) // Create one To-Do so I can check it off in order to do damage api_createNewTaskForUser(DAMAGE_BUTTON) // pause for 1 second Utilities.sleep(1000) // buff STR up to setpoint api_updateUser({"stats.buffs.str" : setpoint - unbuffedStr}) // pause for 1 second Utilities.sleep(1000) // check off the To-Do created const responseTask = api_getUserTasks("todos") const tasksForDamage = JSON.parse(responseTask).data let taskIdForDamage = "" // loop through all To-Do's, see which one matches Text and Notes for the previously-created Damage Button for (var j in tasksForDamage) { if ( (tasksForDamage[j].text == DAMAGE_TEXT) && (tasksForDamage[j].notes == DAMAGE_NOTES) ) { taskIdForDamage = tasksForDamage[j]._id // if it's a match, save ID of the task since that's the easiest way for me to grab it api_scoreTask(taskIdForDamage, "up") // this is the task for me to score } } // reset values back to their original ones, including FCV-able ones that may have changed. Deduct mana cost also // if XP needs to increase (Burst of Flames), do this in this step also if (isFlamesXp) { api_updateUser({"stats.buffs.str" : buffsStr, "stats.hp" : hp, "stats.exp" : xp + valueToChange, "stats.mp" : mp - totalMpCost, "stats.gp" : gp, "stats.lvl" : lvl}) } else { api_updateUser({"stats.buffs.str" : buffsStr, "stats.hp" : hp, "stats.exp" : xp, "stats.mp" : mp - totalMpCost, "stats.gp" : gp, "stats.lvl" : lvl}) } } // calculate setpoint, inputting a low guess and a high guess function setpointIteration (low, high, targetDamage) { let midpoint = Math.floor(((low+high)/2)) // damage at each setpoint let damageLow = damageAtSetpoint(low) let damageMidpoint = damageAtSetpoint(midpoint) let damageHigh = damageAtSetpoint(high) // truth conditions regarding damage and where it falls (compared to the setpoints) let targetAboveLow = (damageLow < targetDamage) ? true : false let targetAboveMidpoint = (damageMidpoint < targetDamage) ? true : false let targetBelowHigh = (damageHigh >= targetDamage) ? true : false // if these values are within 2 of each other, iteration is done if ( ( high - low ) <= 2) { // whichever setpoint exceeds target damage is our setpoint if (!targetAboveLow) { return low } else if (!targetAboveMidpoint) { return midpoint } else if (targetBelowHigh) { return high } else { // This condition shouldn't be possible, but just in case… api_sendPrivateMessage({"message" : MSG_ERROR_DAMAGE_FINAL_ITERATION, "toUserId" : USER_ID}) } } else { // keep iterating // Confirm target is between high and low if (targetAboveLow && targetBelowHigh) { // is it above or below midpoint? If above it, run it again but with midpoint and high being the new low/high, else run it with mid/low being new low/high if (targetAboveMidpoint) { return setpointIteration (midpoint, high, targetDamage) } else { return setpointIteration (low, midpoint, targetDamage) } } else { // if it's not between low and high, something messed up. This condition shouldn't be possible, but just in case… api_sendPrivateMessage({"message" : MSG_ERROR_DAMAGE_SETPOINT, "toUserId" : USER_ID}) } } } // calculate damage for checking off one task at a given value of STR function damageAtSetpoint (str) { return (1+(str*0.005)) * (1.5 + ((4*str)/(200+str)) ) } function api_getUserTasks(type) { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/tasks/user" if (type != "") { url += "?type=" + type } return UrlFetchApp.fetch(url, params) } // Score a task function api_scoreTask(aliasOrId, direction) { var params = { "method" : "post", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/tasks/" if ( (aliasOrId != "") && (direction != "") ) { url += aliasOrId + "/score/" + direction } return UrlFetchApp.fetch(url, params) }