├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── deploy ├── deploy.sh └── web.filter ├── dev.sh ├── generate.js ├── howto.txt ├── nginx ├── commonHeader.conf ├── commonNoFavicon.conf ├── dev.heroesjson.com.conf └── heroesjson.com.conf ├── package.json ├── sandbox ├── cascextract │ ├── .gitignore │ ├── CMakeLists.txt │ └── src │ │ ├── CMakeLists.txt │ │ ├── cascextract.c │ │ ├── cascextract.h │ │ ├── main.c │ │ └── version.h ├── info.txt ├── legacy │ └── heroes.pegjs └── scratchpad.txt ├── shared └── C.js ├── util ├── calc.js └── compareRelease.js └── web ├── .gitignore ├── changelog.json ├── conform.styl ├── generate.js ├── index.dust ├── index.styl └── static ├── rainbow-custom.min.js ├── robots.txt └── solarized-dark.css /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | heroesjson.sublime-project 3 | heroesjson.sublime-workspace 4 | node_modules 5 | CascLib-build 6 | wip 7 | out 8 | images 9 | .DS_Store 10 | dist/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CASCExtractor"] 2 | path = CASCExtractor 3 | url = https://github.com/Kanma/CASCExtractor.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Justin J. Novack 2 | 3 | Permission to use, copy, modify, and/or distribute this software for 4 | any purpose with or without fee is hereby granted, provided that the 5 | above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | json: 2 | node generate.js "/Applications/Heroes of the Storm/" 3 | 4 | diff: 5 | json-diff -C ~/Source/www.heroesjson.com/heroes.json out/heroes.json | less -r 6 | 7 | website: 8 | cd web/; node generate.js "/Applications/Heroes of the Storm/" dev 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # heroesjson 2 | 3 | This project extracts data from Heroes of the Storm data files into JSON files. 4 | 5 | This is then used to generate the website: [http://heroesjson.com](http://heroesjson.com) 6 | 7 | ## Use 8 | 9 | This project meant to run on Linux or Mac OSX. To run you need: 10 | * nodejs 11 | * git 12 | * cmake 13 | 14 | ### Build 15 | 16 | git clone https://github.com/nydus/heroesjson.git 17 | cd heroesjson 18 | ./build.sh 19 | 20 | ### Run 21 | 22 | node generate.js /path/to/heroes/install/dir 23 | 24 | SEE howto.txt FOR MORE DETAILS ON HOW TO RUN 25 | 26 | ### Results 27 | 28 | The resulting JSON files will be in the 'out' directory. 29 | 30 | 31 | ## Credits 32 | 33 | Thank you to the original owner and creator [Sembiance](https://www.github.com/Sembiance) 34 | for the initial project. 35 | 36 | 37 | ## Contributing 38 | 39 | Please do. I have very limited time, and agreed to take over this project just so that 40 | it still breathes live. However, I have many other projects and this one, unfortunately, 41 | falls to the back of the line. There are plenty of [issues](https://github.com/nydus/heroesjson/issues) 42 | to tackle! -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Geting CASCExtractor..." 4 | git submodule update --init --recursive 5 | 6 | echo "Building CASCExtractor..." 7 | mkdir build 8 | cd build 9 | cmake ../CASCExtractor 10 | make 11 | 12 | echo "Installing NPM modules..." 13 | cd ../ 14 | npm install 15 | 16 | echo "Linking shared JS..." 17 | cd node_modules 18 | ln -s ../shared/C.js 19 | 20 | echo "Done." 21 | -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rsync -f "merge web.filter" --delete --delete-excluded -avL ../web/ sandbox:/srv/heroesjson.com/ 3 | scp ../nginx/heroesjson.com.conf sandbox:/srv/ 4 | -------------------------------------------------------------------------------- /deploy/web.filter: -------------------------------------------------------------------------------- 1 | - .gitignore 2 | - generate.js 3 | - *.styl 4 | - *.dust 5 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | node generate.js "/mnt/compendium/data/Heroes of the Storm" dev 4 | node web/generate "/mnt/compendium/data/Heroes of the Storm" dev 5 | node util/compareRelease.js 6 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var base = require("@sembiance/xbase"), 4 | fs = require("fs"), 5 | path = require("path"), 6 | runUtil = require("@sembiance/xutil").run, 7 | fileUtil = require("@sembiance/xutil").file, 8 | moment = require("moment"), 9 | jsen = require("jsen"), 10 | jsonselect = require("JSONSelect"), 11 | libxmljs = require("libxmljs"), 12 | C = require("C"), 13 | rimraf = require("rimraf"), 14 | tiptoe = require("tiptoe"); 15 | 16 | var HOTS_PATH = process.argv[2] || "/Applications/Heroes\ of\ the\ Storm"; 17 | var HOTS_LANG = process.argv[3] || "enus"; 18 | 19 | if(!fs.existsSync(HOTS_PATH)) 20 | { 21 | base.error("Usage: node generate.js [/path/to/hots] [language]"); 22 | process.exit(1); 23 | } 24 | 25 | var HOTS_DATA_PATH = path.join(HOTS_PATH, "HeroesData"); 26 | 27 | if(!fs.existsSync(HOTS_DATA_PATH)) 28 | { 29 | base.error("HeroesData dir not found: %s", HOTS_DATA_PATH); 30 | process.exit(1); 31 | } 32 | 33 | var CASCEXTRATOR_PATH = path.join(__dirname, "build", "bin", "CASCExtractor"); 34 | 35 | var OUT_PATH = path.join(__dirname, "out"); 36 | 37 | var HEROES_OUT_PATH = path.join(OUT_PATH, "heroes.json"); 38 | var MOUNTS_OUT_PATH = path.join(OUT_PATH, "mounts.json"); 39 | 40 | var DEFAULT_NODES = {}; 41 | var NODE_MAPS = {}; 42 | var NODE_MAP_TYPES = ["Hero", "Talent", "Behavior", "Effect", "Abil", "Unit", "Validator", "Weapon", "Button", "Mount", "Actor", "Accumulator" ]; 43 | var NODE_MAP_PREFIX_TYPES = ["Actor"]; 44 | 45 | var NODE_MERGE_PARENT_TYPES = ["Mount"]; 46 | 47 | var HERO_LEVEL_SCALING_MODS = {}; 48 | 49 | var NEEDED_SUBFIXES = [ HOTS_LANG + ".stormdata\\LocalizedData\\GameStrings.txt" ]; 50 | NODE_MAP_TYPES.forEach(function(NODE_MAP_TYPE) 51 | { 52 | NODE_MAPS[NODE_MAP_TYPE] = {}; 53 | NEEDED_SUBFIXES.push("base.stormdata\\GameData\\" + NODE_MAP_TYPE.toProperCase() + "Data.xml"); 54 | }); 55 | 56 | var NEEDED_PREFIXES = ["heroesdata.stormmod"]; 57 | C.EXTRA_HEROES_HEROMODS.forEach(function(EXTRA_HERO) 58 | { 59 | NEEDED_PREFIXES.push("heromods\\" + EXTRA_HERO + ".stormmod"); 60 | }); 61 | 62 | var NEEDED_FILE_PATHS = [ 63 | "mods/core.stormmod/base.stormdata/DataBuildId.txt" 64 | ]; 65 | 66 | NEEDED_PREFIXES.forEach(function(NEEDED_PREFIX) 67 | { 68 | NEEDED_SUBFIXES.forEach(function(NEEDED_SUBFIX) 69 | { 70 | NEEDED_FILE_PATHS.push("mods\\" + NEEDED_PREFIX + "\\" + NEEDED_SUBFIX); 71 | }); 72 | }); 73 | 74 | C.EXTRA_HEROES_GAMEDATA_FILES.forEach(function(EXTRA_HERO) 75 | { 76 | NEEDED_FILE_PATHS.push("mods\\heroesdata.stormmod\\base.stormdata\\GameData\\Heroes\\" + EXTRA_HERO + "Data.xml"); 77 | }); 78 | 79 | C.EXTRA_HEROES_GAMEDATA_FOLDERS.forEach(function(EXTRA_HERO) 80 | { 81 | NEEDED_FILE_PATHS.push("mods\\heroesdata.stormmod\\base.stormdata\\GameData\\Heroes\\" + EXTRA_HERO + "Data\\" + EXTRA_HERO + "Data.xml"); 82 | }); 83 | 84 | Object.forEach(C.EXTRA_MOUNT_DATA_FILES, function(EXTRA_MOUNT_DIR, EXTRA_MOUNT_FILES) 85 | { 86 | EXTRA_MOUNT_FILES.forEach(function(EXTRA_MOUNT_FILE) 87 | { 88 | NEEDED_FILE_PATHS.push("mods\\heroesdata.stormmod\\base.stormdata\\GameData\\Mounts\\" + EXTRA_MOUNT_DIR + "Data\\Mount_" + EXTRA_MOUNT_FILE + "Data.xml"); 89 | }); 90 | }); 91 | 92 | Object.forEach(C.EXTRA_HEROES_HEROMODS_NAMED, function(heroName, gameDataName) 93 | { 94 | NEEDED_FILE_PATHS.push("mods\\heromods\\" + heroName + ".stormmod\\base.stormdata\\GameData\\" + gameDataName + "Data.xml"); 95 | NEEDED_FILE_PATHS.push("mods\\heromods\\" + heroName + ".stormmod\\base.stormdata\\GameData\\HeroData.xml"); 96 | NEEDED_FILE_PATHS.push("mods\\heromods\\" + heroName + ".stormmod\\"+HOTS_LANG+".stormdata\\LocalizedData\\GameStrings.txt"); 97 | }); 98 | 99 | NEEDED_FILE_PATHS = NEEDED_FILE_PATHS.concat(C.EXTRA_XML_FILE_PATHS); 100 | 101 | var S = {}; 102 | var IGNORED_NODE_TYPE_IDS = {"Hero" : ["Random", "AI", "_Empty", "LegacyVOHero", "TestHero"]}; 103 | 104 | tiptoe( 105 | function clearOut() 106 | { 107 | if(process.argv[3]==="dev") 108 | return this(); 109 | 110 | base.info("Clearing 'out' directory..."); 111 | rimraf(OUT_PATH, this); 112 | }, 113 | function createOut() 114 | { 115 | if(process.argv[3]==="dev") 116 | return this(); 117 | 118 | fs.mkdir(OUT_PATH, this); 119 | }, 120 | function copyBuildInfo() 121 | { 122 | if(process.argv[3]==="dev") 123 | return this(); 124 | 125 | base.info("Copying latest .build.info file..."); 126 | fileUtil.copy(path.join(HOTS_PATH, ".build.info"), path.join(HOTS_DATA_PATH, ".build.info"), this); 127 | }, 128 | function extractFiles() 129 | { 130 | if(process.argv[3]==="dev") 131 | return this(); 132 | 133 | base.info("Extracting %d needed files...", NEEDED_FILE_PATHS.length); 134 | NEEDED_FILE_PATHS.parallelForEach(function(NEEDED_FILE_PATH, subcb) 135 | { 136 | runUtil.run(CASCEXTRATOR_PATH, [HOTS_DATA_PATH, "-o", OUT_PATH, "-f", NEEDED_FILE_PATH], {silent:true}, subcb); 137 | }, this, 10); 138 | }, 139 | function loadDataAndSaveJSON() 140 | { 141 | var xmlDocs = []; 142 | 143 | base.info("Loading data..."); 144 | NEEDED_FILE_PATHS.forEach(function(NEEDED_FILE_PATH) 145 | { 146 | var diskPath = path.join(OUT_PATH, NEEDED_FILE_PATH.replaceAll("\\\\", "/")); 147 | if(!fs.existsSync(diskPath)) 148 | { 149 | //base.info("Missing file: %s", NEEDED_FILE_PATH); 150 | return; 151 | } 152 | var fileData = fs.readFileSync(diskPath, {encoding:"utf8"}); 153 | if(diskPath.endsWith("GameStrings.txt")) 154 | { 155 | fileData.split("\n").forEach(function(line) { 156 | S[line.substring(0, line.indexOf("="))] = line.substring(line.indexOf("=")+1).trim(); 157 | }); 158 | } 159 | else if(diskPath.endsWith(".xml")) 160 | { 161 | xmlDocs.push(libxmljs.parseXml(fileData)); 162 | } 163 | }); 164 | 165 | loadMergedNodeMap(xmlDocs); 166 | 167 | mergeNodeParents(); 168 | 169 | base.info("\nProcessing heroes..."); 170 | var heroes = Object.values(NODE_MAPS["Hero"]).map(function(heroNode) { return processHeroNode(heroNode); }).filterEmpty(); 171 | heroes.sort(function(a, b) { return (a.name.startsWith("The ") ? a.name.substring(4) : a.name).localeCompare((b.name.startsWith("The ") ? b.name.substring(4) : b.name)); }); 172 | 173 | base.info("\nValidating %d heroes...", heroes.length); 174 | heroes.forEach(validateHero); 175 | 176 | base.info("\nProcessing mounts..."); 177 | var mounts = Object.values(NODE_MAPS["Mount"]).map(function(mountNode) { return processMountNode(mountNode); }).filterEmpty(); 178 | 179 | base.info("\nValidating %d mounts...", mounts.length); 180 | mounts.forEach(validateMount); 181 | mounts.sort(function(a, b) { return (a.name.startsWith("The ") ? a.name.substring(4) : a.name).localeCompare((b.name.startsWith("The ") ? b.name.substring(4) : b.name)); }); 182 | 183 | base.info("\nSaving JSON..."); 184 | 185 | fs.writeFile(HEROES_OUT_PATH, JSON.stringify(heroes), {encoding:"utf8"}, this.parallel()); 186 | fs.writeFile(MOUNTS_OUT_PATH, JSON.stringify(mounts), {encoding:"utf8"}, this.parallel()); 187 | }, 188 | function finish(err) 189 | { 190 | if(err) 191 | { 192 | base.error(err); 193 | process.exit(1); 194 | } 195 | 196 | base.info("Done."); 197 | 198 | process.exit(0); 199 | } 200 | ); 201 | 202 | function processMountNode(mountNode) 203 | { 204 | var mount = {}; 205 | mount.id = attributeValue(mountNode, "id"); 206 | mount.attributeid = getValue(mountNode, "AttributeId"); 207 | mount.name = S["Mount/Name/" + mount.id]; 208 | 209 | if(!mount.name) { 210 | return undefined; 211 | } 212 | 213 | mount.variation = (+getValue(mountNode, "Flags[@index='IsVariation']", 0) === 1) ? true : false; 214 | 215 | mount.description = S["Mount/Info/" + mount.id]; 216 | // Some mounts share info with their model parent 217 | if(!mount.description && S["Mount/Info/" + getValue(mountNode, "Model")]) { 218 | mount.description = S["Mount/Info/" + getValue(mountNode, "Model")]; 219 | } 220 | 221 | mount.franchise = getValue(mountNode, "Universe"); 222 | mount.releaseDate = processReleaseDate(mountNode.get("ReleaseDate")); 223 | mount.productid = getValue(mountNode, "ProductId"); 224 | mount.category = getValue(mountNode, "MountCategory"); 225 | if(mount.productid) 226 | mount.productid = +mount.productid; 227 | 228 | performMountModifications(mount); 229 | 230 | return mount; 231 | } 232 | 233 | function processHeroNode(heroNode) 234 | { 235 | var hero = {}; 236 | 237 | // Core hero data 238 | hero.id = attributeValue(heroNode, "id"); 239 | 240 | if(C.SKIP_HERO_IDS.contains(hero.id)) 241 | return; 242 | 243 | hero.attributeid = getValue(heroNode, "AttributeId"); 244 | hero.name = S["Unit/Name/" + getValue(heroNode, "Unit", "Hero" + hero.id)] || S[getValue(heroNode, "Name")]; 245 | 246 | if(!hero.name) 247 | { 248 | base.info(heroNode.toString()); 249 | throw new Error("Failed to get name for hero: " + hero.id); 250 | } 251 | 252 | base.info("Processing hero: %s (%s)", hero.name, hero.id); 253 | hero.title = S["Hero/Title/" + hero.id]; 254 | hero.description = S["Hero/Description/" + hero.id]; 255 | 256 | hero.icon = "ui_targetportrait_hero_" + (C.HERO_ID_TEXTURE_RENAMES.hasOwnProperty(hero.id) ? C.HERO_ID_TEXTURE_RENAMES[hero.id] : hero.id) + ".dds"; 257 | 258 | hero.role = getValue(heroNode, "Role"); 259 | if(hero.role==="Damage") 260 | hero.role = "Assassin"; 261 | if(!hero.role) 262 | hero.role = "Warrior"; 263 | 264 | hero.type = !!getValue(heroNode, "Melee") ? "Melee" : "Ranged"; 265 | hero.gender = getValue(heroNode, "Gender", "Male"); 266 | hero.franchise = getValue(heroNode, "Universe", "Starcraft"); 267 | hero.difficulty = getValue(heroNode, "Difficulty", "Easy"); 268 | if(hero.difficulty==="VeryHard") 269 | hero.difficulty = "Very Hard"; 270 | 271 | var ratingsNode = heroNode.get("Ratings"); 272 | if(ratingsNode) 273 | { 274 | hero.ratings = 275 | { 276 | damage : +getValue(ratingsNode, "Damage", attributeValue(ratingsNode, "Damage", 1)), 277 | utility : +getValue(ratingsNode, "Utility", attributeValue(ratingsNode, "Utility", 1)), 278 | survivability : +getValue(ratingsNode, "Survivability", attributeValue(ratingsNode, "Survivability", 1)), 279 | complexity : +getValue(ratingsNode, "Complexity", attributeValue(ratingsNode, "Complexity", 1)), 280 | }; 281 | } 282 | 283 | hero.releaseDate = processReleaseDate(heroNode.get("ReleaseDate")); 284 | 285 | var heroUnitids = [ hero.id ]; 286 | var alternateUnitArrayNodes = heroNode.find("AlternateUnitArray"); 287 | if(alternateUnitArrayNodes && alternateUnitArrayNodes.length>0) 288 | { 289 | alternateUnitArrayNodes.forEach(function(alternateUnitArrayNode) 290 | { 291 | var alternateHeroid = attributeValue(alternateUnitArrayNode, "value"); 292 | base.info("Alternate: ", alternateHeroid); 293 | heroUnitids.push(alternateHeroid); 294 | }); 295 | } 296 | 297 | heroUnitids = heroUnitids.concat(C.ADDITIONAL_HERO_SUBUNIT_IDS[hero.id] || []); 298 | base.info("Sub-units:", hero.id, heroUnitids); 299 | 300 | // Level Scaling Info 301 | HERO_LEVEL_SCALING_MODS[hero.id] = []; 302 | addHeroLevelScalingMods(hero.id, DEFAULT_NODES["Hero"]); 303 | addHeroLevelScalingMods(hero.id, heroNode); 304 | 305 | // Hero Stats 306 | hero.stats = {}; 307 | heroUnitids.forEach(function(heroUnitid) 308 | { 309 | hero.stats[heroUnitid] = getHeroStats(heroUnitid); 310 | if(Object.keys(hero.stats[heroUnitid]).length===0) 311 | delete hero.stats[heroUnitid]; 312 | }); 313 | 314 | // Abilities 315 | hero.abilities = getHeroAbilities(hero.id, hero.name, heroUnitids); 316 | 317 | // Talents 318 | hero.talents = {}; 319 | C.HERO_TALENT_LEVELS.forEach(function(HERO_TALENT_LEVEL) { hero.talents[HERO_TALENT_LEVEL] = []; }); 320 | var talentTreeNodes = heroNode.find("TalentTreeArray").filter(function(talentTreeNode) { return !!!attributeValue(talentTreeNode, "removed"); }); 321 | talentTreeNodes.sort(function(a, b) { return (+((+attributeValue(a, "Tier"))*10)+(+attributeValue(a, "Column")))-(+((+attributeValue(b, "Tier"))*10)+(+attributeValue(b, "Column"))); }); 322 | 323 | talentTreeNodes.forEach(function(talentTreeNode) 324 | { 325 | var talent = {}; 326 | 327 | talent.id = attributeValue(talentTreeNode, "Talent"); 328 | 329 | var talentNode = NODE_MAPS["Talent"][talent.id]; 330 | var faceid = getValue(talentNode, "Face"); 331 | 332 | var talentDescription = S["Button/Tooltip/" + faceid]; 333 | 334 | if(!talentDescription && faceid==="TyrandeHuntersMarkTrueshotAuraTalent") 335 | talentDescription = S["Button/Tooltip/TyrandeTrueshotBowTalent"]; 336 | 337 | if(!talentDescription) 338 | { 339 | base.warn("Missing talent description for hero [%s] and talentid [%s] and faceid [%s]", hero.id, talent.id, faceid); 340 | return; 341 | } 342 | 343 | if(talentDescription.contains("StandardTooltipHeader")) 344 | talent.name = talentDescription.replace(/([^<]+)<.+/, "$1").replace(//gm, "").trim(); 345 | else 346 | talent.name = S[getValue(NODE_MAPS["Button"][faceid], "Name")]; 347 | 348 | if(!talent.name) 349 | talent.name = S["Button/Name/" + faceid]; 350 | 351 | //if(hero.id==="L90ETC") { base.info("Talent: %s\n", talent.id); } 352 | talent.description = getFullDescription(talent.id, talentDescription, hero.id, 0); 353 | talent.icon = getValue(NODE_MAPS["Button"][faceid], "Icon"); 354 | if(!talent.icon) 355 | talent.icon = getValue(NODE_MAPS["Button"][attributeValue(NODE_MAPS["Button"][faceid], "parent")], "Icon"); 356 | 357 | if(!talent.icon) 358 | delete talent.icon; 359 | else 360 | talent.icon = talent.icon.replace(/Assets\\Textures\\/, ""); 361 | 362 | addCooldownInfo(talent, "description"); 363 | 364 | if(!talent.cooldown) 365 | talent.cooldown = getAbilityCooldown(NODE_MAPS["Abil"][getValue(talentNode, "Abil")]); 366 | 367 | var talentPrerequisiteNode = talentTreeNode.get("PrerequisiteTalentArray"); 368 | if(talentPrerequisiteNode) 369 | talent.prerequisite = attributeValue(talentPrerequisiteNode, "value"); 370 | 371 | hero.talents[C.HERO_TALENT_LEVELS[((+attributeValue(talentTreeNode, "Tier"))-1)]].push(talent); 372 | }); 373 | 374 | // Final modifications 375 | performHeroModifications(hero); 376 | 377 | return hero; 378 | } 379 | 380 | function addHeroLevelScalingMods(heroid, heroNode) 381 | { 382 | // console.log('==============',heroNode.toString()); 383 | heroNode.find("LevelScalingArray/Modifications").forEach(function(modNode) 384 | { 385 | var modType = getValue(modNode, "Catalog", attributeValue(modNode, "Catalog")) || 'Undefined'; 386 | /* if(!NODE_MAP_TYPES.contains(modType)) 387 | throw new Error("Unsupported LevelScalingArray Modification Catalog modType: " + modType);*/ 388 | 389 | var modKey = getValue(modNode, "Entry", attributeValue(modNode, "Entry")); 390 | if(!modKey) 391 | throw new Error("No Entry node in LevelScalingArray Modification (" + modKey + ") for hero: " + heroid); 392 | 393 | var modTarget = getValue(modNode, "Field", attributeValue(modNode, "Field")); 394 | if(!modTarget) 395 | throw new Error("No Field node in LevelScalingArray Modification (" + modTarget + ") for hero: " + heroid); 396 | 397 | var modValue = getValue(modNode, "Value", attributeValue(modNode, "Value")); 398 | if(!modValue) 399 | return; 400 | 401 | HERO_LEVEL_SCALING_MODS[heroid].push({type:modType,key:modKey,target:modTarget,value:(+modValue)}); 402 | }); 403 | } 404 | 405 | function getHeroAbilities(heroid, heroName, heroUnitids) 406 | { 407 | var abilities = {}; 408 | 409 | var heroHeroicAbilityids = []; 410 | var heroTraitAbilityids = []; 411 | var heroAbilityids = []; 412 | var heroNode = NODE_MAPS["Hero"][heroid]; 413 | heroNode.find("HeroAbilArray").forEach(function(heroAbilNode) 414 | { 415 | if(!heroAbilNode.get("Flags[@index='ShowInHeroSelect' and @value='1']")) 416 | return; 417 | 418 | var abilityIsTrait = !!heroAbilNode.get("Flags[@index='Trait' and @value='1']"); 419 | 420 | var abilid = attributeValue(heroAbilNode, "Abil"); 421 | if(!abilid) 422 | { 423 | var buttonid = attributeValue(heroAbilNode, "Button"); 424 | if(!buttonid) 425 | throw new Error("No abil or button for: " + heroAbilNode.toString()); 426 | 427 | var descriptionIdsToTry = []; 428 | var buttonidShort = ["HeroSelect", "HeroSelectButton"].mutateOnce(function(buttonSuffix) { if(buttonid.endsWith(buttonSuffix)) { return buttonid.substring(0, buttonid.length-buttonSuffix.length); } }); 429 | if(!buttonidShort) 430 | buttonidShort = buttonid; 431 | descriptionIdsToTry.push(buttonidShort); 432 | descriptionIdsToTry.push(heroid + buttonidShort); 433 | if(abilityIsTrait) 434 | { 435 | descriptionIdsToTry.push(buttonidShort + "Trait"); 436 | if(buttonidShort.contains(heroid)) 437 | descriptionIdsToTry.push(buttonidShort.replace(heroid, heroid + "Trait")); 438 | } 439 | descriptionIdsToTry.push(buttonidShort + "Talent"); 440 | 441 | abilid = descriptionIdsToTry.mutateOnce(function(descriptionIdToTry) { if(S["Button/Tooltip/" + descriptionIdToTry]) { return descriptionIdToTry; }}); 442 | } 443 | 444 | if(abilityIsTrait) 445 | heroTraitAbilityids.push(abilid); 446 | 447 | if(heroAbilNode.get("Flags[@index='Heroic' and @value='1']")) 448 | heroHeroicAbilityids.push(abilid); 449 | 450 | heroAbilityids.push(abilid); 451 | }); 452 | 453 | abilities[heroid] = getUnitAbilities(heroid, heroName, heroAbilityids.concat((C.VALID_UNIT_ABILITY_IDS[heroid] || [])).subtract((C.HERO_SKIP_ABILITY_IDS[heroid] || [])), heroHeroicAbilityids, heroTraitAbilityids, "Hero" + (C.HERO_UNIT_ID_REPLACEMENTS[heroid] || heroid)); 454 | 455 | heroUnitids.forEach(function(heroUnitid) 456 | { 457 | if(heroUnitid===heroid) 458 | return; 459 | 460 | abilities[heroUnitid] = getUnitAbilities(heroid, heroName, heroAbilityids.concat((C.VALID_UNIT_ABILITY_IDS[heroUnitid] || [])).subtract((C.HERO_SKIP_ABILITY_IDS[heroUnitid] || [])), heroHeroicAbilityids, heroTraitAbilityids, heroUnitid); 461 | }); 462 | 463 | heroUnitids.concat([heroid]).forEach(function(heroUnitid) 464 | { 465 | Object.forEach(C.IMPORT_ABILITIES_FROM_SUBUNIT, function(importToid, importFromid) 466 | { 467 | if(importToid!==heroUnitid) 468 | return; 469 | 470 | abilities[importToid] = abilities[importFromid]; 471 | }); 472 | }); 473 | 474 | (C.REMOVE_SUBUNITS[heroid] || []).forEach(function(REMOVE_SUBUNIT) 475 | { 476 | delete abilities[REMOVE_SUBUNIT]; 477 | }); 478 | 479 | if(C.MOUNT_ABILITY_IDS.hasOwnProperty(heroid)) 480 | { 481 | var mountAbility = getUnitAbilities(heroid, heroName, [C.MOUNT_ABILITY_IDS[heroid]], [], [], "Hero" + (C.HERO_MOUNT_UNIT_ID_REPLACEMENTS[heroid] || heroid))[0]; 482 | mountAbility.shortcut = "Z"; 483 | mountAbility.mount = true; 484 | abilities[heroid].push(mountAbility); 485 | } 486 | 487 | return abilities; 488 | } 489 | 490 | function getUnitAbilities(heroid, heroName, heroAbilityids, heroHeroicAbilityids, heroTraitAbilityids, unitid) 491 | { 492 | var SHORTCUT_KEY_ORDER = ["Q", "W", "E", "R", "D", "1", "2", "3", "4", "5"]; 493 | var abilities = []; 494 | 495 | var unitNode = NODE_MAPS["Unit"][unitid]; 496 | if(!unitNode) 497 | return abilities; 498 | 499 | var attributeButtons = unitNode.find("CardLayouts[@index='0']/LayoutButtons"); 500 | if(attributeButtons.length===0) 501 | attributeButtons = unitNode.find("CardLayouts/LayoutButtons"); 502 | if(attributeButtons.length===0) 503 | attributeButtons = []; 504 | 505 | attributeButtons.forEach(function(layoutButtonNode) 506 | { 507 | var buttonRow = attributeValue(layoutButtonNode, "Row", getValue(layoutButtonNode, "Row", null)); 508 | var buttonColumn = attributeValue(layoutButtonNode, "Column", getValue(layoutButtonNode, "Column", null)); 509 | 510 | if(buttonRow===null || buttonColumn===null) 511 | return; 512 | 513 | buttonRow = +buttonRow; 514 | buttonColumn = +buttonColumn; 515 | 516 | var ability = {}; 517 | ability.id = attributeValue(layoutButtonNode, "Face", getValue(layoutButtonNode, "Face")); 518 | 519 | var abilityCmdid = attributeValue(layoutButtonNode, "AbilCmd", getValue(layoutButtonNode, "AbilCmd")); 520 | if(abilityCmdid) 521 | abilityCmdid = abilityCmdid.split(",")[0]; 522 | 523 | if(!heroAbilityids.contains(ability.id) && !heroAbilityids.contains(abilityCmdid)) 524 | return; 525 | 526 | var abilNode = NODE_MAPS["Abil"][ability.id]; 527 | if(!abilNode && abilityCmdid) 528 | abilNode = NODE_MAPS["Abil"][abilityCmdid]; 529 | 530 | if(heroTraitAbilityids.contains(ability.id) || heroTraitAbilityids.contains(abilityCmdid)) 531 | ability.trait = true; 532 | 533 | if(!ability.trait && !abilNode) 534 | throw new Error("Failed to find ability node: " + layoutButtonNode.toString()); 535 | 536 | if(abilNode) 537 | { 538 | var cmdButtonNode = abilNode.get("CmdButtonArray[@index='Execute']"); 539 | if(cmdButtonNode) 540 | ability.icon = getValue(NODE_MAPS["Button"][attributeValue(cmdButtonNode, "DefaultButtonFace")], "Icon"); 541 | 542 | var energyCostNode = abilNode.get("Cost/Vital[@index='Energy']"); 543 | if(energyCostNode) 544 | ability.manaCost = +attributeValue(energyCostNode, "value"); 545 | } 546 | 547 | if(!ability.icon) 548 | ability.icon = getValue(NODE_MAPS["Button"][ability.id], "Icon"); 549 | 550 | if(!ability.icon) 551 | delete ability.icon; 552 | else 553 | ability.icon = ability.icon.replace(/Assets\\Textures\\/, ""); 554 | 555 | if(heroHeroicAbilityids.contains(ability.id) || heroHeroicAbilityids.contains(abilityCmdid)) 556 | ability.heroic = true; 557 | 558 | addAbilityDetails(ability, heroid, heroName, abilityCmdid); 559 | 560 | if(abilNode && !ability.hasOwnProperty("cooldown")) 561 | { 562 | ability.cooldown = getAbilityCooldown(abilNode); 563 | if(!ability.cooldown) 564 | delete ability.cooldown; 565 | 566 | ability.description = ability.description.replace("Cooldown: " + ability.cooldown + " seconds\n", ""); 567 | } 568 | 569 | ability.tempSortOrder = (buttonRow*5)+buttonColumn; 570 | 571 | if(!ability.trait) 572 | { 573 | ability.shortcut = SHORTCUT_KEY_ORDER[ability.tempSortOrder]; 574 | 575 | if(!NODE_MAPS["Abil"][ability.id] && NODE_MAPS["Abil"][abilityCmdid]) 576 | ability.id = abilityCmdid; 577 | } 578 | else 579 | { 580 | if(!heroTraitAbilityids.contains(ability.id) && heroTraitAbilityids.contains(abilityCmdid)) 581 | ability.id = abilityCmdid; 582 | } 583 | 584 | if(C.ABILITY_SHORTCUT_REMAPS.hasOwnProperty(ability.id)) 585 | ability.shortcut = C.ABILITY_SHORTCUT_REMAPS[ability.id]; 586 | 587 | var addAbility = true; 588 | abilities = abilities.filter(function(existingAbility) 589 | { 590 | if(!addAbility || existingAbility.id!==ability.id) 591 | return true; 592 | 593 | if(!existingAbility.shortcut && ability.shortcut) 594 | return false; 595 | 596 | addAbility = false; 597 | 598 | return true; 599 | }); 600 | 601 | if(addAbility) 602 | abilities.push(ability); 603 | }); 604 | 605 | (C.IMPORT_ABILITIES[unitid] || []).forEach(function(abilityToAdd) 606 | { 607 | var ability = {}; 608 | ability.id = abilityToAdd.id; 609 | ability.icon = abilityToAdd.icon; 610 | 611 | addAbilityDetails(ability, heroid, heroName, undefined, abilityToAdd.name); 612 | 613 | if(abilityToAdd.shortcut) 614 | ability.shortcut = abilityToAdd.shortcut; 615 | if(abilityToAdd.trait) 616 | ability.trait = abilityToAdd.trait; 617 | 618 | abilities.push(ability); 619 | }); 620 | 621 | abilities.sort(function(a, b) { return a.tempSortOrder-b.tempSortOrder; }); 622 | abilities.forEach(function(ability) 623 | { 624 | delete ability.tempSortOrder; 625 | }); 626 | 627 | return abilities; 628 | } 629 | 630 | function getAbilityCooldown(abilNode) 631 | { 632 | if(!abilNode) 633 | return; 634 | 635 | var cooldownAttribute = abilNode.get("OffCost/Cooldown[@Location='Unit']/../Charge/TimeUse/@value") || abilNode.get("OffCost/Cooldown[@Location='Unit']/@TimeUse") || 636 | abilNode.get("Cost/Cooldown[@Location='Unit']/../Charge/TimeUse/@value") || abilNode.get("Cost/Cooldown[@Location='Unit']/@TimeUse"); 637 | if(!cooldownAttribute) 638 | return; 639 | 640 | return +cooldownAttribute.value(); 641 | } 642 | 643 | function addAbilityDetails(ability, heroid, heroName, abilityCmdid, abilityName) 644 | { 645 | if(C.USE_ABILITY_NAME.contains(heroid)) 646 | ability.name = abilityName || S["Abil/Name/" + ability.id] || S["Abil/Name/" + abilityCmdid]; 647 | else 648 | ability.name = abilityName || S["Button/Name/" + ability.id] || S["Button/Name/" + abilityCmdid]; 649 | 650 | if(!ability.name) 651 | throw new Error("Failed to get ability name: " + ability.id + " and " + abilityCmdid); 652 | 653 | if(ability.name.startsWith(heroid + " ")) 654 | ability.name = ability.name.substring(heroid.length+1).trim(); 655 | if(ability.name.startsWith(heroName + " ")) 656 | ability.name = ability.name.substring(heroName.length+1).trim(); 657 | 658 | var abilityDescription = S["Button/Tooltip/" + ability.id] || S["Button/Tooltip/" + abilityCmdid]; 659 | if(C.ABILITY_ID_DESCRIPTION_IDS[heroid]) 660 | abilityDescription = S["Button/Tooltip/" + C.ABILITY_ID_DESCRIPTION_IDS[heroid][ability.id]] || abilityDescription; 661 | if(!abilityDescription) 662 | throw new Error("Failed to get ability description: " + ability.id + " and " + abilityCmdid); 663 | 664 | ability.description = getFullDescription(ability.id, abilityDescription, heroid, 0); 665 | 666 | ability.description = ability.description.replace("Heroic Ability\n", "").replace("Heroic Passive\n", "").replace("Trait\n", ""); 667 | 668 | addCooldownInfo(ability, "description"); 669 | 670 | var manaPerSecondMatch = ability.description.match(/Mana:\s*([0-9]+)\s+per\s+second\n/m); 671 | if(manaPerSecondMatch) 672 | { 673 | ability.manaCostPerSecond = +manaPerSecondMatch[1]; 674 | ability.description = ability.description.replace(manaPerSecondMatch[0], ""); 675 | } 676 | 677 | var aimTypeMatch = ability.description.match(/((?:Skillshot)|(?:Area of Effect)|(?:Cone))\n/); 678 | if(aimTypeMatch) 679 | { 680 | ability.aimType = aimTypeMatch[1]; 681 | ability.description = ability.description.replace(aimTypeMatch[0], ""); 682 | } 683 | } 684 | 685 | function addCooldownInfo(o, field) 686 | { 687 | var cooldownMatch = o[field].match(/(?:Charge )?Cooldown:\s*([0-9]+)\s+[sS]econds?\n/m); 688 | if(cooldownMatch) 689 | { 690 | o.cooldown = +cooldownMatch[1]; 691 | o[field] = o[field].replace(cooldownMatch[0], ""); 692 | } 693 | } 694 | 695 | function getHeroStats(heroUnitid) 696 | { 697 | var heroStats = {}; 698 | 699 | var heroUnitNode = NODE_MAPS["Unit"][(!heroUnitid.startsWith("Hero") ? "Hero" : "") + heroUnitid] || NODE_MAPS["Unit"][heroUnitid]; 700 | if(heroUnitNode) 701 | { 702 | heroStats.hp = +getValue(heroUnitNode, "LifeMax") || 0; 703 | heroStats.hpPerLevel = 0; 704 | heroStats.hpRegen = +getValue(heroUnitNode, "LifeRegenRate") || 0; 705 | heroStats.hpRegenPerLevel = 0; 706 | 707 | heroStats.mana = +getValue(heroUnitNode, "EnergyMax", 500) || 0; 708 | heroStats.manaPerLevel = 0; 709 | heroStats.manaRegen = +getValue(heroUnitNode, "EnergyRegenRate", 3) || 0; 710 | heroStats.manaRegenPerLevel = 0; 711 | 712 | (heroUnitNode.find("BehaviorArray") || []).forEach(function(behaviorArrayNode) 713 | { 714 | var behaviorNode = NODE_MAPS["Behavior"][attributeValue(behaviorArrayNode, "Link")]; 715 | if(!behaviorNode) 716 | return; 717 | 718 | if(attributeValue(behaviorNode, "parent")!=="HeroXPCurve") 719 | return; 720 | 721 | var levelOneNode = behaviorNode.get("VeterancyLevelArray[@index='1']/Modification"); 722 | if(!levelOneNode) 723 | return; 724 | 725 | var hpPerLevelAttribute = levelOneNode.get("VitalMaxArray[@index='Life']/@value"); 726 | if(hpPerLevelAttribute) 727 | heroStats.hpPerLevel = (heroStats.hpPerLevel || 0) + (+hpPerLevelAttribute.value()); 728 | 729 | var hpRegenPerLevelAttribute = levelOneNode.get("VitalRegenArray[@index='Life']/@value"); 730 | if(hpRegenPerLevelAttribute) 731 | heroStats.hpRegenPerLevel = (heroStats.hpRegenPerLevel || 0) + (+hpRegenPerLevelAttribute.value()); 732 | 733 | var manaPerLevelAttribute = levelOneNode.get("VitalMaxArray[@index='Energy']/@value"); 734 | if(manaPerLevelAttribute) 735 | heroStats.manaPerLevel = (heroStats.manaPerLevel || 0) + (+manaPerLevelAttribute.value()); 736 | 737 | var manaRegenPerLevelAttribute = levelOneNode.get("VitalRegenArray[@index='Energy']/@value"); 738 | if(manaRegenPerLevelAttribute) 739 | heroStats.manaRegenPerLevel = (heroStats.manaRegenPerLevel || 0) + (+manaRegenPerLevelAttribute.value()); 740 | }); 741 | 742 | if(HERO_LEVEL_SCALING_MODS.hasOwnProperty(heroUnitid)) 743 | { 744 | HERO_LEVEL_SCALING_MODS[heroUnitid].forEach(function(scalingMod) 745 | { 746 | if(scalingMod.type!=="Unit" || scalingMod.value===0) 747 | return; 748 | 749 | if(heroStats.hpPerLevel===0 && scalingMod.target==="LifeMax") 750 | heroStats.hpPerLevel = scalingMod.value*100; 751 | 752 | if(heroStats.hpRegenPerLevel===0 && scalingMod.target==="LifeRegenRate") 753 | heroStats.hpRegenPerLevel = scalingMod.value*100; 754 | }); 755 | } 756 | } 757 | 758 | return heroStats; 759 | } 760 | 761 | function getFullDescription(id, _fullDescription, heroid, heroLevel) 762 | { 763 | var fullDescription = _fullDescription; 764 | 765 | fullDescription = fullDescription.replace(/[^<]+(<.+)/, "$1").replace(/?(.+)/, "$1"); 766 | fullDescription = fullDescription.replace(//g, ""); 767 | 768 | (fullDescription.match(//g) || []).forEach(function(dynamic) 769 | { 770 | var formula = dynamic.match(/ref\s*=\s*"([^"]+)"/)[1]; 771 | if(formula.endsWith(")") && !formula.contains("(")) 772 | formula = formula.substring(0, formula.length-1); 773 | 774 | try 775 | { 776 | C.FORMULA_PRE_REPLACEMENTS.forEach(function(FORMULA_PRE_REPLACEMENT) 777 | { 778 | if(formula.contains(FORMULA_PRE_REPLACEMENT.match)) 779 | formula = formula.replace(FORMULA_PRE_REPLACEMENT.match, FORMULA_PRE_REPLACEMENT.replace); 780 | }); 781 | 782 | formula = formula.replace(/\$BehaviorStackCount:[^$]+\$/g, "0"); 783 | formula = formula.replace(/\[d ref='([^']+)'(?: player='[0-9]')?\/?]/g, "$1"); 784 | 785 | //if(heroid==="Tracer") { base.info("Before: %s", formula); } 786 | formula = formula.replace(/^([ (]*)-/, "$1-1*"); 787 | 788 | (formula.match(/((^\-)|(\(\-))?[A-Za-z][A-Za-z0-9,._\[\]]+/g) || []).map(function(match) { return match.indexOf("(")===0 ? match.substring(1) : match; }).forEach(function(match) 789 | { 790 | var negative = false; 791 | 792 | if(match.startsWith("-")) 793 | { 794 | match = match.substring(1); 795 | negative = true; 796 | } 797 | formula = formula.replace(match, lookupXMLRef(heroid, heroLevel, match, negative)); 798 | }); 799 | //if(heroid==="Tracer") { base.info("after: %s", formula); } 800 | 801 | formula = formula.replace(/[+*/-]$/, ""); 802 | formula = "(".repeat((formula.match(/[)]/g) || []).length-(formula.match(/[(]/g) || []).length) + formula; 803 | 804 | formula = formula.replace(/--/, "+"); 805 | 806 | //if(heroid==="Tracer") { base.info("after prenthesiszed and regex: %s", parenthesize(formula)); base.info("after prenthesiszed x2: %s", parenthesize(parenthesize(formula))); } 807 | 808 | //Talent,ArtanisBladeDashSolariteReaper,AbilityModificationArray[0].Modifications[0].Value)*(100) 809 | 810 | // Heroes formulas are evaluated Left to Right instead of normal math operation order, so we parenthesize everything. ugh. 811 | var result = C.FULLY_PARENTHESIZE.contains(id) ? eval(fullyParenthesize(formula)) : eval(parenthesize(formula)); // jshint ignore:line 812 | 813 | //if(heroid==="Tracer") { base.info("Formula: %s\nResult: %d", formula, result); } 814 | 815 | var MAX_PRECISION = 4; 816 | if(result.toFixed(MAX_PRECISION).length<(""+result).length) 817 | result = +result.toFixed(MAX_PRECISION); 818 | 819 | //var precision = dynamic.match(/precision\s*=\s*"([^"]+)"/) ? +dynamic.match(/precision\s*=\s*"([^"]+)"/)[1] : null; 820 | //if(precision!==null && Math.floor(result)!==result) 821 | // result = result.toFixed(precision); 822 | 823 | fullDescription = fullDescription.replace(dynamic, result); 824 | } 825 | catch(err) 826 | { 827 | base.error("Failed to parse: %s\n(%s)", formula, _fullDescription); 828 | throw err; 829 | } 830 | }); 831 | 832 | fullDescription = fullDescription.replace(/<\/?n\/?>/g, "\n"); 833 | fullDescription = fullDescription.replace(//gm, "").replace(//gm, "").replace(/<\/?[cs]\/?>/g, ""); 834 | fullDescription = fullDescription.replace(//gm, "").replace(/<\/?if\/?>/g, "").trim(); 835 | fullDescription = fullDescription.replace(/ [.] /g, ". "); 836 | fullDescription = fullDescription.replace(/ [.]([0-9]+)/g, " 0.$1"); 837 | while(fullDescription.indexOf("\n\n")!==-1) { fullDescription = fullDescription.replace(/\n\n/g, "\n"); } 838 | 839 | if(heroLevel===0) 840 | { 841 | var fullDescriptionLevel1 = getFullDescription(id, _fullDescription, heroid, 1); 842 | if(fullDescription!==fullDescriptionLevel1) 843 | { 844 | var beforeWords = fullDescription.split(" "); 845 | var afterWords = fullDescriptionLevel1.split(" "); 846 | if(beforeWords.length!==afterWords.length) 847 | throw new Error("Talent description words length MISMATCH " + beforeWords.length + " vs " + afterWords.length + " for hero (" + heroid + ") and talent: " + fullDescription); 848 | 849 | var updatedWords = []; 850 | beforeWords.forEach(function(beforeWord, i) 851 | { 852 | var afterWord = afterWords[i]; 853 | if(beforeWord===afterWord) 854 | { 855 | updatedWords.push(beforeWord); 856 | return; 857 | } 858 | 859 | var endWithPeriod = beforeWord.endsWith("."); 860 | if(endWithPeriod) 861 | { 862 | beforeWord = beforeWord.substring(0, beforeWord.length-1); 863 | afterWord = afterWord.substring(0, afterWord.length-1); 864 | } 865 | 866 | var isPercentage = beforeWord.endsWith("%"); 867 | if(isPercentage) 868 | { 869 | beforeWord = beforeWord.substring(0, beforeWord.length-1); 870 | afterWord = afterWord.substring(0, afterWord.length-1); 871 | } 872 | 873 | var valueDifference = (+afterWord).subtract(+beforeWord); 874 | 875 | var resultWord = beforeWord + (isPercentage ? "%" : "") + " (" + (valueDifference>0 ? "+" : "") + valueDifference + (isPercentage ? "%" : "") + " per level)" + (endWithPeriod ? "." : ""); 876 | 877 | updatedWords.push(resultWord); 878 | }); 879 | 880 | fullDescription = updatedWords.join(" "); 881 | } 882 | } 883 | 884 | return fullDescription; 885 | } 886 | 887 | function parenthesize(formula) 888 | { 889 | var result = []; 890 | var seenOperator = false; 891 | var lastOpenParenLoc = 0; 892 | var seenOneParenClose = true; 893 | formula.replace(/ /g, "").split("").forEach(function(c, i) 894 | { 895 | if("+-/*".contains(c) && seenOperator && seenOneParenClose && !"+-/*(".contains(result.last())) 896 | { 897 | result.splice(lastOpenParenLoc, 0, "("); 898 | result.push(")"); 899 | } 900 | 901 | if(c==="(") 902 | seenOneParenClose = false; 903 | 904 | if(c===")") 905 | seenOneParenClose = true; 906 | 907 | if("+-/*".contains(c) && i!==0 && !"+-/*(".contains(result.last())) 908 | seenOperator = true; 909 | 910 | result.push(c); 911 | }); 912 | 913 | return result.join(""); 914 | } 915 | 916 | function fullyParenthesize(formula) 917 | { 918 | var result = [].pushMany("(", formula.replace(/[^+/*-]/g, "").length+1); 919 | 920 | formula.replace(/ /g, "").split("").forEach(function(c, i) 921 | { 922 | if(c==="(" || c===")") 923 | return; 924 | 925 | if("+-/*".contains(c)) 926 | result.push(")"); 927 | 928 | result.push(c); 929 | }); 930 | 931 | result.push(")"); 932 | 933 | return result.join(""); 934 | } 935 | 936 | function lookupXMLRef(heroid, heroLevel, query, negative) 937 | { 938 | var result = 0; 939 | 940 | C.XMLREF_REPLACEMENTS.forEach(function(XMLREF_REPLACEMENT) 941 | { 942 | if(query===XMLREF_REPLACEMENT.from) 943 | query = XMLREF_REPLACEMENT.to; 944 | }); 945 | 946 | //if(heroid==="Tinker") { base.info("QUERY: %s", query); } 947 | 948 | var mainParts = query.split(","); 949 | 950 | if(!NODE_MAP_TYPES.contains(mainParts[0])) 951 | throw new Error("No valid node map type for XML query: " + query); 952 | 953 | var nodeMap = NODE_MAPS[mainParts[0]]; 954 | if(!nodeMap.hasOwnProperty(mainParts[1])) 955 | { 956 | console.log('===================', query); 957 | base.warn("No valid id for nodeMapType XML parts %s", mainParts); 958 | return result; 959 | } 960 | 961 | var target = nodeMap[mainParts[1]]; 962 | 963 | if(target.childNodes().length===0) 964 | { 965 | if(!C.ALLOWED_EMPTY_XML_REF_IDS.contains(attributeValue(target, "id"))) 966 | base.warn("No child nodes for nodeMapType XML parts [%s] with xml:", mainParts, target.toString()); 967 | return result; 968 | } 969 | 970 | var subparts = mainParts[2].split("."); 971 | 972 | //if(heroid==="Tinker" && query.contains("TalentBucketPromote")) { base.info("Level %d with mainParts [%s] and subparts [%s] and hero mods:", heroLevel, mainParts.join(", "), subparts.join(", ")); base.info(HERO_LEVEL_SCALING_MODS[heroid]); } 973 | 974 | var additionalAmount = 0; 975 | HERO_LEVEL_SCALING_MODS[heroid].forEach(function(HERO_LEVEL_SCALING_MOD) 976 | { 977 | if(HERO_LEVEL_SCALING_MOD.type!==mainParts[0]) 978 | return; 979 | 980 | if(HERO_LEVEL_SCALING_MOD.key!==mainParts[1]) 981 | return; 982 | 983 | if(HERO_LEVEL_SCALING_MOD.target!==subparts[0] && HERO_LEVEL_SCALING_MOD.target!==subparts[0].replace("[0]", "") && 984 | HERO_LEVEL_SCALING_MOD.target!==subparts.join(".") && HERO_LEVEL_SCALING_MOD.target!==subparts.map(function(subpart) { return subpart.replace("[0]", ""); }).join(".")) 985 | return; 986 | 987 | //if(heroid==="Tinker" && query.contains("TalentBucketPromote")) { base.info("Found additional scaling amount of %d", HERO_LEVEL_SCALING_MOD.value); } 988 | additionalAmount = heroLevel*HERO_LEVEL_SCALING_MOD.value; 989 | }); 990 | 991 | //if(heroid==="Tinker" && query.contains("TalentBucketPromote") && additionalAmount===0) { base.info("Failed to find an additional amount for: %s", mainParts.join(",")); } 992 | 993 | //if(heroid==="Tinker") { base.info("Start (negative:%s): %s", negative, subparts); } 994 | subparts.forEach(function(subpart) 995 | { 996 | var xpath = !subpart.match(/\[[0-9]+\]/) ? subpart.replace(/([^[]+)\[([^\]]+)]/, "$1[@index = '$2']") : subpart.replace(/\[([0-9]+)\]/, "[" + (+subpart.match(/\[([0-9]+)\]/)[1]+1) + "]"); 997 | //if(heroid==="Tinker") { base.info("Next xpath: %s\nCurrent target: %s\n", xpath, target.toString()); } 998 | var nextTarget = target.get(xpath); 999 | if(!nextTarget) 1000 | result = +attributeValue(target, xpath.replace(/([^\[]+).*/, "$1")); 1001 | target = nextTarget; 1002 | }); 1003 | 1004 | if(target) 1005 | result = +attributeValue(target, "value"); 1006 | 1007 | if(isNaN(result)) 1008 | { 1009 | if(query.contains("AttributeFactor")) // These are only set at runtime with talent choices 1010 | result = 0; 1011 | else 1012 | throw new Error("Failed to get XML ref [" + query + "], result is NaN for hero: " + heroid); 1013 | } 1014 | 1015 | result += additionalAmount; 1016 | //if(heroid==="Tinker") { base.info("%s => %d", query, result); } 1017 | 1018 | if(negative) 1019 | result = result*-1; 1020 | 1021 | return result; 1022 | } 1023 | 1024 | function performHeroModifications(hero) 1025 | { 1026 | if(C.HERO_MODIFICATIONS.hasOwnProperty(hero.id)) 1027 | { 1028 | C.HERO_MODIFICATIONS[hero.id].forEach(function(HERO_MODIFICATION) 1029 | { 1030 | var match = jsonselect.match(HERO_MODIFICATION.path, hero); 1031 | if(!match || match.length<1) 1032 | { 1033 | base.error("Failed to match [%s] to: %s", HERO_MODIFICATION.path, hero); 1034 | return; 1035 | } 1036 | 1037 | (HERO_MODIFICATION.remove || []).forEach(function(keyToRemove) { delete match[0][keyToRemove]; }); 1038 | 1039 | if(HERO_MODIFICATION.name) 1040 | match[0][HERO_MODIFICATION.name] = HERO_MODIFICATION.value; 1041 | }); 1042 | } 1043 | 1044 | if(C.HERO_SUBUNIT_ABILITIES_MOVE.hasOwnProperty(hero.id)) 1045 | { 1046 | Object.forEach(C.HERO_SUBUNIT_ABILITIES_MOVE[hero.id], function(srcSubunitid, abilityMoveInfo) 1047 | { 1048 | Object.forEach(abilityMoveInfo, function(abilityId, targetSubunitid) 1049 | { 1050 | var match = null; 1051 | hero.abilities[srcSubunitid] = hero.abilities[srcSubunitid].filter(function(ability) { if(ability.id===abilityId) { match = base.clone(ability); } return ability.id!==abilityId; }); 1052 | if(!match) 1053 | base.error("Failed to find hero [%s] with srcSubunitid [%s] and abilityId [%s] and targetSubunitid [%s]", hero.id, srcSubunitid, abilityId, targetSubunitid); 1054 | else 1055 | hero.abilities[targetSubunitid].push(match); 1056 | }); 1057 | }); 1058 | } 1059 | } 1060 | 1061 | function performMountModifications(mount) 1062 | { 1063 | if(C.MOUNT_MODIFICATIONS.hasOwnProperty(mount.id)) 1064 | { 1065 | C.MOUNT_MODIFICATIONS[mount.id].forEach(function(MOUNT_MODIFICATION) 1066 | { 1067 | var match = jsonselect.match(MOUNT_MODIFICATION.path, mount); 1068 | if(!match || match.length<1) 1069 | { 1070 | base.error("Failed to match [%s] to: %s", MOUNT_MODIFICATION.path, mount); 1071 | return; 1072 | } 1073 | 1074 | (MOUNT_MODIFICATION.remove || []).forEach(function(keyToRemove) { delete match[0][keyToRemove]; }); 1075 | 1076 | if(MOUNT_MODIFICATION.name) 1077 | match[0][MOUNT_MODIFICATION.name] = MOUNT_MODIFICATION.value; 1078 | }); 1079 | } 1080 | } 1081 | 1082 | function findParentMount(source, field, value) { 1083 | for (var i = 0; i < source.length; i++) { 1084 | if (source[i][field] === value && source[i].variation === false) { 1085 | return source[i]; 1086 | } 1087 | } 1088 | throw "Could not find object where field '" + field + " === " + value + "'" ; 1089 | } 1090 | 1091 | function validateMount(mount, index, mounts) 1092 | { 1093 | var validator = jsen(C.MOUNT_JSON_SCHEMA); 1094 | if(!validator(mount)) 1095 | { 1096 | // For every fail (usually a variation), copy it from the parent) 1097 | validator.errors.forEach(function(elem) { 1098 | var parent = findParentMount(mounts, "productid", mount.productid); 1099 | for (var item in parent) { 1100 | if (mount[item] === undefined) { 1101 | mount[item] = parent[item]; 1102 | } 1103 | }; 1104 | }); 1105 | // WARNING: I may have a race condition here... 1106 | // Revalidate 1107 | if (!validator(mount)) { 1108 | base.warn("Mount %s (%s) has FAILED VALIDATION", mount.id, mount.name); 1109 | base.info(validator.errors); 1110 | } 1111 | } 1112 | } 1113 | 1114 | function validateHero(hero) 1115 | { 1116 | var validator = jsen(C.HERO_JSON_SCHEMA); 1117 | if(!validator(hero)) 1118 | { 1119 | base.warn("Hero %s (%s) has FAILED VALIDATION", hero.id, hero.name); 1120 | base.info(validator.errors); 1121 | } 1122 | 1123 | Object.forEach(hero.abilities, function(unitName, abilities) 1124 | { 1125 | if(abilities.length!==abilities.map(function(ability) { return ability.name; }).unique().length) 1126 | base.warn("Hero %s has multiple abilities with the same name!", hero.name); 1127 | }); 1128 | } 1129 | 1130 | function loadMergedNodeMap(xmlDocs) 1131 | { 1132 | xmlDocs.forEach(function(xmlDoc) 1133 | { 1134 | xmlDoc.find("/Catalog/*").forEach(function(node) 1135 | { 1136 | var nodeType = NODE_MAP_TYPES.filter(function(NODE_MAP_TYPE) { return node.name()===("C" + NODE_MAP_TYPE); }).concat(NODE_MAP_PREFIX_TYPES.filter(function(NODE_MAP_PREFIX_TYPE) { return node.name().startsWith(NODE_MAP_PREFIX_TYPES); })).unique(); 1137 | if(!nodeType || nodeType.length!==1) 1138 | return; 1139 | 1140 | nodeType = nodeType[0]; 1141 | 1142 | if(node.attr("id") || attributeValue(node, "default")!=="1") 1143 | return; 1144 | 1145 | if(DEFAULT_NODES.hasOwnProperty(nodeType)) 1146 | { 1147 | base.info(DEFAULT_NODES[nodeType].toString()); 1148 | base.info(node.toString()); 1149 | base.error("MORE THAN ONE DEFAULT! NOT GOOD!"); 1150 | process.exit(1); 1151 | } 1152 | 1153 | DEFAULT_NODES[nodeType] = node; 1154 | }); 1155 | }); 1156 | 1157 | xmlDocs.forEach(function(xmlDoc) 1158 | { 1159 | xmlDoc.find("/Catalog/*").forEach(function(node) 1160 | { 1161 | if(!node.attr("id")) 1162 | return; 1163 | 1164 | var nodeType = NODE_MAP_TYPES.filter(function(NODE_MAP_TYPE) { return node.name().startsWith("C" + NODE_MAP_TYPE); }); 1165 | if(!nodeType || nodeType.length!==1) 1166 | return; 1167 | 1168 | nodeType = nodeType[0]; 1169 | 1170 | var nodeid = attributeValue(node, "id"); 1171 | if(IGNORED_NODE_TYPE_IDS.hasOwnProperty(nodeType) && IGNORED_NODE_TYPE_IDS[nodeType].contains(nodeid)) 1172 | return; 1173 | 1174 | if(!NODE_MAPS[nodeType].hasOwnProperty(nodeid)) 1175 | { 1176 | NODE_MAPS[nodeType][nodeid] = node; 1177 | return; 1178 | } 1179 | 1180 | mergeXML(node, NODE_MAPS[nodeType][nodeid]); 1181 | }); 1182 | }); 1183 | } 1184 | 1185 | function mergeNodeParents() 1186 | { 1187 | NODE_MERGE_PARENT_TYPES.forEach(function(NODE_MERGE_PARENT_TYPE) 1188 | { 1189 | Object.forEach(NODE_MAPS[NODE_MERGE_PARENT_TYPE], function(nodeid, node) 1190 | { 1191 | var parentid = attributeValue(node, "parent"); 1192 | if(parentid && NODE_MAPS[NODE_MERGE_PARENT_TYPE].hasOwnProperty(parentid)) 1193 | mergeXML(NODE_MAPS[NODE_MERGE_PARENT_TYPE][parentid], node, true); 1194 | }); 1195 | }); 1196 | } 1197 | 1198 | function processReleaseDate(releaseDateNode) 1199 | { 1200 | return moment(attributeValue(releaseDateNode, "Month", 1) + "-" + attributeValue(releaseDateNode, "Day", 1) + "-" + attributeValue(releaseDateNode, "Year", "2014"), "M-D-YYYY").format("YYYY-MM-DD"); 1201 | } 1202 | 1203 | function mergeXML(fromNode, toNode, dontAddIfPresent) 1204 | { 1205 | fromNode.childNodes().forEach(function(childNode) 1206 | { 1207 | if(childNode.name()==="TalentTreeArray") 1208 | { 1209 | var existingChildNode = toNode.get("TalentTreeArray[@Tier='" + attributeValue(childNode, "Tier") + "' and @Column='" + attributeValue(childNode, "Column") + "']"); 1210 | if(existingChildNode) 1211 | existingChildNode.remove(); 1212 | } 1213 | 1214 | if(!toNode.childNodes().map(function(a) { return a.name(); }).contains(childNode.name()) || !dontAddIfPresent) 1215 | toNode.addChild(childNode.clone()); 1216 | }); 1217 | } 1218 | 1219 | function getValue(node, subnodeName, defaultValue) 1220 | { 1221 | if(!node) 1222 | return defaultValue || undefined; 1223 | 1224 | var subnode = node.get(subnodeName); 1225 | if(!subnode) 1226 | return defaultValue || undefined; 1227 | 1228 | return attributeValue(subnode, "value", defaultValue); 1229 | } 1230 | 1231 | function attributeValue(node, attrName, defaultValue) 1232 | { 1233 | if(!node) 1234 | return defaultValue || undefined; 1235 | 1236 | var attr = node.attr(attrName); 1237 | if(!attr) 1238 | return defaultValue || undefined; 1239 | 1240 | return attr.value(); 1241 | } 1242 | -------------------------------------------------------------------------------- /howto.txt: -------------------------------------------------------------------------------- 1 | Whenever a new patch hits Heroes of the Storm, you basically run two nodejs scripts against the install directory. This generates static JSON and web files which you then deploy using rsync to a server running nginx to server them up. 2 | 3 | Full details: 4 | 1. Update Heroes of the Storm under windows and run it at least once 5 | 2. Copy/make available the entire 'Heroes of the Storm' directory over to where you have heroesjson 6 | 3. The shared/C.js file contains many constants, you will likely need to add any new heroes in the new patch to C.EXTRA_HEROES_GAMEDATA_FOLDERS (Use CascView (http://www.zezula.net/en/casc/main.html) under windows to 'browse' the game files to determine what the correct directory name is for the new heroes) 7 | 4. Run: node generate.js "/path/to/Heroes of the Storm" 8 | 5. Fix any issues that are shown, usually this involves changing one or more constants in C.js or in some cases fixing something in generate.js 9 | 6. Update web/changelog.json with a new entry 10 | 7. Run: node web/generate.js "/path/to/Heroes of the Storm" 11 | 8. Run: node util/compareRelease.js 12 | 9. If everything in the above compare looks ok, finally manually look at the new heroes.json file and check out any new heroes and make sure things look ok (I use Chrome with the JSONView extension (https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc) and simply open up the heroes.json file with my web browser from disk) 13 | 10. Check 'dev.heroesjson.com' and make sure the website looks ok 14 | 11. Run: cd deploy && ./deploy.sh 15 | 12. Check 'heroesjson.com' and make sure it looks ok 16 | -------------------------------------------------------------------------------- /nginx/commonHeader.conf: -------------------------------------------------------------------------------- 1 | location ~* \.(eot|ttf|woff)$ 2 | { 3 | add_header Access-Control-Allow-Origin *; 4 | } 5 | -------------------------------------------------------------------------------- /nginx/commonNoFavicon.conf: -------------------------------------------------------------------------------- 1 | location = /favicon.ico 2 | { 3 | return 204; 4 | access_log off; 5 | log_not_found off; 6 | } 7 | -------------------------------------------------------------------------------- /nginx/dev.heroesjson.com.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen dev.heroesjson.com; 3 | server_name dev.heroesjson.com; 4 | 5 | root /mnt/compendium/DevLab/heroesjson/web; 6 | 7 | include commonHeader.conf; 8 | 9 | expires epoch; 10 | 11 | location = /json/ { 12 | autoindex on; 13 | } 14 | 15 | location /json { 16 | add_header Content-Disposition "attachment"; 17 | add_header Access-Control-Allow-Origin "*"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nginx/heroesjson.com.conf: -------------------------------------------------------------------------------- 1 | server 2 | { 3 | listen heroesjson.com; 4 | server_name heroesjson.com; 5 | 6 | root /srv/heroesjson.com/; 7 | 8 | include commonHeader.conf; 9 | include commonNoFavicon.conf; 10 | 11 | error_log /usr/local/nginx/logs/heroesjson.com_error.log; 12 | 13 | location = /index.html 14 | { 15 | expires epoch; 16 | } 17 | 18 | location = /json/ { 19 | autoindex on; 20 | } 21 | 22 | location /json { 23 | add_header Content-Disposition "attachment"; 24 | add_header Access-Control-Allow-Origin "*"; 25 | expires epoch; 26 | } 27 | } 28 | 29 | server 30 | { 31 | listen heroesjson.com; 32 | server_name www.heroesjson.com .heroesjson.net .heroesjson.org; 33 | rewrite ^(.*) http://heroesjson.com$1 permanent; 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heroesjson", 3 | "version": "1.0.0", 4 | "description": "Extract Heroes of the Storm game data to JSON", 5 | "private": "true", 6 | "main": "generate.js", 7 | "dependencies": { 8 | "JSONSelect": "^0.4.0", 9 | "cli-color": "^1.0.0", 10 | "glob": "^5.0.15", 11 | "jsen": "^0.6.0", 12 | "moment": "^2.10.6", 13 | "libxmljs": "^0.14.3", 14 | "rimraf": "^2.4.5", 15 | "tiptoe": "^1.0.0", 16 | "@sembiance/xutil": "git://github.com/Sembiance/xutil.git", 17 | "@sembiance/xbase": "git://github.com/Sembiance/xbase.git" 18 | }, 19 | "devDependencies": {}, 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/nydus/heroesjson.git" 26 | }, 27 | "author": "Robert Schultz ", 28 | "contributors": [ 29 | "Justin J. Novack " 30 | ], 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/nydus/heroesjson/issues" 34 | }, 35 | "homepage": "https://github.com/nydus/heroesjson#readme" 36 | } 37 | -------------------------------------------------------------------------------- /sandbox/cascextract/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /sandbox/cascextract/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.6) 2 | 3 | project (cascextract) 4 | add_subdirectory(src) 5 | 6 | -------------------------------------------------------------------------------- /sandbox/cascextract/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.6) 2 | cmake_policy(SET CMP0015 NEW) 3 | 4 | set ( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wunreachable-code -Wpedantic -Wformat=2 -std=c99 -O3 -march=native -D_GNU_SOURCE -fshort-enums -ftrapv -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wstrict-prototypes -Wno-unused-parameter -Wno-missing-field-initializers -D_DEFAULT_SOURCE" ) 5 | 6 | set ( CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g3" ) 7 | # -fno-omit-frame-pointer -fsanitize=address 8 | 9 | #RELEASE BUILD: cmake -DCMAKE_BUILD_TYPE=Release .. 10 | # DEBUG BUILD: cmake -DCMAKE_BUILD_TYPE=Debug .. 11 | 12 | set(cascextract_srcs 13 | cascextract.c 14 | main.c 15 | ) 16 | 17 | add_executable(cascextract ${cascextract_srcs}) 18 | 19 | find_library(CASC_LIBRARY casc ../../CascLib-build/) 20 | target_link_libraries(cascextract "-lm ${CASC_LIBRARY}") 21 | 22 | set(CMAKE_EXE_LINKER_FLAGS "-Wl,-rpath,../../CascLib-build/") 23 | -------------------------------------------------------------------------------- /sandbox/cascextract/src/cascextract.c: -------------------------------------------------------------------------------- 1 | #include "cascextract.h" 2 | 3 | #include "../../CascLib/src/CascLib.h" 4 | 5 | void cascextract(void) 6 | { 7 | HANDLE hStorage; 8 | bool result; 9 | 10 | printf("Opening: %s\n", gConfig.dataPath); 11 | 12 | result = CascOpenStorage(_T(gConfig.dataPath), 0, &hStorage); 13 | if(!result) 14 | { 15 | printf("error! %s\n", strerror(GetLastError())); 16 | } 17 | //bool WINAPI CascOpenStorage(const TCHAR * szDataPath, DWORD dwLocaleMask, HANDLE * phStorage); 18 | 19 | } -------------------------------------------------------------------------------- /sandbox/cascextract/src/cascextract.h: -------------------------------------------------------------------------------- 1 | #ifndef __CASCEXTRACT_H 2 | #define __CASCEXTRACT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "version.h" 12 | 13 | /*----------------------------------------------------------------------------- 14 | * Constants 15 | *----------------------------------------------------------------------------*/ 16 | #define MAX_NAME 255 17 | 18 | 19 | /*----------------------------------------------------------------------------- 20 | * Data types 21 | *----------------------------------------------------------------------------*/ 22 | typedef struct 23 | { 24 | char * dataPath; 25 | } Config; 26 | 27 | /*----------------------------------------------------------------------------- 28 | * Globals 29 | *----------------------------------------------------------------------------*/ 30 | extern Config gConfig; 31 | 32 | /*----------------------------------------------------------------------------- 33 | * Macros 34 | *----------------------------------------------------------------------------*/ 35 | 36 | 37 | /*----------------------------------------------------------------------------- 38 | * Functions prototypes 39 | *----------------------------------------------------------------------------*/ 40 | 41 | // cascextract.c 42 | void cascextract(void); 43 | 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /sandbox/cascextract/src/main.c: -------------------------------------------------------------------------------- 1 | #include "cascextract.h" 2 | 3 | #include 4 | 5 | Config gConfig; 6 | 7 | static void usage(void) 8 | { 9 | fprintf(stderr, 10 | "cascextract %s\n" 11 | "\n" 12 | "Usage: cascextract [OPTIONS] /path/to/game/\n" 13 | " -h, --help Output this help and exit\n" 14 | " -V, --version Output version and exit\n" 15 | "\n", CASCEXTRACT_VERSION); 16 | exit(EXIT_FAILURE); 17 | } 18 | 19 | static void parse_options(int argc, char **argv) 20 | { 21 | int i; 22 | 23 | for(i=1;idec2 ? dec1 : dec2; 29 | var n; 30 | if(op==="+") 31 | n = (num1+num2); 32 | else if(op==="*") 33 | n = (num1*num2); 34 | else if(op==="-") 35 | n = (num1-num2); 36 | 37 | n = n.toFixed(fixed); 38 | return +n; 39 | } 40 | } 41 | 42 | START 43 | = any 44 | 45 | any 46 | = multiplicative 47 | / divisive 48 | / additive 49 | / subtractive 50 | / primary 51 | 52 | primary 53 | = float 54 | / integer 55 | / xmlref 56 | / "-" _ any:any { console.log("-%s", any); return -1*any; } 57 | / _ "(" _ any:any _ ")" _ { console.log("(%s)", any); return any; } 58 | / "" { return 0; } 59 | 60 | additive 61 | = left:primary _ "+" _ right:any { console.log("%s + %s", left, right); return doNumbers(left, right, "+"); } 62 | 63 | multiplicative 64 | = left:primary _ "*" _ right:any { console.log("%s * %s", left, right); return doNumbers(left, right, "*"); } 65 | 66 | subtractive 67 | = left:primary _ "-" _ right:any { console.log("%s - %s", left, right); return doNumbers(left, right, "-"); } 68 | 69 | divisive 70 | = left:primary _ "/" _ right:any { console.log("%s / %s", left, right); return left/right; } 71 | 72 | float "float" 73 | = neg:"-"? _? left:[0-9]* "." right:[0-9]+ { return parseFloat((neg ? "-" : "") + left.join("") + "." + right.join("")); } 74 | 75 | integer "integer" 76 | = neg:"-"? _? digits:[0-9]+ { return parseInt((neg ? "-" : "") + digits.join(""), 10); } 77 | 78 | xmlref "xmlref" 79 | = neg:"-"? _? chars:[A-Za-z0-9,._\[\]]+ { return lookupXMLRef(heroid, heroLevel, chars.join(""), neg); } 80 | 81 | _ "whitespace" 82 | = whitespace* 83 | 84 | __ "whitespace" 85 | = whitespace+ 86 | 87 | whitespace "whitespace" 88 | = [ \t\v\f\r\n\u00A0\uFEFF\u1680\u180E\u2000-\u200A\u202F\u205F\u3000] 89 | 90 | 91 | //1-Behavior,DamageReductionRanged25Controller,DamageResponse.ModifyFraction*100 92 | -------------------------------------------------------------------------------- /sandbox/scratchpad.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nydus/heroesjson/44647a1efe46393b3a3f49448deb1c1b7c64fe61/sandbox/scratchpad.txt -------------------------------------------------------------------------------- /shared/C.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var base = require("@sembiance/xbase"); // jshint ignore:line 4 | 5 | // Extra heroes in the 'heromods' folder 6 | exports.EXTRA_HEROES_HEROMODS = ["chogall"]; 7 | 8 | // Extra hero files in "mods/heromods/" + heroName + ".stormmod/base.stormdata/GameData/" + gameDataName + "Data.xml" 9 | exports.EXTRA_HEROES_HEROMODS_NAMED = 10 | { 11 | "chogall" : "ChoGall", 12 | "wizard" : "Wizard", 13 | "necromancer" : "Necromancer", 14 | "dehaka" : "Dehaka", 15 | "tracer" : "Tracer", 16 | "chromie" : "Chromie", 17 | "medivh" : "Medivh", 18 | "guldan" : "Guldan", 19 | "auriel" : "Auriel", 20 | "alarak" : "Alarak", 21 | "zarya" : "Zarya", 22 | "samuro" : "Samuro" 23 | }; 24 | 25 | exports.SKIP_HERO_IDS = ["GreymaneWorgen", "ChoGallBundleProduct"]; 26 | 27 | exports.HERO_ID_TEXTURE_RENAMES = { }; 28 | // { "Dryad" : "lunara"}; 29 | 30 | exports.EXTRA_XML_FILE_PATHS = []; 31 | 32 | // Extra mount data files in GameData/Mounts/Data/Mount_Data.xml 33 | exports.EXTRA_MOUNT_DATA_FILES = { 34 | "Cloud9Nexagon" : ["Ridesurf_Cloud9Nexagon"], 35 | "Felstalker" : ["Ride_Felstalker"], 36 | "Horse" : ["Horse_ArmoredWarSteed", 37 | "Horse_Common", 38 | "Horse_HeadlessHorseman", 39 | "Horse_IllidansNightmare", 40 | "Horse_JudgementCharger", 41 | "Horse_MalthaelsPhantom", 42 | "Horse_MarshalsOutrider", 43 | "Horse_Nazeebra", 44 | "Horse_NexusCharger", 45 | "Horse_RainbowUnicorn", 46 | "Horse_TyraelsCharger", 47 | "Horse_Demonic" ], 48 | "LionGreymane" : ["Ride_LionGreymane"], 49 | "LunarDragon" : ["Ride_LunarDragon"], 50 | "StarChariot" : ["Ridesurf_StarChariot"], 51 | "Starbreaker" : ["Ridebike_Starbreaker"], 52 | "TreasureGoblinWinter" : ["Ride_TreasureGoblinWinter"] 53 | }; 54 | 55 | // Extra hero data files GameData/Heroes/Data.xml 56 | exports.EXTRA_HEROES_GAMEDATA_FILES = ["Zagara"]; 57 | 58 | // Extra hero subfolder files GameData/Heroes/Data/Data.xml 59 | // "Expansion" heroes that have moved into "main" modules. 60 | exports.EXTRA_HEROES_GAMEDATA_FOLDERS = [ 61 | "Anubarak", 62 | "Artanis", 63 | "Azmodan", 64 | "Butcher", 65 | "Chen", 66 | "Crusader", 67 | "DemonHunter", 68 | "Dryad", 69 | "Genn", 70 | "Jaina", 71 | "Kaelthas", 72 | "Leoric", 73 | "LostVikings", 74 | "Medic", 75 | "Monk", 76 | "Murky", 77 | "Necromancer", 78 | "Rexxar", 79 | "Stitches", 80 | "Sylvanas", 81 | "Thrall", 82 | "Uther", 83 | "WitchDoctor", 84 | "Wizard", 85 | "Tinker" 86 | ]; 87 | 88 | exports.HERO_MODIFICATIONS = 89 | { 90 | "Crusader" : [ { path : ":root", name : "ratings", value : { damage : 3, utility : 6, survivability : 10, complexity : 4 } } ], 91 | "Chen" : [ { path : ":root", name : "releaseDate", value : "2014-09-10" } ], 92 | "Abathur" : [ { path : ":root .abilities .AbathurSymbiote *:nth-child(1)", name : "aimType", value : "Skillshot"} ], 93 | "Azmodan" : 94 | [ 95 | { path : ":root .abilities .Azmodan *:nth-child(3)", name : "manaCostPerSecond", value : 16} // Unknown where this can be found 96 | ], 97 | "Arthas" : 98 | [ 99 | { path : ":root .abilities .Arthas *:nth-child(2)", name : "manaCostPerSecond", value : 15} // Somehow 'ArthasDeathAndDecayTog' leads to CBehaviorBuff 'DeathAndDecay' which has a 100 | ], 101 | "Medic" : [ { path : ":root .abilities .Medic *:nth-child(6)", remove : ["cooldown"]} ], 102 | "Wizard" : [ { path : ":root", name : "releaseDate", value : "2016-02-02" } ], 103 | "Guldan" : [ { path : ":root", name : "ratings", value : { damage : 8, utility : 3, survivability : 5, complexity : 6 } } ], 104 | }; 105 | 106 | exports.MOUNT_MODIFICATIONS = 107 | { 108 | "Random" : [ 109 | { path : ":root", name : "description", value : "A random mount."}, 110 | { path : ":root", name : "category", value : "Random"} 111 | ], 112 | "Mechanospider" : [ { path : ":root", name : "franchise", value : "Warcraft"} ], 113 | "CountessKerriganBatForm" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 114 | "ZagaraWings" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 115 | "CyberWolf" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 116 | "Felstalker" : [ { path : ":root", name : "franchise", value : "Warcraft"} ], 117 | "CyberWolfGold" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 118 | "CyberWolfBlack" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 119 | "MarshallRaynorHorse" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 120 | "MechaTassadarMorphForm" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 121 | "VoidSpeeder" : [ { path : ":root", name : "franchise", value : "Starcraft"} ], 122 | "Vulture" : [ { path : ":root", name : "franchise", value : "Starcraft"} ] 123 | }; 124 | 125 | exports.FULLY_PARENTHESIZE = ["StitchesCombatStyleToxicGas"]; 126 | 127 | exports.HERO_UNIT_ID_REPLACEMENTS = 128 | { 129 | "LostVikings" : "LostVikingsController" 130 | }; 131 | 132 | exports.ADDITIONAL_HERO_SUBUNIT_IDS = 133 | { 134 | // "Chen" : ["ChenStormEarthFire"], 135 | "Uther" : ["UtherSpirit"], 136 | "Rexxar" : ["RexxarMisha"] 137 | }; 138 | 139 | exports.VALID_UNIT_ABILITY_IDS = 140 | { 141 | "AbathurSymbiote" : ["AbathurSymbioteCancel", "AbathurSymbioteStab", "AbathurSymbioteSpikeBurst", "AbathurSymbioteCarapace"], 142 | "HeroBaleog" : ["LostVikingsPressA", "LostVikingsSpinToWin", "LostVikingsNorseForce", "LostVikingsNordicAttackSquad", "LostVikingsVikingBribery"], 143 | "HeroErik" : ["LostVikingsPressA", "LostVikingsSpinToWin", "LostVikingsNorseForce", "LostVikingsNordicAttackSquad", "LostVikingsVikingBribery"], 144 | "HeroOlaf" : ["LostVikingsPressA", "LostVikingsSpinToWin", "LostVikingsNorseForce", "LostVikingsNordicAttackSquad", "LostVikingsVikingBribery"], 145 | // "Chen" : ["ChenStormEarthFireRetargetSpirits", "ChenStormEarthFireSpread", "ChenStormEarthFireTriAttack"], 146 | "TychusOdin" : ["TychusCommandeerOdinAnnihilate", "TychusCommandeerOdinRagnarokMissiles"], 147 | "Tychus" : ["TychusOdinThrusters"], 148 | "Uther" : ["UtherFlashofLight"], 149 | "Rexxar" : ["RexxarMishaFollow", "RexxarMishaFollowCancel"], 150 | "Greymane" : ["GreymaneDisengage", "GreymaneRazorSwipe"], 151 | "Xul" : ["NecromancerBoneArmor"] 152 | }; 153 | 154 | exports.ACTIVATABLE_ABILITY_IDS = 155 | { 156 | "Xul" : ["NecromancerBoneArmor"], 157 | "Rexxar" : ["RexxarMishaFollow", "RexxarMishaFollowCancel"] 158 | }; 159 | 160 | exports.ABILITY_SHORTCUT_REMAPS = 161 | { 162 | "RexxarMishaFollow" : "D", 163 | "RexxarMishaFollowCancel" : "D" 164 | }; 165 | 166 | exports.ALLOWED_EMPTY_XML_REF_IDS = ["Artifact_AP_Base", "TalentGatheringPowerCarry", "GreymaneWizenedDuelistCarry"]; 167 | 168 | exports.HERO_SUBUNIT_ABILITIES_MOVE = 169 | { 170 | "Tychus" : { "Tychus" : { "TychusOdinThrusters" : "TychusOdin" } }, 171 | "Uther" : { "Uther" : { "UtherFlashofLight" : "UtherSpirit" } } 172 | }; 173 | 174 | exports.HERO_MOUNT_UNIT_ID_REPLACEMENTS = 175 | { 176 | "LostVikings" : "Baleog" 177 | }; 178 | 179 | exports.HERO_SKIP_ABILITY_IDS = 180 | { 181 | "Rehgar" : ["RehgarGhostWolfActivate"] 182 | }; 183 | 184 | exports.MOUNT_ABILITY_IDS = 185 | { 186 | "Abathur" : "AbathurDeepTunnel", 187 | "FaerieDragon" : "FaerieDragonPhaseShiftFlight", 188 | "Falstad" : "FalstadFlight", 189 | "SgtHammer" : "Thrusters", 190 | "LostVikings" : "LostVikingsGoGoGo", 191 | "Rehgar" : "RehgarGhostWolfActivate", 192 | "Gall" : "GallHurryUpOaf" 193 | }; 194 | 195 | exports.FORMULA_PRE_REPLACEMENTS = 196 | [ 197 | // { // deprecated 2016-05-12 198 | // match : "$GalaxyVar:libGDHL_gv_bALHeroKerriganAssimilationRangedDamageModifier$", 199 | // replace : "0.1" 200 | // }, 201 | // { // deprecated 2016-05-12 202 | // match : "$GalaxyVar:libGDHL_gv_bALHeroKerriganAssimilationBaseModifier$", 203 | // replace : "0.1" 204 | // }, 205 | { 206 | match : "Behavior,CrusaderPunishStackingSlow,Modification.UnifiedMoveSpeedFactor*(-100)6", 207 | replace : "Behavior,CrusaderPunishStackingSlow,Modification.UnifiedMoveSpeedFactor*(-100)*6" 208 | }, 209 | { 210 | match : "Behavior,LostVikingVikingHoardCarryBehavior,Modification.VitalRegenArray[Life]", 211 | replace : "1" 212 | }, 213 | { 214 | match : "1-*Behavior,RexxarBarkskinBuff,DamageResponse.ModifyFraction*100", 215 | replace : "1-Behavior,RexxarBarkskinBuff,DamageResponse.ModifyFraction*100" 216 | }, 217 | // { // deprecated 2016-05-12 218 | // match : "Effect,ClairvoyanceRevealedPersistent,ExpireDelay", 219 | // replace : "4" 220 | // }, 221 | { 222 | match : "Effect,ChoConsumingBlazeTalentBlazingBulwarkApplyBlockStack,Count", 223 | replace : "1" 224 | }, 225 | { 226 | match : "Behavior,GreymaneHuntersBlunderbussCarryBehavior,DamageResponse.ModifyFraction", 227 | replace : "1" 228 | }, 229 | { 230 | match : "Behavior,ToothAndClawCarryBehavior,DamageResponse.ModifyFraction", 231 | replace : "1" 232 | }, 233 | { 234 | match : "Upgrade,NovaSnipeMasterDamageUpgrade,EffectArray[2].Value*100", 235 | replace : "12" 236 | }, 237 | { 238 | match : "Upgrade,NovaSnipeMasterDamageUpgrade,MaxLevel", 239 | replace : "5" 240 | }, 241 | { // 42742 242 | match : "Behavior,ArthasFrozenTempestFrigidWindsAttackSpeedDebuff,Modification.AdditiveAttackSpeedFactor*(1/Behavior,ArthasFrozenTempestCaster,Period)(-100)", 243 | replace : "Behavior,ArthasFrozenTempestFrigidWindsAttackSpeedDebuff,Modification.AdditiveAttackSpeedFactor*(1/Behavior,ArthasFrozenTempestCaster,Period)*(-100)" 244 | }, 245 | { // 42742 246 | match : "Behavior,ChromieTimeTrapChronoSicknessSlow,MaxStackCount", 247 | replace : "1" 248 | }, 249 | { // 42742 250 | match : "$BehaviorTokenCount:AurielRayOfHeavenReservoirOfHopeQuestToken$*Behavior,AurielRayOfHeavenReservoirOfHopeBonusEnergy,Modification.VitalMaxArray[Energy])", 251 | replace : "Behavior,AurielRayOfHeavenReservoirOfHopeBonusEnergy,Modification.VitalMaxArray[Energy])" 252 | }, 253 | { // 42742 254 | match : "((Effect,ZaryaExpulsionZoneInitialSearchArea,AreaArray[0].Radius+.Value+Talent,ZaryaExpulsionZoneClearOut,AbilityModificationArray[0].Modifications[1].Value)/Effect,ZaryaExpulsionZoneInitialSearchArea,AreaArray[0].Radius)-1)*100", 255 | replace : "((Effect,ZaryaExpulsionZoneInitialSearchArea,AreaArray[0].Radius+Talent,ZaryaExpulsionZoneClearOut,AbilityModificationArray[0].Modifications[1].Value)/Effect,ZaryaExpulsionZoneInitialSearchArea,AreaArray[0].Radius)-1)*100" 256 | } 257 | 258 | 259 | 260 | ]; 261 | 262 | exports.XMLREF_REPLACEMENTS = 263 | [ 264 | // { // deprecated 2016-05-12 265 | // from : "Effect,ArcaneIntellectBasicAttackManaRestore,VitalArray[2].Change", 266 | // to : "Effect,ArcaneIntellectBasicAttackManaRestore,VitalArray[0].Change", 267 | // }, 268 | // { // deprecated 2016-05-12 269 | // from : "Effect,ArcaneIntellectAbilityDamageManaRestore,VitalArray[2].Change", 270 | // to : "Effect,ArcaneIntellectAbilityDamageManaRestore,VitalArray[0].Change" 271 | // }, 272 | // { // deprecated 2016-05-12 273 | // from : "Effect,FrostmourneHungersManaRestoreModifyUnit,VitalArray[2].Change", 274 | // to : "Effect,FrostmourneHungersManaRestoreModifyUnit,VitalArray[0].Change" 275 | // }, 276 | // { // deprecated 2016-05-12 277 | // from : "Behavior,FeralHeartCarryBehavior,Modification.VitalRegenMultiplier[2]", 278 | // to : "Behavior,FeralHeartCarryBehavior,Modification.VitalRegenMultiplier[1]" 279 | // }, 280 | { 281 | from : "Behavior,TalentBucketVigorousAssault,Modification.VitalDamageLeechArray[0].KindArray[2]", 282 | to : "Behavior,TalentBucketVigorousAssault,Modification.VitalDamageLeechArray[0].KindArray[0]" 283 | }, 284 | // { // deprecated 2016-05-12 285 | // from : "Behavior,TalentBucketVampiricAssaultTychus,Modification.VitalDamageLeechArray[0].KindArray[2]", 286 | // to : "Behavior,TalentBucketVampiricAssaultTychus,Modification.VitalDamageLeechArray[0].KindArray[0]" 287 | // }, 288 | // { // deprecated 2016-05-12 289 | // from : "Effect,StormBoltRefundMasteryModifyUnit,Cost[0].Fraction.Vital[2]", 290 | // to : "Effect,StormBoltRefundMasteryModifyUnit,Cost[0].Fraction.Vital[0]" 291 | // }, 292 | // { // deprecated 2016-05-12 293 | // from : "Abil,MuradinStormBolt,Cost[0].Vital[2]", 294 | // to : "Abil,MuradinStormBolt,Cost[0].Vital[0]" 295 | // }, 296 | { 297 | from : "Behavior,TalentBucketVigorousStrike,Modification.VitalDamageLeechArray[0].KindArray[2]", 298 | to : "Behavior,TalentBucketVigorousStrike,Modification.VitalDamageLeechArray[0].KindArray[0]" 299 | }, 300 | { 301 | from : "Effect,OdinRagnarokMissilesDamage,Amount", 302 | to : "Effect,TychusOdinRagnarokMissilesDamage,Amount" 303 | }, 304 | { 305 | from : "Effect,JainaArcaneIntellectBasicAttackManaRestore,VitalArray[2].Change", 306 | to : "Effect,JainaArcaneIntellectBasicAttackManaRestore,VitalArray[0].Change" 307 | }, 308 | { 309 | from : "Effect,JainaArcaneIntellectAbilityDamageManaRestore,VitalArray[2].Change", 310 | to : "Effect,JainaArcaneIntellectAbilityDamageManaRestore,VitalArray[0].Change" 311 | }, 312 | { // 42742 313 | from : 'Behavior,FalstadHammerGains,Modification.VitalDamageLeechArray[0].KindArray[2]', 314 | to : 'Behavior,FalstadHammerGains,Modification.VitalDamageLeechArray[0].KindArray[0]' 315 | }, 316 | { // 42742 317 | from : 'Unit,HeroChromie,Sight', 318 | to : 'Behavior,ChromieDragonsBreathDeepBreathingMaxStack,Modification.SightBonus' 319 | } 320 | ]; 321 | 322 | exports.REMOVE_SUBUNITS = 323 | { 324 | "LostVikings" : ["HeroBaleog", "HeroErik", "HeroOlaf"], 325 | "Chen" : ["HeroChenEarth", "HeroChenFire", "HeroChenStorm"], 326 | "Medic" : ["MedicMedivacDropship"], 327 | "Medivh" : ["HeroMedivhRaven"] 328 | }; 329 | 330 | exports.IMPORT_ABILITIES_FROM_SUBUNIT = 331 | { 332 | "LostVikings" : "HeroBaleog" 333 | }; 334 | 335 | exports.IMPORT_ABILITIES = 336 | { 337 | "HeroBaleog" : 338 | [ 339 | { 340 | id : "LostVikingSelectOlaf", 341 | shortcut : "1", 342 | name : "Select Olaf", 343 | icon : "storm_ui_icon_lostvikings_selectolaf.dds" 344 | }, 345 | { 346 | id : "LostVikingSelectBaleog", 347 | shortcut : "2", 348 | name : "Select Baleog", 349 | icon : "storm_ui_icon_lostvikings_selectbaleog.dds" 350 | }, 351 | { 352 | id : "LostVikingSelectErik", 353 | shortcut : "3", 354 | name : "Select Erik", 355 | icon : "storm_ui_icon_lostvikings_selecterik.dds" 356 | }, 357 | { 358 | id : "LostVikingSelectAll", 359 | shortcut : "4", 360 | name : "Select All Vikings", 361 | icon : "storm_ui_icon_lostvikings_selectall.dds" 362 | } 363 | ] 364 | }; 365 | 366 | exports.USE_ABILITY_NAME = 367 | [ 368 | "FaerieDragon", "Tassadar" 369 | ]; 370 | 371 | exports.ABILITY_ID_DESCRIPTION_IDS = 372 | { 373 | "Rehgar" : {"RehgarGhostWolfActivate" : "RehgarGhostWolf"}, 374 | "Jaina" : {"JainaConeOfCold" : "JainaConeofCold"} 375 | }; 376 | 377 | exports.HERO_MAX_LEVEL = 20; 378 | 379 | exports.HERO_TALENT_LEVELS = [1, 4, 7, 10, 13, 16, 20]; 380 | 381 | exports.MOUNT_JSON_SCHEMA = 382 | { 383 | name : "mount", 384 | type : "object", 385 | additionalProperties : true, 386 | properties : 387 | { 388 | id : { type : "string", minLength : 1 }, 389 | attributeid : { type : "string", minLength : 1 }, 390 | name : { type : "string", minLength : 1 }, 391 | description : { type : "string", minLength : 1 }, 392 | releaseDate : { type : "string", pattern : "2[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" }, 393 | variation : { type : "boolean" }, 394 | productid : { type : "integer" }, 395 | category : { type : "string", minLength : 1 }, 396 | } 397 | }; 398 | 399 | exports.MOUNT_JSON_SCHEMA.required = Object.keys(exports.MOUNT_JSON_SCHEMA.properties); 400 | exports.MOUNT_JSON_SCHEMA.required.remove("productid"); 401 | 402 | exports.HERO_JSON_SCHEMA = 403 | { 404 | name : "hero", 405 | type : "object", 406 | additionalProperties : false, 407 | properties : 408 | { 409 | id : { type : "string", minLength : 1 }, 410 | attributeid : { type : "string", minLength : 1 }, 411 | name : { type : "string", minLength : 1 }, 412 | title : { type : "string", minLength : 1 }, 413 | description : { type : "string", minLength : 1 }, 414 | icon : { type : "string", minLength : 1 }, 415 | role : { type : "string", enum : ["Assassin", "Warrior", "Support", "Specialist"] }, 416 | type : { type : "string", enum : ["Melee", "Ranged"] }, 417 | gender : { type : "string", enum : ["Female", "Male"] }, 418 | franchise : { type : "string", minLength : 1 }, 419 | difficulty : { type : "string", enum : ["Easy", "Medium", "Hard", "Very Hard"] }, 420 | releaseDate : { type : "string", pattern : "2[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" }, 421 | stats : 422 | { 423 | type : "object", 424 | additionalProperties : 425 | { 426 | type : "object", 427 | additionalProperties : false, 428 | required : ["hp", "hpPerLevel", "hpRegen", "hpRegenPerLevel", "mana", "manaPerLevel", "manaRegen", "manaRegenPerLevel"], 429 | properties : 430 | { 431 | hp : { type : "number", minimum : 0 }, 432 | hpPerLevel : { type : "number", minimum : 0 }, 433 | hpRegen : { type : "number", minimum : 0 }, 434 | hpRegenPerLevel : { type : "number", minimum : 0 }, 435 | mana : { type : "number", minimum : 0 }, 436 | manaPerLevel : { type : "number", minimum : 0 }, 437 | manaRegen : { type : "number", minimum : 0 }, 438 | manaRegenPerLevel : { type : "number", minimum : 0 } 439 | } 440 | } 441 | }, 442 | ratings : 443 | { 444 | type : "object", 445 | additionalProperties : false, 446 | required : ["damage", "utility", "survivability", "complexity"], 447 | properties : 448 | { 449 | damage : { type : "integer", minimum : 1, maximum : 10 }, 450 | utility : { type : "integer", minimum : 1, maximum : 10 }, 451 | survivability : { type : "integer", minimum : 1, maximum : 10 }, 452 | complexity : { type : "integer", minimum : 1, maximum : 10 } 453 | } 454 | }, 455 | talents : 456 | { 457 | type : "object", 458 | additionalProperties : false, 459 | properties : {} 460 | }, 461 | abilities : 462 | { 463 | type : "object", 464 | additionalProperties : 465 | { 466 | type : "array", 467 | minItems : 1, 468 | items : 469 | { 470 | type : "object", 471 | additionalProperties : false, 472 | required : ["id", "name", "description", "icon"], 473 | properties : 474 | { 475 | id : { type : "string", minLength : 1 }, 476 | name : { type : "string", minLength : 1 }, 477 | description : { type : "string", minLength : 1 }, 478 | trait : { type : "boolean" }, 479 | heroic : { type : "boolean" }, 480 | cooldown : { type : "number", minimum : 0 }, 481 | manaCost : { type : "number", minimum : 0 }, 482 | manaCostPerSecond : { type : "number", minimum : 0 }, 483 | aimType : { type : "string", minLength : 1 }, 484 | shortcut : { type : "string", minLength : 1, maxLength : 1 }, 485 | icon : { type : "string", minLength : 1 } 486 | } 487 | } 488 | } 489 | } 490 | } 491 | }; 492 | 493 | var HERO_TALENT_TIER_JSON_SCHEMA = 494 | { 495 | type : "array", 496 | minItems : 1, 497 | maxItems : 7, 498 | items : 499 | { 500 | type : "object", 501 | additionalProperties : false, 502 | required : ["id", "name", "description", "icon"], 503 | properties : 504 | { 505 | id : { type : "string", minLength : 1 }, 506 | name : { type : "string", minLength : 1 }, 507 | description : { type : "string", minLength : 1 }, 508 | prerequisite : { type : "string", minLength : 1 }, 509 | cooldown : { type : "number", minimum : 0 }, 510 | icon : { type : "string", minLength : 1 } 511 | } 512 | } 513 | }; 514 | 515 | exports.HERO_TALENT_LEVELS.forEach(function(HERO_TALENT_LEVEL) { exports.HERO_JSON_SCHEMA.properties.talents.properties[HERO_TALENT_LEVEL] = base.clone(HERO_TALENT_TIER_JSON_SCHEMA, true); }); 516 | exports.HERO_JSON_SCHEMA.properties.talents.required = exports.HERO_TALENT_LEVELS.map(function(HERO_TALENT_LEVEL) { return ""+HERO_TALENT_LEVEL; }); 517 | 518 | exports.HERO_JSON_SCHEMA.required = Object.keys(exports.HERO_JSON_SCHEMA.properties); 519 | 520 | exports.HERO_JSON_SCHEMA.properties.talents.properties[10].minItems = 2; 521 | exports.HERO_JSON_SCHEMA.properties.talents.properties[10].maxItems = 3; 522 | -------------------------------------------------------------------------------- /util/calc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var base = require("xbase"), 4 | fs = require("fs"), 5 | path = require("path"); 6 | 7 | //var test = "1-0.75*(100+2)+9/7+-2"; 8 | //var test = "100 * -0.25"; 9 | //var test = "100*-0.5"; 10 | //var test = "-1*-2"; 11 | var test = "(2.025-1.35/1.35)*100"; 12 | 13 | base.info(test); 14 | base.info(parenthesize(test)); 15 | base.info(eval(parenthesize(test))); 16 | 17 | base.info("\n"); 18 | base.info(test); 19 | base.info(fullyParenthesize(test)); 20 | base.info(eval(fullyParenthesize(test))); 21 | 22 | function parenthesize(formula) 23 | { 24 | var result = []; 25 | var seenOperator = false; 26 | var lastOpenParenLoc = 0; 27 | var seenOneParenClose = true; 28 | formula.replace(/ /g, "").split("").forEach(function(c, i) 29 | { 30 | if("+-/*".contains(c) && seenOperator && seenOneParenClose && !"+-/*(".contains(result.last())) 31 | { 32 | result.splice(lastOpenParenLoc, 0, "("); 33 | result.push(")"); 34 | } 35 | 36 | if(c==="(") 37 | seenOneParenClose = false; 38 | 39 | if(c===")") 40 | seenOneParenClose = true; 41 | 42 | if("+-/*".contains(c) && i!==0 && !"+-/*(".contains(result.last())) 43 | seenOperator = true; 44 | 45 | result.push(c); 46 | }); 47 | 48 | return result.join(""); 49 | } 50 | 51 | function fullyParenthesize(formula) 52 | { 53 | var result = [].pushMany("(", formula.replace(/[^+/*-]/g, "").length+1); 54 | 55 | formula.replace(/ /g, "").split("").forEach(function(c, i) 56 | { 57 | if(c==="(" || c===")") 58 | return; 59 | 60 | if("+-/*".contains(c)) 61 | result.push(")"); 62 | 63 | result.push(c); 64 | }); 65 | 66 | result.push(")"); 67 | 68 | return result.join(""); 69 | } -------------------------------------------------------------------------------- /util/compareRelease.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*global setImmediate: true*/ 3 | 4 | var base = require("xbase"), 5 | fs = require("fs"), 6 | glob = require("glob"), 7 | diffUtil = require("xutil").diff, 8 | httpUtil = require("xutil").http, 9 | path = require("path"), 10 | tiptoe = require("tiptoe"); 11 | 12 | tiptoe( 13 | function getHeroesJSON() 14 | { 15 | httpUtil.get("http://heroesjson.com/json/heroes.json", this.parallel()); 16 | fs.readFile(path.join(__dirname, "..", "web", "json", "heroes.json"), {encoding : "utf8"}, this.parallel()); 17 | }, 18 | function compareHeroes(oldJSON, newJSON) 19 | { 20 | var oldData = JSON.parse(oldJSON[0]).mutate(function(hero, result) { result[hero.id] = hero; return result; }, {}); 21 | var newData = JSON.parse(newJSON).mutate(function(hero, result) { result[hero.id] = hero; return result; }, {}); 22 | 23 | Object.keys(newData).subtract(Object.keys(oldData)).forEach(function(newKey) { base.info("New hero: %s", newKey); delete newData[newKey]; }); 24 | 25 | var result = diffUtil.diff(oldData, newData, {compareArraysDirectly:true, arrayKey : "id"}); 26 | if(result) 27 | console.log(result); 28 | 29 | this(); 30 | }, 31 | function getMountsJSON() 32 | { 33 | httpUtil.get("http://heroesjson.com/json/mounts.json", this.parallel()); 34 | fs.readFile(path.join(__dirname, "..", "web", "json", "mounts.json"), {encoding : "utf8"}, this.parallel()); 35 | }, 36 | function compareMounts(oldJSON, newJSON) 37 | { 38 | var oldData = JSON.parse(oldJSON[0]).mutate(function(mount, result) { result[mount.id] = mount; return result; }, {}); 39 | var newData = JSON.parse(newJSON).mutate(function(mount, result) { result[mount.id] = mount; return result; }, {}); 40 | 41 | Object.keys(newData).subtract(Object.keys(oldData)).forEach(function(newKey) { base.info("New mount: %s", newKey); delete newData[newKey]; }); 42 | 43 | var result = diffUtil.diff(oldData, newData, {compareArraysDirectly:true, arrayKey : "id"}); 44 | if(result) 45 | console.log(result); 46 | 47 | this(); 48 | }, 49 | function finish(err) 50 | { 51 | if(err) 52 | { 53 | base.error(err.stack); 54 | base.error(err); 55 | process.exit(1); 56 | } 57 | 58 | process.exit(0); 59 | } 60 | ); 61 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | json 2 | index.html 3 | index.css 4 | images.zip 5 | -------------------------------------------------------------------------------- /web/changelog.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "2.7.19", 4 | "when": "2016-07-22", 5 | "patchVersion": "44737", 6 | "changes": [ 7 | "http://us.battle.net/heroes/en/blog/20185029/", 8 | "http://us.battle.net/heroes/en/blog/20176357/", 9 | "http://us.battle.net/heroes/en/blog/20164442/" 10 | ] 11 | }, 12 | { 13 | "version": "2.7.18", 14 | "when": "2016-06-29", 15 | "patchVersion": "44124", 16 | "changes": [ 17 | "http://us.battle.net/heroes/en/blog/20167050/" 18 | ] 19 | }, 20 | { 21 | "version": "2.7.17", 22 | "when": "2016-06-14", 23 | "patchVersion": "43571", 24 | "changes": [ 25 | "http://us.battle.net/heroes/en/blog/20143449" 26 | ] 27 | }, 28 | { 29 | "version": "2.7.16", 30 | "when": "2016-05-25", 31 | "patchVersion": "43170", 32 | "changes": [ 33 | "http://us.battle.net/heroes/en/blog/20131411" 34 | ] 35 | }, 36 | { 37 | "version": "2.7.15", 38 | "when": "2016-05-19", 39 | "patchVersion": "43051", 40 | "changes": [ 41 | "http://us.battle.net/heroes/en/blog/20120032/", 42 | "http://us.battle.net/heroes/en/blog/20118416/" 43 | ] 44 | }, 45 | { 46 | "version": "2.7.14", 47 | "when": "2016-05-07", 48 | "patchVersion": "42590", 49 | "changes": [ 50 | "http://us.battle.net/heroes/en/blog/20102776/", 51 | "http://us.battle.net/heroes/en/blog/20097945/" 52 | ] 53 | }, 54 | { 55 | "version": "2.7.12", 56 | "when": "2016-04-21", 57 | "patchVersion": "42406", 58 | "changes": [ 59 | ] 60 | }, 61 | { 62 | "version": "2.7.11", 63 | "when": "2016-04-19", 64 | "patchVersion": "42273", 65 | "changes": [ 66 | "http://us.battle.net/heroes/en/blog/20099322" 67 | ] 68 | }, 69 | { 70 | "version": "2.7.10", 71 | "when": "2016-04-12", 72 | "patchVersion": "42178", 73 | "changes": [ 74 | "http://us.battle.net/heroes/en/blog/20090404" 75 | ] 76 | }, 77 | { 78 | "version": "2.7.9", 79 | "when": "2016-03-29", 80 | "patchVersion": "41810", 81 | "changes": [ 82 | "http://us.battle.net/heroes/en/blog/20063493" 83 | ] 84 | }, 85 | { 86 | "version": "2.7.8", 87 | "when": "2016-03-16", 88 | "patchVersion": "41504", 89 | "changes": [ 90 | "http://us.battle.net/heroes/en/blog/20057110" 91 | ] 92 | }, 93 | { 94 | "version": "2.7.7", 95 | "when": "2016-03-09", 96 | "patchVersion": "41393", 97 | "changes": [ 98 | "http://us.battle.net/heroes/en/blog/20058183" 99 | ] 100 | }, 101 | { 102 | "version" : "2.7.6", 103 | "when": "2016-03-02", 104 | "patchVersion": "41150", 105 | "changes" : [ 106 | "Added Xul", 107 | "http://us.battle.net/heroes/en/blog/20049139" 108 | ] 109 | }, 110 | { 111 | "version" : "2.7.5", 112 | "when" : "2016-02-19", 113 | "patchVersion" : "40798", 114 | "changes" : [ 115 | "http://us.battle.net/heroes/en/blog/20038832/heroes-of-the-storm-balance-update-notes-february-17-2016-2-17-2016" 116 | ] 117 | }, 118 | { 119 | "version" : "2.7.4", 120 | "when" : "2016-02-12", 121 | "patchVersion" : "40697", 122 | "changes" : [ 123 | "Updated Nova's Snipe Mastery talent" 124 | ] 125 | }, 126 | { 127 | "version" : "2.7.3", 128 | "when" : "2016-02-05", 129 | "patchVersion" : "40431", 130 | "changes" : [ 131 | "Fixed the 'Felstalker' mount to have the correct 'franchise'." 132 | ] 133 | }, 134 | { 135 | "version" : "2.7.2", 136 | "when" : "2016-02-04", 137 | "patchVersion" : "40431", 138 | "changes" : [ 139 | "Fixed some mount info and added more mount info." 140 | ] 141 | }, 142 | { 143 | "version" : "2.7.1", 144 | "when" : "2016-02-04", 145 | "patchVersion" : "40431", 146 | "changes" : [ 147 | "Added documentation for mounts.json file.", 148 | "Added some missing mounts." 149 | ] 150 | }, 151 | { 152 | "version" : "2.7.0", 153 | "when" : "2016-02-02", 154 | "patchVersion" : "40431", 155 | "changes" : [ 156 | "Added Li-Ming!" 157 | ] 158 | }, 159 | { 160 | "version" : "2.6.1", 161 | "when" : "2016-02-01", 162 | "patchVersion" : "40322", 163 | "changes" : [ 164 | "Updated to latest patch version.", 165 | "Fixed the missing hpPerLevel and hpRegenPerLevel stats. These are percentages now." 166 | ] 167 | }, 168 | { 169 | "version" : "2.6.0", 170 | "when" : "2015-12-15", 171 | "patchVersion" : "39595", 172 | "changes" : [ 173 | "Added Greymane!" 174 | ] 175 | }, 176 | { 177 | "version" : "2.5.0", 178 | "when" : "2015-12-15", 179 | "patchVersion" : "39595", 180 | "changes" : [ 181 | "Added Lunara!" 182 | ] 183 | }, 184 | { 185 | "version" : "2.4.0", 186 | "when" : "2015-11-22", 187 | "patchVersion" : "39153", 188 | "changes" : [ 189 | "Added Cho!", 190 | "Added Gall!" 191 | ] 192 | }, 193 | { 194 | "version" : "2.3.2", 195 | "when" : "2015-10-27", 196 | "patchVersion" : "38793", 197 | "changes" : [ 198 | "Updated to latest patch version.", 199 | "Fixed the 'manaCostPerSecond' field for Arthas ability Frozen Tempest", 200 | "Fixed the 'cooldown' field for Medic trait Caduceus Reactor." 201 | ] 202 | }, 203 | { 204 | "version" : "2.3.1", 205 | "when" : "2015-10-07", 206 | "patchVersion" : "38236", 207 | "changes" : [ 208 | "Fixed the 'shortcut' field for the Artanis ability Twin Blades." 209 | ] 210 | }, 211 | { 212 | "version" : "2.3.0", 213 | "when" : "2015-10-06", 214 | "patchVersion" : "38236", 215 | "changes" : [ 216 | "Added Lt. Morales!", 217 | "Added Artanis!" 218 | ] 219 | }, 220 | { 221 | "version" : "2.2.2", 222 | "when" : "2015-09-14", 223 | "patchVersion" : "37569", 224 | "changes" : [ 225 | "Fixed the 'description' field for the Toxic Gas talent." 226 | ] 227 | }, 228 | { 229 | "version" : "2.2.1", 230 | "when" : "2015-09-14", 231 | "patchVersion" : "37569", 232 | "changes" : [ 233 | "Fixed the 'cooldown' field for the talents Ice Block, Improved Ice Block and Feign Death." 234 | ] 235 | }, 236 | { 237 | "version" : "2.2.0", 238 | "when" : "2015-09-08", 239 | "patchVersion" : "37569", 240 | "changes" : [ 241 | "Added Rexxar!" 242 | ] 243 | }, 244 | { 245 | "version" : "2.1.5", 246 | "when" : "2015-09-03", 247 | "patchVersion" : "37117", 248 | "changes" : [ 249 | "Fixed the 'description' field for the Kerrigan ability Assimilation... again." 250 | ] 251 | }, 252 | { 253 | "version" : "2.1.4", 254 | "when" : "2015-08-27", 255 | "patchVersion" : "37117", 256 | "changes" : [ 257 | "Fixed the 'description' field for the Kerrigan ability Assimilation." 258 | ] 259 | }, 260 | { 261 | "version" : "2.1.3", 262 | "when" : "2015-08-24", 263 | "patchVersion" : "37117", 264 | "changes" : [ 265 | "Added missing 'per level' information to 17 abilities and talents." 266 | ] 267 | }, 268 | { 269 | "version" : "2.1.2", 270 | "when" : "2015-08-24", 271 | "patchVersion" : "37117", 272 | "changes" : [ 273 | "Fixed the 'description' field for Rehgar's Ghost Wolf ability and labeled it correctly as a mount." 274 | ] 275 | }, 276 | { 277 | "version" : "2.1.1", 278 | "when" : "2015-08-18", 279 | "patchVersion" : "37117", 280 | "changes" : [ 281 | "Fixed the 'name' field for some talents for both Brightwing and Tassadar.", 282 | "Fixed multiple cooldowns across many abilities and talents." 283 | ] 284 | }, 285 | { 286 | "version" : "2.1.0", 287 | "when" : "2015-08-18", 288 | "patchVersion" : "37117", 289 | "changes" : [ 290 | "Added Monk!", 291 | "Updated to latest patch data." 292 | ] 293 | }, 294 | { 295 | "version" : "2.0.9", 296 | "when" : "2015-08-17", 297 | "patchVersion" : "36536", 298 | "changes" : [ 299 | "Fixed the 'description' field for the Diablo ability Black Soulstone." 300 | ] 301 | }, 302 | { 303 | "version" : "2.0.8", 304 | "when" : "2015-08-17", 305 | "patchVersion" : "36536", 306 | "changes" : [ 307 | "Added an 'attributeid' field for each hero." 308 | ] 309 | }, 310 | { 311 | "version" : "2.0.7", 312 | "when" : "2015-08-09", 313 | "patchVersion" : "36536", 314 | "changes" : [ 315 | "Fixed values for talents First Aid, Promote and Bound Minion." 316 | ] 317 | }, 318 | { 319 | "version" : "2.0.6", 320 | "when" : "2015-08-01", 321 | "patchVersion" : "36536", 322 | "changes" : [ 323 | "Added missing 'per level' information to talents: Envenom, Burning Rage, Fury of the Storm." 324 | ] 325 | }, 326 | { 327 | "version" : "2.0.5", 328 | "when" : "2015-07-31", 329 | "patchVersion" : "36536", 330 | "changes" : [ 331 | "Added missing 'per level' information to 21 abilities and talents." 332 | ] 333 | }, 334 | { 335 | "version" : "2.0.4", 336 | "when" : "2015-07-12", 337 | "patchVersion" : "36144", 338 | "changes" : [ 339 | "Added a missing trait for the Lost Vikings." 340 | ] 341 | }, 342 | { 343 | "version" : "2.0.3", 344 | "when" : "2015-07-12", 345 | "patchVersion" : "36144", 346 | "changes" : [ 347 | "Added two missing abilities for the Lost Vikings.", 348 | "Added subunit TychusOdin to Tychus including abilities and stats.", 349 | "Added a missing ability to Uther." 350 | ] 351 | }, 352 | { 353 | "version" : "2.0.2", 354 | "when" : "2015-06-30", 355 | "patchVersion" : "36144", 356 | "changes" : [ 357 | "Added hero special mount abilities.", 358 | "Added 'cooldown' fields to several abilities.", 359 | "Removed an extraneous negative sign from Rehgar's Stormcaller talent." 360 | ] 361 | }, 362 | { 363 | "version" : "2.0.1", 364 | "when" : "2015-06-30", 365 | "patchVersion" : "36144", 366 | "changes" : [ 367 | "Added 'icon' field to each hero." 368 | ] 369 | }, 370 | { 371 | "version" : "2.0.0", 372 | "when" : "2015-06-30", 373 | "patchVersion" : "36144", 374 | "changes" : [ 375 | "Added Butcher and Leoric!", 376 | "Updated to latest patch data." 377 | ] 378 | }, 379 | { 380 | "version" : "1.2.2", 381 | "when" : "2015-06-29", 382 | "patchVersion" : "35702", 383 | "changes" : [ 384 | "Fixed several errors with numbers for certain talents." 385 | ] 386 | }, 387 | { 388 | "version" : "1.2.1", 389 | "when" : "2015-06-26", 390 | "patchVersion" : "35702", 391 | "changes" : [ 392 | "Fixed some incorrectly named abilities." 393 | ] 394 | }, 395 | { 396 | "version" : "1.2.0", 397 | "when" : "2015-06-26", 398 | "patchVersion" : "35702", 399 | "changes" : [ 400 | "Fixed multiple errors in abilities and talents that showed NaN instead of the proper value.", 401 | "Added 'icon' field to each talent and ability." 402 | ] 403 | }, 404 | { 405 | "version" : "1.1.2", 406 | "when" : "2015-06-25", 407 | "patchVersion" : "35702", 408 | "changes" : [ 409 | "Added 'description' field to each hero." 410 | ] 411 | }, 412 | { 413 | "version" : "1.1.1", 414 | "when" : "2015-06-22", 415 | "patchVersion" : "35702", 416 | "changes" : [ 417 | "Joined Lost Vikings abilities into one entry for LostVikings and added the switch viking abilities." 418 | ] 419 | }, 420 | { 421 | "version" : "1.1.0", 422 | "when" : "2015-06-22", 423 | "patchVersion" : "35702", 424 | "changes" : [ 425 | "Added secondary unit abilities (Abathur, Lost Vikings).", 426 | "Added a 'shortcut' field to abilities to see how they map in game." 427 | ] 428 | }, 429 | { 430 | "version" : "1.0.0", 431 | "when" : "2015-06-21", 432 | "patchVersion" : "35702", 433 | "changes" : [ 434 | "First release!" 435 | ] 436 | } 437 | ] 438 | -------------------------------------------------------------------------------- /web/conform.styl: -------------------------------------------------------------------------------- 1 | vendors = webkit moz o official 2 | 3 | border-radius() 4 | -webkit-border-radius arguments 5 | -moz-border-radius arguments 6 | border-radius arguments 7 | 8 | border-image() 9 | -webkit-border-image arguments 10 | -moz-border-image arguments 11 | -o-border-image arguments 12 | 13 | box-sizing() 14 | -webkit-box-sizing arguments 15 | -moz-box-sizing arguments 16 | -o-box-sizing arguments 17 | box-sizing arguments 18 | 19 | opacity(n) 20 | opacity n 21 | if support-for-ie 22 | -ms-filter "progid:DXImageTransform.Microsoft.Alpha(Opacity=" + round(n * 100) + ")"; 23 | filter unquote("progid:DXImageTransform.Microsoft.Alpha(Opacity=" + round(n * 100) + ")") 24 | 25 | box-shadow() 26 | -webkit-box-shadow arguments 27 | -moz-box-shadow arguments 28 | box-shadow arguments 29 | 30 | my-transition() 31 | -webkit-transition arguments 32 | -moz-transition arguments 33 | -ms-transition arguments 34 | -o-transition arguments 35 | transition arguments 36 | 37 | transition-box-shadow() 38 | -webkit-transition -webkit-box-shadow arguments 39 | -moz-transition -moz-box-shadow arguments 40 | -ms-transition -ms-box-shadow arguments 41 | -o-transition -o-box-shadow arguments 42 | transition box-shadow arguments 43 | 44 | transition-transform() 45 | -webkit-transition -webkit-transform arguments 46 | -moz-transition -moz-transform arguments 47 | -ms-transition -ms-transform arguments 48 | -o-transition -o-transform arguments 49 | 50 | transform() 51 | -webkit-transform arguments 52 | -moz-transform arguments 53 | -ms-transform arguments 54 | -o-transform arguments 55 | transform arguments 56 | 57 | transform-origin() 58 | -webkit-transform-origin arguments 59 | -moz-transform-origin arguments 60 | -ms-transform-origin arguments 61 | -o-transform-origin arguments 62 | transform-origin arguments 63 | 64 | background-size() 65 | -webkit-background-size arguments 66 | -moz-background-size arguments 67 | -o-background-size arguments 68 | background-size arguments 69 | 70 | mask-image() 71 | -webkit-mask-image arguments 72 | -moz-mask-image arguments 73 | -o-mask-image arguments 74 | mask-image arguments 75 | 76 | text-size-adjust() 77 | -webkit-text-size-adjust arguments 78 | -moz-text-size-adjust arguments 79 | -ms-text-size-adjust arguments 80 | text-size-adjust arguments 81 | 82 | user-select() 83 | if arguments=="element" 84 | -webkit-user-select text 85 | else 86 | -webkit-user-select arguments 87 | 88 | -khtml-user-select arguments 89 | 90 | if arguments=="none" 91 | -moz-user-select -moz-none 92 | else 93 | -moz-user-select arguments 94 | 95 | -ms-user-select arguments 96 | user-select arguments 97 | 98 | background-linear-gradient-legacy() 99 | background-image: -webkit-gradient(arguments); 100 | 101 | background-linear-gradient() 102 | background-image -webkit-linear-gradient(arguments) 103 | background-image -moz-linear-gradient(arguments) 104 | background-image -ms-linear-gradient(arguments) 105 | background-image -o-linear-gradient(arguments) 106 | background-image linear-gradient(arguments) 107 | 108 | background-radial-gradient() 109 | background-image -webkit-radial-gradient(arguments) 110 | background-image -moz-radial-gradient(arguments) 111 | background-image -ms-radial-gradient(arguments) 112 | background-image -o-radial-gradient(arguments) 113 | background-image radial-gradient(arguments) 114 | 115 | animation() 116 | -webkit-animation arguments 117 | -moz-animation arguments 118 | -o-animation arguments 119 | animation arguments 120 | 121 | /* 122 | linear-gradient(direction, from, to) 123 | if support-for-ie 124 | add-property(filter, s("progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorstr=%s, EndColorstr=%s)", from, to)); 125 | add-property(-ms-filter, s("progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorstr=%s, EndColorstr=%s)", from, to)); 126 | add-property(background, s("-webkit-gradient(linear, %s top, %s bottom, from(%s), to(%s))", direction, direction, from, to)); 127 | add-property(background, s("-webkit-linear-gradient(%s, %s, %s)", direction, from, to)); 128 | add-property(background, s("-ms-linear-gradient(%s, %s, %s)", direction, from, to)); 129 | add-property(background, s("-moz-linear-gradient(%s, %s, %s)", direction, from, to)); 130 | add-property(background, s("-o-linear-gradient(%s, %s, %s)", direction, from, to)); 131 | add-property(background, s("linear-gradient(%s, %s, %s)", direction, from, to)); 132 | */ 133 | 134 | background-opacity(level, r=0, g=0, b=0) 135 | background rgb(r, g, b) 136 | background rgba(r, g, b, level) 137 | 138 | add-sprite(width, height, positionX, positionY, paddingTop, paddingRight, paddingBottom, paddingLeft) 139 | width width 140 | height height 141 | background-position positionX positionY 142 | background-repeat no-repeat 143 | background-clip content-box 144 | padding-top paddingTop 145 | padding-right paddingRight 146 | padding-bottom paddingBottom 147 | padding-left paddingLeft 148 | 149 | &.selected 150 | background-position 50% 50%, positionX positionY 151 | background-repeat no-repeat, no-repeat 152 | background-clip border-box, content-box 153 | -------------------------------------------------------------------------------- /web/generate.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var base = require("xbase"), 4 | rimraf = require("rimraf"), 5 | fs = require("fs"), 6 | moment = require("moment"), 7 | glob = require("glob"), 8 | path = require("path"), 9 | runUtil = require("xutil").run, 10 | fileUtil = require("xutil").file, 11 | dustUtil = require("xutil").dust, 12 | printUtil = require("xutil").print, 13 | tiptoe = require("tiptoe"); 14 | 15 | if(process.argv.length<3 || !fs.existsSync(process.argv[2])) 16 | { 17 | base.error("Usage: node extractImages.js /path/to/hots"); 18 | process.exit(1); 19 | } 20 | 21 | var HOTS_PATH = process.argv[2]; 22 | var HOTS_DATA_PATH = path.join(HOTS_PATH, "HeroesData"); 23 | 24 | if(!fs.existsSync(HOTS_DATA_PATH)) 25 | { 26 | base.error("HeroesData dir not found: %s", HOTS_DATA_PATH); 27 | process.exit(1); 28 | } 29 | var dustData = 30 | { 31 | }; 32 | 33 | var WEB_OUT_PATH = path.join(__dirname, "dist"); 34 | 35 | var IMAGES_FULL_PATH = path.join(__dirname, "..", "images", "mods", "heroes.stormmod", "base.stormassets", "Assets", "Textures"); 36 | 37 | var ZIP_PATH = path.join(__dirname, "images.zip"); 38 | 39 | var CASCEXTRATOR_PATH = path.join(__dirname, "..", "build", "bin", "CASCExtractor"); 40 | 41 | var IMAGE_OUT_PATH = path.join(__dirname, "..", "images"); 42 | 43 | var HEROES_JSON_PATH = path.join(__dirname, "..", "out", "heroes.json"); 44 | 45 | var IMAGE_ASSETS_PATH = "mods/heroes.stormmod/base.stormassets/Assets/Textures/"; 46 | 47 | 48 | tiptoe( 49 | function removeJSONDirectory() 50 | { 51 | base.info("Clearing and creating out path..."); 52 | rimraf(path.join(WEB_OUT_PATH), this); 53 | }, 54 | function createJSONDirectory() 55 | { 56 | fs.mkdir(path.join(WEB_OUT_PATH), this); 57 | }, 58 | function extractingBuildVersion() 59 | { 60 | if(process.argv[3]==="dev") 61 | return this(); 62 | 63 | base.info("Extracting build version..."); 64 | runUtil.run(CASCEXTRATOR_PATH, [HOTS_DATA_PATH, "-o", WEB_OUT_PATH, "-f", "mods\\core.stormmod\\base.stormdata\\DataBuildId.txt"], {silent:true}, this); 65 | }, 66 | function readAndCleanupBuildVersion() 67 | { 68 | if(process.argv[3]==="dev") 69 | return this(); 70 | 71 | base.info("Data Build Version: %s", fs.readFileSync(path.join(WEB_OUT_PATH, "mods", "core.stormmod", "base.stormdata", "DataBuildId.txt"), {encoding:"utf8"}).trim("B").trim()); 72 | rimraf(path.join(WEB_OUT_PATH, "mods"), this); 73 | }, 74 | function findStaticContent() 75 | { 76 | base.info("Finding static content..."); 77 | glob(path.join(__dirname, "static", "*"), this); 78 | }, 79 | function processStaticContent(content) 80 | { 81 | base.info("Copying static content..."); 82 | content.serialForEach(function(staticFile, subcb) 83 | { 84 | fileUtil.copy(staticFile, path.join(WEB_OUT_PATH, path.basename(staticFile)), subcb); 85 | }, this); 86 | }, 87 | function findJSON() 88 | { 89 | base.info("Finding JSON files..."); 90 | glob(path.join(__dirname, "..", "out", "*.json"), this); 91 | }, 92 | function processJSON(jsonFiles) 93 | { 94 | base.info("Copying JSON files..."); 95 | jsonFiles.serialForEach(function(jsonFile, subcb) 96 | { 97 | fileUtil.copy(jsonFile, path.join(WEB_OUT_PATH, path.basename(jsonFile)), subcb); 98 | }, this); 99 | }, 100 | function render() 101 | { 102 | base.info("Rendering index..."); 103 | 104 | dustData.heroesSize = printUtil.toSize(fs.statSync(path.join(WEB_OUT_PATH, "heroes.json")).size, 1); 105 | dustData.mountsSize = printUtil.toSize(fs.statSync(path.join(WEB_OUT_PATH, "mounts.json")).size, 1); 106 | dustData.changeLog = JSON.parse(fs.readFileSync(path.join(__dirname, "changelog.json"), {encoding : "utf8"})).map(function(o) { o.when = moment(o.when, "YYYY-MM-DD").format("MMM D, YYYY"); return o; }); 107 | dustData.lastUpdated = dustData.changeLog[0].when; 108 | dustData.version = dustData.changeLog[0].version; 109 | dustData.patchVersion = dustData.changeLog[0].patchVersion; 110 | 111 | dustUtil.render(__dirname, "index", dustData, { keepWhitespace : true }, this); 112 | }, 113 | function save(html) 114 | { 115 | fs.writeFile(path.join(WEB_OUT_PATH, "index.html"), html, {encoding:"utf8"}, this); 116 | }, 117 | function extractImages() 118 | { 119 | if(process.argv[3]==="dev") 120 | return this(); 121 | 122 | base.info("Extracting images..."); 123 | extractAllImages(this); 124 | }, 125 | function getImagesToZip() 126 | { 127 | if(process.argv[3]==="dev") 128 | return this(); 129 | 130 | base.info("Finding extracted images..."); 131 | glob(path.join(IMAGES_FULL_PATH, "*.png"), this); 132 | }, 133 | function zipImages(images) 134 | { 135 | if(process.argv[3]==="dev") 136 | return this(); 137 | 138 | base.info("Zipping images..."); 139 | runUtil.run("zip", ["-r", ZIP_PATH].concat(images.map(function(image) { return path.basename(image); })), {silent:true, cwd : IMAGES_FULL_PATH}, this); 140 | }, 141 | function finish(err) 142 | { 143 | if(err) 144 | { 145 | base.error(err.stack); 146 | base.error(err); 147 | process.exit(1); 148 | } 149 | 150 | process.exit(0); 151 | } 152 | ); 153 | 154 | function extractAllImages(cb) 155 | { 156 | tiptoe( 157 | function clearOut() 158 | { 159 | base.info("\tClearing 'images' directory..."); 160 | rimraf(IMAGE_OUT_PATH, this); 161 | }, 162 | function createOut() 163 | { 164 | fs.mkdir(IMAGE_OUT_PATH, this); 165 | }, 166 | function copyBuildInfo() 167 | { 168 | base.info("\tCopying latest .build.info file..."); 169 | fileUtil.copy(path.join(HOTS_PATH, ".build.info"), path.join(HOTS_DATA_PATH, ".build.info"), this); 170 | }, 171 | function loadJSON() 172 | { 173 | base.info("\tLoading JSON..."); 174 | fs.readFile(HEROES_JSON_PATH, {encoding:"utf8"}, this); 175 | }, 176 | function extractFiles(heroesRaw) 177 | { 178 | var imageFiles = JSON.parse(heroesRaw).map(function(hero) { return Object.values(hero.talents).flatten().concat(Object.values(hero.abilities).flatten()).concat([hero]).map(function(item) { return item.icon; }); }).flatten().unique(); 179 | base.info("\tExtracting %d image files...", imageFiles.length); 180 | imageFiles.parallelForEach(extractImage, this, 1); 181 | }, 182 | cb 183 | ); 184 | } 185 | 186 | function extractImage(imageFile, cb) 187 | { 188 | tiptoe( 189 | function extractImage() 190 | { 191 | console.log('.'); 192 | runUtil.run(CASCEXTRATOR_PATH, [HOTS_DATA_PATH, "-o", IMAGE_OUT_PATH, "-f", IMAGE_ASSETS_PATH + imageFile], {silent:true}, this); 193 | }, 194 | function convertImage() 195 | { 196 | runUtil.run("convert", [path.join(IMAGE_OUT_PATH, IMAGE_ASSETS_PATH.replaceAll("\\\\", "/"), imageFile), path.join(IMAGE_OUT_PATH, IMAGE_ASSETS_PATH.replaceAll("\\\\", "/"), imageFile + ".png")], {silent:true}, this); 197 | }, 198 | function removeOldImage() 199 | { 200 | fs.unlink(path.join(IMAGE_OUT_PATH, IMAGE_ASSETS_PATH.replaceAll("\\\\", "/"), imageFile), this); 201 | }, 202 | cb 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /web/index.dust: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Heroes of the Storm data in JSON format 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

Heroes of the Storm JSON

25 |

This project provides up to date Heroes of the Storm data in JSON format for developers to easily use in their projects.
26 |
27 | For more details see: Example Hero and Documentation
28 |
29 | Any bugs or feedback, please address them on the heroesjson issue tracker.
30 |

31 |
32 |
33 |

Current Version: {version}

34 |

  Patch Version: {patchVersion} [changes from last patch]

35 |

   Last Updated: {lastUpdated}(change log)

36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 | 57 | 60 | 61 | 62 |
Download JSON Data
47 | heroes.json 48 | 50 | {heroesSize} 51 |
55 | mounts.json 56 | 58 | {mountsSize} 59 |
63 |
64 |
65 | 66 |

heroes.json Documentation(back to top)

67 |

All JSON files are UTF8 encoded and may contain UTF8 characters.
68 |
The 'heroes.json' file is an array where each entry is a hero object with key/value pairs.
69 |
Below you will find a table detailing each key.
70 |
71 | The heroes are sorted by 'name'.
72 |
73 | All data is extracted directly from the Heros of the Storm game files.
74 |
75 |

76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 181 | 182 | 183 | 184 | 238 | 239 | 240 | 241 | 315 | 316 | 317 | 318 | 362 | 363 | 364 |
KeyExampleDescription
id"Nova"The internal ID of this hero.
attributeid"Nova"The internal AttributeID of this hero.
name"Nova"The name of this hero.
title"Dominion Ghost"The title of this hero.
description"Automatically cloaks while out of combat. Snipes enemies from afar, and creates Decoys of herself to fool enemies."The description of this hero.
role"Assassin"The role of this hero.
type"Ranged"The type of this hero.
gender"Female"The gender of this hero.
franchise"Starcraft"Which universe/franchise this hero comes from.
difficulty"Medium"The level of difficulty to play this hero.
releaseDate"2014-07-22"The release date for this hero. Date format: YYYY-MM-DD
icon"ui_targetportrait_hero_Nova.dds"The internal filename for the icon used for this hero.
ratings 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 |
Type: object
KeyExampleDescription
damage10A rating between 1 and 10 for this hero's damage.
utility4A rating between 1 and 10 for this hero's utility.
survivability2A rating between 1 and 10 for this hero's survivability.
complexity5A rating between 1 and 10 for this hero's complexity.
180 |
stats 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 |
Type: object
Keys are hero/unit id's, values are objects:
KeyExampleDescription
hp700The base HP for this hero.
hpPerLevel4.5How much additional 'hp' this hero gains per level. This is a PERCENTAGE.
hpRegen1.457How much 'hp' this hero regenerates per second in game.
hpRegenPerLevel4.5How much additional 'hpRegen' this hero gains per level. This is a PERCENTAGE.
mana500The base mana for this hero.
manaPerLevel10How much additional 'mana' this hero gains per level.
manaRegen3How much 'mana' this hero regenerates per second in game.
manaRegenPerLevel0.0976How much additional 'manaRegen' this hero gains per level.
237 |
abilities 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 |
Type: object
Keys are hero/unit id's, values are arrays of ability objects:
KeyExampleDescription
id"NovaSnipeStorm"The internal ID of this ability.
name"Snipe"The name of the ability.
description"Deals 115 (+31 per level) damage to the first enemy hit."The description of the ability.
shortcut"Q"The default in-game shortcut key for this ability.
cooldown10The cooldown, in seconds, of the ability.
manaCost65How much mana the ability costs to use.
manaCostPerSecond15How much mana the ability costs to maintain per second.
aimType"Skillshot"The type of aiming used for the ability.
heroictruePresent and set to true if this ability is a heroic ability.
traittruePresent and set to true if this ability is a trait.
mounttruePresent and set to true if this ability is a mount ability.
icon"storm_ui_icon_nova_snipe.dds"The internal filename for the icon used for this ability.
314 |
talents 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 |
Type: object
Keys are the level of the talents.
Values are arrays of talent objects.
The talents are sorted in the order they appear in game.
KeyExampleDescription
id"NovaHeroicAbilityTripleTap"The internal ID of this talent.
name"Triple Tap"The name of this talent.
description"Locks in on the target Hero, then fires 3 shots that strike the first Hero or Structure they come in contact with for 80 (+33 per level) damage each."The description of this talent.
cooldown100The cooldown of this talent.
prerequisite"NovaHeroicAbilityTripleTap"The prerequisite talent id needed in order to take this talent.
icon"storm_ui_icon_nova_tripletap.dds"The internal filename for the icon used for this talent.
361 |
365 |
366 |
367 |

mounts.json Documentation(back to top)

368 |

All JSON files are UTF8 encoded and may contain UTF8 characters.
369 |
The 'mounts.json' file is an array where each entry is a mount object with key/value pairs.
370 |
Below you will find a table detailing each key.
371 |
372 | The mounts are sorted by 'name'.
373 |
374 | All data is extracted directly from the Heros of the Storm game files.
375 |
376 |

377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 |
KeyExampleDescription
id"MoneyPig"The internal ID of this mount.
attributeid"Mpig"The internal AttributeID of this mount.
name"Piggy Bank"The name of this mount.
description"This little piggy went, "Whee whee whee!" all the way to the bank!"The description of this mount.
franchise"Heroes"Which universe/franchise this mount comes from.
releaseDate"2014-10-14"The release date for this mount. Date format: YYYY-MM-DD
productid10072The internal product id for this mount. Not present for all mounts.
category"Ride"The category for this mount.
428 |
429 |
430 | 431 |

Example Hero(back to top)

432 |

433 | {
434 |   "id": "Nova",
435 |   "name": "Nova",
436 |   "title": "Dominion Ghost",
437 |   "description": "Automatically cloaks while out of combat. Snipes enemies from afar, and creates Decoys of herself to fool enemies.",
438 |   "role": "Assassin",
439 |   "type": "Ranged",
440 |   "gender": "Female",
441 |   "franchise": "Starcraft",
442 |   "difficulty": "Medium",
443 |   "ratings": {
444 |     "damage": 10,
445 |     "utility": 4,
446 |     "survivability": 2,
447 |     "complexity": 5
448 |   },
449 |   "stats": {
450 |     "Nova": {
451 |       "hp": 700,
452 |       "hpPerLevel": 110,
453 |       "hpRegen": 1.457,
454 |       "hpRegenPerLevel": 0.2265,
455 |       "mana": 500,
456 |       "manaPerLevel": 10,
457 |       "manaRegen": 3,
458 |       "manaRegenPerLevel": 0.0976
459 |     }
460 |   },
461 |   "abilities": {
462 |     "Nova": [
463 |       {
464 |         "id": "NovaSnipeStorm",
465 |         "icon": "storm_ui_icon_nova_snipe.dds",
466 |         "manaCost": 65,
467 |         "name": "Snipe",
468 |         "description": "Deals 115 (+31 per level) damage to the first enemy hit.",
469 |         "cooldown": 10,
470 |         "aimType": "Skillshot",
471 |         "shortcut": "Q"
472 |       },
473 |       {
474 |         "id": "NovaPinningShot",
475 |         "icon": "storm_ui_icon_Nova_PinningShot.dds",
476 |         "manaCost": 65,
477 |         "name": "Pinning Shot",
478 |         "description": "Deal 40 (+10 per level) damage to an enemy and slow it by 30% for 2.25 seconds.",
479 |         "cooldown": 12,
480 |         "shortcut": "W"
481 |       },
482 |       {
483 |         "id": "NovaHoloDecoy",
484 |         "icon": "storm_ui_icon_nova_holodecoy.dds",
485 |         "manaCost": 50,
486 |         "name": "Holo Decoy",
487 |         "description": "Create a Decoy for 5 seconds that appears to attack enemies. \nUsing this Ability does not break Cloak.",
488 |         "cooldown": 15,
489 |         "shortcut": "E"
490 |       },
491 |       {
492 |         "id": "NovaTripleTap",
493 |         "icon": "storm_ui_icon_nova_tripletap.dds",
494 |         "manaCost": 100,
495 |         "heroic": true,
496 |         "name": "Triple Tap",
497 |         "description": "Locks in on the target Hero, then fires 3 shots that hit the first Hero or Structure they come in contact with for 80 (+33 per level) damage each.",
498 |         "cooldown": 100,
499 |         "shortcut": "R"
500 |       },
501 |       {
502 |         "id": "NovaPrecisionStrike",
503 |         "icon": "storm_ui_icon_nova_orbitalstrike.dds",
504 |         "manaCost": 100,
505 |         "heroic": true,
506 |         "name": "Precision Strike",
507 |         "description": "After a 1.5 second delay, deals 300 (+35 per level) damage to enemies within an area. Unlimited range.",
508 |         "cooldown": 60,
509 |         "shortcut": "R"
510 |       },
511 |       {
512 |         "id": "NovaPermanentCloakSniper",
513 |         "trait": true,
514 |         "icon": "storm_ui_icon_nova_personalcloaking.dds",
515 |         "name": "Permanent Cloak, Sniper",
516 |         "description": "Gain Stealth when out of combat for 3 seconds. Taking damage, attacking, or channeling reveals you.\nBasic Attack range is 20% further than other ranged Heroes, and you see 10% further than other Heroes."
517 |       }
518 |     ]
519 |   },
520 |   "talents": {
521 |     "1": [
522 |       {
523 |         "id": "GenericTalentConjurersPursuit",
524 |         "name": "Conjurer's Pursuit",
525 |         "description": "Increases Mana Regeneration by 0.5 per second. Every 3 Regeneration Globes gathered increases this bonus by 0.25.",
526 |         "icon": "storm_btn_d3_monk_mantraofevasion.dds"
527 |       },
528 |       {
529 |         "id": "NovaMasteryPsiOpRangefinder",
530 |         "name": "Psi-Op Rangefinder",
531 |         "description": "Increases Snipe's range by 20% and reduces the Cooldown by 2 seconds.",
532 |         "icon": "storm_ui_icon_nova_snipe.dds"
533 |       },
534 |       {
535 |         "id": "NovaMasteryAmbushSnipe",
536 |         "name": "Ambush Snipe",
537 |         "description": "Increases Snipe's damage by 20% when used from Cloak or within one second of being Cloaked.",
538 |         "icon": "storm_ui_icon_nova_snipe.dds"
539 |       },
540 |       {
541 |         "id": "NovaMasteryTazerRounds",
542 |         "name": "Tazer Rounds",
543 |         "description": "Increases the duration of Pinning Shot's slow to 4 seconds.",
544 |         "icon": "storm_ui_icon_Nova_PinningShot.dds"
545 |       }
546 |     ],
547 |     "4": [
548 |       {
549 |         "id": "NovaMasteryPerfectShotSnipe",
550 |         "name": "Perfect Shot",
551 |         "description": "Hitting an enemy Hero with Snipe refunds 50% of the Mana cost. Killing an enemy Hero with Snipe refunds 100% of the Mana cost.",
552 |         "icon": "storm_ui_icon_nova_snipe.dds"
553 |       },
554 |       {
555 |         "id": "NovaExtendedProjection",
556 |         "name": "Remote Delivery",
557 |         "description": "Reduces the cooldown of Holo Decoy by 3 seconds, and increases the range by 100%.",
558 |         "icon": "storm_ui_icon_nova_holodecoy.dds"
559 |       },
560 |       {
561 |         "id": "GenericTalentGatheringPower",
562 |         "name": "Gathering Power",
563 |         "description": "Passively grants 5% Ability Power. Each Hero takedown increases this bonus by 2% to a maximum of 15%. This bonus Ability Power is reset to 5% on death.",
564 |         "icon": "storm_temp_war3_btncontrolmagic.dds"
565 |       },
566 |       {
567 |         "id": "GenericTalentEnvenom",
568 |         "name": "Envenom",
569 |         "description": "Activate to poison an enemy Hero, dealing 180 damage over 5 seconds.",
570 |         "icon": "storm_temp_war3_btnpoisonarrow.dds",
571 |         "cooldown": 60
572 |       }
573 |     ],
574 |     "7": [
575 |       {
576 |         "id": "NovaMasteryExplosiveShot",
577 |         "name": "Explosive Round",
578 |         "description": "Snipe also deals 50% damage to enemies near the impact.",
579 |         "icon": "storm_ui_icon_nova_snipe.dds"
580 |       },
581 |       {
582 |         "id": "NovaCombatStyleOneintheChamber",
583 |         "name": "One in the Chamber",
584 |         "description": "After using an ability, your next Basic Attack deals 80% additional damage.",
585 |         "icon": "storm_btn-extra_int_0.dds"
586 |       },
587 |       {
588 |         "id": "NovaCombatStyleAntiArmorShells",
589 |         "name": "Anti-Armor Shells",
590 |         "description": "Your Basic Attacks deal 250% damage, but your Attack Speed is proportionally slower.",
591 |         "icon": "storm_temp_btn-upgrade-terran-u238shells.dds"
592 |       },
593 |       {
594 |         "id": "NovaMasteryCovertOpsPinningShot",
595 |         "name": "Covert Ops",
596 |         "description": "Increases the Movement Speed slow of Pinning Shot by 1% for every second that Nova is Cloaked, to a maximum of a 50% slow. Bonus fades when Nova is un-Cloaked for one second.",
597 |         "icon": "storm_ui_icon_Nova_PinningShot.dds"
598 |       }
599 |     ],
600 |     "10": [
601 |       {
602 |         "id": "NovaHeroicAbilityTripleTap",
603 |         "name": "Triple Tap",
604 |         "description": "Locks in on the target Hero, then fires 3 shots that strike the first Hero or Structure they come in contact with for 80 (+33 per level) damage each.",
605 |         "icon": "storm_ui_icon_nova_tripletap.dds",
606 |         "cooldown": 100
607 |       },
608 |       {
609 |         "id": "NovaHeroicAbilityPrecisionStrike",
610 |         "name": "Precision Strike",
611 |         "description": "After a 1.5 second delay, deals 300 (+35 per level) damage in an area. Unlimited range.",
612 |         "icon": "storm_ui_icon_nova_orbitalstrike.dds",
613 |         "cooldown": 60
614 |       }
615 |     ],
616 |     "13": [
617 |       {
618 |         "id": "NovaMasteryHoloDrone",
619 |         "name": "Lethal Decoy",
620 |         "description": "Holo Decoy now deals 25% of Nova's damage.",
621 |         "icon": "storm_ui_icon_nova_holodecoy.dds"
622 |       },
623 |       {
624 |         "id": "NovaCombatStyleAdvancedCloaking",
625 |         "name": "Advanced Cloaking",
626 |         "description": "While Stealthed from Permanent Cloak, your Movement Speed is increased by 25% and you heal for 1.95% of your maximum Health per second.",
627 |         "icon": "storm_ui_icon_nova_personalcloaking.dds"
628 |       },
629 |       {
630 |         "id": "NovaCombatStyleMyKill",
631 |         "name": "Headshot",
632 |         "description": "Reduces your Ability cooldowns by 4 seconds when you kill an enemy Hero.",
633 |         "icon": "storm_temp_btn-upgrade-terran-infantryweaponslevel2.dds"
634 |       },
635 |       {
636 |         "id": "GenericTalentSpellShield",
637 |         "name": "Spell Shield",
638 |         "description": "Upon taking Ability Damage, reduce that damage and further Ability Damage by 50% for 2 seconds.  Can only trigger once every 30 seconds.",
639 |         "icon": "storm_temp_btn-ability-protoss-hardenedshields.dds"
640 |       }
641 |     ],
642 |     "16": [
643 |       {
644 |         "id": "NovaRailgun",
645 |         "name": "Railgun",
646 |         "description": "Snipe penetrates through the first enemy hit and deals 50% damage to subsequent targets. Snipe cooldown is reduced by 1 second for each target hit.",
647 |         "icon": "storm_ui_icon_nova_snipe.dds"
648 |       },
649 |       {
650 |         "id": "NovaMasteryCripplingShot",
651 |         "name": "Crippling Shot",
652 |         "description": "Enemies hit by Pinning shot become Vulnerable, taking 25% increased damage for the duration of the slow.",
653 |         "icon": "storm_ui_icon_Nova_PinningShot.dds"
654 |       },
655 |       {
656 |         "id": "NovaMasteryDoubleFakeHoloDecoy",
657 |         "name": "Double Fake",
658 |         "description": "Casting Holo Decoy creates an additional Decoy at your current location.",
659 |         "icon": "storm_ui_icon_nova_holodecoy.dds"
660 |       },
661 |       {
662 |         "id": "GenericTalentOverdrive",
663 |         "name": "Overdrive",
664 |         "description": "Activate to increase Ability Power by 25% and Mana costs by 40% for 5 seconds.",
665 |         "icon": "storm_btn_d3_wizard_archon.dds",
666 |         "cooldown": 25
667 |       }
668 |     ],
669 |     "20": [
670 |       {
671 |         "id": "NovaMasteryFastReload",
672 |         "name": "Fast Reload",
673 |         "description": "Triple Tap's cooldown is reset if it kills an enemy Hero.",
674 |         "icon": "storm_ui_icon_nova_tripletap.dds",
675 |         "prerequisite": "NovaHeroicAbilityTripleTap"
676 |       },
677 |       {
678 |         "id": "NovaMasteryPrecisionBarrage",
679 |         "name": "Precision Barrage",
680 |         "description": "Precision Strike now holds two charges with a short cooldown.",
681 |         "icon": "storm_ui_icon_nova_orbitalstrike.dds",
682 |         "prerequisite": "NovaHeroicAbilityPrecisionStrike"
683 |       },
684 |       {
685 |         "id": "GenericTalentRewind",
686 |         "name": "Rewind",
687 |         "description": "Activate to reset the cooldowns of your Basic Abilities.",
688 |         "icon": "storm_btn_d3_wizard_slowtime.dds",
689 |         "cooldown": 60
690 |       },
691 |       {
692 |         "id": "GenericTalentFlashoftheStorms",
693 |         "name": "Bolt of the Storm",
694 |         "description": "Activate to teleport to a nearby location.",
695 |         "icon": "storm_temp_btn-ability-protoss-blink-color.dds",
696 |         "cooldown": 40
697 |       }
698 |     ]
699 |   }
700 | }
701 |         
702 |
703 | 704 |

Change Log(back to top)

705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | {#changeLog} 716 | 717 | 718 | 719 | 724 | {/changeLog} 725 | 726 |
VersionDatePatch VersionChanges
{version}{when}{patchVersion} 720 |
    721 | {#changes}
  • {.|s}
  • 722 | {/changes}
723 |
727 |
728 | 729 |

License and Copyright(back to top)

730 |

The JSON files contain data that is Copyright (c) Blizzard Entertainment - All Rights Reserved
731 |
732 | This website is not affiliated with Blizzard Entertainment in any way.
733 |

734 |
735 | 736 |

Thanks(back to top)

737 |
    738 |
  • Thank you VERY MUCH to these folks who reported bugs and helped in other ways: 739 |
      740 |
    • kenavr, ry__ry, Gnejs Development, Obiyer
    • 741 |
    742 |
  • 743 |
  • Solarized is a great color scheme, I used a bunch of those colors here on this page
  • 744 |
  • Blizzard Entertainment for creating quality games that I have been playing since 1994!
  • 745 |
746 |
747 |
748 |
749 | 750 |

To-Do List(back to top)

751 |
    752 |
  • Other Languages
  • 753 |
  • Hero/Unit default attack damage/speed
  • 754 |
755 |
756 |

Source Code(back to top)

757 |

Source code used to generate the JSON is available here: https://github.com/nydus/heroesjson
758 |
759 | 760 | 761 | -------------------------------------------------------------------------------- /web/index.styl: -------------------------------------------------------------------------------- 1 | @import "conform" 2 | 3 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, section, summary, time, mark, audio, video 4 | margin 0 5 | padding 0 6 | border 0 7 | font inherit 8 | vertical-align baseline 9 | 10 | body, html 11 | background-color #313131 12 | font-family 'Monaco', courier, monospace 13 | color #cccccc 14 | 15 | body 16 | padding 0 1.5em 1.5em 1.5em 17 | font-size 100% 18 | 19 | ul 20 | margin-left 1.5em 21 | 22 | h2 23 | font-size 160% 24 | font-weight bold 25 | 26 | .value 27 | color #cb4b16 28 | 29 | a 30 | font-size 50% 31 | margin-left 2.0em 32 | 33 | h1 34 | font-size 160% 35 | padding 0.5em 0.5em 0.5em 0 36 | font-weight bold 37 | 38 | a 39 | font-size 40% 40 | margin-left 2.0em 41 | 42 | a, a:visited 43 | color #268bd2 44 | 45 | p, li 46 | line-height 1.4 47 | font-size 120% 48 | 49 | hr 50 | height 1px 51 | border 0 52 | background-linear-gradient left, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0) 53 | 54 | pre 55 | border 1px solid black 56 | 57 | strong 58 | font-weight bold 59 | 60 | table 61 | color #839496 62 | border-spacing 0 63 | border-collapse collapse 64 | margin-bottom 4.0em 65 | 66 | th, td 67 | border 1px solid black 68 | 69 | th 70 | background-color #073642 71 | font-weight bold 72 | vertical-align bottom 73 | padding 0.5em 1.0em 0.2em 1.0em 74 | 75 | td 76 | background-color #002b36 77 | text-align right 78 | vertical-align top 79 | padding 0.2em 0.5em 1.0em 1.0em 80 | 81 | td:last-child 82 | text-align left 83 | 84 | td table 85 | margin-bottom 0 86 | 87 | table.downloads 88 | tr:first-child th 89 | font-size 140% 90 | 91 | th.spacer 92 | width auto 93 | 94 | td 95 | width 12.0em 96 | padding 0.5em 1.0em 97 | text-align center 98 | vertical-align middle 99 | 100 | td.sizeColLeft 101 | text-align left 102 | line-height 1.2em 103 | border-right 0 104 | 105 | td.sizeColRight 106 | text-align right 107 | line-height 1.2em 108 | border-left 0 109 | 110 | table.changeLog 111 | td:nth-child(2) 112 | white-space nowrap 113 | -------------------------------------------------------------------------------- /web/static/rainbow-custom.min.js: -------------------------------------------------------------------------------- 1 | /* Rainbow v1.1.8 rainbowco.de | included languages: generic, javascript, html, css */ 2 | window.Rainbow=function(){function q(a){var b,c=a.getAttribute&&a.getAttribute("data-language")||0;if(!c){a=a.attributes;for(b=0;b=e[d][c])delete e[d][c],delete j[d][c];if(a>=c&&ac&&b'+b+""}function s(a,b,c,h){var f=a.exec(c);if(f){++t;!b.name&&"string"==typeof b.matches[0]&&(b.name=b.matches[0],delete b.matches[0]);var k=f[0],i=f.index,u=f[0].length+i,g=function(){function f(){s(a,b,c,h)}t%100>0?f():setTimeout(f,0)};if(C(i,u))g();else{var m=v(b.matches),l=function(a,c,h){if(a>=c.length)h(k);else{var d=f[c[a]];if(d){var e=b.matches[c[a]],i=e.language,g=e.name&&e.matches? 4 | e.matches:e,j=function(b,d,e){var i;i=0;var g;for(g=1;g/g,">").replace(/&(?![\w\#]+;)/g, 6 | "&"),b,c)}function o(a,b,c){if(b