// 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 */ /* ========================================== */ // Sections below are for each of the categories of my scripts. // If you want to skip a section, you can collapse the outline by clicking on the arrow in the left margin. /* ========================================== */ /* Section: Automated FCV Scripts */ // Choose which potions/buttons you want. 1 indicates you want it, 0 indicates you don't. const CREATE_EXPERIENCE_POTION = 0 // Pay 25 GP to gain 150 XP const CREATE_MANA_POTION = 0 // Pay 25 GP to gain 30 MP const CREATE_DRAINING_POTION = 0 // Pay 30 MP to gain 25 GP const CREATE_FATIGUING_POTION = 0 // Pay 15 HP to gain 25 GP const CREATE_STUPEFYING_POTION = 0 // Pay 150 XP to gain 25 GP const CREATE_HP_FOR_MP = 0 // Pay 5 HP to gain 10 MP const CREATE_XP_FOR_MP = 0 // Pay 50 XP to gain 10 MP // Creating editable/customizable Fix Character Values buttons // How many customizable buttons do you want? Enter any number between 1 and 5. // If you don't want any, enter 0. const NUMBER_OF_BUTTONS = 0 /* ========================================== */ /* Section: Custom skills */ // There are a lot of custom skills, so I've organized them into two broad categories: buffs and non-buffs. // Within each category, I've grouped them further. // If you want to skip a section, you can collapse the outline by clicking on the arrow in the left margin. // Cross-Class Stat Buffs // Buffs to all four stats (STR, INT, CON, PER) const CREATE_OMNI_BUFF = 0 // Change this 0 to a 1 if you want a skill that buffs all four stats for 55 MP. // Buffs to one stat const CREATE_VALOROUS_PRESENCE_SELF = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength for 10 MP. const CREATE_EARTHQUAKE_SELF = 0 // Change this 0 to a 1 if you want a skill that buffs your Intelligence for 17.5 MP. const CREATE_PROTECTIVE_AURA_SELF = 0 // Change this 0 to a 1 if you want a skill that buffs your Constitution for 15 MP. const CREATE_TOOLS_OF_THE_TRADE_SELF = 0 // Change this 0 to a 1 if you want a skill that buffs your Perception for 12.5 MP. // Buffs to three stats const CREATE_TRIPLE_BUFF_STR_INT_CON = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength, Intelligence, and Constitution for 42.5 MP. const CREATE_TRIPLE_BUFF_STR_INT_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength, Intelligence, and Perception for 40 MP. const CREATE_TRIPLE_BUFF_STR_CON_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength, Constitution, and Perception for 37.5 MP. const CREATE_TRIPLE_BUFF_INT_CON_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Intelligence, Constitution, and Perception for 45 MP. // Buffs to two stats const CREATE_DOUBLE_BUFF_STR_INT = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength and Intelligence for 27.5 MP. const CREATE_DOUBLE_BUFF_STR_CON = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength and Constitution for 25 MP. const CREATE_DOUBLE_BUFF_STR_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Strength and Perception for 22.5 MP. const CREATE_DOUBLE_BUFF_INT_CON = 0 // Change this 0 to a 1 if you want a skill that buffs your Intelligence and Constitution for 32.5 MP. const CREATE_DOUBLE_BUFF_INT_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Intelligence and Perception for 30 MP. const CREATE_DOUBLE_BUFF_CON_PER = 0 // Change this 0 to a 1 if you want a skill that buffs your Constitution and Perception for 27.5 MP. // Cross-Class Skills // If you choose Pickpocket, Backstab, and/or Burst of Flames, you'll also need to select a few other options, so scroll to that section after. // Skills to create 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. // Options for Pickpocket, Backstab, and/or Burst of Flames // 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 Daily and always use that Daily 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 Daily 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" /* ========================================== */ /* Section: Game modes */ // Bad Day Mode // If you want to create the button for Bad Day Mode, change the 0 to a 1 in the line below. const CREATE_BAD_DAY_MODE_BUTTON = 0 // Partial Healing Mode // If you don't want to activate Partial Healing, keep the number below as a 0. // If you do want to, select which version you want to use // If you want to heal a set number when leveling up (such as 4.5 HP), select 2. // If you want to heal a percent of your remaining health (such as 25%), select 3. const VERSION_PARTIAL_HEALING = 0 // How much HP do you want to gain each level-up? Fill in the correct one (set value or percent of remaining health) const HP_SET_VALUE_PER_LEVEL = 4.5 // Enter a number between 0 and 50. const HP_PERCENT_PER_LEVEL = 50.0 // Do not put a percent sign after the number here. Enter a number between 0 and 100. // Duel Mode // If you want to duel, change the 0 to a 1 in the line below. It will create the duel buttons. const I_WANT_TO_DUEL = 0 /* ========================================== */ /* Section: Just for fun */ const CREATE_RANDOM = 0 // Change this 0 to a 1 if you want the Random Transformation (self only) item const CREATE_SNOWBALL = 0 // Change this 0 to a 1 if you want the Snowball (self only) Transformation Item const CREATE_SHINY_SEED = 0 // Change this 0 to a 1 if you want the Shiny Seed (self only) Transformation Item const CREATE_SEAFOAM = 0 // Change this 0 to a 1 if you want the Seafoam (self only) Transformation Item const CREATE_SPOOKY_SPARKLES = 0 // Change this 0 to a 1 if you want the Spooky Sparkles (self only) Transformation Item /* ========================================== */ /* [Users] Optional customizations to fill in */ /* ========================================== */ // Do you want to get private message notifications? (examples include if you're already at max Mana or exceeded the limit for daily potion usage) // If you don't want them, change the 1 to a 0 in the line below const NOTIFICATIONS_ON = 1 // Sections below are for each of the categories of my scripts, which allows you to skip to the sections you want. /* ========================================== */ /* Section: Automated FCV Scripts */ // What do you want to call each editable button? Cutomize the titles here, or leave them as is (Editable Button) const BUTTON_1_TEXT = "Editable Button 1" const BUTTON_2_TEXT = "Editable Button 2" const BUTTON_3_TEXT = "Editable Button 3" const BUTTON_4_TEXT = "Editable Button 4" const BUTTON_5_TEXT = "Editable Button 5" /* ========================================== */ /* Section: Custom skills */ /* ========================================== */ /* Section: Game modes */ // Partial Healing Mode // Do you want a button that lets you enter/exit Partial Healing Mode? If yes, change the 0 to a 1 below. // If you don't have a button, you can still enter/exit the Mode by manually running the function "togglePartialHealingMode" const PARTIAL_HEALING_TOGGLE_BUTTON = 0 // Dueling Mode - scoring // Do you want the duel-related buttons to have a tag? If yes, type it below. const TAG_FOR_DUEL_BUTTONS = "typeTagHere" /* ========================================== */ /* Section: Just for fun */ // Do you want to randomly transform for free each day at Cron? If yes, change the 0 to a 1 in the line below. const AUTOMATIC_TRANSFORM_RANDOM = 0 /* ========================================== */ /* [Users] Do not edit code below this line */ /* ========================================== */ const AUTHOR_ID = "0034eb14-b4d8-494e-8386-d3f33cff7922" const SCRIPT_NAME = "mike_the_monk all scripts combined" 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 // Global constants: buttons and messages // Conversion rates between stats const XP_CONVERSION_RATE = 6 // 150 XP = 25 GP const MP_CONVERSION_RATE = 1.2 // 30 MP = 25 GP const HP_CONVERSION_RATE = 0.6 // 15 HP = 25 GP const HP_FOR_MP_CONVERSION_RATE = MP_CONVERSION_RATE / HP_CONVERSION_RATE // 5 HP = 10 MP const XP_FOR_MP_CONVERSION_RATE = MP_CONVERSION_RATE / XP_CONVERSION_RATE // 50 XP = 10 MP // Parts of messages that get reused - these are for partial messages, not full. Full messages are elsewhere. const MSG_LEVEL_LOCK_START = "You must be at least level " const MSG_LEVEL_LOCK_END = " to use this " const MSG_DAILY_USAGE_EXCEEDED_START = "You can only use this " const MSG_DAILY_USAGE_EXCEEDED_END = " times each day." const MSG_DAILY_USAGE_EXCEEDED_XP_START_1 = "The maximum XP you can " const MSG_DAILY_USAGE_EXCEEDED_XP_START_2 = " each day from this " const MSG_DAILY_USAGE_EXCEEDED_XP_START = MSG_DAILY_USAGE_EXCEEDED_XP_START_1 + "gain" + MSG_DAILY_USAGE_EXCEEDED_XP_START_2 const MSG_DAILY_USAGE_EXCEEDED_XP_START_PAY = MSG_DAILY_USAGE_EXCEEDED_XP_START_1 + "pay" + MSG_DAILY_USAGE_EXCEEDED_XP_START_2 const MSG_DAILY_USAGE_EXCEEDED_XP_MID_1 = " is " const MSG_DAILY_USAGE_EXCEEDED_XP_MID_2 = " XP, which is " const MSG_DAILY_USAGE_EXCEEDED_XP_END = " of what's needed to level up." const MSG_XP_DAILY_USAGE = MSG_DAILY_USAGE_EXCEEDED_XP_END const MSG_ALREADY_AT_MAX_MANA_START = "You are already have maximum mana. This " const MSG_ALREADY_AT_MAX_MANA_MID = " had no effect and did not count towards your daily limit of " const MSG_NEAR_MAX_MANA_END = " filled you to maximum Mana, but not beyond the max." const MSG_INSUFFICIENT_START = "Insufficient " const MSG_INSUFFICIENT_START_MANA = MSG_INSUFFICIENT_START + "Mana" const MSG_INSUFFICIENT_START_HEALTH = MSG_INSUFFICIENT_START + "Health" const MSG_INSUFFICIENT_START_EXPERIENCE = MSG_INSUFFICIENT_START + "Experience" const MSG_INSUFFICIENT_END = " to use this " const MSG_INSUFFICIENT_END_POTION = MSG_INSUFFICIENT_END + "potion." const MSG_INSUFFICIENT_END_SKILL = MSG_INSUFFICIENT_END + "skill." const NOTES_START = "Pay " const NOTES_MID1 = ", gain " const NOTES_MID2 = " (Instant Use). Maximum " const NOTES_END = " per day." const EDITABLE_FCV_HELP_FORMATTING = " For help formatting the notes section of the button, see the [wiki page](https://habitica.fandom.com/wiki/Editable_Fix_Character_Values_Buttons)." const INSUFFICIENT_COST_MSG_START = "You don't have enough " const INSUFFICIENT_COST_MSG_END = " to pay for this skill." const INSUFFICIENT_COST_MSG_VARIABLE_4 = "HP or XP or MP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_3H = "XP or MP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_3X = "HP or MP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_3M = "HP or XP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_3G = "HP or XP or MP" const INSUFFICIENT_COST_MSG_VARIABLE_2HX = "HP or XP" const INSUFFICIENT_COST_MSG_VARIABLE_2HM = "HP or MP" const INSUFFICIENT_COST_MSG_VARIABLE_2HG = "HP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_2XM = "XP or MP" const INSUFFICIENT_COST_MSG_VARIABLE_2XG = "XP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_2MG = "MP or GP" const INSUFFICIENT_COST_MSG_VARIABLE_1H = "HP" const INSUFFICIENT_COST_MSG_VARIABLE_1X = "XP" const INSUFFICIENT_COST_MSG_VARIABLE_1M = "MP" const INSUFFICIENT_COST_MSG_VARIABLE_1G = "GP" 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_NOTES_MP = " MP." const MSG_NOTES_TRIPLE_BUFF = "Buff these three of your stats according to each unbuffed value. " const MSG_NOTES_DOUBLE_BUFF = "Buff both of these stats for you according to each unbuffed value. " const PARTIAL_HEALING_MODE_TEXT_START = "Click to " const PARTIAL_HEALING_MODE_TEXT_END = " Partial Healing Mode" const PARTIAL_HEALING_MODE_NOTES_START = "You are " const PARTIAL_HEALING_MODE_NOTES_MID = " in Partial Healing Mode. Click this button to " const PARTIAL_HEALING_MODE_NOTES_END = " that Mode." const MSG_INSUFFICIENT_GP_START = "Insufficient gold: you need at least " const MSG_INSUFFICIENT_GP_END = " GP to transform" // Level locks // Automated FCV scripts const XP_POTION_LEVEL_LOCK = 21 const MP_POTION_LEVEL_LOCK = 11 const ANTI_MP_POTION_LEVEL_LOCK = 11 const ANTI_XP_POTION_LEVEL_LOCK = 21 const HP_FOR_MP_LEVEL_LOCK = 11 const XP_FOR_MP_LEVEL_LOCK = 11 // Custom skills const LEVEL_LOCK_BUFFS = 13 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 // Mana (or other) cost // Automated FCV scripts const XP_POTION_COST = 25 const MP_POTION_COST = 25 const ANTI_MP_POTION_MP_COST = 30 const ANTI_HP_POTION_HP_COST = 15 const ANTI_XP_POTION_XP_COST = 150 const HP_COST_HP_FOR_MP = 5 const XP_COST_XP_FOR_MP = 50 // Custom skills const OMNI_BUFF_MP_COST = 55 const VALOROUS_PRESENCE_SELF_MP_COST = 10 const EARTHQUAKE_SELF_MP_COST = 17.5 const PROTECTIVE_AURA_SELF_MP_COST = 15 const TOOLS_OF_THE_TRADE_SELF_MP_COST = 12.5 const TRIPLE_BUFF_STR_INT_CON_MP_COST = 42.5 const TRIPLE_BUFF_STR_INT_PER_MP_COST = 40 const TRIPLE_BUFF_STR_CON_PER_MP_COST = 37.5 const TRIPLE_BUFF_INT_CON_PER_MP_COST = 45 const DOUBLE_BUFF_STR_INT_MP_COST = 27.5 const DOUBLE_BUFF_STR_CON_MP_COST = 25 const DOUBLE_BUFF_STR_PER_MP_COST = 22.5 const DOUBLE_BUFF_INT_CON_MP_COST = 32.5 const DOUBLE_BUFF_INT_PER_MP_COST = 30 const DOUBLE_BUFF_CON_PER_MP_COST = 27.5 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 // Just For Fun const COST_TO_TRANSFORM = 15 const COST_TO_TRANSFORM_BACK = 5 // Usage limits // Automated FCV scripts const MAX_DAILY_XP_POTION_USAGE = "1/3" const MAX_DAILY_MP_POTION_USAGE = 4 const MAX_DAILY_ANTI_MP_POTION_USAGE = 4 const MAX_DAILY_ANTI_HP_POTION_USAGE = 4 const MAX_DAILY_ANTI_XP_POTION_USAGE = 4 const MAX_DAILY_HP_FOR_MP_USAGE = 12 const MAX_DAILY_XP_FOR_MP_USAGE = MAX_DAILY_XP_POTION_USAGE // Button text and stats // Automated FCV scripts const XP_POTION_TEXT = "**Experience Potion** ![Yellow potion](https://raw.githubusercontent.com/mike-the-monk/Habitica-customizations/main/Automated%20FCV%20scripts/images/experience%20potion%20large%20transparent%20wide%20432.png)" const XP_POTION_ALIAS = "XPpotion" const XP_GAIN = XP_CONVERSION_RATE * XP_POTION_COST const XP_POTION_NOTES = "Gain " + XP_GAIN + " Experience (Instant Use). Maximum per day: no more than " + MAX_DAILY_XP_POTION_USAGE + MSG_XP_DAILY_USAGE const MP_POTION_TEXT = "**Mana Potion** ![Blue potion](https://raw.githubusercontent.com/mike-the-monk/Habitica-customizations/main/Automated%20FCV%20scripts/images/mana%20potion%20large%20transparent%20wide%20432.png)" const MP_POTION_ALIAS = "MPpotion" const MP_GAIN = MP_CONVERSION_RATE * MP_POTION_COST const MP_POTION_NOTES = "Recover " + MP_GAIN + " Mana (Instant Use). Maximum " + MAX_DAILY_MP_POTION_USAGE + " per day." const ANTI_MP_POTION_TEXT = "**Draining Potion** ![Orange-brown potion](https://raw.githubusercontent.com/mike-the-monk/Habitica-customizations/main/Automated%20FCV%20scripts/images/draining%20potion%20transparent%20large%20wide%20432.png)" const ANTI_MP_POTION_ALIAS = "antiMPpotion" const ANTI_MP_POTION_VALUE = "0" const ANTI_MP_POTION_GP_GAIN = ANTI_MP_POTION_MP_COST / MP_CONVERSION_RATE const ANTI_MP_POTION_NOTES = NOTES_START + ANTI_MP_POTION_MP_COST + " MP" + NOTES_MID1 + ANTI_MP_POTION_GP_GAIN + " GP" + NOTES_MID2 + MAX_DAILY_ANTI_MP_POTION_USAGE + NOTES_END const ANTI_HP_POTION_TEXT = "**Fatiguing Potion** ![Teal potion](https://raw.githubusercontent.com/mike-the-monk/Habitica-customizations/main/Automated%20FCV%20scripts/images/fatiguing%20potion%20transparent%20large%20wide%20432.png)" const ANTI_HP_POTION_ALIAS = "antiHPpotion" const ANTI_HP_POTION_VALUE = "0" const ANTI_HP_POTION_GP_GAIN = ANTI_HP_POTION_HP_COST / HP_CONVERSION_RATE const ANTI_HP_POTION_NOTES = NOTES_START + ANTI_HP_POTION_HP_COST + " HP" + NOTES_MID1 + ANTI_HP_POTION_GP_GAIN + " GP" + NOTES_MID2 + MAX_DAILY_ANTI_HP_POTION_USAGE + NOTES_END const ANTI_XP_POTION_TEXT = "**Stupefying Potion** ![Indigo potion](https://raw.githubusercontent.com/mike-the-monk/Habitica-customizations/main/Automated%20FCV%20scripts/images/stupefying%20potion%20transparent%20large%20wide%20432.png)" const ANTI_XP_POTION_ALIAS = "antiXPpotion" const ANTI_XP_POTION_VALUE = "0" const ANTI_XP_POTION_GP_GAIN = ANTI_XP_POTION_XP_COST / XP_CONVERSION_RATE const ANTI_XP_POTION_NOTES = NOTES_START + ANTI_XP_POTION_XP_COST + " XP" + NOTES_MID1 + ANTI_XP_POTION_GP_GAIN + " GP" + NOTES_MID2 + MAX_DAILY_ANTI_XP_POTION_USAGE + NOTES_END const HP_FOR_MP_TEXT = "**Swap Health for Mana**" const HP_FOR_MP_ALIAS = "HPforMP" const HP_FOR_MP_VALUE = "0" const MP_GAIN_HP_FOR_MP = HP_COST_HP_FOR_MP * HP_FOR_MP_CONVERSION_RATE const HP_FOR_MP_NOTES = NOTES_START + HP_COST_HP_FOR_MP + " HP" + NOTES_MID1 + MP_GAIN_HP_FOR_MP + " MP" + NOTES_MID2 + MAX_DAILY_HP_FOR_MP_USAGE + NOTES_END const XP_FOR_MP_TEXT = "**Swap Experience for Mana**" const XP_FOR_MP_ALIAS = "XPforMP" const XP_FOR_MP_VALUE = "0" const MP_GAIN_XP_FOR_MP = XP_COST_XP_FOR_MP * XP_FOR_MP_CONVERSION_RATE const XP_FOR_MP_NOTES = NOTES_START + XP_COST_XP_FOR_MP + " XP" + NOTES_MID1 + MP_GAIN_XP_FOR_MP + " MP" + NOTES_MID2 + " XP spent per day: " + MAX_DAILY_XP_FOR_MP_USAGE + MSG_XP_DAILY_USAGE // Editable FCV buttons const BUTTON_1_ALIAS = "custom1" const BUTTON_1_NOTES = "HP: +0; XP: +0; MP: +0; GP: +0;" const BUTTON_1_VALUE = "0" const BUTTON_2_ALIAS = "custom2" const BUTTON_2_NOTES = "HP: +0; XP: +0; MP: +0; GP: +0;" const BUTTON_2_VALUE = "0" const BUTTON_3_ALIAS = "custom3" const BUTTON_3_NOTES = "HP: +0; XP: +0; MP: +0; GP: +0;" const BUTTON_3_VALUE = "0" const BUTTON_4_ALIAS = "custom4" const BUTTON_4_NOTES = "HP: +0; XP: +0; MP: +0; GP: +0;" const BUTTON_4_VALUE = "0" const BUTTON_5_ALIAS = "custom5" const BUTTON_5_NOTES = "HP: +0; XP: +0; MP: +0; GP: +0;" const BUTTON_5_VALUE = "0" // Custom skills // Cross-Class Stat Buffs const OMNI_BUFF_TEXT = "**Omni Buff (self only)**" const OMNI_BUFF_ALIAS = "omni" const OMNI_BUFF_NOTES = "Buff all four of your stats according to each unbuffed value. " + OMNI_BUFF_MP_COST + MSG_NOTES_MP const OMNI_BUFF_VALUE = "0" const VALOROUS_PRESENCE_SELF_TEXT = "**Valorous Presence (self only)**" const VALOROUS_PRESENCE_SELF_ALIAS = "1str" const VALOROUS_PRESENCE_SELF_NOTES = "Your boldness buffs your Strength! (Based on: Unbuffed STR). " + VALOROUS_PRESENCE_SELF_MP_COST + MSG_NOTES_MP const VALOROUS_PRESENCE_SELF_VALUE = "0" const EARTHQUAKE_SELF_TEXT = "**Earthquake (self only)**" const EARTHQUAKE_SELF_ALIAS = "1int" const EARTHQUAKE_SELF_NOTES = "Your mental power shakes the earth and buffs your Intelligence! (Based on: Unbuffed INT). " + EARTHQUAKE_SELF_MP_COST + MSG_NOTES_MP const EARTHQUAKE_SELF_VALUE = "0" const PROTECTIVE_AURA_SELF_TEXT = "**Protective Aura (self only)**" const PROTECTIVE_AURA_SELF_ALIAS = "1con" const PROTECTIVE_AURA_SELF_NOTES = "You shield yourself by buffing your Constitution! (Based on: Unbuffed CON). " + PROTECTIVE_AURA_SELF_MP_COST + MSG_NOTES_MP const PROTECTIVE_AURA_SELF_VALUE = "0" const TOOLS_OF_THE_TRADE_SELF_TEXT = "**Tools of the Trade (self only)**" const TOOLS_OF_THE_TRADE_SELF_ALIAS = "1per" const TOOLS_OF_THE_TRADE_SELF_NOTES = "Your tricky talents buff your Perception! (Based on: Unbuffed PER). " + TOOLS_OF_THE_TRADE_SELF_MP_COST + MSG_NOTES_MP const TOOLS_OF_THE_TRADE_SELF_VALUE = "0" const TRIPLE_BUFF_STR_INT_CON_TEXT = "**Triple Buff: Strength, Intelligence, and Constitution**" const TRIPLE_BUFF_STR_INT_CON_ALIAS = "3sic" const TRIPLE_BUFF_STR_INT_CON_NOTES = MSG_NOTES_TRIPLE_BUFF + TRIPLE_BUFF_STR_INT_CON_MP_COST + MSG_NOTES_MP const TRIPLE_BUFF_STR_INT_CON_VALUE = "0" const TRIPLE_BUFF_STR_INT_PER_TEXT = "**Triple Buff: Strength, Intelligence, and Perception**" const TRIPLE_BUFF_STR_INT_PER_ALIAS = "3sip" const TRIPLE_BUFF_STR_INT_PER_NOTES = MSG_NOTES_TRIPLE_BUFF + TRIPLE_BUFF_STR_INT_PER_MP_COST + MSG_NOTES_MP const TRIPLE_BUFF_STR_INT_PER_VALUE = "0" const TRIPLE_BUFF_STR_CON_PER_TEXT = "**Triple Buff: Strength, Constitution, and Perception**" const TRIPLE_BUFF_STR_CON_PER_ALIAS = "3scp" const TRIPLE_BUFF_STR_CON_PER_NOTES = MSG_NOTES_TRIPLE_BUFF + TRIPLE_BUFF_STR_CON_PER_MP_COST + MSG_NOTES_MP const TRIPLE_BUFF_STR_CON_PER_VALUE = "0" const TRIPLE_BUFF_INT_CON_PER_TEXT = "**Triple Buff: Intelligence, Constitution, and Perception**" const TRIPLE_BUFF_INT_CON_PER_ALIAS = "3icp" const TRIPLE_BUFF_INT_CON_PER_NOTES = MSG_NOTES_TRIPLE_BUFF + TRIPLE_BUFF_INT_CON_PER_MP_COST + MSG_NOTES_MP const TRIPLE_BUFF_INT_CON_PER_VALUE = "0" const DOUBLE_BUFF_STR_INT_TEXT = "**Double Buff: Strength and Intelligence**" const DOUBLE_BUFF_STR_INT_ALIAS = "2si" const DOUBLE_BUFF_STR_INT_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_STR_INT_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_STR_INT_VALUE = "0" const DOUBLE_BUFF_STR_CON_TEXT = "**Double Buff: Strength and Constitution**" const DOUBLE_BUFF_STR_CON_ALIAS = "2sc" const DOUBLE_BUFF_STR_CON_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_STR_CON_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_STR_CON_VALUE = "0" const DOUBLE_BUFF_STR_PER_TEXT = "**Double Buff: Strength and Perception**" const DOUBLE_BUFF_STR_PER_ALIAS = "2sp" const DOUBLE_BUFF_STR_PER_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_STR_PER_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_STR_PER_VALUE = "0" const DOUBLE_BUFF_INT_CON_TEXT = "**Double Buff: Intelligence and Constitution**" const DOUBLE_BUFF_INT_CON_ALIAS = "2ic" const DOUBLE_BUFF_INT_CON_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_INT_CON_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_INT_CON_VALUE = "0" const DOUBLE_BUFF_INT_PER_TEXT = "**Double Buff: Intelligence and Perception**" const DOUBLE_BUFF_INT_PER_ALIAS = "2ip" const DOUBLE_BUFF_INT_PER_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_INT_PER_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_INT_PER_VALUE = "0" const DOUBLE_BUFF_CON_PER_TEXT = "**Double Buff: Constitution and Perception**" const DOUBLE_BUFF_CON_PER_ALIAS = "2cp" const DOUBLE_BUFF_CON_PER_NOTES = MSG_NOTES_DOUBLE_BUFF + DOUBLE_BUFF_CON_PER_MP_COST + MSG_NOTES_MP const DOUBLE_BUFF_CON_PER_VALUE = "0" // Cross-Class Skills 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 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 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 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 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 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_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_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 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 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 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 DAMAGE_TEXT = "Boss damage" const DAMAGE_NOTES = "used to create boss damage as part of a custom script" // Game Modes const BAD_DAY_MODE_TEXT = "**Bad Day Mode**" const BAD_DAY_MODE_ALIAS = "badDayMode" const BAD_DAY_MODE_NOTES = "Declare today a 'day of low expectations', fully healing you, buffing your CON to 9999, and Stealthing all your Dailies so you take no damage if you don't complete them. This wears off tomorrow at Cron." const BAD_DAY_MODE_VALUE = "0" // Button notes&text change depending on whether you are/aren't in Partial Healing Mode const PARTIAL_HEALING_MODE_TEXT_EXIT = PARTIAL_HEALING_MODE_TEXT_START + "exit" + PARTIAL_HEALING_MODE_TEXT_END const PARTIAL_HEALING_MODE_TEXT_ENTER = PARTIAL_HEALING_MODE_TEXT_START + "enter" + PARTIAL_HEALING_MODE_TEXT_END const PARTIAL_HEALING_MODE_NOTES_EXIT = PARTIAL_HEALING_MODE_NOTES_START + "currently" + PARTIAL_HEALING_MODE_NOTES_MID + "exit" + PARTIAL_HEALING_MODE_NOTES_END const PARTIAL_HEALING_MODE_NOTES_ENTER = PARTIAL_HEALING_MODE_NOTES_START + "not currently" + PARTIAL_HEALING_MODE_NOTES_MID + "enter" + PARTIAL_HEALING_MODE_NOTES_END const PARTIAL_HEALING_MODE_ALIAS = "PARTIAL_HEALING_TOGGLE" const PARTIAL_HEALING_MODE_VALUE = "0" // Duel Mode const MSG_WIKI_PAGE_STARTING_DUEL = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Starting_your_duel)" const MSG_WIKI_PAGE_SCORE = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Checking_your_score)" const MSG_WIKI_PAGE_ENDING_DUEL = " [wiki page](https://habitica.fandom.com/wiki/Dueling_Script#Ending_your_duel)" const MSG_DUEL_SCORE_PART1 = "Your duel score is " const MSG_DUEL_SCORE_PART2 = " points out of " const MSG_DUEL_SCORE_PART3 = ". *Accurate as of when you most recently clicked this button.*" const MSG_DUEL_SCORE_VICTORY1 = "You won! Check your [group](" const MSG_DUEL_SCORE_VICTORY2 = ") for instructions for your duel's announcer bot (" const MSG_DUEL_SCORE_VICTORY3 = ") for how to end the duel." // Prior to this message, the first letter will either be Y or y const BUTTON_NOTES_DUEL_START_AND_END = "our Group's duel announcer bot will tell you what to paste here. Then, click this button" // Button that starts a duel const DUEL_START_TEXT = "Click to start a duel" const DUEL_START_ALIAS = "duelStartButton" const DUEL_START_NOTES = "Y" + BUTTON_NOTES_DUEL_START_AND_END + " to start your duel. For more info, see the" + MSG_WIKI_PAGE_STARTING_DUEL + " for instructions." const DUEL_VALUE = "0" // Once a duel is in progress, button to get score const DUEL_SCORE_TEXT = "To see your current duel score, click this button and refresh the page" const DUEL_SCORE_ALIAS = "duelScoreButton" const DUEL_SCORE_NOTES = MSG_DUEL_SCORE_PART1 + "0 points" + MSG_DUEL_SCORE_PART3 // value is the same as the other buttons // Once a duel is in progress, button to end duel const DUEL_END_TEXT = "Click to end a duel" const DUEL_END_ALIAS = "duelEndButton" const DUEL_END_NOTES = "When your duel ends, y" + BUTTON_NOTES_DUEL_START_AND_END + ", which ends the duel and awards GP to the winner. For more info, see the" + MSG_WIKI_PAGE_ENDING_DUEL + " for instructions." // value is the same as the other buttons // Just For Fun const TRANSFORMATION_NOTES_END_1 = "Costs " const TRANSFORMATION_NOTES_END_2 = " GP, lasts until tomorrow. Click again to pay " const TRANSFORMATION_NOTES_END_3 = " GP to return to normal." const TRANSFORMATION_NOTES_END = TRANSFORMATION_NOTES_END_1 + COST_TO_TRANSFORM + TRANSFORMATION_NOTES_END_2 + COST_TO_TRANSFORM_BACK + TRANSFORMATION_NOTES_END_3 const SNOWBALL_TEXT = "**Snowball (self only)**" const SNOWBALL_ALIAS = "snowball" const SNOWBALL_NOTES = "Turn yourself into a cool snowman! " + TRANSFORMATION_NOTES_END const SNOWBALL_VALUE = "0" const SHINY_SEED_TEXT = "**Shiny Seed (self only)**" const SHINY_SEED_ALIAS = "shiny_seed" const SHINY_SEED_NOTES = "Turn yourself into a joyous flower! " + TRANSFORMATION_NOTES_END const SHINY_SEED_VALUE = "0" const SEAFOAM_TEXT = "**Seafoam (self only)**" const SEAFOAM_ALIAS = "seafoam" const SEAFOAM_NOTES = "Turn yourself into a sea creature! " + TRANSFORMATION_NOTES_END const SEAFOAM_VALUE = "0" const SPOOKY_SPARKLES_TEXT = "**Spooky Sparkles (self only)**" const SPOOKY_SPARKLES_ALIAS = "spooky_sparkles" const SPOOKY_SPARKLES_NOTES = "Turn yourself into a transparent pal! " + TRANSFORMATION_NOTES_END const SPOOKY_SPARKLES_VALUE = "0" const RANDOM_TEXT = "**Random Transformation (self only)**" const RANDOM_ALIAS = "random" const RANDOM_NOTES = "Turn yourself into a snowman, flower, starfish, or ghost! " + TRANSFORMATION_NOTES_END const RANDOM_VALUE = "0" // Messages // Insufficient cost messages const MSG_INSUFFICIENT_MANA_POTION = MSG_INSUFFICIENT_START_MANA + MSG_INSUFFICIENT_END_POTION const MSG_INSUFFICIENT_MANA_SKILL = MSG_INSUFFICIENT_START_MANA + MSG_INSUFFICIENT_END_SKILL const MSG_INSUFFICIENT_HEALTH_POTION = MSG_INSUFFICIENT_START_HEALTH + MSG_INSUFFICIENT_END_POTION const MSG_INSUFFICIENT_HEALTH_SKILL = MSG_INSUFFICIENT_START_HEALTH + MSG_INSUFFICIENT_END_SKILL const MSG_INSUFFICIENT_EXPERIENCE_POTION = MSG_INSUFFICIENT_START_EXPERIENCE + MSG_INSUFFICIENT_END_POTION const MSG_INSUFFICIENT_EXPERIENCE_SKILL = MSG_INSUFFICIENT_START_EXPERIENCE + MSG_INSUFFICIENT_END_SKILL // Other messages const MSG_CONTACT_AUTHOR1 = " contact the author of this script (@mike_the_monk)" const MSG_CONTACT_AUTHOR2 = " by sending him a [private message](https://habitica.com/private-messages?uuid=0034eb14-b4d8-494e-8386-d3f33cff7922) in Habitica." const MSG_ERROR_DAMAGE_START = "Error computing" const MSG_ERROR_DAMAGE_MID = " damage. To debug, please" + MSG_CONTACT_AUTHOR1 + MSG_CONTACT_AUTHOR2 const MSG_ERROR_DAMAGE_END = " Note to author: " const MSG_ERROR_DAMAGE_START_MID_END = MSG_ERROR_DAMAGE_START + MSG_ERROR_DAMAGE_MID + MSG_ERROR_DAMAGE_END // Automated FCV scripts const MSG_ALREADY_AT_MAX_MANA_POTION = MSG_ALREADY_AT_MAX_MANA_START + "potion" + MSG_ALREADY_AT_MAX_MANA_MID + MAX_DAILY_MP_POTION_USAGE + " potions." const MSG_ALREADY_AT_MAX_MANA_SKILL = MSG_ALREADY_AT_MAX_MANA_START + "skill" + MSG_ALREADY_AT_MAX_MANA_MID + MAX_DAILY_XP_FOR_MP_USAGE + MSG_XP_DAILY_USAGE const MSG_NEAR_MAX_MANA_POTION = "This potion" + MSG_NEAR_MAX_MANA_END const MSG_NEAR_MAX_MANA_SKILL = "This skill" + MSG_NEAR_MAX_MANA_END // Editable FCV Buttons // error messages: semicolon const SEMICOLON_ERROR_MSG_START = "Error: there should be a semicolon following '" const SEMICOLON_ERROR_MSG_END = "'." + EDITABLE_FCV_HELP_FORMATTING // error message: number after + or - const NUMBER_AFTER_DIRECTION_ERROR_MSG = "Error: there should be a number after the direction sign (+ or -) and before the semicolon." + EDITABLE_FCV_HELP_FORMATTING // error messages: missing a + or - const MISSING_DIRECTION_ERROR_MSG_START = "Error: there should be a '+' or a '-' after '" const MISSING_DIRECTION_ERROR_MSG_END = "'." + EDITABLE_FCV_HELP_FORMATTING // Custom skills 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_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 = MSG_ERROR_DAMAGE_START + " Sneak Attack" + MSG_ERROR_DAMAGE_MID const MSG_ERROR_DAMAGE_FINAL_ITERATION = MSG_ERROR_DAMAGE_START_MID_END + "final iteration triggered this message." const MSG_ERROR_DAMAGE_SETPOINT = MSG_ERROR_DAMAGE_START_MID_END + "target damage not between low and high setpoints)." // Game Modes const MSG_ALREADY_IN_BAD_DAY_MODE = "You are already in Bad Day Mode, so clicking the button additional times has no effect." // Duel Mode const MSG_INSUFFICIENT_GP = "You don't have enough GP for your wager. Please increase your GP and then click the button to start the duel." const ERROR_MSG_SWITCH_CASE_FAILURE1 = "Error with function `createCase`, variable `caseDuel` resolved to " const ERROR_MSG_SWITCH_CASE_FAILURE2 = ". Please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 const PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END1 = " If you think you've received this message in error, please see the" const PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END2 = " or" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 const ERROR_MSG_DUEL_ALREADY_ACTIVE = "You are already participating in a duel and cannot start another one yet." + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END1 + MSG_WIKI_PAGE_STARTING_DUEL + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END2 const ERROR_MSG_NOT_IN_DUEL_SCORE = "You are not currently participating in a duel and therefore don't have a score to show." + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END1 + MSG_WIKI_PAGE_SCORE + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END2 const ERROR_MSG_NOT_IN_DUEL_END = "You are not currently participating in a duel and therefore don't have a duel to end." + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END1 + MSG_WIKI_PAGE_ENDING_DUEL + PARTIAL_ERROR_MSG_INVALID_BUTTON_CLICK_END2 const ERROR_MSG_SINGULAR_START = "There was 1 error in trying to start the duel (in the Notes section of the button), and it occurred at " const ERROR_MSG_SINGULAR_END = ". Please check formatting and try again. If you need help, consult the" + MSG_WIKI_PAGE_STARTING_DUEL + "." const ERROR_MSG_WAGER = "Wager" const ERROR_MSG_SCORE_NEEDED = "Score Needed to win" const ERROR_MSG_TIMESTAMP_IDENTIFIER = "Timestamp Identifier for duel (which is given to you by the announcer bot)" const ERROR_MSG_GROUP_ID = "Group ID" const ERROR_MSG_AND = ", and " const ERROR_MSG_PLURAL_START = "There were " const ERROR_MSG_PLURAL_MID = " errors in in trying to start the duel (in the Notes section of the button), and they occurred at the following locations: " const ERROR_MSG_PLURAL_END = ERROR_MSG_SINGULAR_END const ERROR_MSG_SEMICOLONS_START = "There was an error when trying to start the duel. The Notes section of the button is supposed to have 7 semicolons. Yours has " const ERROR_MSG_SEMICOLONS_END = ERROR_MSG_SINGULAR_END const ERROR_MSG_INVALID_END_STRING_BEGINNING = "There was an error when trying to end the duel. " const ERROR_MSG_INVALID_END_STRING_ENDING = "Please make sure you copied/pasted the correct thing from the duel announcer bot and try again." const ERROR_MSG_INVALID_END_STRING_SEARCH_TERM = "The Notes section of the button needs to contain the phrase `ENDING DUEL;`\n\n" const ERROR_MSG_INVALID_END_STRING_HASH1 = "What you pasted in the Notes section of the button did not match what the script was expecting. " const ERROR_MSG_INVALID_END_STRING_HASH2 = " \n\nIf the problem persists and you can't figure out why, please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 + "\n\nYou can also manually end the duel by pasting `ENDING DUEL: DEBUG END` into the Notes section of the button that ends the duel, which will end the duel but not declare you the winner." const ERROR_MSG_VALIDITY = "Error with variable `validity`, it resolved to an unexpected number" + ERROR_MSG_SWITCH_CASE_FAILURE2 const ERROR_MSG_AUTOMATED_SELF = "This script posted the following error message to the Group where you are dueling, but in case it failed to post there, you are receiving it as a private message also:\n\n" const ERROR_MSG_AUTOMATED_START = "*This is an automated message generated by the duel script.*\n\nERROR DETECTED: " const ERROR_MSG_AUTOMATED_MESSAGE_DIDNT_POST = "The script shows I triggered the victory condition, but could not find the automated message that is supposed to post to the Group when that happens. Please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 const ERROR_MSG_AUTOMATED_BOT_DIDNT_REPLY = "The script shows that the bot didn't respond to my automated message earlier (victory condition), which means there is likely an issue with how the script saved the bot username. Please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 const ERROR_MSG_AUTOMATED_BOT_FOUND_INVALID = "The script shows I triggered the victory condition but the bot found the generated string invalid, which means there is likely an issue with how the script saved the duel parameters. Please" + MSG_CONTACT_AUTHOR1 + " to troubleshoot" + MSG_CONTACT_AUTHOR2 // Search terms the script is listening for const TASK_ALIAS_PLACEHOLDER = "placeholder" const DUEL_END = "ENDING DUEL:" const DUEL_END_DEBUG = "DEBUG END" const AUTOMATED_MSG_END_DUEL = "END DUEL" const AUTOMATED_MSG_BOT_DECLARES_WINNER = "We have a winner!" const AUTOMATED_MSG_BOT_INVALID_END_STRING = "there was an error trying to end the duel" // Just for Fun const MSG_INSUFFICIENT_GP_TRANSFORM_BACK = MSG_INSUFFICIENT_GP_START + COST_TO_TRANSFORM_BACK + MSG_INSUFFICIENT_GP_END + " back." const MSG_INSUFFICIENT_GP_TRANSFORM = MSG_INSUFFICIENT_GP_START + COST_TO_TRANSFORM + MSG_INSUFFICIENT_GP_END + "." // Buttons // Automated FCV scripts const XP_POTION_BUTTON = { "text": XP_POTION_TEXT, "type": "reward", "alias": XP_POTION_ALIAS, "notes": XP_POTION_NOTES, "value": XP_POTION_COST, } const MP_POTION_BUTTON = { "text": MP_POTION_TEXT, "type": "reward", "alias": MP_POTION_ALIAS, "notes": MP_POTION_NOTES, "value": MP_POTION_COST, } const ANTI_MP_POTION_BUTTON = { "text": ANTI_MP_POTION_TEXT, "type": "reward", "alias": ANTI_MP_POTION_ALIAS, "notes": ANTI_MP_POTION_NOTES, "value": ANTI_MP_POTION_VALUE, } const ANTI_HP_POTION_BUTTON = { "text": ANTI_HP_POTION_TEXT, "type": "reward", "alias": ANTI_HP_POTION_ALIAS, "notes": ANTI_HP_POTION_NOTES, "value": ANTI_HP_POTION_VALUE, } const ANTI_XP_POTION_BUTTON = { "text": ANTI_XP_POTION_TEXT, "type": "reward", "alias": ANTI_XP_POTION_ALIAS, "notes": ANTI_XP_POTION_NOTES, "value": ANTI_XP_POTION_VALUE, } const HP_FOR_MP_BUTTON = { "text": HP_FOR_MP_TEXT, "type": "reward", "alias": HP_FOR_MP_ALIAS, "notes": HP_FOR_MP_NOTES, "value": HP_FOR_MP_VALUE, } const XP_FOR_MP_BUTTON = { "text": XP_FOR_MP_TEXT, "type": "reward", "alias": XP_FOR_MP_ALIAS, "notes": XP_FOR_MP_NOTES, "value": XP_FOR_MP_VALUE, } // Editable FCV buttons const BUTTON_1_BUTTON = { "text": BUTTON_1_TEXT, "type": "reward", "alias": BUTTON_1_ALIAS, "notes": BUTTON_1_NOTES, "value": BUTTON_1_VALUE, } const BUTTON_2_BUTTON = { "text": BUTTON_2_TEXT, "type": "reward", "alias": BUTTON_2_ALIAS, "notes": BUTTON_2_NOTES, "value": BUTTON_2_VALUE, } const BUTTON_3_BUTTON = { "text": BUTTON_3_TEXT, "type": "reward", "alias": BUTTON_3_ALIAS, "notes": BUTTON_3_NOTES, "value": BUTTON_3_VALUE, } const BUTTON_4_BUTTON = { "text": BUTTON_4_TEXT, "type": "reward", "alias": BUTTON_4_ALIAS, "notes": BUTTON_4_NOTES, "value": BUTTON_4_VALUE, } const BUTTON_5_BUTTON = { "text": BUTTON_5_TEXT, "type": "reward", "alias": BUTTON_5_ALIAS, "notes": BUTTON_5_NOTES, "value": BUTTON_5_VALUE, } // Custom skills // Cross-Class Stat Buffs const OMNI_BUFF_BUTTON = { "text": OMNI_BUFF_TEXT, "type": "reward", "alias": OMNI_BUFF_ALIAS, "notes": OMNI_BUFF_NOTES, "value": OMNI_BUFF_VALUE, } const VALOROUS_PRESENCE_SELF_BUTTON = { "text": VALOROUS_PRESENCE_SELF_TEXT, "type": "reward", "alias": VALOROUS_PRESENCE_SELF_ALIAS, "notes": VALOROUS_PRESENCE_SELF_NOTES, "value": VALOROUS_PRESENCE_SELF_VALUE, } const EARTHQUAKE_SELF_BUTTON = { "text": EARTHQUAKE_SELF_TEXT, "type": "reward", "alias": EARTHQUAKE_SELF_ALIAS, "notes": EARTHQUAKE_SELF_NOTES, "value": EARTHQUAKE_SELF_VALUE, } const PROTECTIVE_AURA_SELF_BUTTON = { "text": PROTECTIVE_AURA_SELF_TEXT, "type": "reward", "alias": PROTECTIVE_AURA_SELF_ALIAS, "notes": PROTECTIVE_AURA_SELF_NOTES, "value": PROTECTIVE_AURA_SELF_VALUE, } const TOOLS_OF_THE_TRADE_SELF_BUTTON = { "text": TOOLS_OF_THE_TRADE_SELF_TEXT, "type": "reward", "alias": TOOLS_OF_THE_TRADE_SELF_ALIAS, "notes": TOOLS_OF_THE_TRADE_SELF_NOTES, "value": TOOLS_OF_THE_TRADE_SELF_VALUE, } const TRIPLE_BUFF_STR_INT_CON_BUTTON = { "text": TRIPLE_BUFF_STR_INT_CON_TEXT, "type": "reward", "alias": TRIPLE_BUFF_STR_INT_CON_ALIAS, "notes": TRIPLE_BUFF_STR_INT_CON_NOTES, "value": TRIPLE_BUFF_STR_INT_CON_VALUE, } const TRIPLE_BUFF_STR_INT_PER_BUTTON = { "text": TRIPLE_BUFF_STR_INT_PER_TEXT, "type": "reward", "alias": TRIPLE_BUFF_STR_INT_PER_ALIAS, "notes": TRIPLE_BUFF_STR_INT_PER_NOTES, "value": TRIPLE_BUFF_STR_INT_PER_VALUE, } const TRIPLE_BUFF_STR_CON_PER_BUTTON = { "text": TRIPLE_BUFF_STR_CON_PER_TEXT, "type": "reward", "alias": TRIPLE_BUFF_STR_CON_PER_ALIAS, "notes": TRIPLE_BUFF_STR_CON_PER_NOTES, "value": TRIPLE_BUFF_STR_CON_PER_VALUE, } const TRIPLE_BUFF_INT_CON_PER_BUTTON = { "text": TRIPLE_BUFF_INT_CON_PER_TEXT, "type": "reward", "alias": TRIPLE_BUFF_INT_CON_PER_ALIAS, "notes": TRIPLE_BUFF_INT_CON_PER_NOTES, "value": TRIPLE_BUFF_INT_CON_PER_VALUE, } const DOUBLE_BUFF_STR_INT_BUTTON = { "text": DOUBLE_BUFF_STR_INT_TEXT, "type": "reward", "alias": DOUBLE_BUFF_STR_INT_ALIAS, "notes": DOUBLE_BUFF_STR_INT_NOTES, "value": DOUBLE_BUFF_STR_INT_VALUE, } const DOUBLE_BUFF_STR_CON_BUTTON = { "text": DOUBLE_BUFF_STR_CON_TEXT, "type": "reward", "alias": DOUBLE_BUFF_STR_CON_ALIAS, "notes": DOUBLE_BUFF_STR_CON_NOTES, "value": DOUBLE_BUFF_STR_CON_VALUE, } const DOUBLE_BUFF_STR_PER_BUTTON = { "text": DOUBLE_BUFF_STR_PER_TEXT, "type": "reward", "alias": DOUBLE_BUFF_STR_PER_ALIAS, "notes": DOUBLE_BUFF_STR_PER_NOTES, "value": DOUBLE_BUFF_STR_PER_VALUE, } const DOUBLE_BUFF_INT_CON_BUTTON = { "text": DOUBLE_BUFF_INT_CON_TEXT, "type": "reward", "alias": DOUBLE_BUFF_INT_CON_ALIAS, "notes": DOUBLE_BUFF_INT_CON_NOTES, "value": DOUBLE_BUFF_INT_CON_VALUE, } const DOUBLE_BUFF_INT_PER_BUTTON = { "text": DOUBLE_BUFF_INT_PER_TEXT, "type": "reward", "alias": DOUBLE_BUFF_INT_PER_ALIAS, "notes": DOUBLE_BUFF_INT_PER_NOTES, "value": DOUBLE_BUFF_INT_PER_VALUE, } const DOUBLE_BUFF_CON_PER_BUTTON = { "text": DOUBLE_BUFF_CON_PER_TEXT, "type": "reward", "alias": DOUBLE_BUFF_CON_PER_ALIAS, "notes": DOUBLE_BUFF_CON_PER_NOTES, "value": DOUBLE_BUFF_CON_PER_VALUE, } // Cross-Class Skills const PICKPOCKET_BUTTON = { "text": PICKPOCKET_TEXT, "type": "reward", "alias": PICKPOCKET_ALIAS, "notes": PICKPOCKET_NOTES, "value": PICKPOCKET_VALUE, } const BACKSTAB_BUTTON = { "text": BACKSTAB_TEXT, "type": "reward", "alias": BACKSTAB_ALIAS, "notes": BACKSTAB_NOTES, "value": BACKSTAB_VALUE, } const STEALTH_BUTTON = { "text": STEALTH_TEXT, "type": "reward", "alias": STEALTH_ALIAS, "notes": STEALTH_NOTES, "value": STEALTH_VALUE, } 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_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_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_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_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_BUTTON = { "text": BRUTAL_SMASH_TEXT, "type": "reward", "alias": BRUTAL_SMASH_ALIAS, "notes": BRUTAL_SMASH_NOTES, "value": BRUTAL_SMASH_VALUE, } 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_BUTTON = { "text": SNEAK_ATTACK_TEXT, "type": "reward", "alias": SNEAK_ATTACK_ALIAS, "notes": SNEAK_ATTACK_NOTES, "value": SNEAK_ATTACK_VALUE, } const DAMAGE_BUTTON = { "text": DAMAGE_TEXT, "type": "todo", "notes": DAMAGE_NOTES, "priority": 0.1, } // Game Modes const BAD_DAY_MODE_BUTTON = { "text": BAD_DAY_MODE_TEXT, "type": "reward", "alias": BAD_DAY_MODE_ALIAS, "notes": BAD_DAY_MODE_NOTES, "value": BAD_DAY_MODE_VALUE, } // Button initializes assuming they are in Partial Healing Mode const PARTIAL_HEALING_MODE_BUTTON = { "text": PARTIAL_HEALING_MODE_TEXT_EXIT, "type": "reward", "alias": PARTIAL_HEALING_MODE_ALIAS, "notes": PARTIAL_HEALING_MODE_NOTES_EXIT, "value": PARTIAL_HEALING_MODE_VALUE, } const DUEL_START_BUTTON = { "text": DUEL_START_TEXT, "type": "reward", "alias": DUEL_START_ALIAS, "notes": DUEL_START_NOTES, "value": DUEL_VALUE, } const DUEL_SCORE_BUTTON = { "text": DUEL_SCORE_TEXT, "type": "reward", "alias": DUEL_SCORE_ALIAS, "notes": DUEL_SCORE_NOTES, "value": DUEL_VALUE, } const DUEL_END_BUTTON = { "text": DUEL_END_TEXT, "type": "reward", "alias": DUEL_END_ALIAS, "notes": DUEL_END_NOTES, "value": DUEL_VALUE, } // Just For Fun const SNOWBALL_BUTTON = { "text": SNOWBALL_TEXT, "type": "reward", "alias": SNOWBALL_ALIAS, "notes": SNOWBALL_NOTES, "value": SNOWBALL_VALUE, } const SHINY_SEED_BUTTON = { "text": SHINY_SEED_TEXT, "type": "reward", "alias": SHINY_SEED_ALIAS, "notes": SHINY_SEED_NOTES, "value": SHINY_SEED_VALUE, } const SEAFOAM_BUTTON = { "text": SEAFOAM_TEXT, "type": "reward", "alias": SEAFOAM_ALIAS, "notes": SEAFOAM_NOTES, "value": SEAFOAM_VALUE, } const SPOOKY_SPARKLES_BUTTON = { "text": SPOOKY_SPARKLES_TEXT, "type": "reward", "alias": SPOOKY_SPARKLES_ALIAS, "notes": SPOOKY_SPARKLES_NOTES, "value": SPOOKY_SPARKLES_VALUE, } const RANDOM_BUTTON = { "text": RANDOM_TEXT, "type": "reward", "alias": RANDOM_ALIAS, "notes": RANDOM_NOTES, "value": RANDOM_VALUE, } // Global constants: keys and counters const CRON_COUNT_KEY = "CRON_COUNT_KEY" const TIMESTAMP_KEY = "TIMESTAMP_KEY" const TIMESTAMP_NONSCRIPT_BUTTONS_KEY = "TIMESTAMP_NONSCRIPT_BUTTONS_KEY" const XP_USAGE_KEY = "XP_USAGE_KEY" const MP_USAGE_KEY = "MP_USAGE_KEY" const ANTI_MP_USAGE_KEY = "ANTI_MP_USAGE_KEY" const ANTI_HP_USAGE_KEY = "ANTI_HP_USAGE_KEY" const ANTI_XP_USAGE_KEY = "ANTI_XP_USAGE_KEY" const HP_FOR_MP_KEY = "HP_FOR_MP_KEY" const XP_FOR_MP_KEY = "XP_FOR_MP_KEY" const HP_KEY = "HP_KEY" const LVL_KEY = "LVL_KEY" const PARTIAL_HEALING_MODE_ACTIVE_KEY = "PARTIAL_HEALING_MODE_ACTIVE_KEY" const DUEL_CURRENTLY_ACTIVE_KEY = "DUEL_CURRENTLY_ACTIVE_KEY" const DUEL_SCORE_KEY = "DUEL_SCORE_KEY" const SELF_USERNAME_KEY = "SELF_USERNAME_KEY" const OPPONENT_USERNAME_KEY = "OPPONENT_USERNAME_KEY" const WAGER_KEY = "WAGER_KEY" const SCORE_NEEDED_GOAL_KEY = "SCORE_NEEDED_GOAL_KEY" const GROUP_ID_KEY = "GROUP_ID_KEY" const ANNOUNCER_BOT_USERNAME_KEY = "ANNOUNCER_BOT_USERNAME_KEY" const TIMESTAMP_IDENTIFIER_KEY = "TIMESTAMP_IDENTIFIER_KEY" const TROUBLESHOOTING_KEY = "TROUBLESHOOTING_KEY" var cronCountKey = "" var timestampKey = "" var timestampNonscriptButtonsKey = "" var xpUsageKey = "" var mpUsageKey = "" var AntiMpUsageKey = "" var AntiHpUsageKey = "" var AntiXpUsageKey = "" var HpForMpUsageKey = "" var XpForMpUsageKey = "" var hpKey = "" var lvlKey = "" var partialHealingModeActiveKey = "" var duelCurrentlyActiveKey = "" var duelScoreKey = "" var selfUsernameKey = "" var opponentUsernameKey = "" var wagerKey = "" var scoreNeededGoalKey = "" var groupIdKey = "" var announcerBotUsernameKey = "" var timestampIdentifierKey = "" var troubleshootingKey = "" // Main body of code // Setup function doOneTimeSetup() { // Initialize counter of API calls let count = 0 let newCount = 0 // Create buttons count = createNewButtons() // Check count of API calls count++ // Creating webhook takes 2 API calls newCount = checkApiCount(count) count = newCount // create the webhook const options = { "scored" : true, } const payload = { "url" : WEB_APP_URL, "label" : SCRIPT_NAME + " Webhook", "type" : "taskActivity", "options" : options, } apiMult_createNewWebhookNoDuplicates(payload) // Check count of API calls newCount = checkApiCount(count) count = newCount // set script properties so they carry over to next session initScriptProperties() // If Partial Healing is active, also initialize its script properties if (VERSION_PARTIAL_HEALING > 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount initScriptPropertiesPartialHealing() } // If dueling, initialize duel values. Does not require API calls. if (I_WANT_TO_DUEL == 1){ resetDuelValues() } } // If API calls are nearing the limit, wait one minute before proceeding function checkApiCount(initialCount){ // Absolute max is 30 per minute, so this gives me wiggle room in case I miscount if (initialCount > 26) { // Wait a full minute, resetting Habitic's limit of 30 per minute Utilities.sleep(60000) return 0 // Reset the counter if you've waited a full minute } else { let newCount = initialCount + 1 // Increment counter return newCount } } function createNewButtons(){ // Initialize API call counter. If user is creating a lot of buttons, you'll need to create them over the course of several minutes. let count = 0 let newCount = 0 // These are purposely not if-else statements, and are in reverse order (i.e. topmost line of code becomes bottommost button in Habitica) if (I_WANT_TO_DUEL == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DUEL_START_BUTTON]) // If relevant, add tag to task (i.e. only if the user created a tag for these buttons) // It takes 2 API calls to do so if (TAG_FOR_DUEL_BUTTONS != "typeTagHere") { count++ // Increments 1 of the 2 API calls newCount = checkApiCount(count) // Increments the 2nd of the 2 count = newCount checkAndAddTagToTask(DUEL_START_ALIAS, "none") } } if (CREATE_SPOOKY_SPARKLES == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([SPOOKY_SPARKLES_BUTTON]) } if (CREATE_SEAFOAM == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([SEAFOAM_BUTTON]) } if (CREATE_SHINY_SEED == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([SHINY_SEED_BUTTON]) } if (CREATE_SNOWBALL == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([SNOWBALL_BUTTON]) } if (CREATE_RANDOM == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([RANDOM_BUTTON]) } if (PARTIAL_HEALING_TOGGLE_BUTTON == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([PARTIAL_HEALING_MODE_BUTTON]) } if (CREATE_BAD_DAY_MODE_BUTTON == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BAD_DAY_MODE_BUTTON]) } if (CREATE_SNEAK_ATTACK == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([SNEAK_ATTACK_BUTTON]) } if (CREATE_RADIANT_SHIELD == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([RADIANT_SHIELD_BUTTON]) } if (CREATE_BRUTAL_SMASH == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BRUTAL_SMASH_BUTTON]) } if (CREATE_BURST_OF_FLAMES_XP_PLUS_DAMAGE == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BURST_OF_FLAMES_XP_PLUS_DAMAGE_BUTTON]) } if (CREATE_BURST_OF_FLAMES_DAMAGE_ONLY == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BURST_OF_FLAMES_DAMAGE_ONLY_BUTTON]) } if (CREATE_BURST_OF_FLAMES_XP_ONLY == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BURST_OF_FLAMES_XP_ONLY_BUTTON]) } if (CREATE_CHILLING_FROST == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([CHILLING_FROST_BUTTON]) } if (CREATE_HEALING_LIGHT == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([HEALING_LIGHT_BUTTON]) } if (CREATE_STEALTH == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([STEALTH_BUTTON]) } if (CREATE_BACKSTAB == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([BACKSTAB_BUTTON]) } if (CREATE_PICKPOCKET == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([PICKPOCKET_BUTTON]) } if (CREATE_DOUBLE_BUFF_CON_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_CON_PER_BUTTON]) } if (CREATE_DOUBLE_BUFF_INT_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_INT_PER_BUTTON]) } if (CREATE_DOUBLE_BUFF_INT_CON == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_INT_CON_BUTTON]) } if (CREATE_DOUBLE_BUFF_STR_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_STR_PER_BUTTON]) } if (CREATE_DOUBLE_BUFF_STR_CON == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_STR_CON_BUTTON]) } if (CREATE_DOUBLE_BUFF_STR_INT == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([DOUBLE_BUFF_STR_INT_BUTTON]) } if (CREATE_TRIPLE_BUFF_INT_CON_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([TRIPLE_BUFF_INT_CON_PER_BUTTON]) } if (CREATE_TRIPLE_BUFF_STR_CON_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([TRIPLE_BUFF_STR_CON_PER_BUTTON]) } if (CREATE_TRIPLE_BUFF_STR_INT_PER == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([TRIPLE_BUFF_STR_INT_PER_BUTTON]) } if (CREATE_TRIPLE_BUFF_STR_INT_CON == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([TRIPLE_BUFF_STR_INT_CON_BUTTON]) } if (CREATE_TOOLS_OF_THE_TRADE_SELF == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([TOOLS_OF_THE_TRADE_SELF_BUTTON]) } if (CREATE_PROTECTIVE_AURA_SELF == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([PROTECTIVE_AURA_SELF_BUTTON]) } if (CREATE_EARTHQUAKE_SELF == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([EARTHQUAKE_SELF_BUTTON]) } if (CREATE_VALOROUS_PRESENCE_SELF == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([VALOROUS_PRESENCE_SELF_BUTTON]) } if (CREATE_OMNI_BUFF == 1) { // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([OMNI_BUFF_BUTTON]) } if (NUMBER_OF_BUTTONS > 0) { // Check count of API calls count += NUMBER_OF_BUTTONS - 1 // Since this would add multiple buttons, I need to increment the counter (the -1 is from the fact that checkApiCount already increments it by 1) newCount = checkApiCount(count) count = newCount createEditableFcvButtons() } if (CREATE_XP_FOR_MP == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([XP_FOR_MP_BUTTON]) } if (CREATE_HP_FOR_MP == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([HP_FOR_MP_BUTTON]) } if (CREATE_STUPEFYING_POTION == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([ANTI_XP_POTION_BUTTON]) } if (CREATE_FATIGUING_POTION == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([ANTI_HP_POTION_BUTTON]) } if (CREATE_DRAINING_POTION == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([ANTI_MP_POTION_BUTTON]) } if (CREATE_MANA_POTION == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([MP_POTION_BUTTON]) } if (CREATE_EXPERIENCE_POTION == 1){ // Check count of API calls newCount = checkApiCount(count) count = newCount api_createNewTaskForUser([XP_POTION_BUTTON]) } return count // I will need the count for further initializing } // Creates the Editable FCV Buttons function createEditableFcvButtons(){ // Create 1-5 buttons var numberOfButtons = sanitizeInput(NUMBER_OF_BUTTONS, 1, 5) if ( numberOfButtons == 5 ) { api_createNewTaskForUser([BUTTON_5_BUTTON, BUTTON_4_BUTTON, BUTTON_3_BUTTON, BUTTON_2_BUTTON, BUTTON_1_BUTTON]) } else if ( numberOfButtons == 4 ) { api_createNewTaskForUser([BUTTON_4_BUTTON, BUTTON_3_BUTTON, BUTTON_2_BUTTON, BUTTON_1_BUTTON]) } else if ( numberOfButtons == 3 ) { api_createNewTaskForUser([BUTTON_3_BUTTON, BUTTON_2_BUTTON, BUTTON_1_BUTTON]) } else if ( numberOfButtons == 2 ) { api_createNewTaskForUser([BUTTON_2_BUTTON, BUTTON_1_BUTTON]) } else if ( numberOfButtons == 1 ) { api_createNewTaskForUser([BUTTON_1_BUTTON]) } } // Sets initial properties that will be used/saved later. function initScriptProperties() { const responseUser = api_getAuthenticatedUserProfile("stats") let user = JSON.parse(responseUser).data let cronCount = user.flags.cronCount scriptProperties.setProperty(CRON_COUNT_KEY, cronCount) var timestampInit = Date.now() scriptProperties.setProperty(TIMESTAMP_KEY, timestampInit) scriptProperties.setProperty(TIMESTAMP_NONSCRIPT_BUTTONS_KEY, timestampInit) // Also set to 0 any counter that gets reset to 0 at Cron resetCounters() } // Partial Healing Mode has its own unique version of the above function function initScriptPropertiesPartialHealing() { var hpKey = HP_KEY var lvlKey = LVL_KEY const responseInit = api_getAuthenticatedUserProfile("stats") user = JSON.parse(responseInit).data var currentHp = user.stats.hp var currentLvl = user.stats.lvl scriptProperties.setProperty(hpKey, currentHp) scriptProperties.setProperty(lvlKey, currentLvl) scriptProperties.setProperty(PARTIAL_HEALING_MODE_ACTIVE_KEY, 1) } // Main // do things when the webhook runs function doPost(e) { const dataContents = JSON.parse(e.postData.contents) const type = dataContents.type const task = dataContents.task // If Automatic Modes are supposed to happen right after Cron, do them doAutomaticModes() if (type == "scored"){ // 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 } // Grab current timestamp let timeEnd = Date.now() // Cases based on which button was pressed let caseButton = caseButtonAlias(sanitizedAlias, task.type) // A value of 30 indicates 30+ seconds have elapsed, 5 indicates 5-30 seconds, and -1 indicates less than 5 seconds let caseRateLimiting = caseTimestamp(caseButton, timeEnd) // Based on rate limiting and which button was pressed, run things. This function includes all the normal buttons and Partial Healing Mode, but not Duel Mode. caseMain(caseRateLimiting, caseButton, sanitizedAlias, timeEnd) // Do duel things if needed. if (I_WANT_TO_DUEL == 1) { doDuel(sanitizedAlias, task.type, task.notes, task._id, task.priority, dataContents.delta) } // Required (by Google) for Apps Script return HtmlService.createHtmlOutput() } } // If an automatic mode is supposed to run at Cron, do it here function doAutomaticModes(){ // Only run if one or more of the automatic modes is selected if (AUTOMATIC_TRANSFORM_RANDOM == 1) { const response = api_getAuthenticatedUserProfile("stats") user = JSON.parse(response).data var cronCountKey = CRON_COUNT_KEY var cronCount = Number(scriptProperties.getProperty(cronCountKey)) // Check if they've Cronned if (cronCount != user.flags.cronCount) { cronCount = user.flags.cronCount resetCounters() // Reset counters here as well // Save value to non-volatile memory scriptProperties.setProperty(cronCountKey, cronCount) // If they want to automatically transform, do so. if (AUTOMATIC_TRANSFORM_RANDOM == 1) { let gp = user.stats.gp Utilities.sleep(2000) randomTransformation(gp + 15) } } } } // Button-specific functions // Runs all script buttons (except duel) function doButtons(sanitizedAlias){ // First, check if they Cronned. Reset counters if yes. const response = api_getAuthenticatedUserProfile("stats,tasksOrder,items.gear.equipped") user = JSON.parse(response).data var cronCountKey = CRON_COUNT_KEY var cronCount = Number(scriptProperties.getProperty(cronCountKey)) if (cronCount != user.flags.cronCount) { cronCount = user.flags.cronCount resetCounters() // Save value to non-volatile memory scriptProperties.setProperty(cronCountKey, cronCount) } // Save stats let hp = user.stats.hp let xp = user.stats.exp let mp = user.stats.mp let gp = user.stats.gp let lvl = user.stats.lvl // Get equipment stats info const responseContent = api_getAllAvailableContentObjects() content = JSON.parse(responseContent).data // Calculate total stats 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 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 // Compute maximum mana and how close they are to max let maxMp = (2 * intelligence) + 30 let mpDiff = maxMp - mp // Do Editable FCV buttons together if ( (sanitizedAlias == BUTTON_1_ALIAS) || (sanitizedAlias == BUTTON_2_ALIAS) || (sanitizedAlias == BUTTON_3_ALIAS) || (sanitizedAlias == BUTTON_4_ALIAS) || (sanitizedAlias == BUTTON_5_ALIAS) ) { var notes = "initial value" // Get all tasks in order to grab notes section of the rewards buttons const responseTasks = api_getUserTasks("rewards") const tasksRewards = JSON.parse(responseTasks).data // Grab notes from the button that was pressed for (var i in tasksRewards) { if (tasksRewards[i].alias == sanitizedAlias) { notes = tasksRewards[i].notes } } // "notes" is now correct based on button, so this function can be outside of the if-statements and switch-case multParseAndEvaluate(hp, xp, mp, gp, notes) } // Transformation buttons are grouped because you check to see if they've already transformed else if ( (sanitizedAlias == SNOWBALL_ALIAS) || (sanitizedAlias == SPOOKY_SPARKLES_ALIAS) || (sanitizedAlias == SHINY_SEED_ALIAS) || (sanitizedAlias == SEAFOAM_ALIAS) || (sanitizedAlias == RANDOM_ALIAS) ){ // See if they've already transformed. If yes, subtract 5 GP (send error message if they don't have enough) and reset them to normal. let isSnowball = user.stats.buffs.snowball let isSpookySparkles = user.stats.buffs.spookySparkles let isShinySeed = user.stats.buffs.shinySeed let isSeafoam = user.stats.buffs.seafoam let passesChecks = checkWhetherToTransform(isSnowball, isSpookySparkles, isShinySeed, isSeafoam, gp) if (passesChecks){ switch (sanitizedAlias){ case SNOWBALL_ALIAS: doButtonSnowballTransformation(gp) break case SPOOKY_SPARKLES_ALIAS: doButtonSpookySparklesTransformation(gp) break case SHINY_SEED_ALIAS: doButtonShinySeedTransformation(gp) break case SEAFOAM_ALIAS: doButtonSeafoamTransformation(gp) break case RANDOM_ALIAS: doButtonRandomTransformation(gp) break } } } // Otherwise, do the other buttons else { switch (sanitizedAlias){ case XP_POTION_ALIAS: var xpUsageKey = XP_USAGE_KEY var xpUsage = Number(scriptProperties.getProperty(xpUsageKey)) doButtonXpPotion(lvl, gp, xp, xpUsage, xpUsageKey) break case MP_POTION_ALIAS: var mpUsageKey = MP_USAGE_KEY var mpUsage = Number(scriptProperties.getProperty(mpUsageKey)) doButtonMpPotion(lvl, gp, mp, mpDiff, mpUsage, mpUsageKey) break case ANTI_MP_POTION_ALIAS: var AntiMpUsageKey = ANTI_MP_USAGE_KEY var AntiMpUsage = Number(scriptProperties.getProperty(AntiMpUsageKey)) doButtonAntiMpPotion(lvl, mp, gp, AntiMpUsageKey, AntiMpUsage) break case ANTI_HP_POTION_ALIAS: var AntiHpUsageKey = ANTI_HP_USAGE_KEY var AntiHpUsage = Number(scriptProperties.getProperty(AntiHpUsageKey)) doButtonAntiHpPotion(hp, gp, AntiHpUsageKey, AntiHpUsage) break case ANTI_XP_POTION_ALIAS: var AntiXpUsageKey = ANTI_XP_USAGE_KEY var AntiXpUsage = Number(scriptProperties.getProperty(AntiXpUsageKey)) doButtonAntiXpPotion(lvl, xp, gp, AntiXpUsageKey, AntiXpUsage) break case HP_FOR_MP_ALIAS: var HpForMpUsageKey = HP_FOR_MP_KEY var HpForMpUsage = Number(scriptProperties.getProperty(HpForMpUsageKey)) doButtonHpForMp(lvl, hp, mp, mpDiff, HpForMpUsage, HpForMpUsageKey) break case XP_FOR_MP_ALIAS: var XpForMpUsageKey = XP_FOR_MP_KEY var XpForMpUsage = Number(scriptProperties.getProperty(XpForMpUsageKey)) doButtonXpForMp(lvl, xp, mp, mpDiff, XpForMpUsage, XpForMpUsageKey) break case OMNI_BUFF_ALIAS: doButtonOmniBuff(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedCon, buffsCon, unbuffedPer, buffsPer) break case VALOROUS_PRESENCE_SELF_ALIAS: doButtonValorousPresenceSelf(lvl, mp, unbuffedStr, buffsStr) break case EARTHQUAKE_SELF_ALIAS: doButtonEarthquakeSelf(lvl, mp, unbuffedInt, buffsInt) break case PROTECTIVE_AURA_SELF_ALIAS: doButtonProtectiveAuraSelf(lvl, mp, unbuffedCon, buffsCon) break case TOOLS_OF_THE_TRADE_SELF_ALIAS: doButtonToolsOfTheTradeSelf(lvl, mp, unbuffedPer, buffsPer) break case TRIPLE_BUFF_STR_INT_CON_ALIAS: doButtonTripeBuffStrIntCon(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedCon, buffsCon) break case TRIPLE_BUFF_STR_INT_PER_ALIAS: doButtonTripeBuffStrIntPer(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedPer, buffsPer) break case TRIPLE_BUFF_STR_CON_PER_ALIAS: doButtonTripeBuffStrConPer(lvl, mp, unbuffedStr, buffsStr, unbuffedCon, buffsCon, unbuffedPer, buffsPer) break case TRIPLE_BUFF_INT_CON_PER_ALIAS: doButtonTripleBuffIntConPer(lvl, mp, unbuffedInt, buffsInt, unbuffedCon, buffsCon, unbuffedPer, buffsPer) break case DOUBLE_BUFF_STR_INT_ALIAS: doButtonDoubleBuffStrInt(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt) break case DOUBLE_BUFF_STR_CON_ALIAS: doButtonDoubleBuffStrCon(lvl, mp, unbuffedStr, buffsStr, unbuffedCon, buffsCon) break case DOUBLE_BUFF_STR_PER_ALIAS: doButtonDoubleBuffStrPer(lvl, mp, unbuffedStr, buffsStr, unbuffedPer, buffsPer) break case DOUBLE_BUFF_INT_CON_ALIAS: doButtonDoubleBuffIntCon(lvl, mp, unbuffedInt, buffsInt, unbuffedCon, buffsCon) break case DOUBLE_BUFF_INT_PER_ALIAS: doButtonDoubleBuffIntPer(lvl, mp, unbuffedInt, buffsInt, unbuffedPer, buffsPer) break case DOUBLE_BUFF_CON_PER_ALIAS: doButtonDoubleBuffConPer(lvl, mp, unbuffedCon, buffsCon, unbuffedPer, buffsPer) break 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, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) break case BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS: doButtonBurstOfFlamesXpAndDamage(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) break case BRUTAL_SMASH_ALIAS: doButtonBrutalSmash(lvl, hp, xp, mp, gp, strength, constitution, intelligence, perception, unbuffedStr, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) break case RADIANT_SHIELD_ALIAS: doButtonRadiantShield(lvl, hp, xp, mp, gp, constitution, intelligence, perception, unbuffedStr, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) break case SNEAK_ATTACK_ALIAS: doButtonSneakAttack(lvl, hp, xp, mp, gp, perception, intelligence, strength, unbuffedStr, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) break case BAD_DAY_MODE_ALIAS: doButtonBadDayMode(buffsCon, stealthedDailies, totalDailies, hp, unbuffedCon) break } } } // Runs duel things from a separate function so that the main function is cleaner function doDuel(sanitizedAlias, taskType, taskNotes, taskId, taskPriority, dataContentsDelta){ var duelCurrentlyActiveKey = DUEL_CURRENTLY_ACTIVE_KEY var duelScoreKey = DUEL_SCORE_KEY var selfUsernameKey = SELF_USERNAME_KEY var opponentUsernameKey = OPPONENT_USERNAME_KEY var wagerKey = WAGER_KEY var scoreNeededGoalKey = SCORE_NEEDED_GOAL_KEY var groupIdKey = GROUP_ID_KEY var announcerBotUsernameKey = ANNOUNCER_BOT_USERNAME_KEY var timestampIdentifierKey = TIMESTAMP_IDENTIFIER_KEY var troubleshootingKey = TROUBLESHOOTING_KEY var duelCurrentlyActive = Number(scriptProperties.getProperty(duelCurrentlyActiveKey)) // Create cases based on which button was pushed and whether currently in a duel let caseDuel = 0 caseDuel = createCase(sanitizedAlias, taskType, duelCurrentlyActive, duelScoreKey, scoreNeededGoalKey) switch (caseDuel) { case 0: // Error condition for if the function failed api_sendPrivateMessageAlways({"message" : ERROR_MSG_SWITCH_CASE_FAILURE1 + 0 + ERROR_MSG_SWITCH_CASE_FAILURE2, "toUserId" : USER_ID}) break case 1: // Victory condition – checks to see if the correct automated messages were posted victoryCondition(troubleshootingKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey) break case 2: // Do nothing (usually due to user not being in a duel) break case 3: // Increasing duel points while in a duel increaseDuelScore(duelScoreKey, scoreNeededGoalKey, groupIdKey, selfUsernameKey, opponentUsernameKey, wagerKey, announcerBotUsernameKey, timestampIdentifierKey, troubleshootingKey, taskPriority, dataContentsDelta, taskType) break case 10: // Error condition that shouldn't be possible to reach api_sendPrivateMessageAlways({"message" : ERROR_MSG_SWITCH_CASE_FAILURE1 + 10 + ERROR_MSG_SWITCH_CASE_FAILURE2, "toUserId" : USER_ID}) break case 11: // Error, clicked the button to start a duel but are already in a duel api_sendPrivateMessageAlways({"message" : ERROR_MSG_DUEL_ALREADY_ACTIVE, "toUserId" : USER_ID}) break case 12: // Normal use of Duel Start button duelStartButton(taskNotes, taskId, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey) break case 21: // Error, clicked the button to see duel score but not currently in a duel api_sendPrivateMessageAlways({"message" : ERROR_MSG_NOT_IN_DUEL_SCORE, "toUserId" : USER_ID}) break case 22: // Normal use of the Duel Score button duelScoreButton(duelScoreKey, scoreNeededGoalKey) break case 31: // Error, clicked the button to end duel but not currently in a duel api_sendPrivateMessageAlways({"message" : ERROR_MSG_NOT_IN_DUEL_END, "toUserId" : USER_ID}) break case 32: // Normal use of the Duel End button duelEndButton(taskNotes, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, timestampIdentifierKey, troubleshootingKey, groupIdKey, announcerBotUsernameKey) break case 40: // Error condition that shouldn't be possible to reach api_sendPrivateMessageAlways({"message" : ERROR_MSG_SWITCH_CASE_FAILURE1 + 40 + ERROR_MSG_SWITCH_CASE_FAILURE2, "toUserId" : USER_ID}) break } } // Automated FCV Scripts // When the XP Potion button is clicked function doButtonXpPotion(lvl, gp, exp, xpUsage, xpUsageKey){ let XpToNextLevel = Math.round((Math.pow(lvl, 2) * 0.25 + 10 * lvl + 139.75) / 10) * 10 let MaxDailyXpUsage = Math.floor(XpToNextLevel * eval(MAX_DAILY_XP_POTION_USAGE)) // If level lock fails, refund the money and send failure message let sufficientLevel = checkLevelLock(lvl, XP_POTION_LEVEL_LOCK, "potion.") if (!sufficientLevel) { api_updateUser({"stats.gp" : gp + XP_POTION_COST}) } else { // If another potion (+XP_GAIN XP) would put them above the max, refund the money and send failure message let usageLimitOkay = checkUsageLimit(xpUsage, XP_GAIN, MaxDailyXpUsage, "potion ", true, false) if (!usageLimitOkay) { api_updateUser({"stats.gp" : gp + XP_POTION_COST}) } else { // If another potion wouldn't make them exceed the max, run as normal and increment counter api_updateUser({"stats.exp" : exp + XP_GAIN}) xpUsage += XP_GAIN // Save new counter value since it's updated scriptProperties.setProperty(xpUsageKey, xpUsage) } } } // When the MP Potion button is clicked function doButtonMpPotion(lvl, gp, mp, mpDiff, mpUsage, mpUsageKey){ // Get equipment stats info const responseContent = api_getAllAvailableContentObjects() content = JSON.parse(responseContent).data // If level lock fails, refund the money and send failure message let sufficientLevel = checkLevelLock(lvl, MP_POTION_LEVEL_LOCK, "potion.") if (!sufficientLevel) { api_updateUser({"stats.gp" : gp + MP_POTION_COST}) } else { // Check if they've exceeded daily usage. If yes, refund the money and send failure message let usageLimitOkay = checkUsageLimit(mpUsage, 1, MAX_DAILY_MP_POTION_USAGE, "potion ", false, false) if (!usageLimitOkay) { api_updateUser({"stats.gp" : gp + MP_POTION_COST}) } else { // Check if they're already at maximum mana. If yes, refund the money and send failure message if (mpDiff <= 0) { api_updateUser({"stats.gp" : gp + MP_POTION_COST}) api_sendPrivateMessage({"message" : MSG_ALREADY_AT_MAX_MANA_POTION, "toUserId" : USER_ID}) } // Check if they're close to maximum mana. If yes, refill them to max but not beyond, and send a message else if (mpDiff <= MP_GAIN) { api_updateUser({"stats.mp" : mp + mpDiff}) mpUsage++ api_sendPrivateMessage({"message" : MSG_NEAR_MAX_MANA_POTION, "toUserId" : USER_ID}) // Save new counter value since it's updated scriptProperties.setProperty(mpUsageKey, mpUsage) } // Run as normal if they're further than 30 MP away from maximum else if (mpDiff > MP_GAIN) { api_updateUser({"stats.mp" : mp + MP_GAIN}) mpUsage ++ // Save new counter value since it's updated scriptProperties.setProperty(mpUsageKey, mpUsage) } } } } // When the Anti-MP Potion button is clicked function doButtonAntiMpPotion(lvl, mp, gp, AntiMpUsageKey, AntiMpUsage){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, ANTI_MP_POTION_LEVEL_LOCK, "potion.") if (sufficientLevel) { // Check if they've exceeded daily usage. If yes, send failure message. let usageLimitOkay = checkUsageLimit(AntiMpUsage, 1, MAX_DAILY_ANTI_MP_POTION_USAGE, "potion ", false, false) if (usageLimitOkay) { // Check if they have sufficient Mana. Send failure message if not. if (mp < ANTI_MP_POTION_MP_COST) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_MANA_POTION, "toUserId" : USER_ID}) } else { // Run potion as normal since all checks passed. api_updateUser({"stats.mp" : mp - ANTI_MP_POTION_MP_COST, "stats.gp" : gp + ANTI_MP_POTION_GP_GAIN}) AntiMpUsage++ // Save new counter value since it's updated scriptProperties.setProperty(AntiMpUsageKey, AntiMpUsage) } } } } // When the Anti-HP Potion button is clicked function doButtonAntiHpPotion(hp, gp, AntiHpUsageKey, AntiHpUsage){ // Check if they've exceeded daily usage. If yes, send failure message. let usageLimitOkay = checkUsageLimit(AntiHpUsage, 1, MAX_DAILY_ANTI_HP_POTION_USAGE, "potion ", false, false) if (usageLimitOkay){ // Check if they have sufficient Health. Send failure message if not. if (hp < ANTI_HP_POTION_HP_COST) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_HEALTH_POTION, "toUserId" : USER_ID}) } else { // Run potion as normal since all checks passed. api_updateUser({"stats.hp" : hp - ANTI_HP_POTION_HP_COST, "stats.gp" : gp + ANTI_HP_POTION_GP_GAIN}) AntiHpUsage++ // Save new counter value since it's updated scriptProperties.setProperty(AntiHpUsageKey, AntiHpUsage) } } } // When the Anti-XP Potion button is clicked function doButtonAntiXpPotion(lvl, exp, gp, AntiXpUsageKey, AntiXpUsage){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, ANTI_XP_POTION_LEVEL_LOCK, "potion.") if (sufficientLevel) { // Check if they've exceeded daily usage. If yes, send failure message. let usageLimitOkay = checkUsageLimit(AntiXpUsage, 1, MAX_DAILY_ANTI_XP_POTION_USAGE, "potion ", false, false) if (usageLimitOkay) { // Check if they have sufficient Experience. Send failure message if not. if (exp < ANTI_XP_POTION_XP_COST) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_EXPERIENCE_POTION, "toUserId" : USER_ID}) } else { // Run potion as normal since all checks passed. api_updateUser({"stats.exp" : exp - ANTI_XP_POTION_XP_COST, "stats.gp" : gp + ANTI_XP_POTION_GP_GAIN}) AntiXpUsage++ // Save new counter value since it's updated scriptProperties.setProperty(AntiXpUsageKey, AntiXpUsage) } } } } // When the HP-for-MP button is clicked function doButtonHpForMp(lvl, hp, mp, mpDiff, HpForMpUsage, HpForMpUsageKey){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, HP_FOR_MP_LEVEL_LOCK, "skill.") if (sufficientLevel) { // Check if they've exceeded daily usage. If yes, send failure message. let usageLimitOkay = checkUsageLimit(HpForMpUsage, 1, MAX_DAILY_HP_FOR_MP_USAGE, "skill ", false, false) if (usageLimitOkay) { // Check if they have sufficient Health. Send failure message if not. if (hp < HP_COST_HP_FOR_MP) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_HEALTH_SKILL, "toUserId" : USER_ID}) } else { // Check if they're already at maximum mana. If yes, send failure message if (mpDiff <= 0) { api_sendPrivateMessage({"message" : MSG_ALREADY_AT_MAX_MANA_SKILL, "toUserId" : USER_ID}) } // Check if they're close to maximum mana. If yes, refill them to max but not beyond, and send a message else if (mpDiff <= MP_GAIN_HP_FOR_MP) { api_updateUser({"stats.hp" : hp - HP_COST_HP_FOR_MP, "stats.mp" : mp + mpDiff}) HpForMpUsage++ api_sendPrivateMessage({"message" : MSG_NEAR_MAX_MANA_SKILL, "toUserId" : USER_ID}) // Save new counter value since it's updated scriptProperties.setProperty(HpForMpUsageKey, HpForMpUsage) } // Otherwise, run as normal else if (mpDiff > MP_GAIN_HP_FOR_MP){ api_updateUser({"stats.hp" : hp - HP_COST_HP_FOR_MP, "stats.mp" : mp + MP_GAIN_HP_FOR_MP}) HpForMpUsage++ // Save new counter value since it's updated scriptProperties.setProperty(HpForMpUsageKey, HpForMpUsage) } } } } } // When the XP-for-MP button is clicked function doButtonXpForMp(lvl, exp, mp, mpDiff, XpForMpUsage, XpForMpUsageKey){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, XP_FOR_MP_LEVEL_LOCK, "skill.") if (sufficientLevel){ // Check if another use (50 XP) would cause them to exceed daily usage. If yes, send failure message. let XpToNextLevel = Math.round((Math.pow(lvl, 2) * 0.25 + 10 * lvl + 139.75) / 10) * 10 let MaxDailyXpUsage = Math.floor(XpToNextLevel * eval(MAX_DAILY_XP_FOR_MP_USAGE)) let usageLimitOkay = checkUsageLimit(XpForMpUsage, XP_COST_XP_FOR_MP, MaxDailyXpUsage, "skill ", true, true) if (usageLimitOkay) { // Check if they have sufficient Experience. Send failure message if not. if (exp < XP_COST_XP_FOR_MP) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_EXPERIENCE_SKILL, "toUserId" : USER_ID}) } else{ // Check if they're already at maximum mana. If yes, send failure message if (mpDiff <= 0) { api_sendPrivateMessage({"message" : MSG_ALREADY_AT_MAX_MANA_SKILL, "toUserId" : USER_ID}); } // Check if they're close to maximum mana. If yes, refill them to max but not beyond, and send a message else if (mpDiff <= MP_GAIN_XP_FOR_MP){ api_updateUser({"stats.mp" : mp + mpDiff, "stats.exp" : exp - XP_COST_XP_FOR_MP}) XpForMpUsage += XP_COST_XP_FOR_MP api_sendPrivateMessage({"message" : MSG_NEAR_MAX_MANA_SKILL, "toUserId" : USER_ID}) // Save new counter value since it's updated scriptProperties.setProperty(XpForMpUsageKey, XpForMpUsage) } // Otherwise, run as normal else if (mpDiff > MP_GAIN_XP_FOR_MP){ api_updateUser({"stats.mp" : mp + MP_GAIN_XP_FOR_MP, "stats.exp" : exp - XP_COST_XP_FOR_MP}) XpForMpUsage += XP_COST_XP_FOR_MP // Save new counter value since it's updated scriptProperties.setProperty(XpForMpUsageKey, XpForMpUsage) } } } } } // Custom Skills // Cross-Class Stat Buffs // Buffs of 4 stats // When the Omni-Buff button is clicked function doButtonOmniBuff(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedCon, buffsCon, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, OMNI_BUFF_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) earthquake(unbuffedInt, buffsInt) protectiveAura(unbuffedCon, buffsCon) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // Buffs of 1 stat // When the Valorous Presence (Self) button is clicked function doButtonValorousPresenceSelf(lvl, mp, unbuffedStr, buffsStr){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, VALOROUS_PRESENCE_SELF_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) } } } // When the Earthquake (Self) button is clicked function doButtonEarthquakeSelf(lvl, mp, unbuffedInt, buffsInt){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, EARTHQUAKE_SELF_MP_COST, false) if (enoughMana) { earthquake(unbuffedInt, buffsInt) } } } // When the Protective Aura (Self) button is clicked function doButtonProtectiveAuraSelf(lvl, mp, unbuffedCon, buffsCon){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, PROTECTIVE_AURA_SELF_MP_COST, false) if (enoughMana) { protectiveAura(unbuffedCon, buffsCon) } } } // When the Tools of the Trade (Self) button is clicked function doButtonToolsOfTheTradeSelf(lvl, mp, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, TOOLS_OF_THE_TRADE_SELF_MP_COST, false) if (enoughMana) { toolsOfTheTrade(unbuffedPer, buffsPer) } } } // Buffs of 3 stats // When the Tripe Buff StrIntCon button is clicked function doButtonTripeBuffStrIntCon(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedCon, buffsCon){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, TRIPLE_BUFF_STR_INT_CON_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) earthquake(unbuffedInt, buffsInt) protectiveAura(unbuffedCon, buffsCon) } } } // When the Tripe Buff StrIntPer button is clicked function doButtonTripeBuffStrIntPer(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, TRIPLE_BUFF_STR_INT_PER_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) earthquake(unbuffedInt, buffsInt) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // When the Tripe Buff StrConPer button is clicked function doButtonTripeBuffStrConPer(lvl, mp, unbuffedStr, buffsStr, unbuffedCon, buffsCon, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, TRIPLE_BUFF_STR_CON_PER_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) protectiveAura(unbuffedCon, buffsCon) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // When the Triple Buff IntConPer button is clicked function doButtonTripleBuffIntConPer(lvl, mp, unbuffedInt, buffsInt, unbuffedCon, buffsCon, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, TRIPLE_BUFF_INT_CON_PER_MP_COST, false) if (enoughMana) { earthquake(unbuffedInt, buffsInt) protectiveAura(unbuffedCon, buffsCon) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // Buffs of 2 stats // When the Double Buff StrInt button is clicked function doButtonDoubleBuffStrInt(lvl, mp, unbuffedStr, buffsStr, unbuffedInt, buffsInt){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_STR_INT_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) earthquake(unbuffedInt, buffsInt) } } } // When the Double Buff StrCon button is clicked function doButtonDoubleBuffStrCon(lvl, mp, unbuffedStr, buffsStr, unbuffedCon, buffsCon){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_STR_CON_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) protectiveAura(unbuffedCon, buffsCon) } } } // When the Double Buff StrPer button is clicked function doButtonDoubleBuffStrPer(lvl, mp, unbuffedStr, buffsStr, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_STR_PER_MP_COST, false) if (enoughMana) { valorousPresence(unbuffedStr, buffsStr) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // When the Double Buff IntCon button is clicked function doButtonDoubleBuffIntCon(lvl, mp, unbuffedInt, buffsInt, unbuffedCon, buffsCon){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_INT_CON_MP_COST, false) if (enoughMana) { earthquake(unbuffedInt, buffsInt) protectiveAura(unbuffedCon, buffsCon) } } } // When the Double Buff IntPer button is clicked function doButtonDoubleBuffIntPer(lvl, mp, unbuffedInt, buffsInt, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_INT_PER_MP_COST, false) if (enoughMana) { earthquake(unbuffedInt, buffsInt) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // When the Double Buff ConPer button is clicked function doButtonDoubleBuffConPer(lvl, mp, unbuffedCon, buffsCon, unbuffedPer, buffsPer){ // If level lock fails, send failure message let sufficientLevel = checkLevelLock(lvl, LEVEL_LOCK_BUFFS, "skill.") if (sufficientLevel) { let enoughMana = checkMana(mp, DOUBLE_BUFF_CON_PER_MP_COST, false) if (enoughMana) { protectiveAura(unbuffedCon, buffsCon) toolsOfTheTrade(unbuffedPer, buffsPer) } } } // Cross-Class Skills // Rogue skills (non-buff, non-boss-damage) // 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) } } } } // Healer skills (non-buff, non-boss-damage) // 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}) } } } // Mage skills (non-buff, non-boss-damage) // 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}) } } } // Boss damage skills // When the Burst of Flames (damage only) button is clicked function doButtonBurstOfFlamesDamageOnly(lvl, hp, xp, mp, gp, intelligence, perception, unbuffedStr, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer){ 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, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer){ 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, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer){ 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, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer){ 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, unbuffedInt, unbuffedPer, buffsStr, buffsInt, buffsPer) { 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) } } } // Game Modes // When the Bad Day Mode button is clicked function doButtonBadDayMode(constitutionBuffs, stealthedDailies, totalDailies, hp, unbuffedConstitution){ // Check if they're already in Bad Day Mode by seeing if CON is really high and all Dailies are Stealthed and HP is high if ( ( constitutionBuffs > 8000 ) && ( stealthedDailies >= totalDailies ) && ( hp >= 45 ) ) { api_sendPrivateMessage({"message" : MSG_ALREADY_IN_BAD_DAY_MODE, "toUserId" : USER_ID}) } else { // Full heal api_updateUser({"stats.hp" : 50}) // Set CON to 9999 let buffCon = 9999 - unbuffedConstitution api_updateUser({"stats.buffs.con" : buffCon}) // Stealth all Dailies api_updateUser({"stats.buffs.stealth" : totalDailies}) } } // Enters or exits from Partial Healing Mode function togglePartialHealingMode(){ var partialHealingModeActiveKey = PARTIAL_HEALING_MODE_ACTIVE_KEY var partialHealingModeActive = Number(scriptProperties.getProperty(partialHealingModeActiveKey)) // If not in Partial Healing Mode, enter it. if (partialHealingModeActive == 0) { scriptProperties.setProperty(partialHealingModeActiveKey, 1) // Save HP and level initScriptPropertiesPartialHealing() // Update button if the user created it if (PARTIAL_HEALING_TOGGLE_BUTTON == 1) { api_updateTask(PARTIAL_HEALING_MODE_ALIAS, {"text": PARTIAL_HEALING_MODE_TEXT_EXIT, "notes": PARTIAL_HEALING_MODE_NOTES_EXIT,}) } } // If in Partial Healing Mode, exit it. else if (partialHealingModeActive == 1) { scriptProperties.setProperty(partialHealingModeActiveKey, 0) // Update button if the user created it if (PARTIAL_HEALING_TOGGLE_BUTTON == 1) { api_updateTask(PARTIAL_HEALING_MODE_ALIAS, {"text": PARTIAL_HEALING_MODE_TEXT_ENTER, "notes": PARTIAL_HEALING_MODE_NOTES_ENTER,}) } } } // Runs Partial Healing Mode function doPartialHealingMode(){ // Retrieve saved values for HP and level var hpKey = HP_KEY var lvlKey = LVL_KEY var prevHp = Number(scriptProperties.getProperty(hpKey)) var prevLvl = Number(scriptProperties.getProperty(lvlKey)) // Get current values for HP and level const response = api_getAuthenticatedUserProfile("stats") user = JSON.parse(response).data let currentHp = user.stats.hp let currentLvl = user.stats.lvl // See if they've leveled up. if (currentLvl > prevLvl) { // If yes, run correct version of Partial Healing if ( VERSION_PARTIAL_HEALING == 2 ) { // Value to heal must be between 0 and 50 let valueToHeal = sanitizeInput(HP_SET_VALUE_PER_LEVEL, 0, 50) // New HP must be between 0 and 50 let newHp = sanitizeInput(valueToHeal + prevHp, 0, 50) // Heal the correct amount. api_updateUser({"stats.hp" : newHp}) // Save new HP scriptProperties.setProperty(hpKey, newHp) } else if ( VERSION_PARTIAL_HEALING == 3 ) { // Percent to heal must be between 0 and 100 let percentToHeal = sanitizeInput (HP_PERCENT_PER_LEVEL, 0, 100) let healing = ( percentToHeal / 100 ) * ( 50 - prevHp ) // New HP must be between 0 and 50 let newHpPercent = sanitizeInput(healing + prevHp, 0, 50) // Heal the correct amount. api_updateUser({"stats.hp" : newHpPercent}) // Save new HP scriptProperties.setProperty(hpKey, newHpPercent) } // Irrespective of version, save new level scriptProperties.setProperty(lvlKey, currentLvl) } else { // If they haven't leveled up since the last time the script ran... // If previous and current HP are the same, no need to re-save. Otherwise, do. if (prevHp != currentHp) { scriptProperties.setProperty(hpKey, currentHp) } } } // Duel script // Once you've achieved victory, checks if the correct message posted and does other error-checking actions. function victoryCondition(troubleshootingKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey){ var troubleshootingFlag = Number(scriptProperties.getProperty(troubleshootingKey)) // If troubleshooting flag is active, do nothing. Only do things if it is equal to 0 if (troubleshootingFlag == 0) { var groupId = scriptProperties.getProperty(groupIdKey) var timestampIdentifier = Number(scriptProperties.getProperty(timestampIdentifierKey)) var announcerBotUsername = scriptProperties.getProperty(announcerBotUsernameKey) // Update Duel Score button to indicate victory (and do this regardless of any of the conditions below) if (groupId != "") { let groupUrl = "https://habitica.com/groups/" + groupId var newDuelScoreNotes = MSG_DUEL_SCORE_VICTORY1 + groupUrl + MSG_DUEL_SCORE_VICTORY2 + announcerBotUsername + MSG_DUEL_SCORE_VICTORY3 api_updateTask(DUEL_SCORE_ALIAS, {"notes" : newDuelScoreNotes}) } // Check if the automated Duel End message successfully posted or not var msgIdAuto = findAutomatedDuelEndMessages("user", groupId, "not needed", timestampIdentifier, announcerBotUsername, AUTOMATED_MSG_END_DUEL) if (msgIdAuto == "nope") { // User message was supposed to post but didn't. // Flip the troubleshooting flag troubleshootingFlag = 1 scriptProperties.setProperty(TROUBLESHOOTING_KEY, troubleshootingFlag) // Post error message to the Gparty and also send it as a private message to the user in case posting to the party fails api_postChatMessageToGroup({"message": ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_MESSAGE_DIDNT_POST}) api_sendPrivateMessageAlways({"message" : ERROR_MSG_AUTOMATED_SELF + ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_MESSAGE_DIDNT_POST, "toUserId" : USER_ID}) } else { // Find announcer bot response to the automated post. First, check the "valid" automated message let msgIdBotValid = findAutomatedDuelEndMessages("bot", groupId, msgIdAuto, timestampIdentifier, announcerBotUsername, AUTOMATED_MSG_BOT_DECLARES_WINNER) if (msgIdBotValid == "nope") { // Next, check the "invalid" automated meessage let msgIdBotInvalid = findAutomatedDuelEndMessages("bot", groupId, msgIdAuto, timestampIdentifier, announcerBotUsername, AUTOMATED_MSG_BOT_INVALID_END_STRING) if (msgIdBotInvalid == "nope") { // This condition shouldn't be possible. It means the user message posted but the bot didn't resppond either way (valid or invalid) // This means that was probably an issue with the bot username (possibly saving incorrectly or not tagging the bot). // Flip the troubleshooting flag troubleshootingFlag = 1 scriptProperties.setProperty(TROUBLESHOOTING_KEY, troubleshootingFlag) // Post error message to the party and also send it as a private message to the user in case posting to the party fails api_postChatMessageToGroup({"message": ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_BOT_DIDNT_REPLY}) api_sendPrivateMessageAlways({"message" : ERROR_MSG_AUTOMATED_SELF + ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_BOT_DIDNT_REPLY, "toUserId" : USER_ID}) } else { // This condition means the bot found the users automated message as invalid. But, user's duel score indicates they should've won. // Therefore, it's most likely that the string generated by the user's script messed up. // Flip the troubleshooting flag troubleshootingFlag = 1 scriptProperties.setProperty(TROUBLESHOOTING_KEY, troubleshootingFlag) // Post error message to the party and also send it as a private message to the user in case posting to the party fails api_postChatMessageToGroup({"message": ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_BOT_DIDNT_REPLY}) api_sendPrivateMessageAlways({"message" : ERROR_MSG_AUTOMATED_SELF + ERROR_MSG_AUTOMATED_START + ERROR_MSG_AUTOMATED_BOT_FOUND_INVALID, "toUserId" : USER_ID}) } } else { // Message posted and bot found it valid. User message is no longer needed, so delete. api_deleteChatMessage(groupId, msgIdAuto) // Flip the troubleshooting flag so that this script does not post additional automated messages // A 2 indicates victory, unlike 1, which indicates error troubleshootingFlag = 2 scriptProperties.setProperty(TROUBLESHOOTING_KEY, troubleshootingFlag) } } } } // Increases duel score while duel is running. Also checks to see if user has won. function increaseDuelScore(duelScoreKey, scoreNeededGoalKey, groupIdKey, selfUsernameKey, opponentUsernameKey, wagerKey, announcerBotUsernameKey, timestampIdentifierKey, troubleshootingKey, taskDifficulty, taskDelta, taskType){ // Retrieve saved values let duelScore = Number(scriptProperties.getProperty(duelScoreKey)) let scoreNeededGoal = Number(scriptProperties.getProperty(scoreNeededGoalKey)) var multiplier = 1 // If Daily or To-Do. if ( taskType == "habit" ) { multiplier = 0.5 // Habits score half the points of Daily or To-Do. } // Calculate duel points for this task let duelPointsRaw = taskDelta * taskDifficulty * multiplier // If it's not a number, then set it to 0 if (Number.isNaN(duelPointsRaw)) { duelPointsRaw = 0 } // Round to two decimal points let duelPoints = ( Math.round( duelPointsRaw * 100 ) / 100 ) // Add to running total. Round. Save. let duelScoreRaw = duelScore + duelPoints duelScore = ( Math.round( duelScoreRaw * 100 ) / 100 ) scriptProperties.setProperty(duelScoreKey, duelScore) // Check against Score Needed / Goal. If victory, create Duel End string for duel bot to check/evaluate. if (duelScore >= scoreNeededGoal) { let DUEL_END_MSG = createDuelEndString(selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, announcerBotUsernameKey, timestampIdentifierKey) var groupId = scriptProperties.getProperty(groupIdKey) api_postChatMessageToGroup({"message": DUEL_END_MSG}, groupId) // Wait 5 seconds Utilities.sleep(5000) // Check to see if automated message posted correctly victoryCondition(troubleshootingKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey) } } // Does correct actions when Duel Start button is pressed function duelStartButton(taskNotes, taskIdStart, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey){ // Check if valid string, send error message if not, and if valid, parse and save values and start duel and deletes Duel Start button. parseDuelStartString(taskNotes, taskIdStart, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey) } // Does correct actions when Duel Score button is pressed function duelScoreButton(duelScoreKey, scoreNeededGoalKey){ // Retrieve saved values let duelScore = Number(scriptProperties.getProperty(duelScoreKey)) let scoreNeededGoal = Number(scriptProperties.getProperty(scoreNeededGoalKey)) let updatedNotes = MSG_DUEL_SCORE_PART1 + duelScore + MSG_DUEL_SCORE_PART2 + scoreNeededGoal + MSG_DUEL_SCORE_PART3 // Tell user their duel score by updating the button api_updateTask(DUEL_SCORE_ALIAS, {"notes" : updatedNotes}) } // Does correct actions when Duel End button is pressed function duelEndButton(taskNotes, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, timestampIdentifierKey, troubleshootingKey, groupIdKey, announcerBotUsernameKey){ // Initialize value. -2 indicates some kind of invalid string, so default to that. let validity = -2 // Check if search terms are found let duelEndIndex = findIndex(DUEL_END, taskNotes, true) // Since the user will be copy/pasting, I want this to be case-sensitive let duelEndDebugIndex = findIndex(DUEL_END_DEBUG, taskNotes, false) // Since the user will type this one, I don't want to be case-sensitive // Set "validity" to the correct value if (duelEndIndex == -1) { validity = -3 // User didn't paste in a new message at all into the Notes section } else { if (duelEndDebugIndex != -1) { validity = 3 // 3 indicates you aren't the winner, i.e. the override command won't make you win the duel. } else { // Create hashes for duel parameters let hashAllExpected = createDuelHash(selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, timestampIdentifierKey) let hashSelfUserId = createHash(USER_ID) // for indicating who won let hashSumIfWinner = hashAllExpected + hashSelfUserId // Remove search term from beginning of Notes let inputStringTemp = taskNotes let length = inputStringTemp.length inputStringTemp = createSubstring(taskNotes, duelEndIndex + 12, length) // offset is 12 since "ENDING DUEL:" has 12 characters taskNotes = inputStringTemp // -2 is used to indicate an invalid string, 3 to indicate you didn't win, 5 to indicate you did. validity = parseDuelEndString(taskNotes, hashAllExpected, hashSumIfWinner) } } // Now that "validity" is correct, do the appropriate actions if (validity == -3) { // Error message tells them that the search term "ENDING DUEL: DEBUG END" was not found. api_sendPrivateMessageAlways({"message" : ERROR_MSG_INVALID_END_STRING_BEGINNING + ERROR_MSG_INVALID_END_STRING_SEARCH_TERM + ERROR_MSG_INVALID_END_STRING_ENDING, "toUserId" : USER_ID}) } else if (validity == -2) { // Error message tells them that what they entered did not match with the scrippt was expecting. // Also tells them about the manual override command "ENDING DUEL: DEBUG END". api_sendPrivateMessageAlways({"message" : ERROR_MSG_INVALID_END_STRING_BEGINNING + ERROR_MSG_INVALID_END_STRING_HASH1 + ERROR_MSG_INVALID_END_STRING_ENDING + ERROR_MSG_INVALID_END_STRING_HASH2, "toUserId" : USER_ID}) } else if ( (validity == 3) || (validity == 5) ) { // If valid // Grab Notes field from the Duel End button, and grab its Task ID also, and ID for Duel Score buton const responseTasks2 = api_getUserTasks("rewards") const tasksRewards2 = JSON.parse(responseTasks2).data for (var i in tasksRewards2) { if (tasksRewards2[i].alias == DUEL_END_ALIAS ) { var taskIdEnd = tasksRewards2[i]._id } else if (tasksRewards2[i].alias == DUEL_SCORE_ALIAS ) { var taskIdScore = tasksRewards2[i]._id } } // Retrieve saved values var groupId = scriptProperties.getProperty(groupIdKey) var timestampIdentifier = Number(scriptProperties.getProperty(timestampIdentifierKey)) var announcerBotUsername = scriptProperties.getProperty(announcerBotUsernameKey) // If you didn't win if (validity == 3) { // Reset values, create new Duel Start button, delete Duel Score and Duel End buttons, delete automated message if it hasn't happened yet. endDuel(taskIdEnd, taskIdScore, troubleshootingKey, groupId, timestampIdentifier, announcerBotUsername) } // If you did win, do the same thing but first gain your GP winnings. else if (validity == 5) { // Calculate winnings let wagerWinner = Number(scriptProperties.getProperty(wagerKey)) let gpWon = wagerWinner * 2 // Grab current GP const responseUserWinner = api_getAuthenticatedUserProfile("stats") const userWinner = JSON.parse(responseUserWinner).data var gp = userWinner.stats.gp // Gain GP api_updateUser({"stats.gp" : gp + gpWon}) // Reset values, create new Duel Start button, delete Duel Score and Duel End buttons, delete automated message if it hasn't happened yet. endDuel(taskIdEnd, taskIdScore, troubleshootingKey, groupId, timestampIdentifier, announcerBotUsername) } } else { // This condition shouldn't be possible api_sendPrivateMessageAlways({"message" : ERROR_MSG_VALIDITY, "toUserId" : USER_ID}) } } // Resets duel values, creates a new instance of the Duel Start button, and deletes the Duel Score and Duel End buttons. function endDuel(taskIdEnd, taskIdScore, troubleshootingKey, groupId, timestampIdentifier, announcerBotUsername){ // Check to see if the automated message was deleted or not. If not, delete it. var troubleshootingFlag = Number(scriptProperties.getProperty(troubleshootingKey)) if (troubleshootingFlag != 2) { let msgIdAuto = findAutomatedDuelEndMessages("user", groupId, "not needed", timestampIdentifier, announcerBotUsername, AUTOMATED_MSG_END_DUEL) if (msgIdAuto == "nope") { // If the message can't be found, do nothing } else { api_deleteChatMessage(groupId, msgIdAuto) } } // Reset all saved values resetDuelValues() // Create the Duel Start button api_createNewTaskForUser([DUEL_START_BUTTON]) // If relevant, add tag to task (i.e. only if the user created a tag for these buttons) checkAndAddTagToTask(DUEL_START_ALIAS, "none") // Delete the Duel Score and Duel End buttons api_deleteTask(taskIdEnd) api_deleteTask(taskIdScore) } // Just for Fun // When the Snowball Transformation button is clicked function doButtonSnowballTransformation(gp){ api_updateUser({"stats.buffs.snowball" : true, "stats.gp" : gp - 15}) } // When the Spooky Sparkles Transformation button is clicked function doButtonSpookySparklesTransformation(gp){ api_updateUser({"stats.buffs.spookySparkles" : true, "stats.gp" : gp - 15}) } // When the Shiny Seed Transformation button is clicked function doButtonShinySeedTransformation(gp){ api_updateUser({"stats.buffs.shinySeed" : true, "stats.gp" : gp - 15}) } // When the Seafoam Transformation button is clicked function doButtonSeafoamTransformation(gp){ api_updateUser({"stats.buffs.seafoam" : true, "stats.gp" : gp - 15}) } // When the Random Transformation button is clicked function doButtonRandomTransformation(gp){ let rand = Math.floor((Math.random() * 4) + 1) switch (rand){ case 1: api_updateUser({"stats.buffs.snowball" : true, "stats.gp" : gp - 15}) break case 2: api_updateUser({"stats.buffs.spookySparkles" : true, "stats.gp" : gp - 15}) break case 3: api_updateUser({"stats.buffs.shinySeed" : true, "stats.gp" : gp - 15}) break case 4: api_updateUser({"stats.buffs.seafoam" : true, "stats.gp" : gp - 15}) break } } // Switch-cases (logic) // Cases based on timestamp, for rate-limiting purposes function caseTimestamp(caseButton, timeEnd){ // Initialize as blank var timestampKeyToUse = "" // Initialize var timestampNonscriptButtonsKey = TIMESTAMP_NONSCRIPT_BUTTONS_KEY var timestampKey = TIMESTAMP_KEY // Retrieve saved values. Which timestamp to use depends on which kind of button was pressed if (caseButton > 3000) { // Indicates a non-script button timestampKeyToUse = timestampNonscriptButtonsKey } else { // Indicates a script button timestampKeyToUse = timestampKey } // Retrieve the correct timestamp(start) let timeStart = Number(scriptProperties.getProperty(timestampKeyToUse)) // If it's not initialized, do so if ( (timeStart == 0) || (timeStart == "") || (timeStart == undefined) || (timeStart == null) ) { timeStart = Date.now() } // Based on difference between timeStart and timeEnd, create cases var caseRateLimiting = -1 // Indicates less than 5 seconds have elapsed. This is the default value. if ( (timeEnd - timeStart) >= 30000 ) { caseRateLimiting = 30 // Indicates 30+ seconds have elapsed } else if ( (timeEnd - timeStart) >= 5000 ) { caseRateLimiting = 5 // Indicates between 5 and 30 seconds have elapsed } return caseRateLimiting } // Cases based on which button was pressed. Some buttons would affect Partial Healing Mode results, others would not function caseButtonAlias(sanitizedAlias, taskType){ // I want users to be able to toggle in/out of Game Modes irrespective of timestamp if (sanitizedAlias == PARTIAL_HEALING_MODE_ALIAS) { return 1111 // Partial Healing Mode gets its own case number } else { // Buttons that can affect HP (up or down) or XP (up) if ( (sanitizedAlias == XP_POTION_ALIAS) || (sanitizedAlias == ANTI_HP_POTION_ALIAS) || (sanitizedAlias == HP_FOR_MP_ALIAS) || (sanitizedAlias == BUTTON_1_ALIAS) || (sanitizedAlias == BUTTON_2_ALIAS) || (sanitizedAlias == BUTTON_3_ALIAS) || (sanitizedAlias == BUTTON_4_ALIAS) || (sanitizedAlias == BUTTON_5_ALIAS) || (sanitizedAlias == BACKSTAB_ALIAS) || (sanitizedAlias == HEALING_LIGHT_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_XP_ONLY_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_XP_PLUS_DAMAGE_ALIAS) ) { return 2222 } // All other script buttons except duel (duel buttons take care of themselves elsewhere) else if ( (sanitizedAlias == MP_POTION_ALIAS) || (sanitizedAlias == ANTI_MP_POTION_ALIAS) || (sanitizedAlias == ANTI_XP_POTION_ALIAS) || (sanitizedAlias == XP_FOR_MP_ALIAS) || (sanitizedAlias == OMNI_BUFF_ALIAS) || (sanitizedAlias == VALOROUS_PRESENCE_SELF_ALIAS) || (sanitizedAlias == EARTHQUAKE_SELF_ALIAS) || (sanitizedAlias == PROTECTIVE_AURA_SELF_ALIAS) || (sanitizedAlias == TOOLS_OF_THE_TRADE_SELF_ALIAS) || (sanitizedAlias == TRIPLE_BUFF_STR_INT_CON_ALIAS) || (sanitizedAlias == TRIPLE_BUFF_STR_INT_PER_ALIAS) || (sanitizedAlias == TRIPLE_BUFF_STR_CON_PER_ALIAS) || (sanitizedAlias == TRIPLE_BUFF_INT_CON_PER_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_STR_INT_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_STR_CON_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_STR_PER_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_INT_CON_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_INT_PER_ALIAS) || (sanitizedAlias == DOUBLE_BUFF_CON_PER_ALIAS) || (sanitizedAlias == PICKPOCKET_ALIAS) || (sanitizedAlias == STEALTH_ALIAS) || (sanitizedAlias == CHILLING_FROST_ALIAS) || (sanitizedAlias == BURST_OF_FLAMES_DAMAGE_ONLY_ALIAS) || (sanitizedAlias == BRUTAL_SMASH_ALIAS) || (sanitizedAlias == RADIANT_SHIELD_ALIAS) || (sanitizedAlias == SNEAK_ATTACK_ALIAS) || (sanitizedAlias == SNOWBALL_ALIAS) || (sanitizedAlias == SPOOKY_SPARKLES_ALIAS) || (sanitizedAlias == SHINY_SEED_ALIAS) || (sanitizedAlias == SEAFOAM_ALIAS) || (sanitizedAlias == RANDOM_ALIAS) ) { return 2345 } // For all non-script non-reward tasks else if ( ( taskType == "habit" ) || ( taskType == "daily") || ( taskType == "todo") ) { return 3333 } else { return 4444 } } } // Cases based on timestamp and alias function caseMain(caseRateLimiting, caseButtonAlias, sanitizedAlias, timeEnd){ // If in Partial Healing Mode, initialize those variables. if (VERSION_PARTIAL_HEALING >= 2) { var partialHealingModeActiveKey = PARTIAL_HEALING_MODE_ACTIVE_KEY var partialHealingModeActive = sanitizeInput(Number(scriptProperties.getProperty(partialHealingModeActiveKey)),0,1) } else { partialHealingModeActive = 0 } // Rate limiting for most Game Modes is 5 seconds if (caseRateLimiting >= 5) { // If user pressed Toggle Partial Healing Mode button, do it if (caseButtonAlias == 1111) { togglePartialHealingMode() // 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) // Since it's a script button, use that timestamp key scriptProperties.setProperty(TIMESTAMP_KEY, timeEnd) } else if (caseButtonAlias == 1234) { doButtons(sanitizedAlias) // These buttons also get to run // If Partial Healing Mode is active, these buttons will trigger it as well if (partialHealingModeActive == 1) { // Wait 1 second Utilities.sleep(1000) doPartialHealingMode() } // 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) // Since it's a script button, use that timestamp key scriptProperties.setProperty(TIMESTAMP_KEY, timeEnd) } else { // If in Partial Healing Mode and user pressed a non-script non-reward button, run the Mode as normal if ( (caseButtonAlias == 3333) && (partialHealingModeActive == 1) ) { doPartialHealingMode() // 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) // Since it's not a script button, use that timestamp key scriptProperties.setProperty(TIMESTAMP_NONSCRIPT_BUTTONS_KEY, timeEnd) } else { // For script buttons, rate limiting is 30 seconds if (caseRateLimiting == 30) { // Run script buttons if they were pressed if ( (caseButtonAlias == 2222) || (caseButtonAlias == 2345) ) { doButtons(sanitizedAlias) // 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) // Since it's a script button, use that timestamp key scriptProperties.setProperty(TIMESTAMP_KEY, timeEnd) } // If in Partial Healing Mode AND a button ran that might affect Partial Healing Mode, run the Mode now (after the button already ran) if ( (partialHealingModeActive == 1) && (caseButtonAlias == 2222) ) { // Wait 1 second Utilities.sleep(1000) doPartialHealingMode() // 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) // Since it's a script button, use that timestamp key scriptProperties.setProperty(TIMESTAMP_KEY, timeEnd) } } } } } } // Duel script cases function createCase(taskAliasOrPlaceholder, taskType, duelCurrentlyActive, duelScoreKey, scoreNeededGoalKey){ if (taskAliasOrPlaceholder == DUEL_START_ALIAS) { if (duelCurrentlyActive == 1) { return 11 // Error, clicked the button to start a duel but are already in a duel } else if (duelCurrentlyActive == 0) { return 12 // Normal use of Duel Start button } else { return 10 // Error condition that shouldn't be possible to reach } } else if (taskAliasOrPlaceholder == DUEL_END_ALIAS) { if (duelCurrentlyActive == 0) { return 31 // Error, clicked the button to end duel but not currently in a duel } else if (duelCurrentlyActive == 1) { return 32 // Normal use of the Duel End button } else { return 30 // Error condition that shouldn't be possible to reach } } else { // If any button other than Duel Start/End is pressed, check if user has already achieved victory (if a duel is in progress) if (duelCurrentlyActive == 1) { // Retrieve saved values let duelScore = Number(scriptProperties.getProperty(duelScoreKey)) let scoreNeededGoal = Number(scriptProperties.getProperty(scoreNeededGoalKey)) if (duelScore >= scoreNeededGoal) { return 1 // Victory condition – checks to see if the correct automated messages were posted } else { // If not victory, see which button was pressed and do appropriate actions. if (taskAliasOrPlaceholder == DUEL_SCORE_ALIAS) { return 22 // Normal use of the Duel Score button } else { // Only increase duel score if a non-Reward task was clicked if ( ( taskType == "habit" ) || ( taskType == "daily") || ( taskType == "todo") ) { return 3 // Increasing duel score while in a duel } else { return 2 // Do nothing } } } } else if (duelCurrentlyActive == 0) { // If a duel is not currently active, see which button was pressed and do appropriate actions. if (taskAliasOrPlaceholder == DUEL_SCORE_ALIAS) { return 21 // Error, clicked the button to see duel score but not currently in a duel } else { return 2 // Do nothing since user is not in a duel } } else { return 40 // Error condition that shouldn't be possible to reach } } } // FUNCTION LIBRARY: TABLE OF CONTENTS // 1. API basics – functions made directly from Habitica's API. Each individual script probably won't use all of these, but that's okay. // 2. Non-API functions // 2A. Basics – functions that can be used in a wide variety of scripts // 2B. Skills – functions used to make custom skills // 2X. Debug functions – while the specific kind of debug function depends greatly on the script, they do share some commonalities // 3. Idiosyncratic functions – functions that are used only in a single script. // If a script has no idiosyncratic functions (i.e. all its functions are accounted for in the above categories), it will not be listed in this section. // Not listed here: Dueling Script – Announcer Bot, since you run that script from a secondary/bot account, not your main account, and thus won't run it alongside any other scripts. // 3A. Editable Fix Character Values Buttons // 3B. Dueling Script – Scoring // 3C. Transform Yourself Year-Round // 1. API BASICS // CHALLENGE // CHAT // Gets chat messages given a Group ID function api_getChatMessagesFromGroup(groupId) { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups/" + groupId + "/chat" return UrlFetchApp.fetch(url, params) } // Posts a chat message to a group function api_postChatMessageToGroup(payload, groupId) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups/" + groupId + "/chat" return UrlFetchApp.fetch(url, params) } // Deletes a chat message function api_deleteChatMessage(groupId, chatId) { const params = { "method" : "delete", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/groups/" if (groupId != "") { url += groupId url += "/chat/" if (chatId != "") { url += chatId } } return UrlFetchApp.fetch(url, params) } // CONTENT // Gets equipment info function api_getAllAvailableContentObjects() { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/content" return UrlFetchApp.fetch(url, params) } // DATA EXPORT // GROUP // Gets all of a user's Groups (Guilds or party) function api_getGroups(payload) { const params = { "method" : "get", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/groups" return UrlFetchApp.fetch(url, params) } // Gets Party ID of user's party function getParty(){ let paramsTemplate = { "method": "get", "headers": HEADERS, } let response = UrlFetchApp.fetch("https://habitica.com/api/v3/groups/party", paramsTemplate) let party = JSON.parse(response).data let partyId = party._id return partyId } // INBOX // MEMBER // 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) } } // 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) } // NOTIFICATION // QUEST // TAG // Create a new tag if it hasn't been done yet, get its ID if it has function apiMult_createNewTagNoDuplicates(tagName){ const response = api_getTags() const tags = JSON.parse(response).data // Initialize these let duplicateExists = 0 let tagId = "newlyCreated" for (var i in tags) { if (tags[i].name == tagName) { duplicateExists++ // If there is a match, grab the tag ID tagId = tags[i].id } } // If no duplicate, create new tag if (duplicateExists == 0) { api_createNewTag({"name": tagName}) } return tagId } // Gets all of a user's tags function api_getTags() { const params = { "method" : "get", "headers" : HEADERS, "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/tags" return UrlFetchApp.fetch(url, params) } // Create new tag function api_createNewTag(payload) { const params = { "method" : "post", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/tags" return UrlFetchApp.fetch(url, params) } // TASK // Create custom tasks, including 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) } // Gets user tasks 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) } // Updates a task function api_updateTask(taskIdOrAlias, payload) { const params = { "method" : "put", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/tasks/" + taskIdOrAlias return UrlFetchApp.fetch(url, params) } // Delete a task function api_deleteTask(taskIdOrAlias) { const params = { "method" : "delete", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/tasks/" if (taskIdOrAlias != "") { url += taskIdOrAlias } return UrlFetchApp.fetch(url, params) } // Add a tag to a task function api_addTagToTask(taskIdOrAlias, tagId){ const params = { "method" : "post", "headers" : HEADERS, "muteHttpExceptions" : true, } var url = "https://habitica.com/api/v3/tasks/" if ( (taskIdOrAlias != "") && (taskIdOrAlias != undefined) && (taskIdOrAlias != null) ) { url += taskIdOrAlias + "/tags/" if ( (tagId != "") && (tagId != undefined) && (tagId != null) ) { url += tagId } } return UrlFetchApp.fetch(url, params) } // USER // 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) } // 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) } // WEBHOOK // 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) } // Updates webhook function api_updateWebhook(webhookUrl, payload){ const params = { "method" : "put", "headers" : HEADERS, "contentType" : "application/json", "payload" : JSON.stringify(payload), "muteHttpExceptions" : true, } const url = "https://habitica.com/api/v3/user/webhook/" + webhookUrl return UrlFetchApp.fetch(url, params) } // 2. NON-API FUNCTIONS // 2A. BASICS // 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 } } // Check if (daily) usage exceeded, send message if yes. function checkUsageLimit(usageCounter, newUsageIncrement, maxUsage, messageMidVariable, isForXp, isForXpPaid){ if ( (usageCounter + newUsageIncrement) > maxUsage ) { let errorMsg = MSG_DAILY_USAGE_EXCEEDED_START + messageMidVariable + maxUsage + MSG_DAILY_USAGE_EXCEEDED_END // Since the max usage for XP potion/skill is different, the message will be different. if (isForXp) { // Message start differs if it's XP gained vs. XP paid if (isForXpPaid) { errorMsg = MSG_DAILY_USAGE_EXCEEDED_XP_START_PAY } else { errorMsg = MSG_DAILY_USAGE_EXCEEDED_XP_START } errorMsg += messageMidVariable + MSG_DAILY_USAGE_EXCEEDED_XP_MID_1 + maxUsage + MSG_DAILY_USAGE_EXCEEDED_XP_MID_2 + MAX_DAILY_XP_POTION_USAGE + MSG_DAILY_USAGE_EXCEEDED_XP_END } api_sendPrivateMessage({"message" : errorMsg, "toUserId" : USER_ID}) return false } else { return true } } // Resets usage counters, usually done at Cron function resetCounters(){ scriptProperties.setProperty(XP_USAGE_KEY, 0) scriptProperties.setProperty(MP_USAGE_KEY, 0) scriptProperties.setProperty(ANTI_MP_USAGE_KEY, 0) scriptProperties.setProperty(ANTI_HP_USAGE_KEY, 0) scriptProperties.setProperty(ANTI_XP_USAGE_KEY, 0) scriptProperties.setProperty(HP_FOR_MP_KEY, 0) scriptProperties.setProperty(XP_FOR_MP_KEY, 0) } // NUMERIC-RELATED FUNCTIONS // 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 } } } // Checks if input is a number, returns -2 if not (to indicate error), and ensures the result is positive (greater than 0) function checkIfNumber(input){ // Turn it into a number let inputAsNumber = parseFloat(input) // If it's not a number, return -2 let isNan = Number.isNaN(inputAsNumber) if (isNan) { return -2 } else { let parameter = sanitizeInput(inputAsNumber, 0, "none") return parameter } } // STRING-RELATED FUNCTIONS // Find the index of a given search term. Choose whether it's case-sensitive or not. function findIndex(searchTerm, stringToSearch, caseSensitive) { if (caseSensitive){ return stringToSearch.indexOf(searchTerm) } else { let uppercaseSearchTerm = searchTerm.toUpperCase() let uppercaseStringToSearch = stringToSearch.toUpperCase() return uppercaseStringToSearch.indexOf(uppercaseSearchTerm) } } // Finds the semicolon after the search term previously searched. Builds in an offset to account for the number of characters in the search term. function findIndexOfSemicolon(stringToSearch, startingAt, offset) { return stringToSearch.indexOf(";", startingAt + offset) } // Finds the period after the search term previously searched. Builds in an offset to account for the number of characters in the search term. function findIndexOfPeriod(stringToSearch, startingAt, offset) { return stringToSearch.indexOf(".", startingAt + offset) } // Creates and trims a substring between the given indices function createSubstring(string, startingIndex, endingIndex) { var untrimmed = string.substring(startingIndex,endingIndex) var trimmed = untrimmed.trim() return trimmed } // Alphabetizes characters from two input strings. function alphabetizeString(inputString1, inputString2) { let str1 = inputString1 let str2 = inputString2 // Combine/concatenate let combinedString = str1.concat(str2) // Split by character, sort alphabetically, join back together. return combinedString.split('').sort().join('').trim() } // If the first character is a colon, remove it if yes function removeFirstColon(input) { if (input.charAt(0) == ":") { let resultRemoved = input.substring(1) return resultRemoved } else { let result = input return result } } // USERNAME-RELATED FUNCTIONS // Check it first character is @ or [, remove if yes. Useful for parsing out usernames from the link to their profile. function checkFirstCharacter(stringToCheck){ // initialize let result = stringToCheck // When grabbing a username string, it might instead grab [@username](URL). I want username only. let indexUrlOpen = findIndex("[@", stringToCheck) let indexUrlMid = findIndex("](", stringToCheck) let indexUrlClose = findIndex(")", stringToCheck) // Check if all of them are found. Initiate Boolean it false, flip it if all indices are not -1 // Also check if they are in the right order. let allFound = false let rightOrder = false if ( (indexUrlOpen != -1) && (indexUrlMid != -1) && (indexUrlClose != -1) ) { allFound = true if ( (indexUrlOpen < indexUrlMid) && (indexUrlMid < indexUrlClose) ) { rightOrder = true } } // If all are found and it's in the right order, trim string so it's just username. if (allFound && rightOrder) { result = createSubstring(stringToCheck, indexUrlOpen + 2, indexUrlMid) return result } else { // Check if the first character is @ if (stringToCheck.charAt(0) == "@") { result = stringToCheck.substring(1) return result } else { return result } } } // Adds "@" to beginning of a username so you can tag them. function appendAtSymbol(username){ // First, check if @ is already first character, remove if yes. let baseUsername = checkFirstCharacter(username) let taggedUsername = "@" + baseUsername return taggedUsername } // Checks if the first character in a username is @, adds if not. function checkNameFormatting(inputString) { var newString = inputString if ( inputString.charAt(0) == "@") { return newString } else { newString = "@" + inputString return newString } } // UUID RELATED FUNCTIONS – Applies to user IDs, Guild IDs, party IDs, etc. // Checks if a UUID is in the correct formatting: 00000000-0000-4000-A000-000000000000 function checkUuidFormat(uuidToCheck){ // Trim, just in case there was extra whitespace let stringTemp = uuidToCheck.trim() // Total length of string should be 36 (Four total dashes and 32 other hexadecimal characters) let length = stringTemp.length if (length != 36) { return false } else { // I'm going to slightly cheat here: if I alphabetize the characters, I can sort out the dashes. let stringTemp2 = stringTemp.split('').sort().join('').trim() // Checks that total number of dashes is only 4, and ensures that the first 4 characters are dashes let dashCount = checkDashes(stringTemp2) if (dashCount != 4) { return false } else { // Remove the first four characters from the string, we already know they are dashes let stringShorter = createSubstring(stringTemp2, 4, length) // Check if the other characters are valid for hexadecimal let isHex = isHexadecimal(stringShorter) if (!isHex){ return false } else { // The character counts are valid, exactly 4 dashes and the rest are hexadecimal (and total lenggth is valid) // The final thing to do is ensure that the string follows the right pattern of hexadecimal and dashes // Parsing the string at the dashes would be the hard way to do it. The easy way is to ensure that the dashes appear at the correct character index let dashLocationsConfirmed = checkDashLocations(stringTemp) // I no longer need the alphabetized string, I need to use the original one if (dashLocationsConfirmed) { return true } else { return false } } } } } // Checks that total number of dashes is only 4, and ensures that the first 4 characters are dashes function checkDashes(stringToCheck){ // First, count them let count = 0 for (let i = 0; i < 36; i++) { if (stringToCheck.charAt(i) == "-" ) { count++ } } // Since the string is alphabetized, I probably don't have to do this, but I will anyway: confirm each of the first four are dashes if (count != 4) { // If more than 4 are dashes, I catch it here return count } else if (count == 4) { for (let j = 0; j < 4; j++) { if (stringToCheck.charAt(j) != "-" ) { // If any of the first 4 characters are not a dash, it fails. End the loop. count = -1 break } } return count } } // Checks if each of the characters are valid for hexadecimal function isHexadecimal(stringToCheck) { regexp = /^[0-9a-fA-F]+$/ if (regexp.test(stringToCheck)) { return true } else { return false } } // Checks that dashes occur at the correct character index locations. Format is 00000000-0000-4000-A000-000000000000 function checkDashLocations(stringToCheck) { let count = 0 // Dashes should be at character index 8, 13, 18, and 23. Jump over the loop at other iterations. for (let i = 0; i < 36; i++) { if ( (i == 8) || (i == 13) || (i == 18) || (i == 23) ) { if (stringToCheck.charAt(i) == "-") { count++ } } else { continue } } if (count == 4) { return true } else { return false } } // VALIDATION/VERIFICATION FUNCTIONS: HASH, CHECKSUM // A good hash function is deterministic, fast, uniformally distributed, and non-invertible. See for more info: https://www.educba.com/javascript-hash/ // I don't need a powerful cryptographic hash like SHA1 or SHA256, that would be overkill for me (not to mention, too slow) function createHash(string) { // Initialize at 0 var hash = 0 // If the length of the string is 0, return 0 if (string.length == 0) { return hash } else { for (var i = 0 ; i 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 } // Calculates how much STR comes from the equipment 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 } // Calculates total INT 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 } // Calculates how much INT comes from the equipment 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 } // Calculates total CON 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 } // Calculates how much CON comes from the equipment 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 } // Calculates total PER 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 } // Calculates how much PER comes from the equipment 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 } // 2B. SKILLS // 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 } } } // Some skills do critical hits function criticalHit (stat, chance) { var targetToBeat = chance * ( (1 + stat ) / 100 ) if (Math.random() <= targetToBeat) { return (1.5 + ( 4 * stat ) / ( stat + 200 ) ) } else { return 1 } } // Some skills require getting task value function getTaskValue(version, presetTaskValue, taskId) { switch (version){ case 2: let value = sanitizeInput(presetTaskValue, 0, 21.27) return value 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 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 } else { // grab value of task ID let valueById = sanitizeInput(valueV4, 0, 21.27) return valueById } } } // BUFF SKILLS // Warrior skill Valorous Presence function valorousPresence(unbuffedStr, buffsStr) { let buffAmount = Math.ceil( ( (unbuffedStr * 20) / (unbuffedStr + 200) ) ) api_updateUser({"stats.buffs.str" : buffsStr + buffAmount}) } // Mage skill Earthquake function earthquake(unbuffedInt, buffsInt) { var buffAmount = Math.ceil( ( (unbuffedInt * 30) / (unbuffedInt + 200) ) ) api_updateUser({"stats.buffs.int" : buffsInt + buffAmount}) } // Healer skill Protective Aura function protectiveAura(unbuffedCon, buffsCon) { var buffAmount = Math.ceil( ( (unbuffedCon * 200) / (unbuffedCon + 200) ) ) api_updateUser({"stats.buffs.con" : buffsCon + buffAmount}) } // Rogue skill Tools of the Trade function toolsOfTheTrade(unbuffedPer, buffsPer) { var buffAmount = Math.ceil( ( (unbuffedPer * 100) / (unbuffedPer + 50) ) ) api_updateUser({"stats.buffs.per" : buffsPer + buffAmount}) } // WARRIOR SKILLS (EXCEPT BUFFS AND BOSS DAMAGE) // All the Warrior skills are buff or boss damage // MAGE SKILLS (EXCEPT BUFFS AND BOSS DAMAGE) // Mage skill Chilling Frost function chillingFrost(mp, mpCost) { api_updateUser({"stats.buffs.streaks" : true, "stats.mp" : mp - mpCost}) } // Mage skill Burst of Flames (XP portion only, not boss damage) function burstOfFlamesXp(int, per, nTimes) { var taskValue = getTaskValue(BURST_OF_FLAMES_VERSION, BURST_OF_FLAMES_TASK_VALUE, BURST_OF_FLAMES_TASK_ID) var multiplier = 1 var modifiedInt = int var bonus = 1 var newXpRunningTotal = 0 for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(per, 0.03) modifiedInt = int * multiplier var bonus = Math.ceil( (taskValue + 1) * (modifiedInt * 0.075 ) ) newXpRunningTotal += ( ( 75 * bonus ) / ( bonus + 37.5) ) } return newXpRunningTotal } // HEALER SKILLS (EXCEPT BUFFS AND BOSS DAMAGE) // Healer skill Healing Light function healingLight (hp, con, int) { var healing = ( ( con + int + 5 ) * 0.075 ) if (hp + healing >= 50) { api_updateUser({"stats.hp" : 50}) } else { api_updateUser({"stats.hp" : hp + healing}) } } // ROGUE SKILLS (EXCEPT BUFFS AND BOSS DAMAGE) // Rogue skill Pickpocket function pickpocket (gp, per) { var taskValue = getTaskValue(PICKPOCKET_VERSION, PICKPOCKET_TASK_VALUE, PICKPOCKET_TASK_ID) var bonus = ( (taskValue + 1) + (per * 0.5 ) ) var newGp = gp + ( ( 25 * bonus ) / ( bonus + 75) ) api_updateUser({"stats.gp" : newGp}) } // Rogue skill Backstab function backstab (gp, xp, str) { var multiplier = criticalHit(str, 0.3) var modifiedStr = str * multiplier var taskValue = getTaskValue(BACKSTAB_VERSION, BACKSTAB_TASK_VALUE, BACKSTAB_TASK_ID) var bonus = ( (taskValue + 1) + (modifiedStr * 0.5 ) ) var newXp = xp + ( ( 75 * bonus ) / ( bonus + 50) ) var newGp = gp + ( ( 18 * bonus ) / ( bonus + 75) ) api_updateUser({"stats.gp" : newGp, "stats.exp" : newXp}) } // Rogue skill Stealth function stealth (totalDailies, prevStealthedDailies, per) { var 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}) } } // BOSS DAMAGE SKILLS (INCLUDING NEW SKILLS FOR HEALER AND ROGUE) // 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) { var smashRunningTotal = 0 var multiplier = 1 var modifiedStr = str for ( var i = 1; i <= nTimes; i++) { multiplier = criticalHit(con, 0.03) modifiedStr = str * multiplier smashRunningTotal += (55*modifiedStr)/(modifiedStr+70) } return smashRunningTotal } // Any multi-use of a boss damage skill requires checking if you have enough MP for the multiple uses // 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) { var numberTimes = Math.ceil(90.71/damageOnce) var damageMulti = damageOnce * numberTimes var 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) var 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 } } // 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) { var radiantRunningTotal = 0 var multiplier = 1 var 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 insurers 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) { var sneakRunningTotal = 0 var multiplier = 1 var 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) { var 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. var 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 var 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) { var midpoint = Math.floor(((low+high)/2)) // damage at each setpoint var damageLow = damageAtSetpoint(low) var damageMidpoint = damageAtSetpoint(midpoint) var damageHigh = damageAtSetpoint(high) // truth conditions regarding damage and where it falls (compared to the setpoints) var targetAboveLow = (damageLow < targetDamage) ? true : false var targetAboveMidpoint = (damageMidpoint < targetDamage) ? true : false var 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)) ) } // 2X. DEBUG FUNCTIONS – SCRIPT DOES NOT TRIGGER THEM, THEY MUST BE TRIGGERED MANUALLY // RESET USAGE COUNTERS // Functions below can reset all counters or from one script. // To reset one counter only, instead use "debugManuallyEditSavedValue". // Also includes a function to reset saved HP/level (for Partial Healing) and duel values. // Does not include toggling into/out of Game Modes (for those, see main body of code for "function toggle[scriptNameMode]") // Manually reset all usage counters. function debugResetCountersAll() { resetCounters() } function debugResetCountersXpAndMpPotion() { scriptProperties.setProperty(XP_USAGE_KEY, 0) scriptProperties.setProperty(MP_USAGE_KEY, 0) } function debugResetCountersAntiPotions(){ scriptProperties.setProperty(ANTI_MP_USAGE_KEY, 0) scriptProperties.setProperty(ANTI_HP_USAGE_KEY, 0) scriptProperties.setProperty(ANTI_XP_USAGE_KEY, 0) } function debugResetCountersHpOrXpForMp(){ scriptProperties.setProperty(HP_FOR_MP_KEY, 0) scriptProperties.setProperty(XP_FOR_MP_KEY, 0) } // Resets saved duel values function debugResetDuelValues(){ resetDuelValues() } // MANUALLY ENTER A MODE // PARTIAL HEALING // In case the Partial Healing script saved the wrong value for HP and level, use FCV to set them to the correct amounts and then run the function below to save them function debugResetSavedHpAndLevel() { const responseInit = api_getAuthenticatedUserProfile("stats") user = JSON.parse(responseInit).data var currentHp = user.stats.hp var currentLvl = user.stats.lvl scriptProperties.setProperty(HP_KEY, currentHp) scriptProperties.setProperty(LVL_KEY, currentLvl) } // RETRIEVE SAVED COUNTERS/VALUES // In case you want to know what values the script saved. // Functions below can retrieve all counters or from one script. // Duel saved values/hashes and Partial Healing (HP and level) are always a separate function because they are so different from the usage counters. // Retrieves saved counters and sends them as a private message function debugGetSavedCountersAll(){ let cronCount = Number(scriptProperties.getProperty(CRON_COUNT_KEY)) let xpUsage = Number(scriptProperties.getProperty(XP_USAGE_KEY)) let mpUsage = Number(scriptProperties.getProperty(MP_USAGE_KEY)) let AntiMpUsage = Number(scriptProperties.getProperty(ANTI_MP_USAGE_KEY)) let AntiHpUsage = Number(scriptProperties.getProperty(ANTI_HP_USAGE_KEY)) let AntiXpUsage = Number(scriptProperties.getProperty(ANTI_XP_USAGE_KEY)) let HpForMpUsage = Number(scriptProperties.getProperty(HP_FOR_MP_KEY)) let XpForMpUsage = Number(scriptProperties.getProperty(XP_FOR_MP_KEY)) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: cronCount is `" + cronCount + MSG_JOINER_AFTER + "xpUsage" + MSG_JOINER_BEFORE + xpUsage + MSG_JOINER_AFTER + "mpUsage" + MSG_JOINER_BEFORE + mpUsage + "AntiMpUsage" + MSG_JOINER_BEFORE + AntiMpUsage + MSG_JOINER_AFTER + "AntiHpUsage" + MSG_JOINER_BEFORE + AntiHpUsage + MSG_JOINER_AFTER + "AntiXpUsage" + MSG_JOINER_BEFORE + AntiXpUsage + MSG_JOINER_AFTER + "HpForMpUsage" + MSG_JOINER_BEFORE + HpForMpUsage + MSG_JOINER_AFTER + "XpForMpUsage" + MSG_JOINER_BEFORE + XpForMpUsage + "`", "toUserId" : USER_ID}) } function debugGetSavedCountersXpAndMpPotion(){ let cronCount = Number(scriptProperties.getProperty(CRON_COUNT_KEY)) let xpUsage = Number(scriptProperties.getProperty(XP_USAGE_KEY)) let mpUsage = Number(scriptProperties.getProperty(MP_USAGE_KEY)) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: cronCount is `" + cronCount + MSG_JOINER_AFTER + "xpUsage" + MSG_JOINER_BEFORE + xpUsage + MSG_JOINER_AFTER + "mpUsage" + MSG_JOINER_BEFORE + mpUsage + "`", "toUserId" : USER_ID}) } function debugGetSavedCountersAntiPotions(){ let cronCount = Number(scriptProperties.getProperty(CRON_COUNT_KEY)) let AntiMpUsage = Number(scriptProperties.getProperty(ANTI_MP_USAGE_KEY)) let AntiHpUsage = Number(scriptProperties.getProperty(ANTI_HP_USAGE_KEY)) let AntiXpUsage = Number(scriptProperties.getProperty(ANTI_XP_USAGE_KEY)) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: cronCount is `" + cronCount + MSG_JOINER_AFTER + "AntiMpUsage" + MSG_JOINER_BEFORE + AntiMpUsage + MSG_JOINER_AFTER + "AntiHpUsage" + MSG_JOINER_BEFORE + AntiHpUsage + MSG_JOINER_AFTER + "AntiXpUsage" + MSG_JOINER_BEFORE + AntiXpUsage + "`", "toUserId" : USER_ID}) } function debugGetSavedCountersHpOrXpForMp(){ let cronCount = Number(scriptProperties.getProperty(CRON_COUNT_KEY)) let HpForMpUsage = Number(scriptProperties.getProperty(HP_FOR_MP_KEY)) let XpForMpUsage = Number(scriptProperties.getProperty(XP_FOR_MP_KEY)) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: cronCount is `" + cronCount + MSG_JOINER_AFTER + "HpForMpUsage" + MSG_JOINER_BEFORE + HpForMpUsage + MSG_JOINER_AFTER + "XpForMpUsage" + MSG_JOINER_BEFORE + XpForMpUsage + "`", "toUserId" : USER_ID}) } // Retrieves saved values and sends them in a private message function debugGetSavedHpAndLevel(){ let savedHp = Number(scriptProperties.getProperty(HP_KEY)) let savedLvl = scriptProperties.getProperty(LVL_KEY) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: savedHp is `" + savedHp + MSG_JOINER_AFTER + "savedLvl" + MSG_JOINER_BEFORE + savedLvl + "`", "toUserId" : USER_ID}) } // Retrieves saved duel values and sends them as a private message function debugGetSavedDuelValues(){ let duelCurrentlyActive = Number(scriptProperties.getProperty(DUEL_CURRENTLY_ACTIVE_KEY)) let duelScore = Number(scriptProperties.getProperty(DUEL_SCORE_KEY)) let selfUsername = scriptProperties.getProperty(SELF_USERNAME_KEY) let opponentUsername = scriptProperties.getProperty(OPPONENT_USERNAME_KEY) let wager = Number(scriptProperties.getProperty(WAGER_KEY)) let scoreNeededGoal = Number(scriptProperties.getProperty(SCORE_NEEDED_GOAL_KEY)) let timestampIdentifier = Number(scriptProperties.getProperty(TIMESTAMP_IDENTIFIER_KEY)) let groupId = scriptProperties.getProperty(GROUP_ID_KEY) let announcerBotUsername = scriptProperties.getProperty(ANNOUNCER_BOT_USERNAME_KEY) let troubleshootingFlag = Number(scriptProperties.getProperty(TROUBLESHOOTING_KEY)) // saving these will make my life easier let MSG_JOINER_BEFORE = " is `" let MSG_JOINER_AFTER = "`, " api_sendPrivateMessageAlways({"message" : "Saved values are as follows: duelCurrentlyActive is `" + duelCurrentlyActive + MSG_JOINER_AFTER + "duelScore" + MSG_JOINER_BEFORE + duelScore + MSG_JOINER_AFTER + "selfUsername" + MSG_JOINER_BEFORE + selfUsername + MSG_JOINER_AFTER + "opponentUsername" + MSG_JOINER_BEFORE + opponentUsername + MSG_JOINER_AFTER + "wager" + MSG_JOINER_BEFORE + wager + MSG_JOINER_AFTER + "scoreNeededGoal" + MSG_JOINER_BEFORE + scoreNeededGoal + MSG_JOINER_AFTER + "timestampIdentifier" + MSG_JOINER_BEFORE + timestampIdentifier + MSG_JOINER_AFTER + "groupId" + MSG_JOINER_BEFORE + groupId + MSG_JOINER_AFTER + "announcerBotUsername" + MSG_JOINER_BEFORE + announcerBotUsername + MSG_JOINER_AFTER + "troubleshootingFlag" + MSG_JOINER_BEFORE + troubleshootingFlag + "`", "toUserId" : USER_ID}) } // Retrieves expected hashes for duel parameter and user ID and sum function debugGetExpectedDuelHashes(){ let hashAllExpected = createDuelHash(SELF_USERNAME_KEY, OPPONENT_USERNAME_KEY, WAGER_KEY, SCORE_NEEDED_GOAL_KEY, TIMESTAMP_IDENTIFIER_KEY) let hashSelfExpected = createHash(USER_ID) let hashSumExpected = hashAllExpected + hashSelfExpected api_sendPrivateMessageAlways({"message" : "Hashes are " + hashAllExpected + ", " + hashSelfExpected + ", " + hashSumExpected, "toUserId" : USER_ID}) } // MANUALLY EDIT SAVED VALUES // The first function lets you edit whichever saved value you want. // The functions below it are for commonly-needed values to edit. // Mannually edits and saves the selected value (in case it saved incorrectly) function debugManuallyEditSavedValue() { // Fill them in below. xpUsage is shown as a sample. let newValue = 0 let key = XP_USAGE_KEY // Save scriptProperties.setProperty(key, newValue) } // If duelCurrentlyActive is stuck at 1, this function resets it to 0 function debugResetFlagDuelCurrentlyActive() { var duelCurrentlyActiveKey = DUEL_CURRENTLY_ACTIVE_KEY var duelCurrentlyActive = Number(scriptProperties.getProperty(duelCurrentlyActiveKey)) duelCurrentlyActive = 0 scriptProperties.setProperty(duelCurrentlyActiveKey, duelCurrentlyActive) } // Clears troubleshooting flag and therefore allows this script to post automated messages to the party // A value of 0 allows automated messages to be posted. 1 and 2 to not. 1 indicates an error, 2 indicates valid/victory function debugClearTroubleshootingFlag(){ scriptProperties.setProperty(TROUBLESHOOTING_KEY, 0) } // Clears troubleshooting flag and therefore allows this script to post automated messages to the party. // Reposts the automated message. Runs "victory condition" function again to see if new post is now valid. function debugClearTroubleshootingFlagAndThenRetry(){ scriptProperties.setProperty(TROUBLESHOOTING_KEY, 0) // Now, run (most of) incrementDuelScore. Since there's no actual task, assign it a difficulty of 0 and a delta of 0 // Running this function will show that the user won, and therefore post the automated message again increaseDuelScore(DUEL_SCORE_KEY, SCORE_NEEDED_GOAL_KEY, GROUP_ID_KEY, SELF_USERNAME_KEY, OPPONENT_USERNAME_KEY, WAGER_KEY, ANNOUNCER_BOT_USERNAME_KEY, TIMESTAMP_IDENTIFIER_KEY, 0, 0, "habit") } // 3. IDIOSYNCRATIC FUNCTIONS // If a script has no idiosyncratic functions (i.e. all its functions are accounted for in the above categories), that script will not be listed in this section. // 3A. EDITABLE FIX CHARACTER VALUES BUTTONS // does parseAndEvaluate function for all four stat values function multParseAndEvaluate(hp, xp, mp, gp, inputString) { // initialize these at their existing values let newHp = hp let newXp = xp let newMp = mp let newGp = gp // Compute total Intelligence const responseContent = api_getAllAvailableContentObjects() content = JSON.parse(responseContent).data let int = calculateIntelligence() let maxMp = (2 * int) + 30 newHp = sanitizeInput(parseAndEvaluate(inputString, "HP:", hp), 0, 50) newXp = sanitizeInput(parseAndEvaluate(inputString, "XP:", xp), 0, "none") newMp = sanitizeInput(parseAndEvaluate(inputString, "MP:", mp), 0, maxMp) newGp = sanitizeInput(parseAndEvaluate(inputString, "GP:", gp), 0, "none") // check if an error happened; 0.184937265 is the value I randomly generated that I'm using to check errors, it's unlikely a user will input it. if ( ( newHp == 0.184937265 ) || ( newXp == 0.184937265 ) || ( newMp == 0.184937265 ) || ( newGp == 0.184937265 ) ) { // do nothing, the error message was already sent } // check if there was insufficient stat(s) to pay the cost; 0.562739481 is the value I'm using to check it, it's unlikely a user will input it. else if ( ( newHp == 0.562739481 ) || ( newXp == 0.562739481 ) || ( newMp == 0.562739481 ) || ( newGp == 0.562739481 ) ) { // send error message based on which value is insufficient, starting with "missing all 4" if ( ( newHp == 0.562739481 ) && ( newXp == 0.562739481 ) && ( newMp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_4 + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } // now try scenarios where 3 stats are missing else if ( ( newXp == 0.562739481 ) && ( newMp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_3H + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newHp == 0.562739481 ) && ( newMp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_3X + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newHp == 0.562739481 ) && ( newXp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_3M + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newHp == 0.562739481 ) && ( newXp == 0.562739481 ) && ( newMp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_3G + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } // now try scenarios where 2 stats are missing else if ( ( newHp == 0.562739481 ) && ( newXp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2HX + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newHp == 0.562739481 ) && ( newMp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2HM + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newHp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2HG + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newXp == 0.562739481 ) && ( newMp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2XM + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newXp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2XG + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newMp == 0.562739481 ) && ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_2MG + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } // finally try scenarios missing only 1 stat else if ( ( newHp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_1H + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newXp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_1X + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newMp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_1M + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } else if ( ( newGp == 0.562739481 ) ) { api_sendPrivateMessageAlways({"message" : INSUFFICIENT_COST_MSG_START + INSUFFICIENT_COST_MSG_VARIABLE_1G + INSUFFICIENT_COST_MSG_END, "toUserId" : USER_ID}) } } else { // update all four stats with a single POST call api_updateUser({"stats.hp" : newHp, "stats.exp" : newXp, "stats.mp" : newMp, "stats.gp" : newGp}) } } // checks input string for the search term and returns the new stat value function parseAndEvaluate(inputString, searchTerm, statValue){ // Find if the search term exists in the string, and if yes, where it starts let index = findIndex(searchTerm, inputString, true) // If search term isn't found, stat value stays the same if ( index == -1 ) { return statValue } else { // Find where the semicolon is in the string (following the search term) let indexSemicolon = findIndexOfSemicolon(inputString, index, 3) // Offset is 3 to account for the search term if ( indexSemicolon == -1 ) { api_sendPrivateMessageAlways({"message" : SEMICOLON_ERROR_MSG_START + searchTerm + SEMICOLON_ERROR_MSG_END, "toUserId" : USER_ID}) return 0.184937265 // this signals to the script that there was an error } // if the two indices are too far apart, the semicolon is likely following the next search term, not the correct one. else if ( ( indexSemicolon - index ) >= 15 ) { api_sendPrivateMessageAlways({"message" : SEMICOLON_ERROR_MSG_START + searchTerm + SEMICOLON_ERROR_MSG_END, "toUserId" : USER_ID}) return 0.184937265 // this signals to the script that there was an error } else { // Create a substring of the characters between the search term and the semicolon, and trim whitespace var substring = createSubstring(inputString, index + 3, indexSemicolon) // Grab direction (up or down) and whether the number is a percent let direction = findDirection(substring, searchTerm) let isPercent = findPercent(substring) if ( direction == "error" ) { return 0.184937265 // this signals to the script that there was an error } // grab the smaller substring of just the numbers, which depends on if there's a percent sign at the end var smallerSubstring = "" let substringLength = substring.length if (isPercent) { smallerSubstring = createSubstring(substring, 1, substringLength - 1) } else { smallerSubstring = createSubstring(substring, 1, substringLength) } // turn it into a number let smallerSubstringAsNumber = parseFloat(smallerSubstring) // If it's not a number, return an error let isNan = Number.isNaN(smallerSubstringAsNumber) if (isNan) { api_sendPrivateMessageAlways({"message" : NUMBER_AFTER_DIRECTION_ERROR_MSG, "toUserId" : USER_ID}) return 0.184937265 // this signals to the script that there was an error } // If it is a number, proceed as normal else { if (isPercent) { // turn the raw number into a percent let percentFromNotes = (smallerSubstringAsNumber / 100) if ( direction == "up") { // multiply the percent times the stat value it's going to be the percent of let percentOfStatValue = statValue * percentFromNotes return (statValue + percentOfStatValue) } else if (direction == "down") { // we must sanitize entries that are beyond 0-100 since we don't want to subtract greater than 100% of a value; this didn't need to happen for positive, just for negative let sanitizedPercent = sanitizeInput(percentFromNotes, 0, 100) // multiply the sanitized percent times the stat value it's going to be the percent of let sanitizedPercentOfStatValue = statValue * sanitizedPercent return (statValue - sanitizedPercentOfStatValue) } else { // it shouldn't be possible for this "else" condition to happen, but just in case... api_sendPrivateMessageAlways({"message" : MISSING_DIRECTION_ERROR_MSG_START + searchTerm + MISSING_DIRECTION_ERROR_MSG_END, "toUserId" : USER_ID}) return 0.184937265 // this signals to the script that there was an error } } else { if ( direction == "up") { return ( statValue + smallerSubstringAsNumber ) } else if (direction == "down") { // check if stat value is sufficient to pay the cost if ( ( statValue - smallerSubstringAsNumber ) < 0 ) { return 0.562739481 // this is the error-checking amount for insufficient stat to pay the cost } else { return ( statValue - smallerSubstringAsNumber ) } } else { // it shouldn't be possible for this "else" condition to happen, but just in case... api_sendPrivateMessageAlways({"message" : MISSING_DIRECTION_ERROR_MSG_START + searchTerm + MISSING_DIRECTION_ERROR_MSG_END, "toUserId" : USER_ID}) return 0.184937265 // this signals to the script that there was an error } } } } } } function findDirection(trimmedSubstring, searchTerm) { if ( trimmedSubstring.charAt(0) == "+") { return "up" } else if ( trimmedSubstring.charAt(0) == "-") { return "down" } else { api_sendPrivateMessageAlways({"message" : MISSING_DIRECTION_ERROR_MSG_START + searchTerm + MISSING_DIRECTION_ERROR_MSG_END, "toUserId" : USER_ID}) return "error" } } function findPercent(trimmedSubstring) { let length = trimmedSubstring.length let isPercent = false if ( trimmedSubstring.charAt(length - 1) == "%" ) { isPercent = true } return isPercent } // 3B. DUELING SCRIPT – SCORING // Check if there's supposed to be a tag for the task, add if yes. Create tag if needed. function checkAndAddTagToTask(taskIdOrAlias, optionalSecondTaskIdOrAlias){ if (TAG_FOR_DUEL_BUTTONS != "typeTagHere"){ var tagId = apiMult_createNewTagNoDuplicates(TAG_FOR_DUEL_BUTTONS) // If the function created the tag anew, it did not grab its ID yet. Do so now. if (tagId == "newlyCreated") { tagId = apiMult_createNewTagNoDuplicates(TAG_FOR_DUEL_BUTTONS) } // Add the tag to the task(s) api_addTagToTask(taskIdOrAlias, tagId) if (optionalSecondTaskIdOrAlias != "none"){ api_addTagToTask(optionalSecondTaskIdOrAlias, tagId) } } } // Finds if the automated message was posted or not function findAutomatedDuelEndMessages(version, groupId, msgIdEarliestAllowed, earliestAllowedTimestamp, announcerBotUsername, searchTerm){ // Initialize so it defaults to the error condition let msgId = "nope" // Grab messages from party let response = api_getChatMessagesFromGroup(groupId) let chatMessages = JSON.parse(response).data let earliestTimestamp = earliestAllowedTimestamp // For "bot" version, I need to find timestamp of the user's automated chat message if (version == "bot") { for (var i in chatMessages) { if (chatMessages[i]._id == msgIdEarliestAllowed) { earliestAllowedTimestamp = chatMessages[i].timestamp } } } // Look for messages posted at or after the earliest allowed timestamp for (var i in chatMessages) { if (chatMessages[i].timestamp >= earliestAllowedTimestamp) { // This version is to look for the automated message from the user if (version == "user") { // Look for messages the user posted if (chatMessages[i].uuid == USER_ID) { let text = chatMessages[i].text // See if both search terms are found (automated message and bot username) let autoMsgIndex = findIndex(searchTerm, text) let botIndex = findIndex(announcerBotUsername, text) if ( (botIndex != -1) && (autoMsgIndex != -1) ) { msgId = chatMessages[i]._id } } } else if (version == "bot") { // For this one, I need to remove the @ symbol from announcer bot username let untaggedAnnouncerBotUsername = checkFirstCharacter(announcerBotUsername) let text = chatMessages[i].text // Look for messages the bot posted if (chatMessages[i].username == untaggedAnnouncerBotUsername) { // See if search term is found let botMsgIndex = findIndex(searchTerm, text) if (botMsgIndex != -1) { msgId = chatMessages[i]._id } } } } } return msgId } // Creates Duel End string for duel bot to check/evaluate function createDuelEndString(selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, announcerBotUsernameKey, timestampIdentifierKey){ // Retrieve saved values var selfUsername = scriptProperties.getProperty(selfUsernameKey) var opponentUsername = scriptProperties.getProperty(opponentUsernameKey) var wager = Number(scriptProperties.getProperty(wagerKey)) var scoreNeededGoal = Number(scriptProperties.getProperty(scoreNeededGoalKey)) var announcerBotUsername = scriptProperties.getProperty(announcerBotUsernameKey) var timestampIdentifier = Number(scriptProperties.getProperty(timestampIdentifierKey)) // Combine self&opponent usernames, then sort characters alphabetically. // This will ensure it's the same regardless of which participant is "self" and which is "opponent" let alphabeticalCombinedUsernames = alphabetizeString(selfUsername, opponentUsername) // Create string of all duel parameters. let SEMICOLON = ";"; let ALL_PARAMETERS = alphabeticalCombinedUsernames + SEMICOLON + wager + SEMICOLON + scoreNeededGoal + SEMICOLON + timestampIdentifier + SEMICOLON // Create a string of both the usernames. Ends in a period for easier parsing. let bothUsernames = selfUsername + SEMICOLON + opponentUsername + SEMICOLON + "." // Create hashes let hashAll = createHash(ALL_PARAMETERS) let hashSelfUserId = createHash(USER_ID) // for indicating who won let hashSum = hashAll + hashSelfUserId // So I don't directly expose the hash of the user ID (mostly for anti-cheating purposes) // Create string of hashes. Ends in a period for easier parsing. let HASHES = hashAll + SEMICOLON + hashSum + SEMICOLON + "." // Generate victory string. Post message to party and tag bot. Bot will be the one to declare victory (after checking if the string is valid) let DUEL_END_MSG = announcerBotUsername + "\nEND DUEL\n." + ALL_PARAMETERS + "." + bothUsernames + HASHES return DUEL_END_MSG } // Checks if string is valid, returns error messages if not. Same with GP for wager. If valid, saves all duel values and starts duel. function parseDuelStartString(inputString, buttonToDeleteTaskId, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, groupIdKey, announcerBotUsernameKey, timestampIdentifierKey) { // Count number of semicolons. 7 are expected. var countSemicolons = ( inputString.match(/\;/g) || [] ).length; // Send error message if there aren't 7 semicolons. Otherwise, parse string. if ( countSemicolons != 7 ) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SEMICOLONS_START + countSemicolons + ERROR_MSG_SEMICOLONS_END, "toUserId" : USER_ID}) } else if ( countSemicolons == 7 ) { // Initialize some values so I can use them later let length = inputString.length let semicolonIndex = 0 // Use first semicolon to parse out self username semicolonIndex = findIndexOfSemicolon(inputString) let selfUsernameTemp = createSubstring(inputString, 0, semicolonIndex) // Check if first character is @. If not, add it. let selfUsername = checkNameFormatting(selfUsernameTemp) // Remove that section from the input string, save as a smaller substring let inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out opponent username semicolonIndex = findIndexOfSemicolon(inputString) let opponentUsernameTemp = createSubstring(inputString, 3, semicolonIndex) // Start at index 3 because of "vs." // Check if first character is @. If not, add it. let opponentUsername = checkNameFormatting(opponentUsernameTemp) // Remove that section from the input string, save as a smaller substring inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out wager semicolonIndex = findIndexOfSemicolon(inputString); let wagerTemp = createSubstring(inputString, 0, semicolonIndex); // If it's not a number, the function will return -2 let wager = checkIfNumber(wagerTemp) // I'll keep parsing the substring in case there are other errors. // Remove that section from the input string, save as a smaller substring inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out Score Needed / Goal semicolonIndex = findIndexOfSemicolon(inputString) let scoreNeededGoalTemp = createSubstring(inputString, 0, semicolonIndex) // If it's not a number, the function will return -2 let scoreNeededGoal = checkIfNumber(scoreNeededGoalTemp) // I'll keep parsing the substring in case there are other errors. // Remove that section from the input string, save as a smaller substring inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out Timestamp identifier semicolonIndex = findIndexOfSemicolon(inputString) let timestampIdentifierTemp = createSubstring(inputString, 0, semicolonIndex) // If it's not a number, the function will return -2 let timestampIdentifier = checkIfNumber(timestampIdentifierTemp) // Remove that section from the input string, save as a smaller substring inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out party ID semicolonIndex = findIndexOfSemicolon(inputString) let groupId = createSubstring(inputString, 0, semicolonIndex) // Check to make sure it's a valid format for a party ID let groupIdFormatCheck = checkUuidFormat(groupId) // Remove that section from the input string, save as a smaller substring inputStringTemp = createSubstring(inputString, semicolonIndex + 1, length) inputString = inputStringTemp length = inputString.length // Repeat to parse out Bot Announcer ID semicolonIndex = findIndexOfSemicolon(inputString) let announcerBotUsernameTemp = createSubstring(inputString, 0, semicolonIndex) // Check if first character is @. If not, add it. let announcerBotUsername = checkNameFormatting(announcerBotUsernameTemp) // If there are any errors, let user know by sending messages. let errorsNotPresent = checkDuelStartErrors(wager, scoreNeededGoal, timestampIdentifier, groupIdFormatCheck) // If no errors, check GP and start duel if (errorsNotPresent) { checkGpAndStartDuel(buttonToDeleteTaskId, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, selfUsername, opponentUsernameKey, opponentUsername, wagerKey, wager, scoreNeededGoalKey, scoreNeededGoal, groupIdKey, groupId, announcerBotUsernameKey, announcerBotUsername, timestampIdentifierKey, timestampIdentifier) } } } // If errors exist in Duel Start string, sends messages. function checkDuelStartErrors(wager, scoreNeededGoal, timestampIdentifier, groupIdFormatCheck) { // Initialize Booleans let wagerIsError = false let scoreNeededGoalIsError = false let timestampIdentifierIsError = false let groupIdError = false let errorCount = 0 if (wager == -2) { wagerIsError = true errorCount++ } if (scoreNeededGoal == -2) { scoreNeededGoalIsError = true errorCount++ } if (timestampIdentifier == -2) { timestampIdentifierIsError = true errorCount++ } if (!groupIdFormatCheck) { // This one was already a Boolean groupIdError = true errorCount++ } if (errorCount == 0) { return true // Indicates no errors } else { // Message differs if there's one error or multiple. if ( errorCount == 1 ) { // Message tells user location of error if (wagerIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SINGULAR_START + ERROR_MSG_WAGER + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } else if (scoreNeededGoalIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SINGULAR_START + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } else if (timestampIdentifierIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SINGULAR_START + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } else if (groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SINGULAR_START + ERROR_MSG_GROUP_ID + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_SINGULAR_START + "UNKNOWN" + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } } else { // If 4 errors, all 4 locations are the cause. if ( errorCount == 4 ) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if ( errorCount == 3 ) { // If three errors, figure out which one is not an error if (!wagerIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (!scoreNeededGoalIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (!timestampIdentifierIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (!groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + "UNKNOWN" + ERROR_MSG_SINGULAR_END, "toUserId" : USER_ID}) } } else if ( errorCount == 2 ) { // If two errors, figure out which two if (wagerIsError) { // This section is for the errors being Wager and something else if (scoreNeededGoalIsError){ api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (timestampIdentifierIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_WAGER + ERROR_MSG_AND + "UNKNOWN" + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } } else if (scoreNeededGoalIsError) { // This section is for the errors being Score Needed Goal and something else if (timestampIdentifierIsError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else if (groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_SCORE_NEEDED + ERROR_MSG_AND + "UNKNOWN" + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } } else if (timestampIdentifierIsError) { // This section is for the errors being Timestamp Identifier and something else if (groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_AND + ERROR_MSG_GROUP_ID + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_TIMESTAMP_IDENTIFIER + ERROR_MSG_AND + "UNKNOWN" + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } } else if (groupIdError) { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + ERROR_MSG_GROUP_ID + ERROR_MSG_AND + "UNKNOWN" + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } } else { api_sendPrivateMessageAlways({"message" : ERROR_MSG_PLURAL_START + errorCount + ERROR_MSG_PLURAL_MID + "UNKNOWN" + ERROR_MSG_PLURAL_END, "toUserId" : USER_ID}) } } return false // Indicates errors } } // Checks GP and starts duel function checkGpAndStartDuel(buttonToDeleteTaskId, duelCurrentlyActiveKey, duelScoreKey, selfUsernameKey, selfUsername, opponentUsernameKey, opponentUsername, wagerKey, wager, scoreNeededGoalKey, scoreNeededGoal, groupIdKey, groupId, announcerBotUsernameKey, announcerBotUsername, timestampIdentifierKey, timestampIdentifier){ const responseUser = api_getAuthenticatedUserProfile("stats") const user = JSON.parse(responseUser).data let gp = user.stats.gp // If not enough GP, send error message. If enough, deduct wager and start duel. if ( gp < wager ) { api_sendPrivateMessageAlways({"message" : MSG_INSUFFICIENT_GP, "toUserId" : USER_ID}) } else { api_updateUser({"stats.gp" : gp - wager}) // Start duel let duelCurrentlyActive = 1 let duelScore = 0 // Save values scriptProperties.setProperty(duelCurrentlyActiveKey, duelCurrentlyActive) scriptProperties.setProperty(duelScoreKey, duelScore) scriptProperties.setProperty(selfUsernameKey, selfUsername) scriptProperties.setProperty(opponentUsernameKey, opponentUsername) scriptProperties.setProperty(wagerKey, wager) scriptProperties.setProperty(scoreNeededGoalKey, scoreNeededGoal) scriptProperties.setProperty(groupIdKey, groupId) scriptProperties.setProperty(announcerBotUsernameKey, announcerBotUsername) scriptProperties.setProperty(timestampIdentifierKey, timestampIdentifier) api_sendPrivateMessage({"message" : "Your duel against " + opponentUsername + " begins now!", "toUserId" : USER_ID}) // Create the other two buttons (Duel Score and Duel End) api_createNewTaskForUser([DUEL_END_BUTTON, DUEL_SCORE_BUTTON]) // If relevant, add tag to task (i.e. only if the user created a tag for these buttons) checkAndAddTagToTask(DUEL_SCORE_ALIAS, DUEL_END_ALIAS) // Delete the Duel Start button api_deleteTask(buttonToDeleteTaskId) } } // Creates hash of the duel parameters function createDuelHash(selfUsernameKey, opponentUsernameKey, wagerKey, scoreNeededGoalKey, timestampIdentifierKey) { // Retrieve saved values var selfUsername = scriptProperties.getProperty(selfUsernameKey) var opponentUsername = scriptProperties.getProperty(opponentUsernameKey) var wager = Number(scriptProperties.getProperty(wagerKey)) var scoreNeededGoal = Number(scriptProperties.getProperty(scoreNeededGoalKey)) var timestampIdentifier = Number(scriptProperties.getProperty(timestampIdentifierKey)) // Combine self&opponent usernames, then sort characters alphabetically. // This will ensure it's the same regardless of which participant is "self" and which is "opponent" let alphabeticalCombinedUsernames = alphabetizeString(selfUsername, opponentUsername) // Create string of all duel parameters. let SEMICOLON = ";" let ALL_PARAMETERS = alphabeticalCombinedUsernames + SEMICOLON + wager + SEMICOLON + scoreNeededGoal + SEMICOLON + timestampIdentifier + SEMICOLON // Create hash let hashAll = createHash(ALL_PARAMETERS) return hashAll } // Parses the Duel End string received from the announcer bot script. Check if valid. Declare winner. function parseDuelEndString(string, hashAllExpected, hashSumIfWinner){ // Count number of semicolons (2 are expected). let countSemicolons = ( string.match(/\;/g) || [] ).length if (countSemicolons != 2) { return -2 // To indicate it's an invalid string } else { // Parse out first hash let semicolonIndex = findIndexOfSemicolon(string) let hashAllTemp = createSubstring(string, 0, semicolonIndex) let hashAllParsed = parseFloat(hashAllTemp) // Parse out second hash let length = string.length let substringTemp = createSubstring(string, semicolonIndex + 1, length) semicolonIndex = findIndexOfSemicolon(substringTemp) let hashSumTemp = createSubstring(substringTemp, 0, semicolonIndex) let hashSumParsed = parseFloat(hashSumTemp) // Compare the first hash to what's expected (given the duel parameters) if (hashAllParsed != hashAllExpected) { return -2 // To indicate it's an invalid string, the hashes don't match } else { // If it's a valid string, check if winner if (hashSumParsed == hashSumIfWinner) { return 5 // To indicate winner } else { return 3 // To indicate you didn't win } } } } // Resets saved duel values function resetDuelValues(){ scriptProperties.setProperty(DUEL_CURRENTLY_ACTIVE_KEY, 0) scriptProperties.setProperty(DUEL_SCORE_KEY, 0) scriptProperties.setProperty(SELF_USERNAME_KEY, "self") scriptProperties.setProperty(OPPONENT_USERNAME_KEY, "name") scriptProperties.setProperty(WAGER_KEY, 0) scriptProperties.setProperty(SCORE_NEEDED_GOAL_KEY, 0) scriptProperties.setProperty(GROUP_ID_KEY, "string") scriptProperties.setProperty(ANNOUNCER_BOT_USERNAME_KEY, "name") scriptProperties.setProperty(TIMESTAMP_IDENTIFIER_KEY, 0) scriptProperties.setProperty(TROUBLESHOOTING_KEY, 0) } // 3C. TRANSFORM YOURSELF YEAR-ROUND // Checks to run before transforming function checkWhetherToTransform(isSnowball, isSpookySparkles, isShinySeed, isSeafoam, gp){ // See if they've already transformed. If yes, subtract 5 GP (send error message if they don't have enough) and reset them to normal. if ( (isSnowball) || (isSpookySparkles) || (isShinySeed) || (isSeafoam) ) { if ( gp >= COST_TO_TRANSFORM_BACK ) { api_updateUser({"stats.buffs.snowball" : false, "stats.buffs.spookySparkles" : false, "stats.buffs.shinySeed" : false, "stats.buffs.seafoam" : false, "stats.gp" : gp - COST_TO_TRANSFORM_BACK}) return false } else { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_GP_TRANSFORM_BACK, "toUserId" : USER_ID}) return false } } // If they don't have the 15 GP, send error message else { if (gp < COST_TO_TRANSFORM) { api_sendPrivateMessage({"message" : MSG_INSUFFICIENT_GP_TRANSFORM, "toUserId" : USER_ID}) return false } // Otherwise, all checks passes else { return true } } }