· 6 years ago · Jan 03, 2020, 11:16 PM
1/* ***********************************
2 *** Copyright (c) 2019 bruk
3 *** This script is free software; you can redistribute it and/or modify
4 *** it under the terms of the GNU General Public License as published by
5 *** the Free Software Foundation; either version 3 of the License, or
6 *** (at your option) any later version.
7 ***
8 ************************************* */
9
10// For more info, help, or to contribute: http://bruk.org/wow
11
12
13// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPORTANT!!! ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14// You need to put your Client ID and client Secret below, inside the quotes
15// Sign up and obtain them here: https://develop.battle.net/
16// Step by step instructions: http://bruk.org/api
17
18var clientID = "";
19
20var clientSecret = "";
21
22// Change this to the threshold you want to start checking for epic gems (ie: if it's 349 anything 350 or above will be checked for epic gems)
23var CONST_EPICGEM_ILVL = 350;
24
25// This is threshold item level where gear is checked for enchants and gems
26var CONST_AUDIT_ILVL = 309;
27
28//If you want to list the uncompleted Mythic dungeons instead of the completed Mythics, change this from false to true
29
30var listMissing = false;
31
32// Option to include data from raider.io
33
34var raiderIO = false;
35
36// Option to display selected Azerite Essences
37// this option uses new parts of the API, and because of that requires an additional call to it
38// folks who are runnning into 'Too Manay Requests' type errors may have troubles with this
39// You must add azTraits to the output array (which starts at line 1284) as well as manually add 3 columns to your sheet
40
41var essencesOn = false;
42
43/* Advanced User Feature: Warcraft Logs best performance average percentile output for normal/heroic/mythic (current tier only)
44 you'll need a warcraftlogs API key, which you can find here:
45 https://www.warcraftlogs.com/profile
46 at the bottom of the page, marked as public key.
47 Make sure you define an Application Name in the box above the keys
48 you will need to add in this line to the output array:
49
50 warcraftLogs,
51
52 The output array starts at around line 1080 and begins with this line:
53 var toonInfo = [
54
55 insert warcraftLogs, where you'd like them to appear in the output
56 You'll then need to add THREE columns to your sheet where you want that info to be, and label them accordingly (normal, heroic, mythic) */
57
58// put your warcraft api key here
59var warcraftlogskey = "";
60
61
62//If you want Legendary items to be marked with a + next to item level (use conditional formatting to change their color) change this to true
63
64var markLegendary = true;
65
66
67// Everything below this, you shouldn't have to edit
68//***************************************************************
69/* globals Utilities, UrlFetchApp, PropertiesService */
70/* exported wow, vercheck, warcraftLogs */
71/*eslint no-unused-vars: 0*/
72
73
74var warcraftLogs = ["No WarcaftLog API key", ":(", ":("];
75
76var current_version = 4.32;
77
78function wow(region,toonName,realmName)
79{
80
81 if (!toonName || !realmName)
82 {
83 return " "; // If there's nothing in the column, don't even bother calling the API
84 }
85
86 Utilities.sleep(Math.floor((Math.random() * 10000) + 1000)); // This is a random sleepy time so that we dont spam the api and get bonked with an error
87
88 var scriptProperties = PropertiesService.getScriptProperties();
89 var token = scriptProperties.getProperty("STORED_TOKEN");
90
91 //Getting rid of any sort of pesky no width white spaces we may run into
92 toonName = toonName.replace(/\s/g, "");
93 region = region.replace(/\s/g, "");
94 realmName = realmName.replace(/[\u200B-\u200D\uFEFF]/g, "");
95
96 region = region.toLowerCase(); // if we don't do this, it screws up the avatar display 9_9
97
98 var options={ muteHttpExceptions:true };
99 var toon = "";
100
101 var toonJSON = UrlFetchApp.fetch("https://"+region+".api.blizzard.com/wow/character/"+realmName+"/"+toonName+"?fields=reputation,statistics,items,quests,achievements,audit,progression,feed,professions,talents&?locale=en_US&access_token="+token+"", options);
102
103 if (!token || toonJSON.toString().length === 0)
104 {
105 var oauth_response = UrlFetchApp.fetch("https://"+region+".battle.net/oauth/token", {
106 "headers" : {
107 "Authorization": "Basic " + Utilities.base64Encode(clientID + ":" + clientSecret),
108 "Cache-Control": "max-age=0"
109 },
110 "payload" : { "grant_type": "client_credentials" }
111 });
112
113 token = JSON.parse(oauth_response.toString()).access_token;
114
115
116 scriptProperties.setProperty("STORED_TOKEN", token);
117 toonJSON = UrlFetchApp.fetch("https://"+region+".api.blizzard.com/wow/character/"+realmName+"/"+toonName+"?fields=reputation,statistics,items,quests,achievements,audit,progression,feed,professions,talents&?locale=en_US&access_token="+token+"", options);
118
119 if (!token)
120 {
121 return "Error getting an API token. Please visit https://develop.battle.net/ and sign up for an account";
122 }
123 }
124
125
126 toon = JSON.parse(toonJSON.toString());
127
128 if (toon.detail || toon.status)
129 {
130 return "API Error, check if your API key is entered properly and that the API is working, check character on Armory to see if it loads";
131 }
132
133 var mainspec = "none";
134 for (var i = 0; i < 4; i++)
135 {
136 if (toon.talents[i].selected === true)
137 {
138 mainspec=toon.talents[i].spec.name;
139 }
140 }
141
142 // figuring out what the class is
143 var classes = {
144 1:"Warrior",
145 2:"Paladin",
146 3:"Hunter",
147 4:"Rogue",
148 5:"Priest",
149 6:"DeathKnight",
150 7:"Shaman",
151 8:"Mage",
152 9:"Warlock",
153 10:"Monk",
154 11:"Druid",
155 12:"Demon Hunter"
156 };
157 var toon_class = classes[toon.class];
158
159 // Azerite Essences
160 var azTraits = [ "-", "-", "-"];
161
162 if (toon.level == 120 && essencesOn == true)
163 {
164
165 var traitsJSON = UrlFetchApp.fetch("https://"+region+".api.blizzard.com/profile/wow/character/"+realmName.toLowerCase().replace("'", "").replace(" ", "-")+"/"+toonName.toLowerCase()+"/equipment?namespace=profile-"+region+"&locale=en_US&access_token="+token+"", options);
166
167 var parsedTraits = JSON.parse(traitsJSON.toString());
168
169 if (parsedTraits.equipped_items[1].slot.type === "NECK") //this will deal with those weirdo nudists who don't have helm/neck equipped
170 {
171 for (i=0; i<3; i++)
172 {
173 if (parsedTraits.equipped_items[1]) //this sort of catches weird bugs for now
174 {
175 if (parsedTraits.equipped_items[1].azerite_details.selected_essences[i])
176 {
177 if (parsedTraits.equipped_items[1].azerite_details.selected_essences[i].rank) // if there's no rank there's no reason to continue
178 {
179 azTraits[i] = parsedTraits.equipped_items[1].azerite_details.selected_essences[i].essence.name + "(" + parsedTraits.equipped_items[1].azerite_details.selected_essences[i].rank + ")";
180 }
181 }
182 }
183 }
184 }
185 }
186
187 // Time to do some gear audits
188 var auditInfo ="";
189
190 var totalGems = [0, 0, 0, 0];
191
192 var gemAudit = [
193 { bool: 0, issue: "Old:" },
194 { bool: 0, issue: "Cheap:" },
195 { bool: 0, issue: "No Leviathan" },
196 { bool: 0, issue: "Missing Epic:" },
197 { bool: 0, issue: "Missing Trinket Punchcard" }
198 ];
199
200 var gemStats = [
201 { value: 0, stat: "Crit" },
202 { value: 0, stat: "Haste" },
203 { value: 0, stat: "Vers" },
204 { value: 0, stat: "Mast" },
205 { value: 0, stat: "%XP" },
206 { value: 0, stat: "%Move" },
207 { value: 0, stat: "Str" },
208 { value: 0, stat: "Agi" },
209 { value: 0, stat: "Int" },
210 { value: 0, stat: "OLD GEMS" },
211 ];
212
213
214 // I love me some look up tables! These are to check if you have a crappy enchant or gem
215 var audit_lookup = {};
216
217 //uncommon gems
218 audit_lookup["153710"] =
219 audit_lookup["153711"] =
220 audit_lookup["153712"] =
221 audit_lookup["153713"] =
222 audit_lookup["153714"] = //XP
223 audit_lookup["153715"] =0; // +Movement
224
225
226 //rare gems
227 audit_lookup["154126"] =
228 audit_lookup["154127"] =
229 audit_lookup["154128"] =
230 audit_lookup["154129"] = 1;
231
232 //unique epic gems
233 audit_lookup["153707"] = //strengh
234 audit_lookup["153708"] = //agility
235 audit_lookup["153709"] = 2; //Int
236
237 // NEW epic gems
238 audit_lookup["168639"] = //crit
239 audit_lookup["168640"] = //mastery
240 audit_lookup["168641"] = //haste
241 audit_lookup["168642"] = 3; //vers
242
243
244 // NEW unique epic gems
245 audit_lookup["168636"] = //strength
246 audit_lookup["168637"] = //agility
247 audit_lookup["168638"] = 2; //int
248
249 // new rare gem (ugh.. thanks)
250 audit_lookup["169220"] = 1; //+movement
251
252
253 //Punch Cards
254 audit_lookup["167556"] =
255 audit_lookup["167672"] =
256 audit_lookup["167677"] =
257 audit_lookup["167689"] =
258 audit_lookup["167693"] =
259 audit_lookup["168435"] =
260 audit_lookup["168631"] =
261 audit_lookup["168632"] =
262 audit_lookup["168633"] =
263 audit_lookup["168648"] =
264 audit_lookup["168657"] =
265 audit_lookup["168671"] =
266 audit_lookup["168741"] =
267 audit_lookup["168742"] =
268 audit_lookup["168743"] =
269 audit_lookup["168744"] =
270 audit_lookup["168745"] =
271 audit_lookup["168746"] =
272 audit_lookup["168747"] =
273 audit_lookup["168748"] =
274 audit_lookup["168749"] =
275 audit_lookup["168750"] =
276 audit_lookup["168751"] =
277 audit_lookup["168752"] =
278 audit_lookup["168756"] =
279 audit_lookup["168785"] =
280 audit_lookup["168798"] =
281 audit_lookup["168800"] =
282 audit_lookup["168909"] =
283 audit_lookup["168910"] =
284 audit_lookup["168912"] =
285 audit_lookup["168913"] =
286 audit_lookup["170507"] =
287 audit_lookup["170508"] =
288 audit_lookup["170509"] =
289 audit_lookup["170510"] = 6;
290
291
292 //ring
293 audit_lookup["5942"] = "Pact +40C";
294 audit_lookup["5943"] = "Pact +40H";
295 audit_lookup["5944"] = "Pact +40M";
296 audit_lookup["5945"] = "Pact +40V";
297 audit_lookup["5938"] = "Seal +30C";
298 audit_lookup["5939"] = "Seal +30H";
299 audit_lookup["5940"] = "Seal +30M";
300 audit_lookup["5941"] = "Seal +30V";
301 audit_lookup["6108"] = "Acrd +60C";
302 audit_lookup["6109"] = "Acrd +60H";
303 audit_lookup["6110"] = "Acrd +60M";
304 audit_lookup["6111"] = "Acrd +60V";
305
306 //weapons
307 audit_lookup["5946"] = "Coastal Surge";
308 audit_lookup["5948"] = "Siphoning";
309 audit_lookup["5949"] = "Torrent of Elements";
310 audit_lookup["5950"] = "Gale-Force";
311 audit_lookup["5962"] = "Versatile Nav";
312 audit_lookup["5963"] = "Quick Nav";
313 audit_lookup["5964"] = "Masterful Nav";
314 audit_lookup["5965"] = "Deadly Nav";
315 audit_lookup["5966"] = "Stalwart Nav";
316 audit_lookup["6148"] = "Force *";
317 audit_lookup["6112"] = "Machinist";
318 audit_lookup["6150"] = "Naga Hide";
319 audit_lookup["6149"] = "Ocean Resto";
320
321 //scopes
322 audit_lookup["5955"] = "Crow's Nest Scope";
323 audit_lookup["5956"] = "Monelite Scope";
324 audit_lookup["5957"] = "Incendiary Ammo";
325 audit_lookup["5958"] = "Frost-Laced Ammo";
326
327 audit_lookup["3847"] = "(DK)Stoneskin Gargoyle";
328 audit_lookup["3368"] = "(DK)Fallen Crusader";
329 audit_lookup["3366"] = "(DK)Lichbane";
330 audit_lookup["3367"] = "(DK)Spellshattering";
331 audit_lookup["3595"] = "(DK)Spellbreaking";
332 audit_lookup["3370"] = "(DK)Razorice";
333
334
335 //shoulder - Leaving Legion ones in incase they are still useful to have in BfA
336 audit_lookup["5440"] = "Scavenger (cloth)";
337 audit_lookup["5441"] = "Gemfinder";
338 audit_lookup["5442"] = "Harvester (herbs/fish)";
339 audit_lookup["5443"] = "Butcher (leather/meat)";
340 audit_lookup["5882"] = "Manaseeker (enchant)";
341 audit_lookup["5881"] = "Salvager (ore/armor)";
342 audit_lookup["5883"] = "Bloodhunter (Blood)";
343 audit_lookup["5900"] = "Zookeeper (Pet)";
344 audit_lookup["5888"] = "Netherdrift";
345 audit_lookup["5899"] = "Builder (Engineer)";
346
347 //gloves
348 audit_lookup["5932"] = "Herb";
349 audit_lookup["5933"] = "Mine";
350 audit_lookup["5934"] = "Skin";
351 audit_lookup["5935"] = "Survey";
352 audit_lookup["5937"] = "Crafting";
353
354 var thumbnail = "http://render-"+region+".worldofwarcraft.com/character/"+ toon.thumbnail;
355
356 var thumbReg = "us";
357 if (region == "eu")
358 {
359 thumbReg = "gb";
360 }
361
362 realmName = realmName.replace("-", "");
363 realmName = realmName.replace(/ /g, "-");
364 realmName = realmName.replace("'", "");
365
366 var armory = "https://worldofwarcraft.com/en-"+thumbReg+"/character/"+realmName+"/"+toonName;
367
368 var allItems={
369 equippedItems:0,
370 totalIlvl:0,
371 upgrade: {
372 total:0,
373 current:0
374 }
375 };
376
377 // Azerite Info
378 var heartOfAzeroth = "-";
379
380 //thanks to github user bloodrash for this function
381 function numberWithCommas(x)
382 {
383 return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
384 }
385
386 if (toon.items.neck)
387 {
388 if (toon.items.neck.quality===6)
389 {
390 heartOfAzeroth = toon.items.neck.azeriteItem.azeriteLevel + " (" + numberWithCommas(toon.items.neck.azeriteItem.azeriteExperience) + " / " + numberWithCommas(toon.items.neck.azeriteItem.azeriteExperienceRemaining) + ")";
391 }
392 }
393
394
395 var enchantableItems=["mainHand","offHand","finger1","finger2","hands"];
396 var azeriteItems=["head","shoulder","chest"];
397
398
399 var getItemInfo = function (item, slot)
400 {
401 allItems[slot] = {
402 ilvl:"\u2063",
403 power:"",
404 enchant:""
405 };
406
407 if (enchantableItems.indexOf(slot)!=-1) //this is to form the array properly later
408 {
409 allItems[slot].enchant = " ";
410 }
411
412 if (item)
413 {
414 allItems.equippedItems++;
415 allItems[slot].ilvl = item.itemLevel;
416 allItems.totalIlvl += item.itemLevel;
417 if (item.quality === 5 && markLegendary)
418 {
419 allItems[slot].ilvl = allItems[slot].ilvl + "+"; // * can be any character you want, use it for your conditional
420 }
421
422 if (item.id == 167555) //some temporary punch card stuff, not sure how robust this'll be
423 {
424 if (!item.tooltipParams.gem2)
425 {
426 gemAudit[4].bool = 1;
427 }
428 }
429
430 else if (item.itemLevel > CONST_AUDIT_ILVL && item.id != 167555)
431 {
432 if (item.tooltipParams.gem0&&item.quality!=6) // don't check artifacts in case people are still using those!
433 {
434 //if statement set up in descending order for gem IDs
435 //new bfa gems
436 if (item.tooltipParams.gem0 > 169219) //new rare
437 {
438 gemStats[5].value = gemStats[5].value+5;
439 }
440
441 //new epics, thanks for putting stats in a different order again!
442 else if (item.tooltipParams.gem0 === 168639)
443 {
444 gemStats[0].value = gemStats[0].value+60;
445 }
446 else if (item.tooltipParams.gem0 === 168640)
447 {
448 gemStats[3].value = gemStats[3].value+60;
449 }
450 else if (item.tooltipParams.gem0 === 168641)
451 {
452 gemStats[1].value = gemStats[1].value+60;
453 }
454 else if (item.tooltipParams.gem0 === 168642)
455 {
456 gemStats[2].value = gemStats[2].value+60;
457 }
458 else if (item.tooltipParams.gem0 > 168635) //NEW unique epic
459 {
460 gemStats[item.tooltipParams.gem0-168636+6].value = gemStats[item.tooltipParams.gem0-168636+6].value+120;
461 }
462 // older bfa gems
463
464 else if (item.tooltipParams.gem0 > 154125) //rare
465 {
466 gemStats[item.tooltipParams.gem0-154126].value = gemStats[item.tooltipParams.gem0-154126].value+40;
467 }
468 else if (item.tooltipParams.gem0 > 153714) // +move
469 {
470 gemStats[5].value = gemStats[5].value+3;
471 }
472 else if (item.tooltipParams.gem0 > 153713) // xp
473 {
474 gemStats[4].value = gemStats[4].value+5;
475 }
476 else if (item.tooltipParams.gem0 > 153709) //uncommon
477 {
478 gemStats[item.tooltipParams.gem0-153710].value = gemStats[item.tooltipParams.gem0-153710].value+30;
479 }
480 else if (item.tooltipParams.gem0 > 153706) //unique epic (kraken)
481 {
482 gemStats[item.tooltipParams.gem0-153707+6].value = gemStats[item.tooltipParams.gem0-153707+6].value+40;
483 }
484 else
485 {
486 gemStats[9].value = gemStats[9].value + 1;
487 }
488
489 if (audit_lookup[item.tooltipParams.gem0] !=2 && audit_lookup[item.tooltipParams.gem0] != 3)
490 {
491 if (item.itemLevel>CONST_EPICGEM_ILVL)
492 {
493 gemAudit[2].bool = 1;
494 gemAudit[3].bool = 1;
495 gemAudit[3].issue += " "+ slot;
496 }
497 }
498
499 else if (audit_lookup[item.tooltipParams.gem0] === 0)
500 {
501 gemAudit[1].bool = 1;
502 gemAudit[1].issue += " " + slot;
503 }
504 else if (audit_lookup[item.tooltipParams.gem0] != 1 && audit_lookup[item.tooltipParams.gem0] !=2 && audit_lookup[item.tooltipParams.gem0] != 3)
505 {
506 gemAudit[0].bool = 1;
507 gemAudit[0].issue += " " + slot;
508
509 }
510
511
512 totalGems[audit_lookup[item.tooltipParams.gem0]]++;
513 }
514
515 if (enchantableItems.indexOf(slot)!=-1)
516 {
517 if (slot=="offHand" && !toon.items.offHand.weaponInfo)
518 {
519 allItems[slot].enchant = ""; //this is just here to make lint happy, wish i could just stick a ; at the end of that if
520 }
521 else
522 {
523 allItems[slot].enchant= "None";
524 if (item.tooltipParams.enchant)
525 {
526 if (audit_lookup[item.tooltipParams.enchant])
527 {
528 allItems[slot].enchant = audit_lookup[item.tooltipParams.enchant];
529 }
530 else
531 {
532 allItems[slot].enchant = "Old";
533 }
534 }
535 }
536 }
537
538 }
539
540 //unfortunately it doesn't seem like there's any valuable info in the tooltip params even though azerite power appears there
541 if (azeriteItems.indexOf(slot)!=-1)
542 {
543 allItems[slot].power= "-";
544 if (item.azeriteEmpoweredItem)
545 {
546 if (item.azeriteEmpoweredItem.azeritePowers[0])
547 {
548 allItems[slot].power=0;
549 for (j=0; j<item.azeriteEmpoweredItem.azeritePowers.length; j++)
550 {
551 if (item.azeriteEmpoweredItem.azeritePowers[j].spellId > 0)
552 {
553 allItems[slot].power = allItems[slot].power+1;
554 }
555 }
556 if (allItems[slot].ilvl > 369)
557 {
558 if (item.azeriteEmpoweredItem.azeritePowers[4] && item.azeriteEmpoweredItem.azeritePowers[4].spellId != 0)
559 {
560 allItems[slot].power = allItems[slot].power + "/5 unlocked";
561 }
562 else
563 {
564 allItems[slot].power = allItems[slot].power + "/4 unlocked"; // for pre-patch 8.1 items
565 }
566 }
567
568 else if (allItems[slot].ilvl > 339)
569 {
570 allItems[slot].power = allItems[slot].power + "/4 unlocked";
571 }
572 else
573 {
574 allItems[slot].power = allItems[slot].power + "/3 unlocked";
575 }
576 }
577 }
578 }
579 }
580 };
581
582 var sortOrder = [
583 "head",
584 "neck",
585 "shoulder",
586 "back",
587 "chest",
588 "wrist",
589 "hands",
590 "waist",
591 "legs",
592 "feet",
593 "finger1",
594 "finger2",
595 "trinket1",
596 "trinket2",
597 "mainHand",
598 "offHand"
599 ];
600
601 for (i = 0; i < sortOrder.length; i++)
602 {
603 getItemInfo(toon.items[sortOrder[i]],sortOrder[i]);
604 }
605
606
607 var bruksOCDswap = function (item1,item2)
608 {
609 if (allItems[item1].ilvl<allItems[item2].ilvl || allItems[item2].ilvl =="265+" || allItems[item2].ilvl =="240+" )
610 {
611 var swapValue = allItems[item1].ilvl;
612 allItems[item1].ilvl = allItems[item2].ilvl;
613 allItems[item2].ilvl = swapValue;
614 }
615 };
616
617
618 bruksOCDswap("finger1","finger2");
619 bruksOCDswap("trinket1","trinket2");
620
621 // /u/orange_gauss supplied this for fixing the double weight of 2handers
622 if (allItems.offHand.ilvl == "\u2063" )
623 {
624 allItems.totalIlvl += allItems.mainHand.ilvl;
625 allItems.equippedItems += 1;
626 }
627
628 allItems.averageIlvl = allItems.totalIlvl / allItems.equippedItems;
629
630 //fall back to max unequipped ilvl if they're currently partially nude
631 if (isNaN(allItems.averageIlvl))
632 {
633 allItems.averageIlvl = toon.items.averageItemLevel;
634 }
635
636 if (toon.audit.emptySockets !== 0)
637 {
638 auditInfo = auditInfo + "Empty Gem Sockets: " + toon.audit.emptySockets + " ";
639 }
640
641
642 if (totalGems[0]+totalGems[1]+totalGems[2]+totalGems[3]>0) //gems exist!
643 {
644 auditInfo = auditInfo + "Gems" ;
645
646 if (totalGems[0] > 0)
647 {
648 auditInfo = auditInfo + " UnCom:" + totalGems[0];
649 }
650
651 if (totalGems[1] > 0)
652 {
653 auditInfo = auditInfo + " Rare:" + totalGems[1];
654 }
655
656 if (totalGems[3] > 0)
657 {
658 auditInfo = auditInfo + " Epic:" + totalGems[3];
659 }
660
661 if (totalGems[2] > 0)
662 {
663 auditInfo = auditInfo + " PrimeEpic:" + totalGems[2];
664 gemAudit[2] = 0;
665
666 }
667
668 for (i=0; i<gemStats.length; i++)
669 {
670 if (gemStats[i].value > 0)
671 {
672 auditInfo = auditInfo + " +" + gemStats[i].value + gemStats[i].stat;
673 }
674 }
675
676 }
677
678 for (i=0; i<gemAudit.length; i++)
679 {
680 if (gemAudit[i].bool > 0)
681 {
682 if (auditInfo.length > 1) //make things a bit more legible
683 {
684 auditInfo = auditInfo + ", ";
685 }
686 auditInfo = auditInfo + gemAudit[i].issue;
687 }
688 }
689
690
691 // lock out "Weekly checker"
692 var todayStamp =new Date();
693 var today = todayStamp.getDay();
694 var sinceYesterday = 0;
695 var now = todayStamp.getHours();
696 var resetTime = new Date();
697
698 var offset = new Date().getTimezoneOffset();
699 offset=offset/60;
700
701 if (region == "us")
702 {
703 resetTime.setHours(15-offset,0,0,0);
704 }
705 else
706 {
707 resetTime.setHours(7-offset,0,0,0);
708 }
709
710
711 sinceYesterday = resetTime.getTime();
712
713
714 //attempt to fix post-midnight pre-reset
715 if (now < 15-offset && now > -1 && region == "us") //if it's after midnight but before 11am
716 {
717 sinceYesterday-=86400000;
718 }
719
720 if (now < 7-offset && now > -1 && region == "eu") //if it's after midnight but before 7am
721 {
722 sinceYesterday-=86400000;
723 }
724
725
726 // now we have to figure out how long it's been since tuesday
727 var sinceTuesday =new Date();
728
729 var reset = region == "eu" ? 3 : 2; // 2 for tuesday, 3 for wednesday
730
731 var midnight = new Date();
732 midnight.setHours(0,0,0,0);
733
734 sinceTuesday = resetTime*1;
735
736 if (today == reset) //it IS tuesday!
737 {
738 //attempt to fix post-midnight pre-reset
739 if ((now < 7-offset && now > -1 && region == "eu") || (now < 15-offset && now > -1 && region == "us")) //if it's after midnight but before 7am
740 {
741 sinceTuesday-=(86400000*7);
742 }
743 }
744
745 if (today > reset)
746 {
747 // wednesday (thurs eu) - saturday
748 sinceTuesday = sinceTuesday-(today-reset)*86400000;
749 }
750
751 else if (today < reset)
752 {
753 // sunday + monday (tues eu)
754 sinceTuesday = sinceTuesday-((7+today-reset))*86400000; // this was 6, but to account for EU it was changed to 7-reset to be either 6 or 5 to account for Wednesday resets
755 }
756
757 // Raid stat sub-categories
758 var CURRENT_XPAC = 7;
759 var raidInstancesSortOrder = [];
760 var raidDifficultySortOrder = ["Raid Finder", "Normal", "Heroic", "Mythic"];
761 for (i = 40; i <= 43; i++) // BfA raids start at 40, increase i <= when more are released
762 {
763 raidInstancesSortOrder.push(toon.progression.raids[i].name);
764 }
765 var instanceDetails = { "dungeons":{},"raids":{} };
766 for (i in raidInstancesSortOrder)
767 {
768 instanceDetails.raids[raidInstancesSortOrder[i]] = {};
769 }
770 var getShortInstanceName = function (inputString)
771 {
772 var split = inputString.split(" ");
773 if (split.length !== 1)
774 {
775 var retstring = "";
776 for (i in split)
777 {
778 retstring = retstring + split[i].slice(0, 1);
779 }
780 return retstring;
781 }
782 else
783 {
784 return split[0].slice(0,3).toUpperCase();
785 }
786 };
787 var getRaidAndBossName = function(inputString)
788 {
789 var info = "";
790
791 //attempt to get boss name, raid, and difficulty by splitting based on this string
792 if (inputString.indexOf("defeats") !== -1)
793 {
794 info = inputString.split(" defeats (");
795 }
796 else if (inputString.indexOf("redemptions") !== -1)
797 {
798 info = inputString.split(" redemptions (");
799 }
800 else if (inputString.indexOf("defenses") !== -1)
801 {
802 info = inputString.split(" defenses (");
803 }
804 else
805 {
806 info = inputString.split(" kills (");
807 }
808 var bossName = info.shift(); // first we get boss name
809 info = info[0].split(" ");
810 var difficultyName = "";
811 var nameForInstance = "";
812 if (info[0] === "Raid")
813 {
814 difficultyName = info.shift() + " " + info.shift(); // Raid Finder
815 nameForInstance = info.join(" ").slice(0, -1); // rest is the name and we remove the last ")"
816 }
817 else if (info[0] !== "Return")
818 {
819 difficultyName = info.shift(); // first info is what difficultie we have
820 nameForInstance = info.join(" ").slice(0, -1); // rest is the name and we remove the last ")"
821 }
822 else // this should only be Return to Karazhan
823 {
824 difficultyName = "Mythic";
825 nameForInstance = info.join(" ").slice(0, -1); // rest is the name and we remove the last ")"
826 }
827 return [bossName, nameForInstance, difficultyName];
828 };
829 for (var instanceNumber in toon.statistics.subCategories[5].subCategories[CURRENT_XPAC].statistics)
830 {
831 var instanceBoss = toon.statistics.subCategories[5].subCategories[CURRENT_XPAC].statistics[instanceNumber];
832 var instanceReturns = getRaidAndBossName(instanceBoss.name);
833 var bossName = instanceReturns[0];
834 var nameOfInstance = instanceReturns[1];
835 var difficultyName = instanceReturns[2];
836 var typeOfInstance = "Dungeon";
837 for (var raid in raidInstancesSortOrder)// this is needed this as "the" is missing from instances.
838 {
839 if (raidInstancesSortOrder[raid].indexOf(nameOfInstance) !== -1)
840 {
841 nameOfInstance = raidInstancesSortOrder[raid];
842 typeOfInstance = "Raid";
843 }
844 }
845 var thisInstance = typeOfInstance === "Raid" ? instanceDetails.raids : instanceDetails.dungeons;
846 thisInstance[nameOfInstance] = thisInstance[nameOfInstance] || {};
847 thisInstance[nameOfInstance][difficultyName] = thisInstance[nameOfInstance][difficultyName] || {};
848 thisInstance[nameOfInstance][difficultyName].bosses = thisInstance[nameOfInstance][difficultyName].bosses || {};
849
850 var infoForBoss = { "kills": instanceBoss.quantity };
851 if (typeOfInstance === "Dungeon" && difficultyName === "Heroic")
852 {
853 infoForBoss.lockout = instanceBoss.lastUpdated > sinceYesterday;
854 }
855 else if (typeOfInstance !== "Dungeon" || difficultyName !== "Normal")// everything except normal dungeons
856 {
857 infoForBoss.lockout = instanceBoss.lastUpdated > sinceTuesday;
858 }
859 if (nameOfInstance.indexOf("Violet Hold")===-1)
860 {
861 thisInstance[nameOfInstance][difficultyName].bosses[bossName] = infoForBoss;
862 }
863 else
864 {
865 var oldInfo = thisInstance[nameOfInstance][difficultyName].bosses["Violet Hold End Boss"] || {};
866 if (oldInfo.kills)
867 {
868 infoForBoss.kills += oldInfo.kills;
869 infoForBoss.lockout = infoForBoss.lockout || oldInfo.lockout; // since 0 is false and 1 is true this will work.
870 }
871 thisInstance[nameOfInstance][difficultyName].bosses["Violet Hold End Boss"] = infoForBoss;
872 }
873 thisInstance[nameOfInstance][difficultyName].kills = thisInstance[nameOfInstance][difficultyName].kills || 0;
874 thisInstance[nameOfInstance][difficultyName].kills += instanceBoss.quantity;
875 }
876 var displayInfo = { "raid": {}, "dungeon": {} };
877 for (var instanceType in instanceDetails)
878 {
879 var instances = instanceDetails[instanceType];
880 var infoOnDifficulty = {};
881 for (var instanceName in instances)
882 {
883 var instance = instances[instanceName];
884 if (instanceType === "raids") // for dungeons we take lockout for all instances, for raid we do it for each instance.
885 {
886 infoOnDifficulty = {};
887 }
888 for (var difficulty in instance)
889 {
890 infoOnDifficulty[difficulty] = infoOnDifficulty[difficulty] || {
891 "activeWeeks":0, "lockout":0, "instanceLength": 0, "progress": 0, "kills": 0
892 };
893 var thisDifficulty = infoOnDifficulty[difficulty];
894 var bosses = instance[difficulty].bosses;
895 for (var boss in bosses)
896 {
897 var bossInfo = bosses[boss];
898 thisDifficulty.activeWeeks = Math.max(thisDifficulty.activeWeeks, bossInfo.kills);
899 thisDifficulty.instanceLength++;
900 thisDifficulty.kills += bossInfo.kills;
901 thisDifficulty.progress += bossInfo.kills === 0 ? 0 : 1;
902 thisDifficulty.lockout += bossInfo.lockout ? 1 : 0;
903 if (instanceType === "dungeons" && difficulty === "Mythic" && bossInfo.lockout)
904 {
905 thisDifficulty.details = thisDifficulty.details ? thisDifficulty.details + ", " + getShortInstanceName(instanceName) : getShortInstanceName(instanceName);
906 }
907 }
908
909 // really didn't want to do it this way, but... Daz needs special code to deal with alliance/horde having different entries in stats
910 if (instanceName == "Battle of Dazar'alor")
911 {
912 thisDifficulty.instanceLength = 9;
913 }
914 }
915 if (instanceType === "raids")
916 {
917 displayInfo.raid[instanceName] = infoOnDifficulty;
918 }
919 }
920 if (instanceType === "dungeons")
921 {
922 displayInfo.dungeon = infoOnDifficulty;
923 }
924 }
925
926
927 //code for displaying missing mythics instead of completed, needs updating of more mythics are added
928 var missingMythics ="Missing: ";
929
930 // remove the undefined if 0 completed, or the "Missing: " if all completed (increment this if more dungeons added)
931 if (!displayInfo.dungeon.Mythic.details || displayInfo.dungeon.Mythic.lockout ==10)
932 {
933 displayInfo.dungeon.Mythic.details = "";
934 missingMythics = "";
935 }
936
937 else if (listMissing == true)
938 {
939 var mythicList = ["ATA", "FRE", "KR", "SotS", "SoB", "ToS", "TM", "TU", "TD", "WM"]; //add abbrvs to list if more are added
940 for (i=0; i<mythicList.length; i++)
941 {
942 var n=displayInfo.dungeon.Mythic.details.search(mythicList[i]);
943 if (n==-1)
944 {
945 missingMythics = missingMythics + mythicList[i] + " ";
946 }
947 }
948 displayInfo.dungeon.Mythic.details = missingMythics;
949 }
950
951 var worldBosses = [52196, 52163, 52169, 52181, 52157, 52166];
952
953 var worldBossKill = "";
954 var warfront = "";
955 var islandExpeditions = "";
956
957 for (i=0; i < toon.quests.length; i++)
958 {
959 if (toon.quests[i] == 56057)
960 {
961 worldBossKill = worldBossKill + "Soulbinder: \u2713 ";
962 }
963 if (toon.quests[i] == 56056)
964 {
965 worldBossKill = worldBossKill + "Terror: \u2713 ";
966 }
967 if (toon.quests[i] == 54895 || toon.quests[i] == 54896)
968 {
969 worldBossKill = worldBossKill + "Ivus: \u2713 ";
970 }
971 if (toon.quests[i] == 52847 || toon.quests[i] == 52848)
972 {
973 worldBossKill = worldBossKill + "Tank: \u2713 ";
974 }
975 if (worldBosses.indexOf(toon.quests[i]) > -1)
976 {
977 worldBossKill = worldBossKill + "Weekly: \u2713 "; //unicode checkmark
978 }
979 if (toon.quests[i] == 53414 || toon.quests[i] == 53416)
980 {
981 warfront = warfront + "Stormgarde: \u2713 ";
982 }
983 if (toon.quests[i] == 53955 || toon.quests[i] == 53992)
984 {
985 warfront = warfront + "Darkshore: \u2713 ";
986 }
987 if (toon.quests[i] == 53435 || toon.quests[i] == 53436)
988 {
989 islandExpeditions = "\u2713 ";
990 }
991 }
992
993
994 var profession1 = "none";
995 var profession2 = "none";
996 var prof1Icon = "none";
997 var prof2Icon = "none";
998 var proftemp = "0";
999
1000 var prof1array = ["-", "-", "-", "-", "-", "-", "-", "-"];
1001 var prof2array = ["-", "-", "-", "-", "-", "-", "-", "-"];
1002
1003
1004 var prof_lookup = {};
1005
1006 prof_lookup.Kul = 7;
1007 prof_lookup.Zandalari = 7;
1008 prof_lookup.Legion = 6;
1009 prof_lookup.Draenor = 5;
1010 prof_lookup.Pandaria = 4;
1011 prof_lookup.Cataclysm = 3;
1012 prof_lookup.Northrend = 2;
1013 prof_lookup.Outland = 1;
1014 prof_lookup[0] = 0;
1015
1016 for ( i = 0; i < toon.professions.primary.length; i++)
1017 {
1018
1019 if (prof1Icon == "none" || prof1Icon == toon.professions.primary[i].icon)
1020 {
1021 prof1Icon = toon.professions.primary[i].icon;
1022 if (toon.professions.primary[i].id < 900)
1023 {
1024 profession1 = toon.professions.primary[i].name;
1025 proftemp[0] = 0;
1026 }
1027 else
1028 {
1029 proftemp = toon.professions.primary[i].name.split(" ");
1030 }
1031
1032 if (toon.professions.primary[i].rank >= toon.professions.primary[i].max)
1033 {
1034 prof1array[prof_lookup[proftemp[0]]]= "\u2713";
1035 }
1036 else
1037 {
1038 prof1array[prof_lookup[proftemp[0]]]= toon.professions.primary[i].rank;
1039 }
1040 }
1041
1042 else if (prof2Icon == "none" || prof2Icon == toon.professions.primary[i].icon)
1043 {
1044 prof2Icon = toon.professions.primary[i].icon;
1045 if (toon.professions.primary[i].id < 900)
1046 {
1047 profession2 = toon.professions.primary[i].name;
1048 proftemp[0] = 0;
1049 }
1050 else
1051 {
1052 proftemp = toon.professions.primary[i].name.split(" ");
1053 }
1054 if (toon.professions.primary[i].rank >= toon.professions.primary[i].max)
1055 {
1056 prof2array[prof_lookup[proftemp[0]]]= "\u2713";
1057 }
1058 else
1059 {
1060 prof2array[prof_lookup[proftemp[0]]]= toon.professions.primary[i].rank;
1061 }
1062 }
1063 }
1064
1065 profession1 = profession1 + " " + prof1array;
1066 profession2 = profession2 + " " + prof2array;
1067
1068 // IDs for mythic+ were provied by @matdemy on twitter and this post: http://us.battle.net/forums/en/bnet/topic/20752275890
1069 var mythicPlus = "";
1070 for (i=0; i<toon.achievements.criteria.length; i++)
1071 {
1072 switch (toon.achievements.criteria[i])
1073 {
1074 case (33096):
1075 mythicPlus = mythicPlus + "m+2: " + toon.achievements.criteriaQuantity[i];
1076 break;
1077
1078 case (33097):
1079 mythicPlus = mythicPlus + " m+5: " + toon.achievements.criteriaQuantity[i];
1080 break;
1081
1082 case (33098):
1083 mythicPlus = mythicPlus + " m+10: " + toon.achievements.criteriaQuantity[i];
1084 break;
1085
1086 case (32028):
1087 mythicPlus = mythicPlus + " m+15: " + toon.achievements.criteriaQuantity[i];
1088 break;
1089
1090 default:
1091 break;
1092 }
1093 }
1094
1095
1096 var reps = [
1097 { "id":2164, "text":"" },//Champions of Azeroth
1098 { "id":2163, "text":"" },//Tortollan Seekers
1099 { "id":2391, "text":"" },//Rustbolt Resistance
1100 // alliance factions
1101 { "id":2160, "text":"" },//Proudmoore Admiralty
1102 { "id":2161, "text":"" },//Order of Embers
1103 { "id":2162, "text":"" },//Storm's Wake
1104 { "id":2159, "text":"" },//7th Legion
1105 { "id":2400, "text":"" },//Waveblade Ankoan
1106 // horde factions
1107 { "id":2103, "text":"" },//Zandalari Empire
1108 { "id":2156, "text":"" },//Talanji's Expedition
1109 { "id":2158, "text":"" },//Voldunai
1110 { "id":2157, "text":"" },//The Honorbound
1111 { "id":2373, "text":"" },//Unshackled
1112
1113 ];
1114
1115 var repStanding = {
1116 0:"Hated",
1117 1:"Hostile",
1118 2:"Unfriendly",
1119 3:"Neutral",
1120 4:"Friendly",
1121 5:"Honored",
1122 6:"Revered",
1123 7:"Exalted",
1124 };
1125
1126
1127 var x = 0;
1128 var reputations = []; //our output array
1129 for (i = 0; i<toon.reputation.length; i++)
1130 {
1131
1132 for (var j = 0; j < reps.length; j++)
1133 {
1134 if (toon.reputation[i].id == reps[j].id && toon.reputation[i].standing > 2)
1135 {
1136 reputations.push(toon.reputation[i].name + " - " + repStanding[toon.reputation[i].standing] + " " + toon.reputation[i].value + "/" + toon.reputation[i].max);
1137 x=x+1;
1138 }
1139 }
1140 }
1141 //keep the array the proper size, so that if we haven't met a faction our columns are still nice (also reps are broken for dark iron and maghar, blizz plz fix)
1142 while (reputations.length < 8) //adjust this int if more reputations are added
1143 {
1144 reputations.push(" ");
1145 }
1146
1147 // Preforming the outputs so they're easier to move around in the output array
1148 var heroicLockouts = displayInfo.dungeon.Heroic.lockout + "/" + displayInfo.dungeon.Heroic.instanceLength;
1149 var heroicProgress = displayInfo.dungeon.Heroic.progress + "/" + displayInfo.dungeon.Heroic.instanceLength + " (" + displayInfo.dungeon.Heroic.kills + ")";
1150 var mythicLockouts = displayInfo.dungeon.Mythic.lockout + "/" + displayInfo.dungeon.Mythic.instanceLength + " " + displayInfo.dungeon.Mythic.details;
1151 var mythicProgress = displayInfo.dungeon.Mythic.progress + "/" + displayInfo.dungeon.Mythic.instanceLength + " (" + displayInfo.dungeon.Mythic.kills + ") ";// + mythicPlus,
1152
1153
1154 //output arrays
1155 var gearIlvl= [];
1156 var azeritePower = [];
1157 var enchants = [];
1158
1159 for (i = 0; i<sortOrder.length; i++)
1160 {
1161 gearIlvl[i] = allItems[sortOrder[i]].ilvl;
1162 if (allItems[sortOrder[i]].power)
1163 {
1164 azeritePower.push(allItems[sortOrder[i]].power);
1165 }
1166 if (i < enchantableItems.length)
1167 {
1168 enchants.push(allItems[enchantableItems[i]].enchant);
1169 }
1170
1171 }
1172
1173 var raidArray=[];
1174
1175 for (i = 0; i < raidInstancesSortOrder.length; i++)
1176 {
1177 for (var k = 0; k < raidDifficultySortOrder.length; k++)
1178 {
1179 var cellInfo = displayInfo.raid[raidInstancesSortOrder[i]][raidDifficultySortOrder[k]];
1180 raidArray[ i*8+k] = cellInfo.lockout + "/" + cellInfo.instanceLength;
1181 }
1182 for (k = 0; k < raidDifficultySortOrder.length; k++)
1183 {
1184 var secondCellInfo = displayInfo.raid[raidInstancesSortOrder[i]][raidDifficultySortOrder[k]];
1185 raidArray[i*8+k+4] = secondCellInfo.progress + "/" + secondCellInfo.instanceLength + " [" + secondCellInfo.activeWeeks + "] (" + secondCellInfo.kills + ")";
1186 }
1187 }
1188
1189
1190 if (raiderIO == true)
1191 {
1192 var raiderJSON = UrlFetchApp.fetch("https://raider.io/api/v1/characters/profile?region="+region+"&realm="+realmName+"&name="+toonName+"&fields=mythic_plus_highest_level_runs,mythic_plus_scores,mythic_plus_weekly_highest_level_runs", options);
1193 var raider = JSON.parse(raiderJSON.toString());
1194
1195 if (!raider.statusCode)
1196 {
1197 if (raider.mythic_plus_weekly_highest_level_runs[0])
1198 {
1199 mythicLockouts = mythicLockouts + " weekly highest M+: " + raider.mythic_plus_weekly_highest_level_runs[0].mythic_level;
1200 }
1201 if (raider.mythic_plus_highest_level_runs[0])
1202 {
1203 mythicProgress = mythicProgress + " highest BfA M+: " + raider.mythic_plus_highest_level_runs[0].mythic_level;
1204 }
1205 if (raider.mythic_plus_scores)
1206 {
1207 mythicProgress = mythicProgress + " Score: " + raider.mythic_plus_scores.all;
1208 }
1209 }
1210 }
1211
1212
1213 function wlogs ()
1214 {
1215 if (!toonName || !realmName)
1216 {
1217 return " "; // If there's nothing in the column, don't even bother calling the API
1218 }
1219 if (!warcraftlogskey)
1220 {
1221 return "Error: No API key entered. Please visit https://www.warcraftlogs.com/profile to obtain one.";
1222 }
1223
1224 toonName = toonName.replace(/\s/g, "");
1225 region = region.replace(/\s/g, "");
1226 realmName = realmName.replace("'", ""); //remove 's
1227 realmName = realmName.replace(/\s/g, "-"); //replace space with -
1228
1229 var logs = "-";
1230
1231 var fetchLogs = UrlFetchApp.fetch("https://www.warcraftlogs.com/v1/rankings/character/"+toonName+"/"+realmName+"/"+region+"?metric=dps&timeframe=historical&api_key="+warcraftlogskey+"", options);
1232 logs = JSON.parse(fetchLogs.toString());
1233
1234 if (!logs[0])
1235 {
1236 var errorArray = ["No logs", " ", ""];
1237 return errorArray;
1238 }
1239
1240 //check to see if the most recent log was a healing one.. if so, we're going to REALLY HOPE this is a full time healer
1241 else if (logs[0].spec == "Restoration" || logs[0].spec == "Mistweaver" || logs[0].spec == "Holy" || logs[0].spec == "Discipline")
1242 {
1243 fetchLogs = UrlFetchApp.fetch("https://www.warcraftlogs.com/v1/rankings/character/"+toonName+"/"+realmName+"/"+region+"?metric=hps&timeframe=historical&api_key="+warcraftlogskey+"", options);
1244 logs = JSON.parse(fetchLogs.toString());
1245 }
1246
1247
1248 if (logs.class || logs.status)
1249 {
1250 return "API Error, check if your API key is entered properly and that the API is working, check character on Armory to see if it loads";
1251 }
1252
1253 var difficultyCounter = [0, 0, 0, 0, 0, 0];
1254 var difficultySums = [0, 0, 0, 0, 0, 0];
1255
1256 for (i=0; i<logs.length; i++)
1257 {
1258 if (logs[i].difficulty < 6)
1259 {
1260 difficultyCounter[logs[i].difficulty] = difficultyCounter[logs[i].difficulty]+1;
1261 difficultySums[logs[i].difficulty] = difficultySums[logs[i].difficulty] + logs[i].percentile;
1262 }
1263 }
1264
1265 //this is to prevent /0 and making an icky output
1266 for (i=0; i<difficultyCounter.length; i++)
1267 {
1268 if (difficultyCounter[i] == 0)
1269 {
1270 difficultyCounter[i] = 1;
1271 }
1272 }
1273
1274 var logInfo = [
1275 difficultySums[3]/difficultyCounter[3],
1276 difficultySums[4]/difficultyCounter[4],
1277 difficultySums[5]/difficultyCounter[5]
1278 ];
1279 return logInfo;
1280 }
1281
1282 if (warcraftlogskey)
1283 {
1284 warcraftLogs = wlogs(region,realmName,toonName);
1285 }
1286
1287 var toonInfo = [
1288
1289 toon_class,
1290 toon.level,
1291 mainspec,
1292 allItems.averageIlvl,
1293
1294 gearIlvl,
1295
1296 heartOfAzeroth,
1297 azeritePower,
1298 enchants,
1299 auditInfo,
1300
1301 worldBossKill,
1302 raidArray,
1303 heroicLockouts,
1304 heroicProgress,
1305 mythicLockouts,
1306 mythicProgress,
1307
1308
1309 profession1,
1310 profession2,
1311
1312 thumbnail,
1313 armory,
1314
1315 reputations,
1316
1317
1318 ];
1319
1320
1321 function flatten(input)
1322 {
1323 var flatter = [];
1324 for (i =0; i < input.length; i++)
1325 {
1326 if (Array.isArray(input[i]))
1327 {
1328 for (j=0; j<input[i].length; j++)
1329 {
1330 flatter.push(input[i][j]);
1331 }
1332 }
1333 else
1334 {
1335 flatter.push(input[i]);
1336 }
1337 }
1338 return flatter;
1339 }
1340
1341 toonInfo = flatten(toonInfo);
1342 return toonInfo;
1343}
1344
1345function vercheck()
1346{
1347 return current_version;
1348}