Advertisement
Guest User

Untitled

a guest
Sep 16th, 2021
168
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. //:+:+:+:+:+: SHADOWRUN 5th Edition API :+:+:+:+:+: //
  2. /**
  3.  * @version: 1.1
  4.  * See additional details in Tech Notes.md including a Change Log
  5. // +:+:+:+:+: SHADOWRUN 5th Edition API :+:+:+:+:+: //
  6. //:+:+:+:+:+: OBJECT CONSTRUCTORS :+:+:+:+:+: //
  7. /**
  8.  * Character @Class defines the Character object needed for the processResults and
  9.  *   findInitiativeScores functions
  10.  * @param {String} characterID: the alphanumeric ID associated with the character;
  11.  *   you can get this from a known TokenID by using the getCharacterIdFromTokenId
  12.  *   helper function.
  13.  * @return {object}:
  14.  * * .name {String} Character's Name
  15.  * * .id {String} Character's characterID
  16.  * * .token {String} Character's TokenID
  17.  * * .src {String} Character's TokenURL
  18.  */
  19. const Shadowrun5e = (() => {
  20. class Character {
  21.   constructor(characterID) {
  22.     this.name = getAttrByName(characterID, 'character_name');
  23.     this.id = characterID;
  24.     this.token = findObjs({represents: characterID, _type: "graphic"})[0].attributes["_id"];
  25.     this.src = findObjs({represents: characterID, _type: "graphic"})[0].attributes["imgsrc"];
  26.     this.type = getAttrByName([characterID], 'sheet_type');
  27.   }
  28. }
  29.  
  30. class IntiativeResult extends Character {
  31.   constructor(characterID) {
  32.     super(characterID)
  33.     this.total = null
  34.   }
  35. }
  36.  
  37. class InitiativeExpression extends Character {
  38.   constructor(characterID, tokenId) {
  39.     super(characterID)
  40.     this.modifier = parseInt(null)
  41.     this.dice = null
  42.     this.expression = undefined
  43.     this.markers = findObjs({_id: tokenId, _type: "graphic"})[0].attributes['statusmarkers']
  44.     this.woundToggle = getAttrByName(characterID, `wounds_toggle`) != 0 ? true : false
  45.     this.wounds = parseInt(getAttrByName(characterID, `wounds`)) || 0
  46.     this.total = null
  47.   }
  48. }
  49.  
  50. /**
  51.  * InitiativeTurn Class defines the InitiativeTurn object needed for the
  52.  *   addInitiativeToTracker function
  53.  * @param {} total:
  54.  * @param {} id:
  55.  * @param {String} name:
  56.  * @return {Object}:
  57.  * * .pr {}
  58.  * * .id {}
  59.  * * .custom {}
  60.  */
  61. class InitiativeTurn {
  62.   constructor(total, id, name) {
  63.     this.pr = total;
  64.     this.id = id ? id : undefined;
  65.     this.custom = name ? name : undefined;
  66.   }
  67. }
  68. //:+:+:+:+:+: END OBJECT CONSTRUCTORS :+:+:+:+:+: //
  69.  
  70. //:+:+:+:+:+: STYLE BUILDERS :+:+:+:+:+: //
  71. // Builder pieces for constructing properly stylish automated responses in chat
  72. const primary    = '#610b0d', secondary = '#666', third = '#e7e6e5', accent = '#333';
  73. const divstyle   = `style="color: #eee;width: 90%; border: 1px solid ${accent}; background-color: #131415; padding: 5px;"`;
  74. const buttons    = `text-align:center; border: 1px solid ${accent}; margin: 3px; padding: 2px; background-color: ${primary}; border-radius: 4px;  box-shadow: 1px 1px 1px ${secondary};`
  75. const astyle     = `style="text-align:center; ${buttons} width: 68%;"`;
  76. const arrowstyle = `style="border: none; border-top: 3px solid transparent; border-bottom: 3px solid transparent; border-left: 195px solid ${secondary}; margin: 5px 0px;"`;
  77. const headstyle  = `style="color: #fff; font-size: 18px; text-align: left; font-constiant: small-caps; font-family: Times, serif; margin-bottom: 2px;"`;
  78. const substyle   = 'style="font-size: 0.8em; line-height: 13px; margin-top: -2px; font-style: italic;"';
  79. const breaks     = `style="border-color:${third}; margin: 5px 2px;"`;
  80. const circles    = `style='font-family:pictos;color: #fff;padding:2%;${buttons} width: 15px;'`
  81. const centered   = `style="text-align:center;"`;
  82. const apiName    = `Shadowrun 5th Edition`;
  83. const version    = '1.1';
  84. const header     = `<div ${divstyle}><div ${headstyle}>${apiName} <span ${substyle}>(v.${version})</span></div><div ${arrowstyle}>`;
  85. const errorMessage = (name, error) => log(`${name}: ${error}`);
  86. const readmeLink = '[Readme](https://github.com/Roll20/roll20-api-scripts/tree/master/Shadowrun%205th%20Edition)';
  87. //:+:+:+:+:+: END STYLE BUILDERS :+:+:+:+:+: //
  88.  
  89. //:+:+:+:+:+: HELPER FUNCTIONS :+:+:+:+:+: //
  90. const sr5HelperFunctions = {
  91.   /**
  92.    * The getCharacterIdFromTokenId function is used in linkTokens, processResults,
  93.    *   findInitiativeScores functions to get the characterID from the tokenID provided
  94.    * @param {String} tokenID: The unique ID of the token, retrieved from the
  95.    *   msg.selected._id (when tokens are selected on the screen) or
  96.    *   msg.inlinerolls.expression (for processResults
  97.    * @return {String} The unique CharacterID of the character associated with the token
  98.    */
  99.   getCharacterIdFromTokenId : (tokenID) => {
  100.     tokenAttributes = findObjs({_id: tokenID, _type: "graphic"});
  101.     return tokenAttributes[0].attributes.represents
  102.   },
  103.  
  104.   /**
  105.   * The getStatusIcons function is used in the tokenLinker and findInitiativeScores
  106.   *   functions; this identifies the current icons active on the token, but we're
  107.   *   specifically looking for the 'matrix' or 'astral' icons
  108.   */
  109.   getStatusIcons: tokenId => findObjs({_id: tokenId, _type: "graphic"})[0].attributes['statusmarkers'],
  110.  
  111.   /**
  112.   * The getTurnorder function checks to see if the current turnorder is empty, and if so,
  113.   *   sets turnorder to an empty object; otherwise, pull turnorder data from the Campaign()
  114.   *   information. If initiative(s) have already been rolled, turnorder is expected to have
  115.   *   four elements by default, but may have many more:
  116.   *   * .pr: the Initiative rolled
  117.   *   * .id: the ID of the Token/graphic object; for our Initiative Counter, this will
  118.   *     be set to -1
  119.   *   * ._pageid: the page ID of the page the turn order is on
  120.   *   * .custom: this element will be used for our Initiatve Counter
  121.   */
  122.   getTurnorder: () => Campaign().get("turnorder") === "" ? [] :  JSON.parse(Campaign().get("turnorder")),
  123.   setTurnorder: (turnorder) => Campaign().set("turnorder", JSON.stringify(turnorder)),
  124.  
  125.   // The findIndex function is used in the addInitiativeToTracker and
  126.   //   sr5InitCounterUpdate functions to find the index of an object in an array
  127.   //   based on a value
  128.   findIndex: (property, match) => property.findIndex(element => Object.values(element).includes(match)),
  129.  
  130.   // The sortDescending function is used in the processInitiative and
  131.   //   sr5InitCounterUpdate functions to sort the array (sortorder) in Descending
  132.   //   order
  133.   sortDescending: (array, key) => array.sort((a,b) => a[key] > b[key] ? -1 : 1),
  134. }
  135. //:+:+:+:+:+: END HELPER FUNCTIONS :+:+:+:+:+: //
  136.  
  137. var sr5api = sr5api || (function() {
  138.   'use strict';
  139.  
  140.   //:+:+:+:+:+: INPUT HANDLER FUNCTION :+:+:+:+:+: //
  141.  
  142.   /**
  143.   * The handleInput function, well, handles input. When you type something in chat, this
  144.   *   function is called by the registerEventHandlers function to analyze chat:message to
  145.   *   determine if its intended for use in the api. If the message starts with !sr5, it
  146.   *   will generate a response.  In a properly formed call, it will analyze what's after
  147.   *   the space-double-dashes for the API functionality that's being called.
  148.   * @param {object} msg: This is the object received on the chat:message event, documented
  149.   *   at https://roll20.zendesk.com/hc/en-us/articles/360037256754-API-Chat#API:Chat-chat:message
  150.   *   In particular, this function is looking for the following in the msg object:
  151.   *   * .content {String} The text that was entered into the chat window. If it doesn't
  152.   *   * .playerid {String} the id of the player who entered the msg
  153.   *     start with "!sr5", we don't care about it here.
  154.   *   * .inlinerolls {Array}
  155.   *     *
  156.   *   * .selected {String} An array of objects the user had selected when the command was
  157.   *     entered:
  158.   *     * id: ID of the token selected
  159.   *     * type: type of the token selected; usually 'graphic'
  160.   *   * .type {String} This should always be 'api'. If it isn't api, we don't care about it
  161.   *     here.
  162.   *   * .who {String} The display name of the player or character that sent the message;
  163.   *     this may include the '(GM)' string in it!
  164.   * @return
  165.   *
  166.   * @TODO: consider checking for msg.content.substring(0,4) === "!sr5" straight off. If
  167.   *   the msg.content doesn't start with that, its not an sr5api call and we don't care
  168.   * @TODO: all of these commands are for tokens associated with characters, so we should
  169.   *   make sure the tokens are characters at some point, maybe as a helper function?
  170.   */
  171.   const handleInput = msg => {
  172.     const args = msg.content.split(" --")
  173.     const who = msg.who.split(' ')[0]
  174.     log(args)
  175.     //Test to see if msg is an sr5 command and api type; this should be happening earlier
  176.     if (args[0] === "!sr5" && msg.type === "api") {
  177.         // Define noTokensSelected constant for feedback on error message for the
  178.         //   linkToken and rollInit commands
  179.         const noTokensSelected = `<div ${centered}>No tokens selected.</div>`;
  180.         // Define selected constant, used in linkToken and rollInit commands to determine
  181.         //   if anything is selected
  182.         const selected = msg.selected;
  183.         // Iterate through the expected arguments: linkToken, initCounter and rollInit
  184.         switch(args[1]) {
  185.           case "linkToken":
  186.               // Check to see if there is a second command word after the sr5 linkToken
  187.               //   command (this should only happen when a link is clicked in the apiMenu)
  188.               if (args[2]) {
  189.                   if (args[2] === 'info') {
  190.                     chatMessage(apiCommands.linkToken.info)
  191.                   } else if (args[2] === 'help') {
  192.                     chatMessage(apiCommands.linkToken.help)
  193.                   } else {
  194.                     chatMessage(`Argument provided for linkToken was invalid: ${msg.content} . Spelling matters, omae!<br /> ${returnMenu}`)
  195.                   }
  196.               } else {
  197.                   //There is not a second command word: if there are tokens selected,
  198.                   //  then call the linkTokens function; if there are no tokens selected
  199.                   //  (selected is undefined) then display an error that no tokens are
  200.                   //  selected, otherwise just throw the apiMenu.
  201.                   //@TODO Whether tokens are selected or not should really be a binary
  202.                   //  thing, there shouldn't be any reason apiMenu gets thrown here
  203.                   selected ? linkTokens(selected, who) : selected === undefined ? chatMessage(noTokensSelected) : apiMenu();
  204.               }
  205.               break;
  206.           case "initCounter":
  207.               //Check to see if there is a second command word after the sr5 initCounter
  208.               //  command (this should only happen when a link is clicked in the apiMenu)
  209.               if (args[2]) {
  210.                   //There is a second word: if it is 'info' or 'help', call the
  211.                   //  chatMessage function to display hardcoded information in the
  212.                   //  apiCommands array, otherwise throw an error
  213.                   if (args[2] === 'info') {
  214.                     chatMessage(apiCommands.initCounter.info)
  215.                   } else if (args[2] === 'help') {
  216.                     chatMessage(apiCommands.initCounter.help)
  217.                   } else {
  218.                     chatMessage(`The argument provided for initCounter was invalid: ${msg.content} . Spelling matters, omae!<br /> ${returnMenu}`)
  219.                   }
  220.               } else {
  221.                   //There is not a second command word, so we're just running the sr5
  222.                   //  addInitiativeCounter function
  223.                   addInitiativeCounter()
  224.               }
  225.               break;
  226.           case "rollInit":
  227.               //Check to see if there is a second command word after the sr5 rollInit
  228.               //  command (this should only happen when a link is clicked in the apiMenu)
  229.               if (args[2]) {
  230.                   //There is a second word: if it is 'info' or 'help', call the
  231.                   //  chatMessage function to display hardcoded information in the
  232.                   //  apiCommands array, otherwise throw an error
  233.                   if (args[2] === 'info') {
  234.                     chatMessage(apiCommands.rollInit.info)
  235.                   } else if (args[2] === 'help') {
  236.                     chatMessage(apiCommands.rollInit.help)
  237.                   } else if (args[2] === 'error') {
  238.                     chatMessage(apiCommands.rollInit.error)
  239.                   } else {
  240.                     chatMessage(`The argument provided for rollInit was invalid: ${msg.content} . Spelling matters, omae!<br /> ${returnMenu}`)
  241.                   }
  242.               } else {
  243.                   //There is not a second command word, so we're just running the sr5
  244.                   //  rollInitiative function if tokens are selected
  245.                   //@TODO Whether tokens are selected or not should really be a binary
  246.                   //  thing, there shouldn't be any reason apiMenu gets thrown here
  247.                   selected ? rollInitiative(selected) : selected === undefined  ? chatMessage(noTokensSelected) : apiMenu();
  248.               }
  249.               break;
  250.           default:
  251.               //This brings up the UI menu if the user typed !sr5 without correct arguements
  252.               apiMenu(who)
  253.         };
  254.     } else if (msg.who === `${apiName} Roll Initiative`) {
  255.         // msg is NOT an sr5 command and api type, so testing to see if the rollInitiative
  256.         //   function threw something into chat; that means we're running the
  257.         //   processInitiative with msg.inlinerolls
  258.         //processInitiative(msg)
  259.     } else if (msg.type === "api" && msg.content.substring(0,4) === "!sr5") {
  260.         //msg is NOT an sr5 command and api type NOR sent to chat by the rollInitiative
  261.         //  function, so we're testing to see if this is even an !sr5 command; if so,
  262.         //  throw them an error that makes sense
  263.         chatMessage(`${msg.content} is an invalid sr5 api command, chummer!<br /> ${returnMenu}`);
  264.     } else {
  265.       //this isn't even an sr5api command, why are we here? Return! Return!
  266.       return;
  267.     }
  268.   },
  269.   //:+:+:+:+:+: END INPUT HANDLER FUNCTION :+:+:+:+:+: //
  270.  
  271.   //:+:+:+:+:+: API MENU FUNCTIONS :+:+:+:+:+: //
  272.   /**
  273.    * The apiMenu function returns a clickable menu for the sr5 api
  274.    * @param {String} who: the id of the person the apiMenu is being whispered to
  275.    * @return A properly formatted menu with each sr5api command linkified, with info and
  276.    *   help buttons
  277.    *
  278.    * @TODO Consider adding a 'feedback' argument because sometimes this just pops up
  279.    *   because you did something wrong. It looks like someone started to add this
  280.    *   functionality and then just got rid of it.
  281.    * @TODO Consider moving this down to the end of the API Menu section
  282.    */
  283.   apiMenu = who => {
  284.     let feedback = ""
  285.     let commandArray = ['linkToken', 'initCounter', 'rollInit']
  286.     commandArray.forEach(command => feedback += menuButtons(command));
  287.     chatMessage(feedback, who);
  288.   },
  289.  
  290.   /**
  291.    * The menuButtons function generates the buttons for the apiMenu function.
  292.    * @param {String} command: The individual sr5 api command text, defined in the apiMenu
  293.    *   function
  294.    * @return {String} feedback: the fully styled row of buttons for the requested command
  295.    */
  296.   menuButtons = command => {
  297.     let feedback = ""
  298.     feedback += `<div ${centered}><a ${astyle} href="!sr5 --${command}">${apiCommands[command].name}</a>`
  299.     feedback += `<a ${circles} href="!sr5 --${command} --info">i</a><a ${circles} href="!sr5 --${command} --help">?</a>`
  300.     feedback += `</div>`
  301.     feedback += `<hr ${breaks} />`
  302.     return feedback
  303.   },
  304.  
  305.   //Build an object with an element for each api command that is also an object
  306.   //  containing name, info and help text
  307.   returnMenu = `<div ${centered}><a ${astyle} href="!sr5">Api Menu</a></div>`,
  308.   apiCommands = {
  309.     linkToken: {
  310.       name: 'Link Tokens',
  311.       info: `<div ${centered}>Link Tokens</div><div>Set a number of defaults on selected tokens then set the default token on the represented character sheets. For full details review the ${readmeLink}</div></div>${returnMenu}`,
  312.       help: `<div ${centered}>Link Tokens</div><div ${centered}>!sr5 --linkToken</div><ol><li>Set token to represent a character sheet in the token settings</li><li>Select a token or multiple tokens</li><li>Run the above command or push the menu button in chat.</li></ol><div>${readmeLink}</div>${returnMenu}`
  313.     },
  314.     initCounter: {
  315.       name: 'Initiative Counter',
  316.       info: `<div ${centered}>Initiative Counter</div><div>Adds a initiative turn to the Turn Order that will count up the Combat Rounds and Initiative Passes. Every time this custom entry gets to the top of a round it will reduce initiative by 10 and remove any entries that are less than 1. If it is the only entry in the Turn Order it will increase the round counter.</div><div>${readmeLink}</div>${returnMenu}`,
  317.       help: `<div ${centered}>Initiative Counter</div><div ${centered}>!sr5 --initCounter</div><ul><li>Add counter before rolling initiative.</li><li>Use arrow at the bottom of the turn tracker to cycle through turns.</li></ul><div>${readmeLink}</div>${returnMenu}`
  318.     },
  319.     rollInit: {
  320.       name: 'Roll Initiative',
  321.       info: `<div ${centered}>Roll Initiative</div><div>Roll initiative for all the selected tokens and add it to the token tracker.</div><div>${readmeLink}</div>${returnMenu}`,
  322.       help: `<div ${centered}>Roll Initiative</div><div ${centered}>!sr5 --rollInit</div><ol><li>Set tokens to represent a characters sheet</li><li>Select a token or multiple tokens.</li><li>Run the above command or push the menu button in chat.</li></ol><div>${readmeLink}</div>${returnMenu}`,
  323.       error: `<div ${centered}><strong>Roll Initiative</strong></div><div ${centered}>Troubleshooting</div><ol> <li>Ensure initiative modifier attributes are valid</li> <li>Ensure initiative dice are valid</li> <li>Change attributes related to intiative then change them back to their original value to toggle sheetworkers</li> </ol> <div>${readmeLink}</div>${returnMenu}`
  324.     }
  325.   },
  326.   //:+:+:+:+:+: END API MENU FUNCTIONS :+:+:+:+:+: //
  327.  
  328.   //:+:+:+:+:+: TOKEN LINKER COMMAND FUNCTIONS:+:+:+:+:+: //
  329.   /**
  330.    * The linkTokens function looks at a Token's Linked character Sheet and set a number
  331.    *   of defaults
  332.    * @param {Array} selected: data on selected tokens
  333.    * @param {String} who
  334.    * @return sets up each character token as hardcoded in the tokenLinker Function
  335.    * @APIObject getObj https://roll20.zendesk.com/hc/en-us/articles/360037772833-API-Function-Documentation#API:FunctionDocumentation-getObj
  336.    * @APIObject setDefaultTokenForCharacter https://roll20.zendesk.com/hc/en-us/articles/360037256774-API-Utility-Functions#API:UtilityFunctions-Character
  337.    *
  338.    * @TODO Why wouldn't this call the Character class to build an object for each
  339.    *   character?
  340.    */
  341.   linkTokens = (selected, who) => {
  342.     selected.forEach(token => {
  343.       const characterID   = sr5HelperFunctions.getCharacterIdFromTokenId(token["_id"]) || false;
  344.       const characterName = getAttrByName(characterID, 'character_name') || "";
  345.       const tokenID       = token["_id"];
  346.       let feedback = '';
  347.       //Check first to see if the token represents a character
  348.       if (characterID) {
  349.         //if this is a character, run the tokenLinker function for the selected
  350.         //  characterID, tokenID and characterName to get the hardcoded update info.
  351.         //@TODO if we use the Character object, maybe consider just passing that to the
  352.         //  tokenLinker function
  353.         const update = tokenLinker(characterID, tokenID, characterName);
  354.         //Set the default token for the represented character sheet
  355.         const tokenGet = getObj("graphic", tokenID);
  356.         const representsCharacter  = getObj('character', characterID);
  357.         //if an update exists (from the tokenLinker function), run the update on the
  358.         //  selected token
  359.         if (update) {
  360.           tokenGet.set(update);
  361.           setDefaultTokenForCharacter(representsCharacter, tokenGet);
  362.           feedback += `<div ${centered}><strong>${characterName}</strong></div><hr ${breaks} /><div ${centered}>Token defaults set!</div>`
  363.         } else {
  364.           //there's no update, so we need to throw an error
  365.           errorMessage('linkTokens', 'Update not found, omae!')
  366.         };
  367.       } else {
  368.         //The token doesn't represent a character, so provide feedback
  369.         //@TODO: Find a way to more clearly identify the offending token?
  370.         feedback += `<div>The selected token does not represent a character. Set a character in the Token settings.</div>`;
  371.       }
  372.       sendChat(`${apiName} Token Linker`, `/w ${who} ${header}</div>${feedback}</div>`)
  373.     });
  374.   },
  375.  
  376.   /**
  377.    * The tokenLinker function attempts to update selected tokens. It is called by the
  378.    * linkTokens function.
  379.    * @param {String} characterID: determined in the linkTokens function
  380.    * @param {String} tokenID: determined in the linkTokens function
  381.    * @param {characterName} determined in the linkTokens function
  382.    * @return {Object} update: an object with the settings for the Token to be used by the
  383.    *   linkTokens function
  384.    *
  385.    */
  386.   tokenLinker = (characterID, tokenID, characterName) => {
  387.     try {
  388.       //get the sheetType of the characterID; expected values are 'grunt', 'pc',
  389.       //  'vehicle', 'host' & 'sprite', selected on the Sheet Select dropdown in the
  390.       //  Options tab of the Shadowrun 5e character sheet
  391.       const sheetType  = getAttrByName(characterID, 'sheet_type');
  392.       //get the statusMarkers of the tokenID token; all we care about are the presence
  393.       //  of the 'matrix' or the 'astral' tokens
  394.       const statusMarkers = sr5HelperFunctions.getStatusIcons(tokenID);
  395.       //if the Matrix status token is active, we identify this with matrixMarker
  396.       const matrixMarker = statusMarkers.includes('matrix') ? 'matrix' : false;
  397.       //set stunCharacters as an array for the character types that have a stun
  398.       //  condition monitor
  399.       const stunCharacters = ['grunt', 'pc'];
  400.       //set physicalCharacters as an array for the sheetTypes that have a physical
  401.       //  condition monitor
  402.       const physicalCharacters = stunCharacters.concat(['vehicle']);
  403.       //set matrixCharacters as an array for the sheetTypes that have a matrix
  404.       //  condition monitor
  405.       const matrixCharacters = ['vehicle', 'host', 'sprite'];
  406.       //set true/false flags for stun, matrix and physical, making sure to include
  407.       //  matrix if the token has the matrixMarker active
  408.       const stun = stunCharacters.includes(sheetType) ? true : false;
  409.       const matrix = matrixCharacters.includes(sheetType) || matrixMarker ? true : false;
  410.       const physical = physicalCharacters.includes(sheetType) ? true : false;
  411.       //create the update object using the default settings
  412.       let update = {
  413.         bar1_value: 0,
  414.         bar2_value: 0,
  415.         bar3_value: 0,
  416.         name: characterName || "",
  417.         bar1_link: "",
  418.         bar2_link: "",
  419.         bar3_link: "",
  420.         showname: true,
  421.         showplayers_bar1: true,
  422.         showplayers_bar2: true,
  423.         showplayers_bar3: true,
  424.         playersedit_name: true,
  425.         playersedit_bar1: true,
  426.         playersedit_bar2: true,
  427.         playersedit_bar3: true,
  428.         light_hassight: true,
  429.         bar1_max: stun ? getAttrByName(characterID, `stun`, "max") || 0 : "",
  430.         bar2_max: matrix ? getAttrByName(characterID, `matrix`, "max") || 0 : "",
  431.         bar3_max: physical ? getAttrByName(characterID, `physical`, "max") || 0 : "",
  432.       };
  433.       //If the sheetType is 'pc', iterate through each of stun, matrix and physical
  434.       //  to first find the character attribute in the character sheet, then update
  435.       //  barX_link information (on the Edit Token page) for each attribute, bar1 is
  436.       //  stun, bar2 is matrix and bar3 is physical.
  437.       //@TODO Correct the error handling to use errorMessage functionality
  438.       if (sheetType === 'pc') {
  439.         ['stun', 'matrix', 'physical'].forEach(attr => {
  440.           const link = findObjs({characterid : characterID, "name": attr});
  441.           const num = attr === 'matrix' && matrixMarker ? 1 : attr === 'stun' ? 2 : 3;
  442.           link[0] ? update[`bar${num}_link`] = link[0].id : log(`Linked attribute not found for bar${num}`);
  443.         });
  444.       }
  445.       return update
  446.     } catch (error) {
  447.       errorMessage('tokenLinker', error)
  448.     };
  449.   },
  450.   //:+:+:+:+:+: END TOKEN LINKER :+:+:+:+:+: //
  451.  
  452.   //:+:+:+:+:+: INITIATIVE COUNTER :+:+:+:+:+: //
  453.   /**
  454.   * The addInitiativeCounter function adds the sr5 Round/Pass placeholder into the
  455.   *   Initiative Tracker
  456.   * @param: None
  457.   * @return: Adds the Initiative Counter to the Initiative Tracker page or, if it already
  458.   *   exists, resets it to round 1, pass 1
  459.   */
  460.   addInitiativeCounter = () => {
  461.     try {
  462.       //first, we'll gather our existing turnorder information and put it into the
  463.       //  turnorder object, if it exists
  464.       let turnorder = sr5HelperFunctions.getTurnorder();
  465.       //If the Initiative Tracker isn't open, open it
  466.       openInitiativePage();
  467.       //Next, establish iniobj object with some default values for 'to', 'check' and 'idx'
  468.       let iniobj = {
  469.         to: [],
  470.         check: '',
  471.         idx: 0
  472.       };
  473.       /**
  474.       * The inicheck function changes the elements of iniobj to try to find an existing
  475.       *   Initiative Counter on the Initiative Tracker page:
  476.       *   * .po: equal to iniobj.to, which is likely an empty object at this point; can't
  477.       *     find where this is used anywhere!!
  478.       *   * .to: the information found in the Campaign's turnorder element; not sure why
  479.       *     this isn't just using the turnorder variable we already established
  480.       *   * .check: this searches through the object in iniobj.to to find the first value
  481.       *     of custom = 'Round / Pass'
  482.       *   * .idx: the index of the value found in .check
  483.       */
  484.       const inicheck = () => {
  485.         iniobj.po = iniobj.to;
  486.         iniobj.to = turnorder;
  487.         iniobj.check = _.find(iniobj.to, obj => obj.custom == 'Round / Pass');
  488.         iniobj.idx = iniobj.to.indexOf(iniobj.check);
  489.       };
  490.       /**
  491.       * The addinit function adds the Initiative Counter to the start of the Campaign's
  492.       *   turnorder object
  493.       * @param: {Object} oTurnOrder the turnorder object from Campaign
  494.       * @return: sets the new turnorder for the Campaign with a fresh Initiative Counter
  495.       *   at 1 / 1
  496.       * @TODO: Check to make sure it doesn't already exist?
  497.       */
  498.       const addini = oTurnOrder => {
  499.         oTurnOrder.unshift({
  500.           id: '-1',
  501.           pr: '1 / 1',
  502.           custom: 'Round / Pass'
  503.         });
  504.         sr5HelperFunctions.setTurnorder(oTurnOrder);
  505.       };
  506.       //Check to see if the Initiative Counter is already on the Initiative Tracker page
  507.       inicheck();
  508.       //If the Initiative Counter doesn't exist, call addini to add it to the counter;
  509.       //  if Iniatitive Counter does exist, remove it, then call addini to add a reset
  510.       //  Initiative Counter to initiative
  511.       if (iniobj.check === undefined) {
  512.         addini(iniobj.to);
  513.       } else{
  514.         iniobj.to.splice(iniobj.idx, 1);
  515.         addini(iniobj.to);
  516.       };
  517.     } catch (error) {
  518.       errorMessage('addInitiativeCounter', error)
  519.     }
  520.   },
  521.   /**
  522.   * The openInitiativePage function opens the campaign's initiative page if it is closed.
  523.   *   This is used in the addInitiativeToTracker and addInitiativeCounter. Full
  524.   *   description of the Campaign object is at
  525.   *   https://roll20.zendesk.com/hc/en-us/articles/360037772793-API-Objects#API:Objects-Campaign
  526.   */
  527.   openInitiativePage = () => {
  528.     if (Campaign().get('initiativepage') === false) {
  529.       Campaign().set('initiativepage', true);
  530.     };
  531.   },
  532.   //:+:+:+:+:+: END INITIATIVE COUNTER :+:+:+:+:+: //
  533.  
  534.   //:+:+:+:+:+: GROUP INITIATIVE :+:+:+:+:+: //
  535.   /**
  536.    * The addInitiativeToTracker function is called by the processInitiative function
  537.    * @param {Object} tokenInitiatives: this object contains data passed from the
  538.    *   processInitiative function, with an element for each roll containing data
  539.    *   gathered as part of the Character class
  540.    * @return: Adds new Initiative roll data to the Initative Tracker window, replacing
  541.    *   any existing initiative rolls for that character
  542.    */
  543.   addInitiativeToTracker = tokenInitiatives => {
  544.     try {
  545.       //first, we'll gather our existing turnorder information and put it into the
  546.       //  turnorder object, if it exists
  547.       let turnorder = sr5HelperFunctions.getTurnorder();
  548.       //next, get the Index of the Initiative Counter we added in the
  549.       //  addInitiativeCounter function
  550.       const counterIndex = sr5HelperFunctions.findIndex(turnorder, 'Round / Pass');
  551.       //Open the Initiative Tracker if not open
  552.       openInitiativePage();
  553.       //Iterate through each element of turnorder
  554.       turnorder.forEach(turn => {
  555.         //find the index to each element within the tokenInitiatives object, given the
  556.         //  Character's .id in the element
  557.         const index = sr5HelperFunctions.findIndex(tokenInitiatives, turn.id)
  558.         //If the index is greater than 0 (meaning its not our Initiative Counter), make
  559.         //  turnorder.pr equal to the initiative total for that element, then remove
  560.         //  that element from the tokenInitiatives object
  561.         if (index >= 0) {
  562.             turn.pr = tokenInitiatives[index].total
  563.             tokenInitiatives.splice(index, 1)
  564.         }
  565.       });
  566.       //Iterate through each element that remains in tokenInitiatives and add it to the
  567.       //  end of turnorder
  568.       //@TODO Investigate why this is even necessary.
  569.       tokenInitiatives.forEach(token => {
  570.         const newInitiative = new InitiativeTurn(token.total, token.token);
  571.         turnorder.push(newInitiative)
  572.       });
  573.       //If our Initiative Counter's index is equal to or greater 0 (meaning its at the
  574.       //  top of the Initiative Tracker), capture the data in the element in the counter
  575.       //  variable, then remove it from turnorder and re-add it to turnorder at the end
  576.       if (counterIndex >= 0) {
  577.         const counter = turnorder[counterIndex]
  578.         turnorder.splice(counterIndex, 1)
  579.         turnorder = sr5HelperFunctions.sortDescending(turnorder, 'pr')
  580.         turnorder.push(counter)
  581.       }
  582.       //Now that turnorder is all sorted, add it back into the Campaign!
  583.       sr5HelperFunctions.setTurnorder(turnorder);
  584.     } catch (error) {
  585.         errorMessage('addInitiativeTracker', error)
  586.     }
  587.   },
  588.   /**
  589.    * The processInitiative function takes information from msg.inlinerolls in the
  590.    *   chracterInitiativeRolls variable, runs it through processResults to obtain an
  591.    *   array with Character objects as elements, then sends that array through
  592.    *   sr5HelperFunctions.sortDescending to sort it by total, then adds that to the
  593.    *   Initiative Tracker with the addInitiativeTracker function.
  594.    * @param: {Object} characterInitiativeRolls: information contained in
  595.    *   msg.inlinerolls, which is then passed off to the processResults function for
  596.    *   processing
  597.    */
  598.    
  599.     processInitiative = (msg, characters) => {
  600.     //characterInitiativeRolls = msg.inlinerolls
  601.     try {
  602.       //First, take the msg.inline information and process it through the processResults
  603.       //  function to get an array populated with Character objects in each element
  604.       //const processedRolls = processResults(characterInitiativeRolls, characters)
  605.       //Then sort processedRolls in sr5HelperFunctions.sortDescending
  606.       const sortedByTotal = sr5HelperFunctions.sortDescending(characters, 'total')
  607.       //Add the sorted initiative roll information to the Initiative Tracker
  608.       displayInitiative(msg, characters)
  609.       addInitiativeToTracker(sortedByTotal)
  610.  
  611.     } catch (error) {
  612.       errorMessage('processInitiative', error)
  613.     }
  614.   },
  615.    
  616.    /*
  617.   processInitiative = characterInitiativeRolls => {
  618.     try {
  619.       //First, take the msg.inline information and process it through the processResults
  620.       //  function to get an array populated with Character objects in each element
  621.       const processedRolls = processResults(characterInitiativeRolls)
  622.       //Then sort processedRolls in sr5HelperFunctions.sortDescending
  623.       const sortedByTotal = sr5HelperFunctions.sortDescending(processedRolls, 'total')
  624.       //Add the sorted initiative roll information to the Initiative Tracker
  625.       addInitiativeToTracker(sortedByTotal)
  626.     } catch (error) {
  627.       errorMessage('processInitiative', error)
  628.     }
  629.   },
  630.   */
  631.   /**
  632.    * The processResults function takes information from msg.inlinerolls and creates a
  633.    *   Character class object. It then adds character.total to the object to include
  634.    *   the result of the Initiative roll.
  635.    * @param: {Object} results: This is the data from msg.inlinerolls, passed from the
  636.    *   processInitiative function. In particular we are interested in the following
  637.    *   elements:
  638.    *   * .expression: we need the tokenID information out of this element
  639.    *   * .results.total: the Initiative that resulted from the die roll
  640.    * @return: An array with an element for each Character object, including
  641.    *   character.total with the Initiative result gathered from msg.inlinerolls
  642.    */
  643.   processResults = (results,characters) => {
  644.     try {
  645.       let array = [];
  646.       //iterate through the msg.inlinerolls data contained in results
  647.       for(let i = 0; i<results[0].inlinerolls.length; i++){
  648.         const character = new IntiativeResult(characters[i].id)
  649.         // add a character.total to the Character object equal to the Initiative rolled
  650.         //  for that element in msg.inlinerolls
  651.         character.total = results[0].inlinerolls[i].results.total
  652.         //add the Character object to our array
  653.         array.push(character)
  654.       };
  655.       processInitiative(results,array)
  656.     } catch (error) {
  657.       errorMessage('processResults', error)
  658.     }
  659.   },
  660.  
  661.   /**
  662.    * The rollInitiative function is base functionality that fires off when
  663.    *   "!sr5 --rollInit" is called in chat. It is called by the handleInput function.
  664.    * @param: {Object} selected: An object with the information contained in msg.selected
  665.    *   for the Tokens currently selected
  666.    * @return: processes and outputs Initiative information to chat
  667.    */
  668.    
  669.   rollInitiative = selected => {
  670.       try{
  671.         const selectedInitiatives = findInitiativeScores(selected);
  672.         let feedback = '';
  673.         selectedInitiatives.forEach(character => {
  674.           const roll = character.expression ? `[[${character.expression}]]` : `<a ${circles} href="!sr5 --rollInit --error">!</a>`
  675.           feedback += `<div style='color: ${accent}; width: 15%; display: inline-block;'>${roll}</div>`
  676.         })
  677.         sendChat(`${apiName} Roll Initiative`, `${header}</div>${feedback}</div>`, (msg) => processResults(msg,selectedInitiatives));
  678.       }catch (error) {
  679.       errorMessage('rollInitiative', error)
  680.       }
  681.     }
  682.    
  683.    
  684.    const displayInitiative = (msg,selected) => {
  685.      try {
  686.        //First, pass the selected Object through the findInitiativeScores function to
  687.        //  process the initiative results and return an array with a Character object for
  688.        // each element
  689.        //Next, establish an empty feedback constant to hold the text we want to build
  690.        let feedback = '';
  691.        //iterate through the selectedInitiatives array and build a themed response to
  692.        //  display in chat
  693.        for (let i = 0; i<selected.length; i++) {
  694.          feedback += `<div style='display: inline-block; border: 1px solid ${third}; border-radius: 5px; padding: 2%; background-color: ${secondary}; margin-bottom: 3%; width: 95%;'>`
  695.          feedback += `<img src='${selected[i].src}' style='margin-right: 2%; width: 20%;'>`
  696.          feedback += `<label style='display: inline-block; font-weight: bold; font-size: 1.3em; color: ${third}; vertical-align: middle; width: 60%;'>${selected[i].name}</label>`
  697.          const roll = libInline.getRollTip(msg[0].inlinerolls[i])
  698.  
  699.          feedback += `<div style='color: ${accent}; width: 15%; display: inline-block;'>${roll}</div>`
  700.          feedback += `</div><br />`
  701.        }
  702.        sendChat(`${apiName} Roll Initiative`, `${header}</div>${feedback}</div>`);
  703.      } catch (error) {
  704.        errorMessage('displayInitiative', error)
  705.      }
  706.    },
  707.   /**
  708.    * The findInitiativeScores function is called by rollInitiative
  709.    * @param {Object} selected: this is an array containing information on each token
  710.    *   selected, taken originally from msg.selected
  711.    * @return: [] of objects with expressions to roll
  712.    */
  713.   findInitiativeScores = selected => {
  714.     try {
  715.       //start off with an empty array
  716.       let array = [];
  717.       //iterate through each element of selected to build our array
  718.       selected.forEach(token => {
  719.         const characterID = sr5HelperFunctions.getCharacterIdFromTokenId(token["_id"]);
  720.         //build a new Character object
  721.         const character = new InitiativeExpression(characterID, token["_id"]);
  722.         //  use the character.types and/or the markers on the Token to determine what
  723.         //  sort of initiative to call: 'matrix', 'astral' or 'initiative'; default is
  724.         //  just 'initiative'
  725.         const matrix = ['sprite', 'host', 'vehicle']
  726.         const intiativeType =
  727.           matrix.includes(character.type) || character.markers.includes('matrix') ? 'matrix' :
  728.           character.markers.includes('astral') ? 'astral' :
  729.           'initiative'
  730.         //search the character sheet for the appropriate initiative modifier (the integer
  731.         //  added to the roll)
  732.         character.modifier = getAttrByName(characterID, `${intiativeType}_mod`);
  733.         //search the character sheet for the appropriate number of initiative dice
  734.         character.dice = getAttrByName(characterID, `${intiativeType}_dice`);
  735.         //if we've gathered both a modifier and dice, then we'll build the roll
  736.         //  character.expression needed for roll20 to roll; note the use of cs0cf0 to
  737.         //  ensure no rolls are shown as critical success or failures
  738.         if (character.modifier && character.dice) {
  739.           const modifier = intiativeType === 'matrix' || !character.woundToggle ? character.modifier : parseInt(character.modifier) + parseInt(character.wounds)
  740.           character.expression = `${modifier}+${character.dice}d6cs0cf0`
  741.         }
  742.         //add the character object we've built to the array
  743.         array.push(character)
  744.       });
  745.       return array
  746.     } catch (error) {
  747.       errorMessage('findInitiativeScores', error)
  748.     }
  749.   },
  750.   //:+:+:+:+:+: END GROUP INITIATIVE :+:+:+:+:+: //
  751.    
  752.   //Send message to chat
  753.   chatMessage = (feedback, who) => sendChat(`${apiName} API`, `/w ${who || 'gm'} ${header}</div>${feedback}</div>`),
  754.  
  755.   registerEventHandlers = () => {
  756.     on('chat:message', handleInput);
  757.   };
  758.  
  759.   return {
  760.     RegisterEventHandlers: registerEventHandlers
  761.   };
  762. }());
  763.  
  764. on("ready",() => {
  765.     'use strict';
  766.     sr5api.RegisterEventHandlers();
  767. });
  768.  
  769.  
  770. //:+:+:+:+:+: INITIATIVE COUNTER :+:+:+:+:+: //
  771. /**
  772.  * The sr5InitCounterUpdate function is called whenever there is a change in the
  773.  *   turnorder for the campaign, usually when the GM clicks the Next Turn arrow button
  774.  *   on the Initiative Tracker. This function checks to see if our Initiative Counter is
  775.  *   at the top of the Initiative Tracker and, if so, updates all Initiatives for the
  776.  *   next pass or round and returns a new turnorder to the campaign with this information
  777.  * @params: None (we'll use sr5HelperFunctions.getTurnorder() to pick up the
  778.  *   current turnorder)
  779.  * @return: Update the Campaign.turnorder with the new Initiative Counter with updated
  780.  *   Round/Pass information
  781.  */
  782. sr5InitCounterUpdate = () => {
  783.   //First, gather information on the current turnorder for the Campaign
  784.   let turnorder = sr5HelperFunctions.getTurnorder();
  785.   //Next, search through turnorder for the index of the element that contains our
  786.   //  Initiative Counter
  787.   const counterIndex = sr5HelperFunctions.findIndex(turnorder, 'Round / Pass');
  788.   //If our Initiative Counter is at the top of the Initiative list, its time to move on
  789.   //  to the next pass or turn!
  790.   if (counterIndex === 0) {
  791.     //get our Initiative Counter object from the turnorder object
  792.     let counter = turnorder[counterIndex]
  793.     //remove our Initiative Counter object from the turnorder object; we're going to
  794.     //  build a new one
  795.     turnorder.splice(counterIndex, 1)
  796.     //Hey! This is the start of the new one!
  797.     let newTurnOrder = []
  798.     //Iterate through turnover.pr (where the Initiative results are stored) to reduce all
  799.     //  the current initiatives by 10 and remove them if that makes them less than 0; if
  800.     //  they're still positive, add them to the newTurnOrder
  801.     turnorder.forEach(element => {
  802.         element.pr -= 10
  803.         element.pr > 0 ? newTurnOrder.push(element) : false;
  804.     });
  805.     //Pass our newTurnOrder through .sortDescending to sort it properly
  806.     newTurnOrder = sr5HelperFunctions.sortDescending(newTurnOrder, 'pr')
  807.     //Start rebuilding our Initiative Counter by splitting out the Round / Pass
  808.     //  information from our counter object
  809.     const split = counter.pr.split(` / `)
  810.     //if the newTurnOrder has no entries (meaning everyone has had all the passes for
  811.     //  that round), turn our string for Round into an integer (base 10!) and add 1 to it
  812.     //  to advance to the next Round
  813.     let round = newTurnOrder.length < 1 ? parseInt(split[0], 10) + 1 : split[0]
  814.     //if the newTurnOrder has no entires, reset the Pass to 1; otherwise turn the string
  815.     //  for our Turn into an integer (base 10!) and add one to it
  816.     let pass = newTurnOrder.length < 1 ? 1 : parseInt(split[1], 10) + 1
  817.     //rebuild our Initiative Counter
  818.     counter.pr = `${round} / ${pass}`
  819.     //if we've got a new Round, fire off an entry in chat to notify everyone that a new
  820.     //  Round has begun
  821.     feedback = `<div ${centered}><label style='display: inline-block; font-weight: bold; font-size: 1.1em; color: ${third}; vertical-align: middle; width: 60%;'>Combat Round ${round}</label></div>`
  822.     newTurnOrder.length < 1 ? sendChat('API', `${header}</div>${feedback}`) : false;
  823.     //Stick the new Initiative Counter into the newTurnOrder
  824.     newTurnOrder.push(counter)
  825.     //Send off the newTurnOrder to the Campaign's turnorder object to make everything
  826.     //  official!
  827.     sr5HelperFunctions.setTurnorder(newTurnOrder);
  828.   };
  829. },
  830. //fire off sr5InitCounterUpdate when the Campaign Turnorder changes!
  831. on('change:campaign:turnorder', () => {
  832.     sr5InitCounterUpdate();
  833. });
  834. //:+:+:+:+:+: END INITIATIVE COUNTER :+:+:+:+:+: //
  835. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement